Search code examples
iosswiftuilazyvstack

SwiftUI 4: ViewThatFits doesn't choose the right view if it's inside a LazyVStack


I followed ViewThatFits tutorial mentioned here, to show and hide a text according to its length and the available space as follows:

struct ExpandableText: View {
    let text: String
    let initialLineLimit: Int
    @State private var isExpanded = false
    @State private var showButton = false
    
    var button: Button<Text> {
        if isExpanded {
            return Button("Show less") {
                isExpanded = false
            }
        } else {
            return Button("Show more") {
                isExpanded = true
            }
        }
    }
    
    var body: some View {
        Text(text)
            .lineLimit(isExpanded ? nil : initialLineLimit)
            .background {
                ViewThatFits(in: .vertical) {
                    Text(text)
                        .hidden()
                    Color.clear
                        .onAppear {
                            showButton = true
                        }
                }
            }
        if showButton {
            button
        }
    }
}

This works fine if I use a VStack with ScrollView like this (copy-paste the code to try it out):

struct ContentView: View {
    
    var items = ["this is test text",
                 
                 "this is test text this is test text",
                 
                 " this is test text this is test text this is test text",
                 
                 "this is test text this is test text this is test text this is test text",
                 
                 "this is test text this is test text this is test text this is test text this is test text",
                 
                 "this is test text this is test text this is test text this is test text this is test text this is test text",
                 
                 "this is a small text",
                 
                 "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text",
                 
                 "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text",
                 
                 "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text",
                 
                 "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text",
    ]
    
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 0) {
                ForEach(items, id: \.self) { item in
                   ExpandableText(text: item, initialLineLimit: 2)
                        .padding()
                }
            }
        }
       
    }
}

However, when I use a LazyVStack instead of a VStack the "show more / less" button appears incorrectly if I scroll up and down, I couldn't find a clue why's this happening with the LazyVStack.


Solution

  • I'm assuming this has something to do with view re-use in a LazyVStack.

    It seems that in the case where the hidden Text should be displayed, the Color.clear is displayed first, and then the hidden Text. Where the hidden Text is too tall, only the Color.clear is displayed, as expected.

    You can fix the issue by setting showButton = false when the hidden Text is "displayed"

    var body: some View {
        Text(text)
            .lineLimit(isExpanded ? nil : initialLineLimit)
            .background {
                ViewThatFits(in: .vertical) {
                    Text(text)
                        .hidden()
                        .onAppear {
                            showButton = false // This fixes the problem
                        }
                    Color.clear
                        .onAppear {
                            showButton = true
                        }
                }
            }
        if showButton {
            button
        }
    }