SwiftUI自定义TextField与UIViewRepresentable的ObservableObject问题及推出视图。

6
我创建了一个`UIViewRepresentable` 来包装`UITextField`,以便在SwiftUI中使用,例如当用户点击回车键时可以更改第一响应者。以下是我的`UIViewRepresentable`(我删除了第一响应者代码以保持简单)。
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 ?? ""
        }        

    }
}

该应用的第一个屏幕有一个“使用电子邮件登录”按钮,点击后会显示 MailView。MailView 中包含一个名为 CustomUIKitTextField 的文本框,并使用 @Published 属性作为 ObservableObject 视图模型的文本框文本。

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()            
        }
    }
}

一切都很顺利,直到我推出另一个视图(例如UsernameView)如MailView。它的实现方式完全相同,但是不知怎么地,CustomUIKitTextField在我完成输入后会收到一个空字符串的updateUIView调用。

还有其他奇怪的行为,例如将MailView和UsernameView包装在另一个NavigationView中时,一切正常。但这显然不是解决问题的方法,因为那样就会有多个NavigationView。

如果在视图模型中使用@State属性而不是@Published属性,也可以正常工作。但我不想使用@State,因为我真的希望保持模型代码与视图分离。

是否有人遇到过同样的问题或类似的问题?

2个回答

5
看起来你正在使用错误的委托方法。`textFieldDidChangeSelection`会产生一些不一致的结果(这似乎是你正在处理的问题)。相反,我建议使用`textFieldDidEndEditing`,它也给你访问传入的控制器,但它保证你得到的对象是作为第一响应者放弃时的对象。这很重要,因为它意味着在获取对象之前属性已经改变并释放了响应者对象。
因此,我建议更改您的委托方法如下:

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

如需更多信息,请参阅此链接中的textFieldDidEndEditing方法:https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619591-textfielddidendediting

以及有关UITextFieldDelegate对象的信息,请参阅此链接:https://developer.apple.com/documentation/uikit/uitextfielddelegate

编辑

根据评论,如果您希望每次更改一个字符时检查文本,应实现此代理函数:

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
}

感谢您的帮助,但我认为这并不能解决我的问题,因为我的视图模型无法得到用户每次输入字符的通知。当然,在某些情况下,这可能足够了,但如果我想要触发搜索功能的请求,这个解决方案就不够用了。 - sinalco12
@sinalco12,我更新了我的答案,并添加了适当的委托函数,以便在每个字符更改时检查文本。 - binaryPilot84

4

我也需要在SwiftUI中使用UITextField表示,并对每个字符更改做出反应,所以我采用了binaryPilot84的答案。虽然UITextFieldDelegate方法textField(_:shouldChangeCharactersIn:replacementString:)很好,但它有一个缺点--每次我们使用这种方法更新文本时,光标都会移动到文本末尾。这可能是期望的行为。但是,对我来说不是,因此我实现了目标操作,如下:

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
    }
}

3
谢谢!我遇到了完全相同的光标跳到结尾的问题,你的解决方案完美地解决了它! - aheze
1
这是最好的解决方案!我也遇到了在字符串开头和中间插入新字符的问题。我从来没有想过可以使用协调器作为 NSObject 来包含 @objc 函数。 - SleepNot
是的,协调器基本上涵盖了旧的iOS模式,无论是目标-动作还是委托以及与它们相关的所有内容。 - Gene Bogdanovich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接