Search code examples
swiftswiftuiuitextfielduitextviewuiviewrepresentable

SwiftUI: updateUIView() function in UIViewRepresentable causes app to freeze and CPU to spike


I have a custom Text Field in SwiftUI, that adjusts the number of lines it has according to the amount of content in the text field. In the app, the user can add text fields, and so I'm storing the heights, and content of the text fields in arrays, and appending to the arrays as more text fields are added.

Whenever I remove the code inside the updateUIView(), the app runs smoothly but of course, the text fields don't appear, but whenever I include it as in the code below, the CPU spikes to 99%, and the app freezes (even when there's only one text field).

Does anyone know why this is happening, and any fixes to it?

struct CustomMultilineTF: UIViewRepresentable {
    
    @Binding var text: String
    @Binding var height: CGFloat
    var placeholder: String
    
    func makeCoordinator() -> Coordinator {
        return CustomMultilineTF.Coordinator(par: self)
    }
    
    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.isEditable = true
        view.isScrollEnabled = true
        view.text = placeholder
        view.font = .systemFont(ofSize: 18)
        view.textColor = UIColor.gray
        view.delegate = context.coordinator
        view.backgroundColor = UIColor.gray.withAlphaComponent(0.05)
        return view
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        DispatchQueue.main.async {
            self.height = uiView.contentSize.height
        }
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: CustomMultilineTF
        init(par: CustomMultilineTF) {
            parent = par
        }
        
        func textViewDidBeginEditing(_ textView: UITextView) {
            if self.parent.text == "" {
                textView.text = ""
                textView.textColor = .black
            }
        }
        
        func textViewDidChange(_ textView: UITextView) {
            DispatchQueue.main.async {
                self.parent.height = textView.contentSize.height
                self.parent.text = textView.text
            }
        }
    }
}

Solution

  • In your updateUIView, you're setting a value to self.height, which is a Binding. My guess is that the @Binding is connected to a property (either another @Binding or a @State on your surrounding view). So, whenever you set a new value to that @Binding, that triggers a refresh of the parent view. That, in turn, ends up calling updateUIView again, and you get into an infinite loop.

    How to solve it probably depends on your architecture needs for the program. If you can get away with not having the parent know the height, you can probably solve it just by having the view update its own height.

    You could also try only setting self.height to a new value if it != the old one -- that would probably short circuit the loop. But, you might end up with other side effects.