在SwiftUI中实现标签列表

时间:2019-12-01 20:56:52

标签: swiftui

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

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

如果tags数组较小,则呈现如下: enter image description here

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

enter image description here

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

3 个答案:

答案 0 :(得分:3)

Federico Zanetello在他的博客Flexible layouts in SwiftUI中分享了一个不错的解决方案。

解决方案是一个名为FlexibleView的自定义视图,该视图计算必要的RowHStack来放置给定的元素,并在需要时将它们包装为多行。 / p>

struct _FlexibleView<Data: Collection, Content: View>: View where Data.Element: Hashable {
  let availableWidth: CGFloat
  let data: Data
  let spacing: CGFloat
  let alignment: HorizontalAlignment
  let content: (Data.Element) -> Content
  @State var elementsSize: [Data.Element: CGSize] = [:]

  var body : some View {
    VStack(alignment: alignment, spacing: spacing) {
      ForEach(computeRows(), id: \.self) { rowElements in
        HStack(spacing: spacing) {
          ForEach(rowElements, id: \.self) { element in
            content(element)
              .fixedSize()
              .readSize { size in
                elementsSize[element] = size
              }
          }
        }
      }
    }
  }

  func computeRows() -> [[Data.Element]] {
    var rows: [[Data.Element]] = [[]]
    var currentRow = 0
    var remainingWidth = availableWidth

    for element in data {
      let elementSize = elementsSize[element, default: CGSize(width: availableWidth, height: 1)]

      if remainingWidth - (elementSize.width + spacing) >= 0 {
        rows[currentRow].append(element)
      } else {
        currentRow = currentRow + 1
        rows.append([element])
        remainingWidth = availableWidth
      }

      remainingWidth = remainingWidth - (elementSize.width + spacing)
    }

    return rows
  }
}

用法:

FlexibleView(
    data: [
    "Here’s", "to", "the", "crazy", "ones", "the", "misfits", "the", "rebels", "the", "troublemakers", "the", "round", "pegs", "in", "the", "square", "holes", "the", "ones", "who", "see", "things", "differently", "they’re", "not", "fond", "of", "rules"
  ],
    spacing: 15,
    alignment: .leading
  ) { item in
    Text(verbatim: item)
      .padding(8)
      .background(
        RoundedRectangle(cornerRadius: 8)
          .fill(Color.gray.opacity(0.2))
       )
  }
  .padding(.horizontal, model.padding)
}

可在https://github.com/zntfdr/FiveStarsCodeSamples上找到完整代码。

flexible view example

答案 1 :(得分:1)

好吧,这是我在此站点上的第一个答案,所以如果我犯了某种堆栈溢出错误,请多多包涵。

我将发布我的解决方案,该解决方案适用于模型,其中标签是否存在于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)
                )
            }
        }
    }
}

答案 2 :(得分:0)

您可以尝试在水平轴上使用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