根据其元素动态调整 GeometryReader 高度

Pas*_*sta 8 swiftui

我正在尝试做一些在我脑海中非常直接的事情。

我想要一个 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)

有问题的视图预览

UmbrellaView 预览

Asp*_*eri 11

由于GeometryReader主题问题的解决方案本质上的“鸡蛋”问题仅在运行时才有可能,因为 1) 初始高度未知 2) 它需要根据所有可用的外部尺寸计算内部尺寸 3) 它需要紧密的外部尺寸到计算的内部尺寸。

所以这是可能的方法(在你的代码中有一些额外的修复)

预览运行运行时2

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)


Mar*_*arK 8

基于@Asperi的代码,我实现了一个通用解决方案。它适用于预览版并与iOS 13+兼容。我的解决方案不使用 DispatchQueue.main.async并且方便@ViewBuilder您在任何您喜欢的视图中进行折腾。将 VerticalFlow 放入 VStack 或 ScrollView 中。设置hSpacingvSpacing到项目。向整个视图添加填充。

简单的例子:

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)

垂直流预览