Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing a tag list in SwiftUI

Tags:

swiftui

I am trying to implement a tag list in SwiftUI but I'm unsure how to get it to wrap the tags to additional lines if the list overflows horizontally. I started with a string array called tags and within SwiftUI I loop through the array and create buttons as follows:

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

If the tags array is small it renders as follows: enter image description here

However, if the array has more values it does this:

enter image description here

The behavior I am looking for is for the last tag (yellow) to wrap to the second line. I realize it is in an HStack, I was hoping I could add a call to lineLimit with a value of greater than one but it doesn't seem to change the behavior. If I change the outer HStack to a VStack, it puts each Button on a separate line, so still not quite the behavior I am trying create. Any guidance would be greatly appreciated.

like image 853
Chris Dellinger Avatar asked Dec 01 '19 20:12

Chris Dellinger


1 Answers

Federico Zanetello shared a nice solution in his blog: Flexible layouts in SwiftUI.

The solution is a custom view called FlexibleView which computes the necessary Row's and HStack's to lay down the given elements and wrap them into multiple rows if needed.

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

Usage:

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

Full code available at https://github.com/zntfdr/FiveStarsCodeSamples.

flexible view example

like image 187
ricardopereira Avatar answered Oct 12 '22 05:10

ricardopereira