【问题标题】:SwiftUI HStack with wrap and dynamic height带有换行和动态高度的 SwiftUI HStack
【发布时间】:2021-12-21 23:43:59
【问题描述】:

我有这个视图来显示我从SwiftUI HStack with Wrap 获得的多行文本标签,但是当我将它添加到 VStack 中时,标签会与我放在下面的任何其他视图重叠。 标签显示正确,但视图本身的高度不在 VStack 内计算。 如何让这个视图使用 is content 的高度?

import SwiftUI

struct TestWrappedLayout: View {
    @State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"]

    var body: some View {
        GeometryReader { geometry in
            self.generateContent(in: geometry)
        }
    }

    private func generateContent(in g: GeometryProxy) -> some View {
        var width = CGFloat.zero
        var height = CGFloat.zero

        return ZStack(alignment: .topLeading) {
            ForEach(self.platforms, id: \.self) { platform in
                self.item(for: platform)
                    .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 platform == self.platforms.last! {
                            width = 0 //last item
                        } else {
                            width -= d.width
                        }
                        return result
                    })
                    .alignmentGuide(.top, computeValue: {d in
                        let result = height
                        if platform == self.platforms.last! {
                            height = 0 // last item
                        }
                        return result
                    })
            }
        }
    }

    func item(for text: String) -> some View {
        Text(text)
            .padding(.all, 5)
            .font(.body)
            .background(Color.blue)
            .foregroundColor(Color.white)
            .cornerRadius(5)
    }
}

struct TestWrappedLayout_Previews: PreviewProvider {
    static var previews: some View {
        TestWrappedLayout()
    }
}

示例代码:

struct ExampleTagsView: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                Text("Platforms:")
                TestWrappedLayout()

                Text("Other Platforms:")
                TestWrappedLayout()
            }
        }
    }
}

struct ExampleTagsView_Previews: PreviewProvider {
    static var previews: some View {
        ExampleTagsView()
    }
}

结果:

【问题讨论】:

  • 您不认为您应该提供对您从哪里获得此代码的帖子的参考吗?
  • 抱歉,刚刚添加。我想在那里添加评论,但我还不能。您对如何解决这个问题有任何想法吗?
  • 您是否会显示您的代码,如何集成该组件?
  • 当然,我刚刚更新了问题

标签: swift xcode swiftui word-wrap zstack


【解决方案1】:

我只是设法通过将 GeometryReader 移到 ExampleTagsView 并在 .alignmentGuide 中使用 platform.first 而不是 last 来解决这个问题

完整代码:

import SwiftUI

struct ExampleTagsView: View {
    var body: some View {
        GeometryReader { geometry in
            ScrollView(.vertical) {
                VStack(alignment: .leading) {
                    Text("Platforms:")
                    TestWrappedLayout(geometry: geometry)

                    Text("Other Platforms:")
                    TestWrappedLayout(geometry: geometry)
                }
            }
        }
    }
}

struct ExampleTagsView_Previews: PreviewProvider {
    static var previews: some View {
        ExampleTagsView()
    }
}

struct TestWrappedLayout: View {
    @State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "PlayStation 5", "Ni", "Xct5Box", "PlayStatavtion", "PlvayStation 2", "PlayStatiadfon 3", "PlaySdatation 4", "PlaySdtation 5"]
    let geometry: GeometryProxy

    var body: some View {
        self.generateContent(in: geometry)
    }

    private func generateContent(in g: GeometryProxy) -> some View {
        var width = CGFloat.zero
        var height = CGFloat.zero

        return ZStack(alignment: .topLeading) {
            ForEach(self.platforms, id: \.self) { platform in
                self.item(for: platform)
                    .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 platform == self.platforms.first! {
                            width = 0 //last item
                        } else {
                            width -= d.width
                        }
                        return result
                    })
                    .alignmentGuide(.top, computeValue: {d in
                        let result = height
                        if platform == self.platforms.first! {
                            height = 0 // last item
                        }
                        return result
                    })
            }
        }
    }

    func item(for text: String) -> some View {
        Text(text)
            .padding(.all, 5)
            .font(.body)
            .background(Color.blue)
            .foregroundColor(Color.white)
            .cornerRadius(5)
    }
}

结果:

【讨论】:

  • 为什么视图中的平台顺序会发生变化?
  • 任天堂在最后一个位置
  • 这个实现有一些问题。如果数组包含三个项目,它只显示其中两个。我不确定是不是因为物品的数量或宽度,但这绝对是一个问题。
  • 如果您尝试使用数组 [games,bananas, College football],例如,由于某种原因,“bananas”将不会显示。
【解决方案2】:

好的,这里有一些更通用和改进的变体(针对最初在 SwiftUI HStack with Wrap 中引入的解决方案)

使用 Xcode 11.4 / iOS 13.4 测试

注意:由于视图高度是动态计算的,因此结果在运行时有效,而不是在预览中

struct TagCloudView: View {
    var tags: [String]

    @State private var totalHeight 
          = CGFloat.zero       // << variant for ScrollView/List
    //    = CGFloat.infinity   // << variant for VStack

    var body: some View {
        VStack {
            GeometryReader { geometry in
                self.generateContent(in: geometry)
            }
        }
        .frame(height: totalHeight)// << variant for ScrollView/List
        //.frame(maxHeight: totalHeight) // << variant for VStack
    }

    private func generateContent(in g: GeometryProxy) -> some View {
        var width = CGFloat.zero
        var height = CGFloat.zero

        return ZStack(alignment: .topLeading) {
            ForEach(self.tags, id: \.self) { tag in
                self.item(for: tag)
                    .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(viewHeightReader($totalHeight))
    }

    private func item(for text: String) -> some View {
        Text(text)
            .padding(.all, 5)
            .font(.body)
            .background(Color.blue)
            .foregroundColor(Color.white)
            .cornerRadius(5)
    }

    private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
        return GeometryReader { geometry -> Color in
            let rect = geometry.frame(in: .local)
            DispatchQueue.main.async {
                binding.wrappedValue = rect.size.height
            }
            return .clear
        }
    }
}

struct TestTagCloudView : View {
    var body: some View {
        VStack {
            Text("Header").font(.largeTitle)
            TagCloudView(tags: ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"])
            Text("Some other text")
            Divider()
            Text("Some other cloud")
            TagCloudView(tags: ["Apple", "Google", "Amazon", "Microsoft", "Oracle", "Facebook"])
        }
    }
}

【讨论】:

  • 如果你将它添加到 ScrollView 或 List 中,这对我来说仍然是同样的问题
  • @Ludyem,已更新。不管怎样,你找到了自己的适应方式真是太好了——我总是很高兴。
  • 太棒了!但是我想通过点击它来选择任何标签。我尝试将 onTabGesture 添加到 item() 中的 Text,但似乎只有第一行标签是“明智的”。我也尝试用按钮替换文本,但没有任何改变。
  • 效果很好。谢谢你。如何使其正确对齐?所以在你的屏幕上,我需要 Oracle 下的 Facebook。以及 Playstation 2 下的 Playstation 3 和 4。
  • @LualdiDylan 嗨,我怎样才能使这个芯片中心对齐而不是左对齐?
【解决方案3】:

我调整了 Asperi 的解决方案以接受任何类型的视图和模型。以为我会在这里分享。我将其添加到 GitHub Gist 并在此处包含代码。

struct WrappingHStack<Model, V>: View where Model: Hashable, V: View {
    typealias ViewGenerator = (Model) -> V
    
    var models: [Model]
    var viewGenerator: ViewGenerator
    var horizontalSpacing: CGFloat = 2
    var verticalSpacing: CGFloat = 0

    @State private var totalHeight
          = CGFloat.zero       // << variant for ScrollView/List
    //    = CGFloat.infinity   // << variant for VStack

    var body: some View {
        VStack {
            GeometryReader { geometry in
                self.generateContent(in: geometry)
            }
        }
        .frame(height: totalHeight)// << variant for ScrollView/List
        //.frame(maxHeight: totalHeight) // << variant for VStack
    }

    private func generateContent(in geometry: GeometryProxy) -> some View {
        var width = CGFloat.zero
        var height = CGFloat.zero

        return ZStack(alignment: .topLeading) {
            ForEach(self.models, id: \.self) { models in
                viewGenerator(models)
                    .padding(.horizontal, horizontalSpacing)
                    .padding(.vertical, verticalSpacing)
                    .alignmentGuide(.leading, computeValue: { dimension in
                        if (abs(width - dimension.width) > geometry.size.width)
                        {
                            width = 0
                            height -= dimension.height
                        }
                        let result = width
                        if models == self.models.last! {
                            width = 0 //last item
                        } else {
                            width -= dimension.width
                        }
                        return result
                    })
                    .alignmentGuide(.top, computeValue: {dimension in
                        let result = height
                        if models == self.models.last! {
                            height = 0 // last item
                        }
                        return result
                    })
            }
        }.background(viewHeightReader($totalHeight))
    }

    private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
        return GeometryReader { geometry -> Color in
            let rect = geometry.frame(in: .local)
            DispatchQueue.main.async {
                binding.wrappedValue = rect.size.height
            }
            return .clear
        }
    }
}

【讨论】:

  • 谢谢,您的更改使解决方案更易于推理和重用。
  • 这是最好的解决方案。顺便问一下,如何做右对齐而不是当前左对齐?
  • 中心对齐怎么样?
【解决方案4】:

您可以使用“fixedSize”将内容包装在一个或两个方向:

  .fixedSize(horizontal: true, vertical: true)

【讨论】:

    猜你喜欢
    • 2022-04-11
    • 2020-07-28
    • 1970-01-01
    • 2021-04-22
    • 2021-04-12
    • 2023-04-07
    • 2020-03-13
    • 1970-01-01
    相关资源
    最近更新 更多