1

I bridge UIKit with SwiftUI as follows:

struct UITextFieldViewRepresentable: UIViewRepresentable {

    @Binding var language: String
    @Binding var text: String

    init(language: Binding<String>, text: Binding<String>) {
        self._language = language
        self._text = text
    }

    func makeUIView(context: Context) -> UITextField {

        let textField = getTextField()
        textField.delegate = context.coordinator

        return textField
    }

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

        // Change the language in the wordTextField here.
        
        if let wordTextField = uiView as? WordTextField {
            wordTextField.language = self.language
        }
    }


    private func getTextField() -> UITextField {

        let textField = WordTextField(frame: .zero)
        textField.language = self.language
        textField.textAlignment = .center
        textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
        
        return textField
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }

    class Coordinator: NSObject, UITextFieldDelegate {

        @Binding var text: String

        init(text: Binding<String>) {
            self._text = text
        }
        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }
    }

    class WordTextField: UITextField {

        var language: String? {
            didSet {
                if self.isFirstResponder{
                    self.resignFirstResponder()
                    self.becomeFirstResponder()
                }
            }
        }

        override var textInputMode: UITextInputMode? {
            if let language = self.language {
                print("text input mode: \(language)")
                for inputMode in UITextInputMode.activeInputModes {
                    if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
                        return inputMode
                    }
                }
            }
            return super.textInputMode
        }
    }
}

And call it as follows:

UITextFieldViewRepresentable(language: $keyboardLanguage, text: $foreignThing)

This works fine in some parts of my app. In other parts, I need a text field which calls a method when the user taps the enter key after entering text. It's written like this:

TextField("", text: Binding<String>(
    get: { self.userAnswer },
    set: {
           self.userAnswer = $0
           self.enableHint()
    }), onCommit: {
           if self.userAnswer.isEmpty {
               answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
           } else {
               answerDisabled = true
               checkAnswer()
           }
    })

I tried implementing the above with UITextFieldViewRepresenatable as follows:

UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
    get: { self.userAnswer },
    set: {
           self.userAnswer = $0
           self.enableHint()
    }), onCommit: {
           if self.userAnswer.isEmpty {
               answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
           } else {
               answerDisabled = true
               checkAnswer()
           }
    })

I'm getting 'compiler couldn't type check this expression in reasonable time' error. I think I've narrowed it down to not implementing .onCommit:{} in my UITextFieldViewRepresentable()

If this is the problem, then I'd like to know how .onCommit:{} can be implemented in UITextFieldViewRepresentable().

Tirna
  • 383
  • 1
  • 12

1 Answers1

1

There are a few mistakes in the UIViewRepresentable implementation:

func makeCoordinator() -> Coordinator {
    return Coordinator(text: $text)
}

This text binding will be out of date when the View is recreated, so change it to:

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

You can store the textField inside the coordinator, make it a lazy, set delegate self, then return it from the make func, eg..

func makeUIView(context: Context) -> UITextField {
    context.coordinator.textField // lazy property that sets delegate self.
}

In update, you need to make use of the new value of the binding, e.g.

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

    // Change the language in the wordTextField here.
    
    if let wordTextField = uiView as? WordTextField {
        wordTextField.language = self.language
    }

    // use the new binding
    context.coordinator.textDidChange = { newText in
        text = newText
    } 
}

class Coordinator: NSObject, UITextFieldDelegate {

    lazy var textField: UITextField = {
        let textField = UITextField()
        textField.delegate = self
        return textField
    }()
    
    var textDidChange: ((String) -> Void)?
    
    func textFieldDidChangeSelection(_ textField: UITextField) {
        textDidChange?(textField.text)
    }
}

You'll see something similar in PaymentButton.swift in Apple's Fruta sample.

Perhaps a simpler way would be this (but I've not tested it yet):

// update
context.coordinator.textBinding = _text

// Coordinator
var textBinding: Binding<String>?

// textViewDidChange
textBinding?.wrappedValue = textField.text
malhal
  • 26,330
  • 7
  • 115
  • 133
  • After applying your edits I'm getting english keybaord instead of foreign when tapping on the foreign text field. I got foreign keyboards before making this edit. – Tirna Jan 24 '23 at 21:31
  • sorry I didn't bother replacing UITextField with WordTextField you'll have to do that everywhere. – malhal Jan 24 '23 at 21:43
  • It's now working for my 2nd code sample in my original question, but not the 4th sample. I still get the compile error I mentioned before. – Tirna Jan 24 '23 at 23:08