我正在尝试做一些在我脑海中非常直接的事情。
我想要一个 VStack 的子视图根据其内容动态更改其高度(下面示例中的 ProblematicView)。
它通常工作得很好,但在这种情况下, ProblematicView 包含一个 GeometryReader (以模拟多行的 HStack)。
但是, GeometryReader 贪婪地占用所有空间(如果您删除 GeometryReader 及其内容,则会发生预期的行为)。不幸的是,在父视图(下面示例中的 UmbrellaView)上,UmbrellaView VStack 将自身的 50% 分配给 ProblematicView,而不是显示视图内容的最小尺寸。
我花了几个小时玩 min/ideal/maxHeight 框架参数,但无济于事。
我想要实现的目标可行吗?
我在底部添加了图片以在视觉上澄清。
struct UmbrellaView: View {
var body: some View {
VStack(spacing: 0) {
ProblematicView()
.background(Color.blue)
ScrollView(.vertical) {
Group {
Text("A little bit about this").font(.system(size: 20))
Divider()
}
Group {
Text("some").font(.system(size: 20))
Divider()
}
Group {
Text("group").font(.system(size: 20)).padding(.bottom)
Divider()
}
Group {
Text("content").font(.system(size: 20))
}
}
}
}
}
struct ProblematicView: View {
var body: some View {
let tags: [String] = ["content", "content 2 ", "content 3"]
var width = CGFloat.zero
var height = CGFloat.zero
return VStack(alignment: .center) {
Text("Some reasonnably long text that changes dynamically do can be any size").background(Color.red)
GeometryReader { g in
ZStack(alignment: .topLeading) {
ForEach(tags, id: \.self) { tag in
TagView(content: tag, color: .red, action: {})
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if tag == tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == tags.last! {
height = 0 // last item
}
return result
})
}
}.background(Color.green)
}.background(Color.blue)
}.background(Color.gray)
}
}
struct TagView: View {
let content: String
let color: Color
let action: () -> Void?
var body: some View {
HStack {
Text(content).padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
Button(action: {}) {
Image(systemName: "xmark.circle").foregroundColor(Color.gray)
}.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 7))
}
.background(color)
.cornerRadius(8.0)
}
}
struct ProblematicView_Previews: PreviewProvider {
static var previews: some View {
return ProblematicView()
}
}
struct UmbrellaView_Previews: PreviewProvider {
static var previews: some View {
return UmbrellaView()
}
}
Run Code Online (Sandbox Code Playgroud)
Asp*_*eri 11
由于GeometryReader主题问题的解决方案本质上的“鸡蛋”问题仅在运行时才有可能,因为 1) 初始高度未知 2) 它需要根据所有可用的外部尺寸计算内部尺寸 3) 它需要紧密的外部尺寸到计算的内部尺寸。
所以这是可能的方法(在你的代码中有一些额外的修复)
1) 预览 2-3) 运行时
代码:
struct ProblematicView: View {
@State private var totalHeight = CGFloat(100) // no matter - just for static Preview !!
@State private var tags: [String] = ["content", "content 2 ", "content 3", "content 4", "content 5"]
var body: some View {
var width = CGFloat.zero
var height = CGFloat.zero
return VStack {
Text("Some reasonnably long text that changes dynamically do can be any size").background(Color.red)
VStack { // << external container
GeometryReader { g in
ZStack(alignment: .topLeading) { // internal container
ForEach(self.tags, id: \.self) { tag in
TagView(content: tag, color: .red, action: {
// self.tags.removeLast() // << just for testing
})
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(Color.green)
.background(GeometryReader {gp -> Color in
DispatchQueue.main.async {
// update on next cycle with calculated height of ZStack !!!
self.totalHeight = gp.size.height
}
return Color.clear
})
}.background(Color.blue)
}.frame(height: totalHeight)
}.background(Color.gray)
}
}
struct TagView: View {
let content: String
let color: Color
let action: (() -> Void)?
var body: some View {
HStack {
Text(content).padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
Button(action: action ?? {}) {
Image(systemName: "xmark.circle").foregroundColor(Color.gray)
}.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 7))
}
.background(color)
.cornerRadius(8.0)
}
}
Run Code Online (Sandbox Code Playgroud)
基于@Asperi的代码,我实现了一个通用解决方案。它适用于预览版并与iOS 13+兼容。我的解决方案不使用 DispatchQueue.main.async并且方便@ViewBuilder您在任何您喜欢的视图中进行折腾。将 VerticalFlow 放入 VStack 或 ScrollView 中。设置hSpacing和vSpacing到项目。向整个视图添加填充。
简单的例子:
struct ContentView: View {
@State var items: [String] = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight"]
var body: some View {
VerticalFlow(items: $items) { item in
Text(item)
}
}
}
Run Code Online (Sandbox Code Playgroud)
VerticalFlow.swift:
import SwiftUI
struct VerticalFlow<Item, ItemView: View>: View {
@Binding var items: [Item]
var hSpacing: CGFloat = 20
var vSpacing: CGFloat = 10
@ViewBuilder var itemViewBuilder: (Item) -> ItemView
@SwiftUI.State private var size: CGSize = .zero
var body: some View {
var width: CGFloat = .zero
var height: CGFloat = .zero
VStack {
GeometryReader { geometryProxy in
ZStack(alignment: .topLeading) {
ForEach(items.indices, id: \.self) { i in
itemViewBuilder(items[i])
.alignmentGuide(.leading) { dimensions in
if abs(width - dimensions.width) > geometryProxy.size.width {
width = 0
height -= dimensions.height + vSpacing
}
let leadingOffset = width
if i == items.count - 1 {
width = 0
} else {
width -= dimensions.width + hSpacing
}
return leadingOffset
}
.alignmentGuide(.top) { dimensions in
let topOffset = height
if i == items.count - 1 {
height = 0
}
return topOffset
}
}
}
.readVerticalFlowSize(to: $size)
}
}
.frame(height: size.height > 0 ? size.height : nil)
}
}
struct VerticalFlow_Previews: PreviewProvider {
@SwiftUI.State static var items: [String] = [
"One 1", "Two 2", "Three 3", "Four 4", "Eleven 5", "Six 6",
"Seven 7", "Eight 8", "Nine 9", "Ten 10", "Eleven 11",
"ASDFGHJKLqwertyyuio d fadsf",
"Poiuytrewq lkjhgfdsa mnbvcxzI 0987654321"
]
static var previews: some View {
VStack {
Text("Text at the top")
VerticalFlow(items: $items) { item in
VerticalFlowItem(systemImage: "pencil.circle", title: item, isSelected: true)
}
Text("Text at the bottom")
}
ScrollView {
VStack {
Text("Text at the top")
VerticalFlow(items: $items) { item in
VerticalFlowItem(systemImage: "pencil.circle", title: item, isSelected: true)
}
Text("Text at the bottom")
}
}
}
}
private struct VerticalFlowItem: View {
let systemImage: String
let title: String
@SwiftUI.State var isSelected: Bool
var body: some View {
HStack {
Image(systemName: systemImage).font(.title3)
Text(title).font(.title3).lineLimit(1)
}
.padding(10)
.foregroundColor(isSelected ? .white : .blue)
.background(isSelected ? Color.blue : Color.white)
.cornerRadius(40)
.overlay(RoundedRectangle(cornerRadius: 40).stroke(Color.blue, lineWidth: 1.5))
.onTapGesture {
isSelected.toggle()
}
}
}
private extension View {
func readVerticalFlowSize(to size: Binding<CGSize>) -> some View {
background(GeometryReader { proxy in
Color.clear.preference(
key: VerticalFlowSizePreferenceKey.self,
value: proxy.size
)
})
.onPreferenceChange(VerticalFlowSizePreferenceKey.self) {
size.wrappedValue = $0
}
}
}
private struct VerticalFlowSizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
let next = nextValue()
if next != .zero {
value = next
}
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1730 次 |
| 最近记录: |