1

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

1
  • How about disabling copy/paste/selection some other way? Like stackoverflow.com/q/32846678/5133585 for example. You shouldn’t really use textFieldShouldBeginEditing this way. Commented Jun 1, 2024 at 10:39

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, it's works almost perfect, but I have one more to done. I need to disable all long/double taps events that launch loupes and autofill. Basically I would like to have only one single tap gesture to be accepted to just activate the field. I tried override gestureRecognizerShouldBegin and return only taps that are not long/double press but with only partial success. I can disable obvius long press events, but while I tap and then make second long press the loupe activated.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.