Search code examples
swiftswiftuiuicollectionviewhstack

SwiftUI - More n badge when inline elements width exceed parent width


Basically, I have a list of tags that I want to show inline but, when the tags are a lot, and their total width exceeds the parent view's width, I want to show a "+n" badge.

Desired behavior

How to achieve the desired behavior starting from this basic code?

struct ParentView: View {
   var body: some View {
      HStack {
         Tag("Apple")
         Tag("Banana")
         Tag("Cherry")
         // more tags...
      }
      .frame(width: 200, alignment: .leading)
   }
}

Any suggestions? Thanks in advance!


Solution

  • 1.

    Find width of each of the badges.

    width of badge = string's width + padding-x
    
    

    2.

    Use this extension to find width of string

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

    I have unsed the default font in iOS since I do not change the font of Text's.

    3.

    Calculate the total width of badges that is less the screen's width.


    
    struct ContentView: View {
        let one = ["One"]
        let two = ["One", "Two"]
        let three = ["One", "Two", "Three"]
        let four = ["One", "Two", "Three", "Four"]
        let five = ["One", "Two", "Three", "Four", "Five"]
        let six = ["One", "Two", "Three", "Four", "Five", "Six"]
        let fruits = ["Apple",  "Banana", "Orange", "Cherry", "Kiwi"]
        let countries = ["France",  "Spain", "Banana Republic", "USA", "Albania", "China", "England"]
        let cities = ["Madrid",  "Oslo", "Washington DC", "Istanbul", "Toronto", "Paris"]
        
        let screenWidth = UIScreen.main.bounds.width // use another number if needed
        
        var body: some View  {
            VStack(alignment: .leading) {
                TagsView(tags: one, screenWidth: screenWidth)
                TagsView(tags: two, screenWidth: screenWidth)
                TagsView(tags: three, screenWidth: screenWidth)
                TagsView(tags: four, screenWidth: screenWidth)
                TagsView(tags: five, screenWidth: screenWidth)
                TagsView(tags: six, screenWidth: screenWidth)
                TagsView(tags: fruits, screenWidth: screenWidth)
                TagsView(tags: countries, screenWidth: screenWidth)
                TagsView(tags: cities, screenWidth: screenWidth)
            }
            
        }
    }
    
    
    struct TagsView: View {
        let spacing: CGFloat = 8
        let padding: CGFloat = 16
        let tags: [String]
        var width: CGFloat = .zero
        var limit = 0
        
        internal init(tags: [String], screenWidth: CGFloat) {
            self.tags = tags
            self.limit = tags.count
            
            for (index, tag) in tags.enumerated()  {
                self.width += tag.widthOfString() + (2 * padding) +  spacing
                let remaining = "\(tags.count - index) +".widthOfString() + (2 * padding)
                if width + remaining >= screenWidth {
                    self.limit = index
                    break
                }
            }
        }
    
        var body: some View {
            HStack(spacing: spacing) {
                ForEach(0..<limit, id: \.self) { index in
                    Tag(padding: padding, text: tags[index])
                }
                
                if limit > 0 && tags.count != limit {
                    Tag(padding: padding, text: "\(tags.count - limit) +")
                }
            }
            .padding(.bottom)
        }
    }
    
    struct Tag:View {
        let padding: CGFloat
        let text: String
        let height: CGFloat = 20
        let pVertical:CGFloat = 8
        var body: some View {
            Text(text)
                .fixedSize()
                .padding(.horizontal, padding)
                .padding(.vertical, pVertical)
                .background(Color.orange.opacity(0.2))
                .foregroundColor(Color.orange)
                .cornerRadius( (height + pVertical * 2) / 2)
                .frame(height: height)
        }
    }
    
    
    extension String {
       func widthOfString(usingFont font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)) -> CGFloat {
            let fontAttributes = [NSAttributedString.Key.font: font]
            let size = self.size(withAttributes: fontAttributes)
            return size.width
        }
    }
    
    
    

    In Dark appearance

    enter image description here