6

Multiline text input is currently not natively supported in SwiftUI (hopefully this feature is added soon!) so I've been trying to use the combine framework to implement a UITextView from UIKit which does support multiline input, however i've been having mixed results.

This is the code i've created to make the Text view:

struct MultilineTextView: UIViewRepresentable {

    @Binding var text: String


    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.isScrollEnabled = true
        view.isEditable = true
        view.isUserInteractionEnabled = true
        view.backgroundColor = UIColor.white
        view.textColor = UIColor.black
        view.font = UIFont.systemFont(ofSize: 17)
        view.delegate = context.coordinator
        return view
    }

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

    func frame(numLines: CGFloat) -> some View {
        let height = UIFont.systemFont(ofSize: 17).lineHeight * numLines
        return self.frame(height: height)
    }

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

    class Coordinator: NSObject, UITextViewDelegate {
        var parent: MultilineTextView

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

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

I've then implemented it in a swiftUI view like:

MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)//.frame(numLines: 4)

And bound it to a state variable:

@State var currentItem:Item

It sort of works. However, the state var currentItem:Item contains an array of strings which I'm then cycling through using buttons which update the string array based on what has been inputted into MultilineTextView. This is where i'm encountering a problem where the MultilineTextView seems to bind to only the first string item in the array, and then it won't change. When I use swiftUI's native TextField view this functionality works fine and I can cycle through the string array and update it by inputting text into the TextField.

I think I must be missing something in the MultilineTextView struct to allow this functionality. Any pointers are gratefully received.

Update: Added model structs

struct Item: Identifiable, Codable {
    let id = UUID()
    var completed = false
    var pairArray:[TextPair]
}

struct TextPair: Identifiable, Codable {
    let id = UUID()
    var textOne:String
    var textTwo:String
}

Edit: So I've done some more digging and I've found what I think is the problem. When the textViewDidChange of the UITextView is triggered, it does send the updated text which I can see in the console. The strange thing is that the updateUIView function then also gets triggered and it updates the UITextView's text with what was in the binding var before the update was sent via textViewDidChange. The result is that the UITextview just refuses to change when you type into it. The strange thing is that it works for the first String in the array, but when the item is changed it won't work anymore.

テッド
  • 776
  • 9
  • 21
  • You might find helpful my approach provided in [How do I create a multiline TextField in SwiftUI?](https://stackoverflow.com/a/58639072/12299030) – Asperi Jan 21 '20 at 12:17
  • @Asperi, thanks offering a potential solution but unfortunately that code still exhibits the same behaviour where it will only bind to the first instance of text in the array and then won't allow any input when the state variable it is bound to is changed to another item of text in the array :/ . Annoying as it works fine with the native TextField. – テッド Jan 21 '20 at 12:40
  • Interesting... can you show what the `Item` is? – Asperi Jan 21 '20 at 12:47
  • Sure, it looks like this: ``` struct Item: Identifiable, Codable { let id = UUID() var completed = false var pairArray:[TextPair] } struct TextPair: Identifiable, Codable { let id = UUID() var textOne:String var textTwo:String } ``` Sorry if the post formatting is wrong, I don't know how to make code fences without the comment editor. – テッド Jan 21 '20 at 12:54
  • @Asperi thanks for the update on the post. To clarify, the items are stored in an environment object called ItemStore which contains all of the Item objects. – テッド Jan 21 '20 at 13:03
  • OP, did the comment answer your question? If so, please accept it. – Bart van Kuik Jan 28 '20 at 19:55
  • This issue is almost identical to: https://stackoverflow.com/q/60437014/9768031 Please see my solution there. – Xaxxus May 15 '21 at 14:24

2 Answers2

2

It appears that SwiftUI creates two variants of UIViewRepresentable, for each binding, but does not switch them when state, ie title is switched... probably due to defect, worth submitting to Apple.

I've found worked workaround (tested with Xcode 11.2 / iOS 13.2), use instead explicitly different views as below

if title {
    MultilineTextView(text: $currentItem.titleEnglish)
} else {
    MultilineTextView(text: $currentItem.pairArray[currentPair].textOne)
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks for your solution. It works for the title but not for the pairArray. Stupid of me to not to have said that 'currentPair' was also a state variable, and that is where my trouble was coming from. See my answer below. – テッド Jan 30 '20 at 13:27
0

So I figured out the problem in the end, the reason why it wasn't updating was because I was passing in a string which was located with TWO state variables. You can see that in the following line, currentItem is one state variable, but currentPair is another state variable that provides an index number to locate a string. The latter was not being updated because it wasn't also being passed into the multiline text view via a binding.

MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)

I thought initially that passing in one would be fine and the parent view would handle the other one but this turns out not to be the case. I solved my problem by making two binding variables so I could locate the string that I wanted in a dynamic way. Sounds stupid now but I couldn't see it at the time.

テッド
  • 776
  • 9
  • 21