在SwiftUI中实现标记列表

在SwiftUI中实现标记列表,swiftui,Swiftui,我试图在SwiftUI中实现标记列表,但我不确定如果列表水平溢出,如何让它将标记包装到其他行。我从一个名为tags的字符串数组开始,在SwiftUI I中循环遍历该数组并创建按钮,如下所示: HStack{ ForEach(tags, id: \.self){tag in Button(action: {}) { HStack { Text(tag) Image(systemName:

我试图在SwiftUI中实现标记列表,但我不确定如果列表水平溢出,如何让它将标记包装到其他行。我从一个名为tags的字符串数组开始,在SwiftUI I中循环遍历该数组并创建按钮,如下所示:

HStack{
    ForEach(tags, id: \.self){tag in
        Button(action: {}) {
            HStack {
                Text(tag)
                Image(systemName: "xmark.circle")
            }
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.orange)
        .cornerRadius(.infinity)
        .lineLimit(1)
    }
}
如果标记数组很小,则按如下方式渲染:

但是,如果数组具有更多值,则会执行以下操作:


我要寻找的行为是将最后一个标记(黄色)换行到第二行。我意识到这是在一个HStack中,我希望我可以添加一个值大于1的对lineLimit的调用,但它似乎没有改变行为。如果我将外部HStack更改为VStack,它会将每个按钮放在单独的行上,因此仍然不是我尝试创建的行为。任何指导都将不胜感激。

您可以尝试使用带水平轴的ScrollView

ScrollView(.horizontal, showsIndicators: true) {
        HStack{
            ForEach(tags, id: \.self){tag in
                Button(action: {}) {
                    HStack {
                        Text(tag)
                        Image(systemName: "xmark.circle")
                    }
                }
                .padding()
                .foregroundColor(.white)
                .background(Color.orange)
                .cornerRadius(.infinity)
                .lineLimit(1)
            }
        }
    }
希望这对你有帮助


如果您不想显示滚动指示器,也可以尝试使用
showsIndicators:false

好的,这是我在这个网站上的第一个答案,如果我犯了某种堆栈溢出错误,请耐心等待

我将发布我的解决方案,该解决方案适用于这样一个模型:标签要么存在于selectedTags集合中,要么不存在于selectedTags集合中,所有可用的标签都存在于allTags集合中。在我的解决方案中,这些设置为绑定,因此可以从应用程序的其他位置注入。另外,我的解决方案将标签按字母顺序排列,因为这是最简单的。如果您想让它们以不同的方式排序,可能需要使用不同于两个独立集的模型

这肯定不会适用于每个人的用例,但因为我找不到自己的答案,而你的问题是我唯一能找到提及这个想法的地方,所以我决定尝试构建一些适合我的东西,并与你分享。希望有帮助:

struct TagList: View {

    @Binding var allTags: Set<String>
    @Binding var selectedTags: Set<String>

    private var orderedTags: [String] { allTags.sorted() }

    private func rowCounts(_ geometry: GeometryProxy) -> [Int] { TagList.rowCounts(tags: orderedTags, padding: 26, parentWidth: geometry.size.width) }

    private func tag(rowCounts: [Int], rowIndex: Int, itemIndex: Int) -> String {
        let sumOfPreviousRows = rowCounts.enumerated().reduce(0) { total, next in
            if next.offset < rowIndex {
                return total + next.element
            } else {
                return total
            }
        }
        let orderedTagsIndex = sumOfPreviousRows + itemIndex
        guard orderedTags.count > orderedTagsIndex else { return "[Unknown]" }
        return orderedTags[orderedTagsIndex]
    }

    var body: some View {
        GeometryReader { geometry in
            VStack(alignment: .leading) {
                ForEach(0 ..< self.rowCounts(geometry).count, id: \.self) { rowIndex in
                    HStack {
                        ForEach(0 ..< self.rowCounts(geometry)[rowIndex], id: \.self) { itemIndex in
                            TagButton(title: self.tag(rowCounts: self.rowCounts(geometry), rowIndex: rowIndex, itemIndex: itemIndex), selectedTags: self.$selectedTags)
                        }
                        Spacer()
                    }.padding(.vertical, 4)
                }
                Spacer()
            }
        }
    }
}

struct TagList_Previews: PreviewProvider {
    static var previews: some View {
        TagList(allTags: .constant(["one", "two", "three"]), selectedTags: .constant(["two"]))
    }
}

extension String {

    func widthOfString(usingFont font: UIFont) -> CGFloat {
        let fontAttributes = [NSAttributedString.Key.font: font]
        let size = self.size(withAttributes: fontAttributes)
        return size.width
    }

}

extension TagList {
    static func rowCounts(tags: [String], padding: CGFloat, parentWidth: CGFloat) -> [Int] {
        let tagWidths = tags.map{$0.widthOfString(usingFont: UIFont.preferredFont(forTextStyle: .headline))}

        var currentLineTotal: CGFloat = 0
        var currentRowCount: Int = 0
        var result: [Int] = []

        for tagWidth in tagWidths {
            let effectiveWidth = tagWidth + (2 * padding)
            if currentLineTotal + effectiveWidth <= parentWidth {
                currentLineTotal += effectiveWidth
                currentRowCount += 1
                guard result.count != 0 else { result.append(1); continue }
                result[result.count - 1] = currentRowCount
            } else {
                currentLineTotal = effectiveWidth
                currentRowCount = 1
                result.append(1)
            }
        }

        return result
    }
}

struct TagButton: View {

    let title: String
    @Binding var selectedTags: Set<String>

    private let vPad: CGFloat = 13
    private let hPad: CGFloat = 22
    private let radius: CGFloat = 24

    var body: some View {
        Button(action: {
            if self.selectedTags.contains(self.title) {
                self.selectedTags.remove(self.title)
            } else {
                self.selectedTags.insert(self.title)
            }
        }) {
            if self.selectedTags.contains(self.title) {
                HStack {
                    Text(title)
                        .font(.headline)
                }
                .padding(.vertical, vPad)
                .padding(.horizontal, hPad)
                .foregroundColor(.white)
                .background(Color.blue)
                .cornerRadius(radius)
                .overlay(
                    RoundedRectangle(cornerRadius: radius)
                        .stroke(Color(UIColor.systemBackground), lineWidth: 1)
                )

            } else {
                HStack {
                    Text(title)
                        .font(.headline)
                        .fontWeight(.light)
                }
                .padding(.vertical, vPad)
                .padding(.horizontal, hPad)
                .foregroundColor(.gray)
                .overlay(
                    RoundedRectangle(cornerRadius: radius)
                        .stroke(Color.gray, lineWidth: 1)
                )
            }
        }
    }
}
struct标记列表:视图{
@绑定var-allTags:Set
@绑定变量selectedTags:Set
私有变量orderedTags:[字符串]{allTags.sorted()}
私有函数行计数(geometry:GeometryProxy)->[Int]{TagList.rowCounts(标记:orderedTags,padding:26,parentWidth:geometry.size.width)}
private func标记(行计数:[Int],行索引:Int,项索引:Int)->String{
让SumofPreiousRows=rowCounts.enumerated()。减少(0){total,下一个在
如果next.offset<行索引{
返回总计+下一个元素
}否则{
返回总数
}
}
让orderedTagsIndex=SumofPreiousRows+itemIndex
guard orderedTags.count>orderedTagsIndex else{return“[未知]”}
返回orderedTags[orderedTagsIndex]
}
var body:一些观点{
GeometryReader{中的几何体
VStack(对齐:。前导){
ForEach(0..CGFloat{
让fontAttributes=[NSAttributedString.Key.font:font]
让size=self.size(withAttributes:fontAttributes)
返回大小。宽度
}
}
扩展标记列表{
静态func行数(标记:[String],填充:CGFloat,parentWidth:CGFloat)->[Int]{
设tagWidths=tags.map{$0.widthOfString(使用字体:UIFont.preferredFont(forTextStyle:.headline))}
var currentLineTotal:CGFloat=0
var currentRowCount:Int=0
变量结果:[Int]=[]
用于标记宽度中的标记宽度{
让effectiveWidth=tagWidth+(2*填充)
如果currentLineTotal+effectiveWidth在他的博客中分享了一个不错的解决方案:

解决方案是一个名为
FlexibleView
的自定义视图,它计算必要的
行和
HStack
,以放置给定元素,并在需要时将它们包装成多行

struct\u FlexibleView:View where Data.Element:Hashable{
让可用宽度为:CGFloat
让数据:数据
让间距:CGFloat
let对齐:水平对齐
让内容:(Data.Element)->content
@状态变量elementsSize:[Data.Element:CGSize]=[:]
var body:一些观点{
VStack(对齐:对齐,间距:间距){
ForEach(computeRows(),id:\.self){rowElements in
HStack(间距:间距){
ForEach(rowElements,id:\.self){中的元素
内容(要素)
.fixedSize()
.readSize{size in
elementsSize[元素]=大小
}
}
}
}
}
}
func computeRows()->[[Data.Element]]{
变量行:[[Data.Element]=[[]]
var currentRow=0
var剩余宽度=可用宽度
对于数据中的元素{
let elementSize=elementsSize[元素,默认值:CGSize(宽度:availableWidth,高度:1)]
如果剩余宽度-(elementSize.width+间距)>=0{
行[currentRow]。追加(元素)
}否则{
currentRow=currentRow+1
行。追加([元素])
剩余宽度=可用宽度
}
remainingWidth=remainingWidth-(elementSize.width+间距)
}
返回行
}
}
用法:

FlexibleView(
数据:[
“这里有”“to”“the”“疯狂”“one”“the”“misfits”“the”“the”“叛军”“the”