带包装的SwiftUI HStack

时间:2019-11-13 17:23:03

标签: swift tags 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)
                        }

enter image description here

此外,蓝色标记中不应包含换行符:

enter image description here

这应该是最后的样子: enter image description here

7 个答案:

答案 0 :(得分:4)

我曾经创建过你需要的东西。

我在 HStack 中使用过 VStack

您传入用于确定最大行宽的 geometryProxy。 我把它传递进来,这样它就可以在滚动视图中使用

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

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

这只是 init 和 final 阶段组合和输出 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()
    }
}

神奇就在这里

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()
        }
    }
}

这里是我使用的扩展

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

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

答案 1 :(得分:3)

以下是一些如何使用alignmentGuide完成此操作的方法。为了避免过多的代码发布,它被简化了,但是希望它是有用的。

这是结果:

swiftui wrapped layout

这是完整的演示代码(自动支持方向):

stateless allocator

答案 2 :(得分:1)

我遇到了同样的问题,为了解决它,我将对象项传递给一个函数,该函数首先为项创建视图,然后通过 UIHostController 我将根据项的宽度计算下一个位置。项目视图然后由函数返回。

import SwiftUI

class TestItem: Identifiable {
    
    var id = UUID()
    var str = ""
    init(str: String) {
        self.str = str
    }
    
}

struct AutoWrap: View {
    
    var tests: [TestItem] = [
        TestItem(str:"Ninetendo"),
        TestItem(str:"XBox"),
        TestItem(str:"PlayStation"),
        TestItem(str:"PlayStation 2"),
        TestItem(str:"PlayStation 3"),
        TestItem(str:"random"),
        TestItem(str:"PlayStation 4"),
    ]
    
    

    
    var body: some View {
        
        var curItemPos: CGPoint = CGPoint(x: 0, y: 0)
        var prevItemWidth: CGFloat = 0
        return GeometryReader { proxy in
            ZStack(alignment: .topLeading) {
                ForEach(tests) { t in
                    generateItem(t: t, curPos: &curItemPos, containerProxy: proxy, prevItemWidth: &prevItemWidth)
                }
            }.padding(5)
        }
    }
    
    func generateItem(t: TestItem, curPos: inout CGPoint, containerProxy: GeometryProxy, prevItemWidth: inout CGFloat, hSpacing: CGFloat = 5, vSpacing: CGFloat = 5) -> some View {
        let viewItem = Text(t.str).padding([.leading, .trailing], 15).background(Color.blue).cornerRadius(25)
        let itemWidth = UIHostingController(rootView: viewItem).view.intrinsicContentSize.width
        let itemHeight = UIHostingController(rootView: viewItem).view.intrinsicContentSize.height
        let newPosX = curPos.x + prevItemWidth + hSpacing
        let newPosX2 = newPosX + itemWidth
        if newPosX2 > containerProxy.size.width {
            curPos.x = hSpacing
            curPos.y += itemHeight + vSpacing
        } else {
            curPos.x = newPosX
        }
        prevItemWidth = itemWidth
        return viewItem.offset(x: curPos.x, y: curPos.y)
    }
}

struct AutoWrap_Previews: PreviewProvider {
    static var previews: some View {
        AutoWrap()
    }
}

答案 3 :(得分:0)

您需要在“文本视图”之后立即处理行配置。如果您需要多行,请不要使用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)

                }
            }

答案 4 :(得分:0)

我找到了一个名为ASCollectionView的框架解决方案。希望这对遇到同样问题的人有所帮助。

答案 5 :(得分:0)

我有类似这样的代码(相当长)。在简单的情况下,它可以正常工作,但是在与几何读取器进行深度嵌套时,它的尺寸不能很好地传播。

如果此视图像Text()那样扩展和流动以扩展父视图内容,那将是很好的选择,但它似乎已明确设置其相对于父视图的高度。

https://gist.github.com/michzio/a0b23ee43a88cbc95f65277070167e29

这是代码中最重要的部分(没有预览和测试数据)

private func flow(in geometry: GeometryProxy) -> some View {
        
        print("Card geometry: \(geometry.size.width) \(geometry.size.height)")
        
        return ZStack(alignment: .topLeading) {
            //Color.clear
            ForEach(data, id: self.dataId) { element in
                self.content(element)
                    .geometryPreference(tag: element\[keyPath: self.dataId\])
                    /*
                    .alignmentGuide(.leading) { d in
                        print("Element: w: \(d.width), h: \(d.height)")
                        if (abs(width - d.width) > geometry.size.width)
                        {
                            width = 0
                            height -= d.height
                        }
                        
                        let result = width
                        
                        if element\[keyPath: self.dataId\] == self.data.last!\[keyPath: self.dataId\] {
                            width = 0 //last item
                        } else {
                            width -= d.width
                        }
                        return result
                    }
                    .alignmentGuide(.top) { d in
                        let result = height
                        if element\[keyPath: self.dataId\] == self.data.last!\[keyPath: self.dataId\] {
                            height = 0 // last item
                        }
                        return result
                    }*/
                    
                    .alignmentGuide(.top) { d in
                        self.alignmentGuides\[element\[keyPath: self.dataId\]\]?.y ?? 0
                    }
                    .alignmentGuide(.leading) { d in
                        self.alignmentGuides\[element\[keyPath: self.dataId\]\]?.x ?? 0
                    }
            }
        }
        .background(Color.pink)
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        //.animation(self.loaded ? .linear(duration: 1) : nil)
        
        .onPreferenceChange(_GeometryPreferenceKey.self, perform: { preferences in
        
                DispatchQueue.main.async {
                    let (alignmentGuides, totalHeight) = self.calculateAlignmentGuides(preferences: preferences, geometry: geometry)
                    self.alignmentGuides = alignmentGuides
                    self.totalHeight = totalHeight
                    self.availableWidth = geometry.size.width
                }
        })
    }
    
    func calculateAlignmentGuides(preferences: \[_GeometryPreference\], geometry: GeometryProxy) -> (\[AnyHashable: CGPoint\], CGFloat) {
        
        var alignmentGuides = \[AnyHashable: CGPoint\]()
        
        var width: CGFloat = 0
        var height: CGFloat = 0
        
        var rowHeights: Set<CGFloat> = \[\]

        preferences.forEach { preference in
            let elementWidth = spacing + preference.rect.width
            
            if width + elementWidth >= geometry.size.width {
                width = 0
                height += (rowHeights.max() ?? 0) + spacing
                //rowHeights.removeAll()
            }
            
            let offset = CGPoint(x: 0 - width, y: 0 - height)
            
            print("Alignment guides offset: \(offset)")
            alignmentGuides\[preference.tag\] = offset
            
            width += elementWidth
            rowHeights.insert(preference.rect.height)
        }

        return (alignmentGuides, height + (rowHeights.max() ?? 0))
    }
}

image

答案 6 :(得分:0)

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