0

I have a reusable struct called ExpandableText that lets me shorten the text I give it (so if I limit it to two lines, it adds a read more and expands) but when the text variable changes on a different view (so the text is edited) it recognizes a change but doesn't update the variable. When I put an onChange in the ExpandableText view and print the newValue and the text value, newValue updates to the change from the other view but the text value remains the old value. I have tried to make text a state variable and onChange assign it to the newValue, but that causes onChange to not even be called. I've also messed with but can't get @Binding var text: String to work with the shrinkText initial value that is being set in the init. From what I've read that seems to be the best option but I keep getting the error that I can't assign Binding<String> to string in that _shrinkText init.

import SwiftUI

struct ExpandableText: View {
    @State private var expanded: Bool = false
    @State private var truncated: Bool = false
    @State private var textChanged: Bool = false
    @State private var shrinkText: String
    private var text: String

    let font: UIFont
    let lineLimit: Int
    
    private var moreLessText: String {
        if !truncated {
            return ""
        } else {
            return self.expanded ? "Read Less" : "Read More"
        }
    }
    
    init(_ text: String, lineLimit: Int, font: UIFont = UIFont(name: "Manrope-Regular", size: 16)!) {
        self.text = text
        self.lineLimit = lineLimit
        _shrinkText =  State(wrappedValue: text)
        self.font = font
    }
    
    var body: some View {
        
        VStack(alignment: .leading) {
            Text(self.expanded ? text : shrinkText)
                .allowsTightening(true)
                .fixedSize(horizontal: false, vertical: true)
                .font(Font.custom("Manrope-Regular", size: 16))
                .dynamicTypeSize(...DynamicTypeSize.large)

            if truncated {
                Button {
                    withAnimation(.easeInOut) {
                        expanded.toggle()
                    }
                } label: {
                    Text(moreLessText)
                        .font(Font.custom("Manrope-SemiBold", size: 16))
                        .dynamicTypeSize(...DynamicTypeSize.large)
                }
            }
        }
        .lineLimit(expanded ? nil : lineLimit)
        .onChange(of: text) { newValue in
            print("text value: \(text)")
            print("new value: \(newValue)")
        }
        
        .background(
            Text(text).lineLimit(lineLimit)
                .background(GeometryReader { visibleTextGeometry in
                    Color.purple.onAppear() {
                        let size = CGSize(width: visibleTextGeometry.size.width, height: .greatestFiniteMagnitude)
                        let attributes:[NSAttributedString.Key:Any] = [NSAttributedString.Key.font: font]
                        var low  = 0
                        var heigh = shrinkText.count
                        var mid = heigh
                        while ((heigh - low) > 1) {
                            let attributedText = NSAttributedString(string: shrinkText + moreLessText, attributes: attributes)
                            let boundingRect = attributedText.boundingRect(with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
                            if boundingRect.size.height > visibleTextGeometry.size.height {
                                truncated = true
                                heigh = mid
                                mid = (heigh + low)/2
                            } else {
                                if mid == text.count {
                                    break
                                } else {
                                    low = mid
                                    mid = (low + heigh)/2
                                }
                            }
                            shrinkText = String("\(text.prefix(mid))...")
                        }
                    }
                })
                .hidden()
        )
        .font(Font.custom("Manrope-Regular", size: 16)).dynamicTypeSize(...DynamicTypeSize.large)
    }
    
}

UPDATED INIT:

struct ExpandableText: View {
    @State private var expanded: Bool = false
    @State private var truncated: Bool = false
    @State private var textChanged: Bool = false
    @State private var shrinkText: String
    @Binding var text: String

    let font: UIFont
    let lineLimit: Int
    
    private var moreLessText: String {
        if !truncated {
            return ""
        } else {
            return self.expanded ? "Read Less" : "Read More"
        }
    }
    
    init(_ text: Binding<String>, lineLimit: Int, font: UIFont = UIFont(name: "Manrope-Regular", size: 16)!) {
        self._text = text
        self.lineLimit = lineLimit
        self._shrinkText = text // <-- error: Cannot assign value of type 'Binding<String>' to type 'State<String>'
        self.font = font
    }
...
}
amelia
  • 71
  • 2
  • 7

1 Answers1

0

I was not able to modify the posted code to make it work, but here's a possible solutions based on SwiftUI: Check if text fit in parent view, otherwise remove that:

struct ExpandableText: View {
    @Binding var text : String
    let lineLimit: Int?
    
    @State private var intrinsicSize: CGSize = .zero
    @State private var truncatedSize: CGSize = .zero
    @State private var hasExpand : Bool = true
    @State private var expanded : Bool = false

    var body: some View {
        VStack{
            
            Text(text)
                .lineLimit(expanded ? nil : lineLimit)//hasExpand && !expanded ? lineLimit : nil)
                .readSize { size in
                    truncatedSize = size
                    hasExpand = truncatedSize != intrinsicSize
                }
                .background(
                    Text(text)
                        .fixedSize(horizontal: false, vertical: true)
                        .hidden()
                        .readSize { size in
                            intrinsicSize = size
                            hasExpand = truncatedSize != intrinsicSize
                        }
                )
        }
    }
}

extension View {
  func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
    background(
      GeometryReader { geometryProxy in
        Color.clear
          .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
      }
    )
    .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
  }
  
  @ViewBuilder func isShow(_ show: Bool) -> some View {
    if show {
      self
    } else {
      self.hidden()
    }
  }
}

struct SizePreferenceKey: PreferenceKey {
  static var defaultValue: CGSize = .zero
  static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

let shortText = "Lorem ipsum dolor sit amet"
let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Purus ut faucibus pulvinar elementum integer enim neque. Vel turpis nunc eget lorem dolor sed viverra ipsum nunc. Sit amet mattis vulputate enim nulla aliquet porttitor lacus. Consequat semper viverra nam libero justo laoreet sit amet cursus. Nunc eget lorem dolor sed viverra ipsum. Porta nibh venenatis cras sed felis eget velit aliquet sagittis. Eget est lorem ipsum dolor sit amet consectetur adipiscing. Purus sit amet luctus venenatis lectus magna fringilla. Aliquet enim tortor at auctor urna nunc. Velit sed ullamcorper morbi tincidunt ornare massa. Risus viverra adipiscing at in tellus integer feugiat scelerisque varius. Vulputate odio ut enim blandit volutpat maecenas volutpat blandit. Arcu non sodales neque sodales ut etiam sit amet. Porta nibh venenatis cras sed felis eget. Quis risus sed vulputate odio ut enim blandit volutpat. Molestie a iaculis at erat pellentesque adipiscing commodo. Tortor id aliquet lectus proin nibh nisl condimentum id."

struct ContentView: View {
    @State private var long = false
    @State private var text = shortText
    
    var body: some View {
        VStack {
            Button("Change text"){ long.toggle(); text = long ? longText : shortText }
            ExpandableText(text: $text, lineLimit: 2)
        }.padding()
    }
}
Math Rules
  • 21
  • 7
  • Thank you for your help! It all works except I receive an error in my ExpandableText init on the _shrinkText line. I added the error I get to the updated code I added – amelia Aug 04 '23 at 14:26
  • I made those changes but still got the error so I thought maybe you were saying to change shrinkText to also a binding and that did get rid of the error, but the expanding of the text no longer works (I think because it's bound to text instead of just text being it's initial value?) – amelia Aug 04 '23 at 19:39