带 Wrap 的 SwiftUI HStack

Flo*_*lle 36 ios swift swiftui hstack

是否有可能完全显示蓝色标签(当前被截断)然后自动换行?

NavigationLink(destination: GameListView()) {
  VStack(alignment: .leading, spacing: 5){
    // Name der Sammlung:
    Text(collection.name)
      .font(.headline)

    // Optional: Für welche Konsolen bzw. Plattformen:
    HStack(alignment: .top, spacing: 10){
      ForEach(collection.platforms, id: \.self) { platform in
        Text(platform)
          .padding(.all, 5)
          .font(.caption)
          .background(Color.blue)
          .foregroundColor(Color.white)
          .cornerRadius(5)
          .lineLimit(1)
      }
    }
  }
  .padding(.vertical, 10)
}
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

此外,蓝色标签中不应有换行符:

在此处输入图片说明

这就是它最终的样子:

在此处输入图片说明

Asp*_*eri 38

Here is some approach of how this could be done using alignmentGuide(s). It is simplified to avoid many code post, but hope it is useful.

Update: There is also updated & improved variant of below solution in my answer for SwiftUI HStack with wrap and dynamic height

This is the result:

swiftui wrapped layout

And here is full demo code (orientation is supported automatically):

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 此解决方案的问题在于,几何阅读器占据了下面的整个空间,如果您尝试在其下方添加更多内容,则会破坏视图 (3认同)

Dan*_*iel 15

对我来说,所有答案都不起作用。要么是因为我有不同类型的元素,要么是因为周围的元素没有正确定位。因此,我最终实现了自己的 WrappingHStack,它的使用方式与 HStack 非常相似。您可以在GitHub 上找到它:WrappingHStack

这是一个例子:

在此输入图像描述

代码:

WrappingHStack {
    Text("WrappingHStack")
        .padding()
        .font(.title)
        .border(Color.black)
    
    Text("can handle different element types")
    
    Image(systemName: "scribble")
        .font(.title)
        .frame(width: 200, height: 20)
        .background(Color.purple)
    
    Text("and loop")
        .bold()
    
    WrappingHStack(1...20, id:\.self) {
        Text("Item: \($0)")
            .padding(3)
            .background(Rectangle().stroke())
    }.frame(minWidth: 250)
}
.padding()
.border(Color.black)
Run Code Online (Sandbox Code Playgroud)


小智 10

这是对蒂米答案的改进。它保留了所有可用的水平空间,修复了一个错误,即如果包装容器的元素少于一整行,它将保留大量空间,从而导致 UI 未对齐,并且还修复了导致内容溢出的错误一些边缘情况:

private struct WrappingHStack: Layout {
    // inspired by: https://stackoverflow.com/a/75672314
    private var horizontalSpacing: CGFloat
    private var verticalSpacing: CGFloat
    public init(horizontalSpacing: CGFloat, verticalSpacing: CGFloat? = nil) {
        self.horizontalSpacing = horizontalSpacing
        self.verticalSpacing = verticalSpacing ?? horizontalSpacing
    }

    public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize {
        guard !subviews.isEmpty else { return .zero }

        let height = subviews.map { $0.sizeThatFits(proposal).height }.max() ?? 0

        var rowWidths = [CGFloat]()
        var currentRowWidth: CGFloat = 0
        subviews.forEach { subview in
            if currentRowWidth + horizontalSpacing + subview.sizeThatFits(proposal).width >= proposal.width ?? 0 {
                rowWidths.append(currentRowWidth)
                currentRowWidth = subview.sizeThatFits(proposal).width
            } else {
                currentRowWidth += horizontalSpacing + subview.sizeThatFits(proposal).width
            }
        }
        rowWidths.append(currentRowWidth)

        let rowCount = CGFloat(rowWidths.count)
        return CGSize(width: max(rowWidths.max() ?? 0, proposal.width ?? 0), height: rowCount * height + (rowCount - 1) * verticalSpacing)
    }

    public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let height = subviews.map { $0.dimensions(in: proposal).height }.max() ?? 0
        guard !subviews.isEmpty else { return }
        var x = bounds.minX
        var y = height / 2 + bounds.minY
        subviews.forEach { subview in
            x += subview.dimensions(in: proposal).width / 2
            if x + subview.dimensions(in: proposal).width / 2 > bounds.maxX {
                x = bounds.minX + subview.dimensions(in: proposal).width / 2
                y += height + verticalSpacing
            }
            subview.place(
                at: CGPoint(x: x, y: y),
                anchor: .center,
                proposal: ProposedViewSize(
                    width: subview.dimensions(in: proposal).width,
                    height: subview.dimensions(in: proposal).height
                )
            )
            x += subview.dimensions(in: proposal).width / 2 + horizontalSpacing
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它:

struct MyView: View {
  var body: some View {
    WrappingHStack(horizontalSpacing: 5) {
      Text("Hello")
      Text("Hello again")
      Text("Hello")
      Text("Hello again")
      Text("Hello")
      Text("Hello again")
      Text("Hello")
      Text("Hello again")
      Text("Hello")
      Text("Hello again")
      Text("Hello")
      Text("Hello again")
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

效果如下:

展示包裹行为


小智 5

我以前曾尝试过创造你所需要的东西。

HStack在 a 中使用过VStack

您传入一个geometryProxy用于确定最大行宽度的。我将其传递进去,以便它可以在滚动视图中使用

我将 SwiftUI 视图包装在 UIHostingController 中以获取每个子项的大小。

然后,我循环遍历视图,将它们添加到行中,直到达到最大宽度,在这种情况下,我开始添加到新行。

这只是组合和输出 VStack 中的行的初始阶段和最后阶段

struct WrappedHStack<Content: View>: View {
    
    private let content: [Content]
    private let spacing: CGFloat = 8
    private let geometry: GeometryProxy
    
    init(geometry: GeometryProxy, content: [Content]) {
        self.content = content
        self.geometry = geometry
    }
    
    var body: some View {
        let rowBuilder = RowBuilder(spacing: spacing,
                                    containerWidth: geometry.size.width)
        
        let rowViews = rowBuilder.generateRows(views: content)
        let finalView = ForEach(rowViews.indices) { rowViews[$0] }
        
        VStack(alignment: .center, spacing: 8) {
            finalView
        }.frame(width: geometry.size.width)
    }
}

extension WrappedHStack {
    
    init<Data, ID: Hashable>(geometry: GeometryProxy, @ViewBuilder content: () -> ForEach<Data, ID, Content>) {
        let views = content()
        self.geometry = geometry
        self.content = views.data.map(views.content)
    }

    init(geometry: GeometryProxy, content: () -> [Content]) {
        self.geometry = geometry
        self.content = content()
    }
}
Run Code Online (Sandbox Code Playgroud)

魔法就在这里发生

extension WrappedHStack {
    struct RowBuilder {
        
        private var spacing: CGFloat
        private var containerWidth: CGFloat
        
        init(spacing: CGFloat, containerWidth: CGFloat) {
            self.spacing = spacing
            self.containerWidth = containerWidth
        }
        
        func generateRows<Content: View>(views: [Content]) -> [AnyView] {
            
            var rows = [AnyView]()
            
            var currentRowViews = [AnyView]()
            var currentRowWidth: CGFloat = 0
            
            for (view) in views {
                let viewWidth = view.getSize().width
                
                if currentRowWidth + viewWidth > containerWidth {
                    rows.append(createRow(for: currentRowViews))
                    currentRowViews = []
                    currentRowWidth = 0
                }
                currentRowViews.append(view.erasedToAnyView())
                currentRowWidth += viewWidth + spacing
            }
            rows.append(createRow(for: currentRowViews))
            return rows
        }
        
        private func createRow(for views: [AnyView]) -> AnyView {
            HStack(alignment: .center, spacing: spacing) {
                ForEach(views.indices) { views[$0] }
            }
            .erasedToAnyView()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我使用的扩展

extension View {
    func erasedToAnyView() -> AnyView {
        AnyView(self)
    }
    
    func getSize() -> CGSize {
        UIHostingController(rootView: self).view.intrinsicContentSize
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以在此处查看完整代码和一些示例: https://gist.github.com/kanesbetas/63e719cb96e644d31bf027194bf4ccdb


E.C*_*oms -4

您需要在文本视图之后立即处理线路配置。如果需要多行,请勿使用 lineLimit(1)。

 HStack(alignment: .top, spacing: 10){
                ForEach(collection.platforms, id: \.self) { platform in
                    Text(platform)
                    .fixedSize(horizontal: false, vertical: true)
                    .lineLimit(10)
                    .multilineTextAlignment(.leading)
                        .padding(.all, 5)
                        .font(.caption)
                        .background(Color.blue)
                        .foregroundColor(Color.white)
                        .cornerRadius(5)

                }
            }
Run Code Online (Sandbox Code Playgroud)