0

Im trying to do custom message text field in SwiftUI. When I start typing into the text field, everything is ok. I can hit enter and jump to newline without any issues, but when I reach the end of the line, the text disappears.

Here is the code:

struct MessageInputField: UIViewRepresentable {
var inputMessage: Binding<String>
var currentFieldHeight: Binding<CGFloat>

func makeUIView(context: Context) -> UITextView {
    let textView = UITextView(frame: .zero) 
    textView.isScrollEnabled = false
    textView.font = UIFont.systemFont(ofSize: 15)
    textView.delegate = context.coordinator
    textView.textContainerInset = .zero
    textView.textContainer.lineFragmentPadding = 0
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
    return textView
}

func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = inputMessage.wrappedValue
    updateTextViewHeight(uiView)
}

private func updateTextViewHeight(_ textView: UITextView) {
    let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude))

        // Calculate max height for 5 lines
        let maxHeight = textView.font!.lineHeight * 5

        // Limit textView height and enable scrolling when content exceeds max height
        if size.height >= maxHeight {
            currentFieldHeight.wrappedValue = maxHeight
            textView.isScrollEnabled = true
            if textView.frame.size.height != maxHeight {
                textView.frame.size.height = maxHeight
            }
        } else {
            currentFieldHeight.wrappedValue = size.height
            textView.isScrollEnabled = false
            if textView.frame.size.height != size.height {
                textView.frame.size.height = size.height
            }
        }
}

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

class Coordinator: NSObject, UITextViewDelegate {
    var parent: MessageInputField

    init(_ parent: MessageInputField) {
        self.parent = parent
    }

    func textViewDidChange(_ textView: UITextView) {
        parent.inputMessage.wrappedValue = textView.text
        parent.updateTextViewHeight(textView)
    }
}

}

Here is the issue:

xxx

soundflix
  • 928
  • 9
  • 22
  • check these links https://stackoverflow.com/questions/60437014/frame-height-problem-with-custom-uiviewrepresentable-uitextview-in-swiftui https://stackoverflow.com/questions/58670201/uitextview-adjust-size-based-on-the-content-in-swiftui, https://stackoverflow.com/questions/56471973/how-do-i-create-a-multiline-textfield-in-swiftui/58639072#58639072 – Chandaboy Aug 04 '23 at 12:22
  • I can confirm the issue. If text reaches the end of the line, the view's height is growing, but not the view's textContainer height, until text size is large enough o fill 5 lines and becomes visible. – soundflix Aug 20 '23 at 21:57

1 Answers1

0

A small change will fix the issue:

import SwiftUI

struct MessageInputField: UIViewRepresentable {
    var inputMessage: Binding<String>
    var maximumLines: Int
    var currentFieldHeight: Binding<CGFloat>
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView(frame: .zero)
        textView.isScrollEnabled = true // <- HERE!
        textView.font = UIFont.systemFont(ofSize: 15)
        textView.delegate = context.coordinator
        textView.textContainerInset = .zero
        textView.textContainer.lineFragmentPadding = 0
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = inputMessage.wrappedValue
        updateTextViewHeight(uiView)
    }
    
    private func updateTextViewHeight(_ textView: UITextView) {
        let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude))
        
        // Calculate max height
        let maxHeight = textView.font!.lineHeight * CGFloat(maximumLines)
        
        // Limit textView height and enable scrolling when content exceeds max height
        if size.height >= maxHeight {
            DispatchQueue.main.async {
                currentFieldHeight.wrappedValue = maxHeight // main queue avoids runtime warning: "Modifying state during view update, this will cause undefined behavior."
            }
            if textView.frame.size.height != maxHeight {
                textView.frame.size.height = maxHeight
            }
        } else {
            DispatchQueue.main.async {
                currentFieldHeight.wrappedValue = size.height // main queue avoids runtime warning: "Modifying state during view update, this will cause undefined behavior."
            }
            if textView.frame.size.height != size.height {
                textView.frame.size.height = size.height
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: MessageInputField
        
        init(_ parent: MessageInputField) {
            self.parent = parent
        }
        
        func textViewDidChange(_ textView: UITextView) {
            parent.inputMessage.wrappedValue = textView.text
            parent.updateTextViewHeight(textView)
        }
    }
}

The key is that textView.isScrollEnabled is now always true, so the textContainer can adjust its height. There's no problem with the scroll indicator because it's anyways only visible when the text is larger than the available space.

As a side note, the binding's value should be changed on the main queue to avoid complaints.

Note also that MessageInputField needs to be wrapped in a frame modifier to respect maxHeight.

import SwiftUI

struct ContentView: View {
    @State private var string = ""
    @State private var height: CGFloat = 0
    
    var body: some View {
        VStack {
            MessageInputField(inputMessage: $string, maximumLines: 5, currentFieldHeight: $height)
                .frame(height: height)
                .padding()
        }
    }
}

I added a maximumLines parameter to make this view reusable.

soundflix
  • 928
  • 9
  • 22