Search code examples
swiftswiftuiuikit

How to support Genmoji with SwiftUI?


I want to support Genmoji input in my SwiftUI TextField or TextEditor, but looking around, it seems there's no SwiftUI only way to do it?

Does this mean we can only support Genmoji through UITextField and UITextView?

If yes, what's the best way to go about it? I currently have this code, but I feel an input lag.

import UIKit

struct IconField: UIViewRepresentable {
    @Binding var text: NSAttributedString?
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UITextView {
        let textField = UITextView()
        textField.delegate = context.coordinator
        
        textField.textColor = .label
        textField.font = .preferredFont(forTextStyle: .body)
        textField.supportsAdaptiveImageGlyph = true
        
        textField.autocorrectionType = .no
        textField.autocapitalizationType = .none
        
        textField.backgroundColor = .clear
        
        return textField
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = text
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: IconField
        
        init(_ parent: IconField) {
            self.parent = parent
        }
        
        func textViewDidChange(_ textField: UITextView) {
            if let attributedText = textField.attributedText {
                parent.text = attributedText
            }
        }
        
        // Delegate method to enforce character limit
        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            // Current attributed text
            let currentAttributedText = textView.attributedText ?? NSAttributedString(string: "")
            
            // Convert to NSMutableAttributedString to handle modifications
            let mutableAttributedText = NSMutableAttributedString(attributedString: currentAttributedText)
            
            // Replace the text in the specified range with the new text
            mutableAttributedText.replaceCharacters(in: range, with: text)
            
            // Check if the updated text exceeds the character limit
            return mutableAttributedText.string.count <= 1
        }
    }
}

Solution

  • Answer with text diffing as requested in comment:

    import UIKit
    
    struct IconField: UIViewRepresentable {
        @Binding var text: NSAttributedString!
        
        func makeCoordinator() -> Coordinator {
            Coordinator()
        }
        
        func makeUIView(context: Context) -> UITextView {
            context.coordinator.textView
        }
        
        func updateUIView(_ uiView: UITextView, context: Context) {
            context.coordinator.parent = self
        }
        
        class Coordinator: NSObject, UITextViewDelegate {
            var parent: IconField! {
                didSet {
                    // diffing the text may improve performance
                    if textView.attributedText != parent.text {
                        textView.attributedText = parent.text
                    }
                }
            }
            
            lazy var textView: UITextView = {
                let textField = UITextView()
                textField.delegate = self
                
                textField.textColor = .label
                textField.font = .preferredFont(forTextStyle: .body)
                textField.supportsAdaptiveImageGlyph = true
                
                textField.autocorrectionType = .no
                textField.autocapitalizationType = .none
                
                textField.backgroundColor = .clear
                
                return textField
            }()
            
            
            func textViewDidChange(_ textField: UITextView) {
                parent.text = textField.attributedText
            }
            
            // Delegate method to enforce character limit
            func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
                // Current attributed text
                let currentAttributedText:NSAttributedString = textView.attributedText
                
                // Convert to NSMutableAttributedString to handle modifications
                let mutableAttributedText = NSMutableAttributedString(attributedString: currentAttributedText)
                
                // Replace the text in the specified range with the new text
                mutableAttributedText.replaceCharacters(in: range, with: text)
                
                // Check if the updated text exceeds the character limit
                return mutableAttributedText.string.count <= 1
            }
        }
    }
    

    Note: I'm not sure shouldChangeTextIn is the correct place to enforce a character limit. E.g. you might want to let them paste in long text and then edit it down to the required length. That would be easier than having them do the editing in another app like say Notes. Also by default SwiftUI does its formatting/validation on submit, i.e. on return press, rather than while editing is happening.