Richard Robinson
Richard Robinson

Reputation: 997

Issue when binding text between UITextField with UIViewRepresentable and SwifUI

I'm trying to make an 'emoji picker' in SwiftUI which brings ups the emoji keyboard, allows the user to select an emoji, and then dismisses the keyboard. I'm using a UITextField wrapped in a UIViewRepresntable with a String Binding, however the string's value never gets updated for some reason.

Here is the code I have so far:

/// Allows a user to pick an emoji character using the Emoji keyboard.
/// - Note: This does not prevent the user from manually switching to other keyboards and inputting a non-Emoji character
struct EmojiPicker: UIViewRepresentable {
    @Binding var emoji: String
    
    func makeUIView(context: UIViewRepresentableContext<EmojiPicker>) -> EmojiUITextField {
        let textField = EmojiUITextField(frame: .zero)
        textField.text = emoji
        textField.delegate = context.coordinator
        textField.autocorrectionType = .no
        textField.returnKeyType = .done
        textField.textAlignment = .center
        textField.tintColor = .clear
        
        return textField
    }
    
    func updateUIView(_ uiView: EmojiUITextField, context: Context) {
        self.emoji = uiView.text!
    }
    
    func makeCoordinator() -> EmojiTextFieldCoordinator {
        return EmojiTextFieldCoordinator(self)
    }
}

internal class EmojiTextFieldCoordinator: NSObject, UITextFieldDelegate {
    var emojiTextField: EmojiPicker
    
    init(_ textField: EmojiPicker) {
        self.emojiTextField = textField
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        self.emojiTextField.emoji = textField.text!
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        textField.text = string
        
        if let text = textField.text, text.count == 1 {
            self.emojiTextField.emoji = textField.text!
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
        
        return true
    }
}

internal class EmojiUITextField: UITextField {
    override var textInputContextIdentifier: String? {
        return ""
    }

    override var textInputMode: UITextInputMode? {
        return UITextInputMode.activeInputModes.first {
            $0.primaryLanguage == "emoji"
        }
    }
    
    override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {
        return []
    }
}

All the resources I've found thus far haven't worked, including this, this, and this

Upvotes: 2

Views: 794

Answers (1)

Asperi
Asperi

Reputation: 257739

Here is fix, the only modified part, (tested with Xcode 12 / iOS 14)

func updateUIView(_ uiView: EmojiUITextField, context: Context) {
    if self.emoji != uiView.text! {     // << update only on change, otherwise
        self.emoji = uiView.text!       // it result in cycle and dropped
    }
}

Here is a view used for testing

struct ContentView: View {
    @State private var text = "<none>"
    var body: some View {
        VStack {
            Text("Get: \(text)")
            EmojiPicker(emoji: $text)
        }
    }
}

Upvotes: 4

Related Questions