Search code examples
swiftlistswiftuitextline

How to show Text more, less for list view cell in SwiftUi


code: with this code i am showing whole survey.description but i want initially description to be 2 lines but if i click on more it has to show remaining lines and need less if click on less it again need to go to 2 lines.

if the description is not more then 2 lines i dont want even "More" as well.. how to do this

please guide me

struct SurveyListView: View {
@Environment(\.dismiss) var dismiss
@StateObject private var viewModel = SurveyViewModel()

@State private var selectedDataID: String?

var body: some View {
    ZStack {
        
        VStack() {
            
            ScrollView{
                ForEach(0..<viewModel.allSurvey.count, id: \.self) { ind in
                    
                    Button {
                        selectedDataID = viewModel.allSurvey[ind].id
                        gotoQuestions = true
                        
                    } label: {
                        surveyListCell(survey: viewModel.allSurvey[ind])
                            .foregroundStyle(.black)
                    }
                   
                }
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                
                .navigationDestination(isPresented: $gotoQuestions) {
                    SurveyQuestionsView(id: selectedDataID ?? "")
                        .toolbar(.hidden, for: .navigationBar)
                }
            }
            Spacer()
        }
    }
    .onAppear{
        viewModel.fetchSurveyList { status in
        }
    }
}

@ViewBuilder func surveyListCell(survey: AllSurvey) -> some View {
    
    VStack(alignment: .leading){
        Text(survey.title ?? "N/A")
            .font(.calibriRegular(with: 18))
            .padding(.horizontal)
            .padding(.bottom, 3)
      
        Text(survey.description ?? "N/A")
            .font(.calibriRegular(with: 16))
            .foregroundStyle(.gray)
        
            .padding(.horizontal)
            .padding(.top, 1)
        HStack{
            Spacer()
            Text("More")
                .font(.calibriRegular(with: 15))
                .padding(.horizontal)
                .foregroundStyle(.green)
                .onTapGesture {
                    
                }
        }
        .padding(.bottom, 5)
        HStack(spacing: 0){
            Text("Published On:")
                .font(.calibriRegular(with: 14))
                .padding(.horizontal)
                .foregroundStyle(.gray)
            Text(survey.publishedOn ?? "N/A")
                .font(.calibriRegular(with: 14))
                .foregroundStyle(.gray)
                .padding(.horizontal)
            Spacer()
        }
        
    }
    .frame(maxWidth: .infinity, alignment: .leading)
}
}

o/p: here if no description also More has to hide and initially need to show 2 lines and click on more then need to show remaining.

enter image description here


Solution

  • The description can be expanded and collapsed by applying a lineLimit to the Text.

    • The initial line limit can be 2.
    • When expanded, the line limit should change to nil, which means, unlimited.

    Whether or not the button to expand/collapse the text is shown should depend on whether the expanded text takes more space (that is, requires more height) than the 2-line text. A GeometryReader can be used to determine this.

    Before adding this functionality, I would suggest a little re-factoring:

    • Your struct AllSurvey should implement Identifiable, if it doesn't already:
    struct AllSurvey: Identifiable {
        let id: String
        // ...
    }
    
    • The ForEach can then iterate over the array of identifiable items, which avoids using an index into the array:
    // ForEach(0..<viewModel.allSurvey.count, id: \.self) { ind in
    ForEach(viewModel.allSurvey) { survey in
        // ...
    }
    
    • Change the function surveyListCell into a View. The body of the function simply becomes the body of the view:
    struct SurveyView: View {
        let survey: AllSurvey
    
        var body: some View {
            VStack(alignment: .leading){
                Text(survey.title ?? "N/A")
                    // ...
    
                // etc.
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
    
    • Use the new view inside the ForEach:
    Button {
        // selectedDataID = viewModel.allSurvey[ind].id
        selectedDataID = survey.id
        gotoQuestions = true
    } label: {
        // surveyListCell(survey: viewModel.allSurvey[ind])
        SurveyView(survey: survey)
            .foregroundStyle(.black)
    }
    

    Now for the new functionality:

    • Add a State variable to SurveyView to hold the maximum number of lines to show:
    @State private var maxLines: Int? = 2
    
    • Apply this value as the lineLimit to the Text showing the description:
    Text(survey.description ?? "N/A")
        .multilineTextAlignment(.leading)
        .lineLimit(maxLines)
        // ...
    
    • The variable should be switched between the values of 2 and nil when the button to expand/collapse the text is tapped. The same button can be used for both actions, the label can change depending on the value of the state variable:
    Text(maxLines == 2 ? "More" : "Less")
        .font(.calibriRegular(with: 15))
        .padding(.horizontal)
        .foregroundStyle(.green)
        .onTapGesture {
            withAnimation { maxLines = maxLines == 2 ? nil : 2 }
        }
    
    • A boolean state variable can indicate whether more lines are available to be shown, or not.
    @State private var hasMoreDescription = false
    
    • This boolean is used to control the visibility of the button:
    HStack{
        Spacer()
        if hasMoreDescription {
            Text(maxLines == 2 ? "More" : "Less")
                // ...
        }
    }
    
    • Show the full description in the background of the Text and use a GeometryReader to measure its height. If the full height is greater than the 2-line height then this means more text is available. This information is stored in the boolean state variable in .onAppear. By applying the modifier .hidden(), this background content stays hidden.
    .background {
        GeometryReader { outer in
            Text(survey.description ?? "")
                .fixedSize(horizontal: false, vertical: true)
                .overlay {
                    GeometryReader { proxy in
                        Color.clear
                            .onAppear {
                                hasMoreDescription = proxy.size.height > outer.size.height
                            }
                    }
                }
                .hidden()
        }
    }
    

    Here is the fully updated view SurveyView:

    struct SurveyView: View {
        let survey: AllSurvey
        @State private var maxLines: Int? = 2
        @State private var hasMoreDescription = false
    
        var body: some View {
            VStack(alignment: .leading){
                Text(survey.title ?? "N/A")
                    // ...
    
                Text(survey.description ?? "N/A")
                    .multilineTextAlignment(.leading)
                    .lineLimit(maxLines)
                    .background {
                        GeometryReader { outer in
                            Text(survey.description ?? "")
                                .fixedSize(horizontal: false, vertical: true)
                                .overlay {
                                    GeometryReader { proxy in
                                        Color.clear
                                            .onAppear {
                                                hasMoreDescription = proxy.size.height > outer.size.height
                                            }
                                    }
                                }
                                .hidden()
                        }
                    }
                    .font(.calibriRegular(with: 16))
                    .foregroundStyle(.gray)
                    .padding(.horizontal)
                    .padding(.top, 1)
                HStack{
                    Spacer()
                    if hasMoreDescription {
                        Text(maxLines == 2 ? "More" : "Less")
                            .font(.calibriRegular(with: 15))
                            .padding(.horizontal)
                            .foregroundStyle(.green)
                            .onTapGesture {
                                withAnimation { maxLines = maxLines == 2 ? nil : 2 }
                            }
                    }
                }
                .padding(.bottom, 5)
    
                HStack(spacing: 0){
                    Text("Published On:")
                        // ...
                    Text(survey.publishedOn ?? "N/A")
                        // ...
                    Spacer()
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
    

    Animation