swiftuiswiftui-textswiftui-sheet

SwiftUI Text with attributed string will not respect lineLimit when inside a sheet


Running into an issue when Text won't expand with attributed string or when inside a sheet. In other words, it will expand when it's using regular string, or if it's not inside a sheet.

Anyone know a good walk-around or an explanation?

import SwiftUI

struct ExpandableAttributedText: View {
    let string: String
    @State private var isExpanded = false

    let maxLines: Int

    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(attributedString)
            // update the it to below and it will start expanding
//            Text(string)
                .lineLimit(isExpanded ? nil : maxLines)
                .textSelection(.enabled)

            
            Button(action: {
                isExpanded.toggle()
            }) {
                Text(isExpanded ? "Show Less" : "Show More")
                    .font(.caption)
                    .foregroundColor(.primary)
            }
        }
    }
    
    private var attributedString: AttributedString {
        var attributed = htmlToAttributedString(string) ?? AttributedString(string)
        attributed.font = .system(size: 16)
        attributed.foregroundColor = colorScheme == .dark ? .white : .black
        return attributed
    }
    
    func htmlToAttributedString(_ htmlString: String) -> AttributedString? {
        guard let data = htmlString.data(using: .utf8) else {
            print("Failed to convert HTML string to data")
            return nil
        }
        
        do {
            let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
                .documentType: NSAttributedString.DocumentType.html,
                .characterEncoding: String.Encoding.utf8.rawValue
            ]
            
            let nsAttributedString = try NSAttributedString(data: data, options: options, documentAttributes: nil)
            
            // Convert NSAttributedString to AttributedString
            return AttributedString(nsAttributedString)
        } catch {
            print("Error converting HTML to AttributedString: \(error)")
            return nil
        }
    }
}

struct ContentView: View {
    @State private var showModel: Bool = false
    var text = """
<a href="http://n.pr/PM-digital"><em>in Apple Podcasts</em></a><em> or at </em><a href="https://n.pr/3HlREPz"><em>plus.npr.org/planetmoney</em></a><em>.</em><br/><br/>Learn more about sponsor message choices: <a href="https://podcastchoices.com/adchoices">podcastchoices.com/adchoices</a><br/><br/><a href="https://www.npr.org/about-npr/179878450/privacy-policy">NPR Privacy Policy</a>
"""
    var body: some View {
        VStack{
            Text("The following will expand")
            ExpandableAttributedText(string: text, maxLines: 4)
                .padding()
            Button(action: {
                showModel.toggle()
            }) {
                Text("Open")
            }
        }
        .sheet(isPresented: $showModel, content: {
            ScrollView{
                // This won't expand with attributedString, as it's in a sheet.
                ExpandableAttributedText(string: text, maxLines: 4)
                    .padding()
            }
        })
    }
    
}

#Preview {
    ContentView()
}


Solution

  • To fix your issue, try this approach, letting the ExpandableAttributedText View manage the state of a var, @State private var attributedText ... instead of using a computed property, as shown in the code.

    struct ExpandableAttributedText: View {
        let string: String
        let maxLines: Int
    
        @State private var isExpanded = false
        @State private var attributedText = AttributedString("") // <--- here
    
        @Environment(\.colorScheme) private var colorScheme
        
        var body: some View {
            VStack(alignment: .leading, spacing: 8) {
                Text(attributedText)  // <--- here
                    .lineLimit(isExpanded ? nil : maxLines)
                    .textSelection(.enabled)
                
                Button(action: {
                    isExpanded.toggle()
                }) {
                    Text(isExpanded ? "Show Less" : "Show More")
                        .font(.caption)
                        .foregroundColor(.blue)
                }
            }
           .onAppear {  // <--- here
               attributedText = htmlToAttributedString(string) ?? AttributedString(string)
               attributedText.font = .system(size: 16)
               attributedText.foregroundColor = colorScheme == .dark ? .white : .black
            }
        }
    
        func htmlToAttributedString(_ htmlString: String) -> AttributedString? {
            guard let data = htmlString.data(using: .utf8) else {
                print("Failed to convert HTML string to data")
                return nil
            }
            
            do {
                let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
                    .documentType: NSAttributedString.DocumentType.html,
                    .characterEncoding: String.Encoding.utf8.rawValue
                ]
                
                let nsAttributedString = try NSAttributedString(data: data, options: options, documentAttributes: nil)
                
                // Convert NSAttributedString to AttributedString
                return AttributedString(nsAttributedString)
            } catch {
                print("Error converting HTML to AttributedString: \(error)")
                return nil
            }
        }
    }
    
    struct ContentView: View {
        @State private var showModel: Bool = false
        
        let text = """
    <a href="http://n.pr/PM-digital"><em>in Apple Podcasts</em></a><em> or at </em><a href="https://n.pr/3HlREPz"><em>plus.npr.org/planetmoney</em></a><em>.</em><br/><br/>Learn more about sponsor message choices: <a href="https://podcastchoices.com/adchoices">podcastchoices.com/adchoices</a><br/><br/><a href="https://www.npr.org/about-npr/179878450/privacy-policy">NPR Privacy Policy</a>
    """
        
        var body: some View {
            VStack{
                Text("The following will expand")
                ExpandableAttributedText(string: text, maxLines: 4)
                    .padding()
                Button(action: {
                    showModel.toggle()
                }) {
                    Text("Open")
                }
            }
            .sheet(isPresented: $showModel) { 
                ScrollView{
                    ExpandableAttributedText(string: text, maxLines: 4)
                        .padding()
                }
            }
        }
        
    }