Search code examples
swiftuiuitextfielduiviewrepresentable

How to set isUserInteractionEnabled to false on specific UIViewRepresentable TextField accompanied with regular SwiftUI Text Fields


I have SwiftUI View where are two normal TextFields and one UIViewRepresentable UITextField.
I need to turn off possibility of copy/paste/select on UIViewRepresentable while typing values.
I'm doing that by set the false value to isUserInteractionEnabled under textFieldShouldBeginEditing function called on coordinator of my CustomUITextField struct (that UIViewRepresentable UITextField).

The problem is that if I press return on some SwiftUI TextFields than textFieldShouldBeginEditing is triggering and setting my CustomUITextField to non-editable before it will be active and therefore the CustomUITextField became untouchable so I can't type any values.

This the MRE

import SwiftUI

struct ContentView: View {
    @State private var name: String = ""
    @State private var phone: String = ""
    @State private var isFirstResponder = false
    
    var body: some View {
        VStack {
            Spacer()
            HStack {
                Text("A")
                TextField("", text: $name)
                    .border(Color.gray.opacity(0.3))
            }
            HStack {
                Text("B")
                CustomUITextField(isFirstResponder: $isFirstResponder, text: $phone)
                    .frame(height: 22)
                    .border(Color.gray.opacity(0.3))
            }
            Spacer()
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

struct CustomUITextField: UIViewRepresentable {
    @Binding var isFirstResponder: Bool
    @Binding var text: String
    
    init(isFirstResponder: Binding<Bool>, text: Binding<String>) {
        self._isFirstResponder = isFirstResponder
        self._text = text
    }
    
    func makeUIView(context: UIViewRepresentableContext<CustomUITextField>) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.text = text
        textField.delegate = context.coordinator
        
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUITextField>) {
        context.coordinator.listenToChanges = false
        if isFirstResponder != uiView.isFirstResponder {
            if isFirstResponder {
                uiView.becomeFirstResponder()
            } else {
                uiView.resignFirstResponder()
            }
        }
        uiView.text = text
        context.coordinator.listenToChanges = true
    }
    
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator(isFirstResponder: $isFirstResponder, text: $text)
        return coordinator
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        @Binding var isFirstResponder: Bool
        @Binding var text: String
        fileprivate var listenToChanges: Bool = false
        
        init(isFirstResponder: Binding<Bool>, text: Binding<String>) {
            self._isFirstResponder = isFirstResponder
            self._text = text
        }
        
        func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
            textField.isUserInteractionEnabled = false
            return true
        }
        
        func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
            textField.isUserInteractionEnabled = true
            return true
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            guard listenToChanges else { return }
            isFirstResponder = textField.isFirstResponder
        }
        
        func textFieldDidEndEditing(_ textField: UITextField) {
            guard listenToChanges else { return }
            isFirstResponder = textField.isFirstResponder
            text = textField.text ?? ""
        }
    }
}

If you enter some value on TextField A and press return that you can't activate TextField B.

I was tried pass boolean value through binding to UIViewRepresentable coordinator while I'm tapping on TextField A to make some if statement in textFieldShouldBeginEditing but that function is triggering before .onSubmit event on A field.

Any thoughts about workaround of this problem?

Thanks in advance


Solution

  • Instead of using textFieldShouldBeginEditing like this, you should do something similar to this answer, i.e. override canPerformAction to return false.

    struct CustomUITextField: UIViewRepresentable {
        @Binding var text: String
        
        class Wrapper: UITextField {
            override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
                false
            }
    
            // if you want to disable the magnifying glass and the "autofill" popup too,
            override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
                !(gestureRecognizer is UILongPressGestureRecognizer)
            }
            
            override func buildMenu(with builder: any UIMenuBuilder) {
                builder.remove(menu: .autoFill)
                super.buildMenu(with: builder)
            }
        }
        
        init(text: Binding<String>) {
            self._text = text
        }
        
        func makeUIView(context: UIViewRepresentableContext<CustomUITextField>) -> UITextField {
            let textField = Wrapper(frame: .zero)
            textField.textContentType = UITextContentType(rawValue: "")
            textField.text = text
            textField.delegate = context.coordinator
            
            return textField
        }
        
        func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUITextField>) {
            context.coordinator.textCallback = { text = $0 }
            uiView.text = text
        }
        
        func makeCoordinator() -> Coordinator {
            .init()
        }
        
        class Coordinator: NSObject, UITextFieldDelegate {
            var textCallback: ((String) -> Void)?
            
            func textFieldDidEndEditing(_ textField: UITextField) {
                textCallback?(textField.text ?? "")
            }
        }
    }
    

    You never know when textFieldShouldBeginEditing is called, so it is generally a bad idea to put any side effects in there.