简体   繁体   中英

SwiftUI Custom TextField with UIViewRepresentable Issue with ObservableObject and pushed View

I created a UIViewRepresentable to wrap UITextField for SwiftUI, so I can eg change the first responder when the enter key was tapped by the user.

This is my UIViewRepresentable (I removed the first responder code to keep it simple)

struct CustomUIKitTextField: UIViewRepresentable {

    @Binding var text: String
    var placeholder: String

    func makeUIView(context: UIViewRepresentableContext<CustomUIKitTextField>) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        textField.placeholder = placeholder
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUIKitTextField>) {
        uiView.text = text
        uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        uiView.setContentCompressionResistancePriority(.required, for: .vertical)
    }

    func makeCoordinator() -> CustomUIKitTextField.Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: CustomUIKitTextField

        init(parent: CustomUIKitTextField) {
            self.parent = parent
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            parent.text = textField.text ?? ""
        }        

    }
}

The first screen of the app has a "Sign in with email" button which pushes MailView that displays a CustomUIKitTextField and uses a @Published property of an ObservableObject view model as the TextField's text.

struct MailView: View {

    @ObservedObject var viewModel: MailSignUpViewModel

    var body: some View {
        VStack {
            CustomUIKitTextField(placeholder: self.viewModel.placeholder,
                             text: self.$viewModel.mailAddress)
                .padding(.top, 30)
                .padding(.bottom, 10)

            NavigationLink(destination: UsernameView(viewModel: UsernameSignUpViewModel())) {
                Text("Next")
            }

            Spacer()            
        }
    }
}

Everything works fine until I push another view like MailView, say eg UsernameView. It is implemented exactly in the same way, but somehow the CustomUIKitTextField gets an updateUIView call with an empty string once I finish typing.

There is additional weird behavior like when I wrap MailView and UsernameView in another NavigationView, everything works fine. But that is obviously not the way to fix it, since I would have multiple NavigationViews then.

It also works when using an @State property instead of a @Published property inside a view model. But I do not want to use @State since I really want to keep the model code outside the view.

Is there anybody who faced the same issue or a similar one?

It looks like you're using the wrong delegate method. textFieldDidChangeSelection will produce some inconsistent results (which is what it sounds like you're dealing with). Instead, I recommend using textFieldDidEndEditing which will also give you access to the passed in control, but it guarantees that you're getting the object as it is resigning the first responder. This is important because it means you're getting the object after the properties have been changed and it's releasing the responder object.

So, I would change your delegate method as follows:


func textFieldDidEndEditing(_ textField: UITextField) {
    parent.text = textField.text ?? ""
}

For more info, see this link for the textFieldDidEndEditing method: https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619591-textfielddidendediting

And this link for info on the UITextFieldDelegate object: https://developer.apple.com/documentation/uikit/uitextfielddelegate

EDIT

Based on the comment, if you're looking to examine the text everytime it changes by one character, you should implement this delegate function:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

     // User pressed the delete key
     if string.isEmpty {
        // If you want to update your query string here after the delete key is pressed, you can rebuild it like we are below
        return true
     }

     //this will build the full string the user intends so we can use it to build our search
     let currentText = textField.text ?? ""
     let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
     // You can set your parent.text here if you like, or you can fire the search function as well

     // When you're done though, return true to indicate that the textField should display the changes from the user

     return true
}

I also needed a UITextField representation in SwiftUI, which reacts to every character change, so I went with the answer by binaryPilot84 . While the UITextFieldDelegate method textField(_:shouldChangeCharactersIn:replacementString:) is great, it has one caveat -- every time we update the text with this method, the cursor moves to the end of the text . It might be desired behavior. However, it was not for me, so I implemented target-action like so:

public func makeUIView(context: Context) -> UITextField {
    let textField = UITextField()
    
    textField.addTarget(context.coordinator, action: #selector(context.coordinator.textChanged), for: .editingChanged)
    
    return textField
}


public final class Coordinator: NSObject {
    @Binding private var text: String
    
    public init(text: Binding<String>) {
        self._text = text
    }
    
    @objc func textChanged(_ sender: UITextField) {
        guard let text = sender.text else { return }
        self.text = text
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM