Volodymyr Bobyr
Volodymyr Bobyr

Reputation: 424

How do I know whether I reached the Text().lineLimit() limit in SwiftUI

I want to have a Text view that is limited to 3 lines.

But, if the text could continue for more than 3 lines, I want to have a button that removes the line limit; this button is hidden otherwise.

In order to do this, I would need some way to know whether the limit has been reached.

Is there any way to do this without doing some hacky stuff with GeometryReader?

Upvotes: 2

Views: 1582

Answers (2)

Ashley Mills
Ashley Mills

Reputation: 53121

In iOS 16, you can achieve this using ViewThatFits, which will pick the largest child view that fits the available space:

struct LineLimitView: View {
    
    let text: String
    let limit: Int
    
    @State private var isExpanded = false
    @State private var canBeExpanded = false

    var body: some View {
        VStack {
            Text(text)
                .lineLimit(isExpanded ? nil : limit)
            
                // Background matches the size of the Text
                .background {
                    
                    // Pick the first child view that fits vertically
                    ViewThatFits(in: .vertical) {
                        
                        // This Text has no line limit, so if it's is larger than the
                        // "outer" Text, ViewThatFits will pick the next view
                        Text(text)
                            .hidden()
                        // Color expands to fill the background, so will always be picked
                        // if the text above is too large
                        Color.clear
                            .onAppear {
                                canBeExpanded = true
                            }
                    }
                }
            if canBeExpanded {
                Button(isExpanded ? "Less" : "More…") {
                    withAnimation {
                        isExpanded.toggle()
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    
    let short = "Hello, World! This is a short example text that fits in three lines."

    let long = "Hello, World! This is a longer example text that will not fit in three lines. Hello, World! This is a longer example text. Hello, World! This is a longer example text. Hello, World! This is a longer example text."

    var body: some View {
        VStack(alignment: .leading) {
            
            GroupBox {
                LineLimitView(text: short, limit: 3)
            }

            GroupBox {
                LineLimitView(text: long, limit: 3)
            }
            
            HStack(alignment: .top) {
                LineLimitView(text: short, limit: 4)
                LineLimitView(text: long, limit: 4)
            }
        }
        .padding()
    }
}

I pinched @ChrisR's ContentView for this 🦹‍♂️

enter image description here

Upvotes: 7

ChrisR
ChrisR

Reputation: 12125

Having said that GeometryReader is just fine to use, here is a solution to your question that uses it:

struct ContentView: View {
    
    let short = "Hello, World! This is a short example text that fits in three lines."

    let long = "Hello, World! This is a longer example text that will not fit in three lines. Hello, World! This is a longer example text. Hello, World! This is a longer example text. Hello, World! This is a longer example text."

    var body: some View {
        VStack(alignment: .leading) {
            
            GroupBox {
                LineLimitView(text: short, limit: 3)
            }

            GroupBox {
                LineLimitView(text: long, limit: 3)
            }
            
            HStack(alignment: .top) {
                LineLimitView(text: short, limit: 4)
                LineLimitView(text: long, limit: 4)
            }
        }
        .padding()
    }
}


struct LineLimitView: View {
    
    let text: String
    let limit: Int
    
    @State private var height = CGFloat.zero
    @State private var showAll = false

    var body: some View {
        
        VStack(alignment: .leading) {
            
            if showAll {
                Text(text)
            } else if height < CGFloat(limit) * 21 {
                Text(text)
                    .lineLimit(limit)
           } else {
                Text(text)
                    .lineLimit(limit)
                Button("Show all") { showAll = true }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
         
         // calculates the height of the text
        .background( GeometryReader { geo in 
            Color.clear.onAppear { height = geo.size.height }
        } )
    }
}

Upvotes: -1

Related Questions