如何使用 Combine 来跟踪 UIViewRepresentable 类中的 UITextField 更改? [英] How can I use Combine to track UITextField changes in a UIViewRepresentable class?

查看:17
本文介绍了如何使用 Combine 来跟踪 UIViewRepresentable 类中的 UITextField 更改?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我创建了一个自定义文本字段,我想利用结合.为了在文本字段中的文本更改时收到通知,我目前使用自定义修饰符.它运行良好,但我希望这段代码可以在我的 CustomTextField 结构中.

I have created a custom text field and I'd like to take advantage of Combine. In order to be notified whenever text changes in my text field, I currently use a custom modifier. It works well, but I want this code could inside my CustomTextField struct.

我的 CustomTextField 结构符合 UIViewRepresentable.在这个结构体中,有一个名为 Coordinator 的 NSObject 类,它符合 UITextFieldDelegate.

My CustomTextField struct conforms to UIViewRepresentable. Inside this struct, there is a NSObject class called Coordinator and it conforms to UITextFieldDelegate.

我已经在使用其他 UITextField 委托方法,但找不到与我已经使用自定义修饰符完全相同的方法.有些方法很接近,但并不完全按照我希望的方式行事.无论如何,我觉得最好将这个新的自定义 textFieldDidChange 方法放在 Coordinator 类中.

I'm already using other UITextField delegate methods, but couldn't find one that does exactly what I already do with my custom modifier. Some methods are close, but don't quite behave the way I want them to. Anyway, I feel it would be best to put this new custom textFieldDidChange method in the Coordinator class.

这是我的自定义修饰符

private let textFieldDidChange = NotificationCenter.default
    .publisher(for: UITextField.textDidChangeNotification)
    .map { $0.object as! UITextField}


struct CustomModifer: ViewModifier {

     func body(content: Content) -> some View {
         content
             .tag(1)
             .onReceive(textFieldDidChange) { data in

                //do something

             }
    }
}

我的 CustomTextField 用于 SwiftUI 视图,并附加了我的自定义修饰符.当文本字段发生变化时,我可以做一些事情.修改器也使用Combine.它工作得很好,但我不希望此功能采用修饰符的形式.我想在我的 Coordinator 类中使用它,以及我的 UITextFieldDelegate 方法.

My CustomTextField is used in a SwiftUI view, with my custom modifier attached to it. I’m able to do things when ever there are changes to the text field. The modifier is also using Combine. It works great, but I don't want this functionality to be in the form of a modifier. I want to use it in my Coordinator class, along with my UITextFieldDelegate methods.

这是我的 CustomTextField

struct CustomTextField: UIViewRepresentable {

    var isFirstResponder: Bool = false
    @EnvironmentObject var authenticationViewModel: AuthenticationViewModel

    func makeCoordinator() -> Coordinator {
        return Coordinator(authenticationViewModel: self._authenticationViewModel)
    }

    class Coordinator: NSObject, UITextFieldDelegate {

        var didBecomeFirstResponder = false
        @EnvironmentObject var authenticationViewModel: AuthenticationViewModel

        init(authenticationViewModel: EnvironmentObject<AuthenticationViewModel>)
        {
            self._authenticationViewModel = authenticationViewModel
        }

        // Limit the amount of characters that can be typed in the field
        func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

            let currentText = textField.text ?? ""
            guard let stringRange = Range(range, in: currentText) else { return false }
            let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
            return updatedText.count <= 14
        }

        /* I want to put my textFieldDidChange method right here */

        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * */


        func textFieldDidEndEditing(_ textField: UITextField) {

            textField.resignFirstResponder()
            textField.endEditing(true)
        }

    }

    func makeUIView(context: Context) -> UITextField {

        let textField = UITextField()
        textField.delegate = context.coordinator
        textField.placeholder = context.coordinator.authenticationViewModel.placeholder
        textField.font = .systemFont(ofSize: 33, weight: .bold)
        textField.keyboardType = .numberPad

        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {

        let textField = uiView
        textField.text = self.authenticationViewModel.text
    }
}

struct CustomTextField_Previews: PreviewProvider {

    static var previews: some View {
        CustomTextField()
            .previewLayout(.fixed(width: 270, height: 55))
            .previewDisplayName("Custom Textfield")
            .previewDevice(.none)
    }
}

我一直在观看有关 Combine 的视频,我想开始在我正在构建的新应用中使用它.我真的认为在这种情况下使用它是正确的,但仍然不太确定如何实现它.我真的很感激一个例子.

I've been watching videos about Combine and I'd like to start utilising it in a new app I'm building. I really think it's the right thing to use in this situation, but still not quite sure how to pull this off. I'd really appreciate an example.

总结:

我想向我的 Coordinator 类添加一个名为 textFieldDidChange 的函数,并且每次我的文本字段发生更改时都应该触发它.它必须使用Combine.

I want to add a function called textFieldDidChange to my Coordinator class, and it should be triggered every time there is a change to my text field. It must utilise Combine.

提前致谢

推荐答案

更新答案

在查看您更新的问题后,我意识到我的原始答案可能需要进行一些清理.我已将模型和协调器合并为一个类,虽然它对我的示例有效,但并不总是可行或可取的.如果模型和协调器不能相同,则不能依赖模型属性的 didSet 方法来更新 textField.因此,我使用模型中的 @Published 变量免费获得了 Combine 发布者.

Updated Answer

After looking at your updated question, I realized my original answer could use some cleaning up. I had collapsed the model and coordinator into one class, which, while it worked for my example, is not always feasible or desirable. If the model and coordinator cannot be the same, then you can't rely on the model property's didSet method to update the textField. So instead, I'm making use of the Combine publisher we get for free using a @Published variable inside our model.

我们需要做的关键事情是:

The key things we need to do are to:

  1. 通过保持 model.texttextField.text 同步来制作单一的事实来源

  1. Make a single source of truth by keeping model.text and textField.text in sync

  1. 使用@Published 属性包装器提供的发布者在model.text 更改时更新textField.text

  1. Use the publisher provided by the @Published property wrapper to update textField.text when model.text changes

textfield 上使用 .addTarget(:action:for) 方法更新 model.text.text 变化

Use the .addTarget(:action:for) method on textField to update model.text when textfield.text changes

  • 当我们的模型发生变化时,执行一个名为 textDidChange 的闭包.

    (我更喜欢在 #1.2 中使用 .addTarget 而不是通过 NotificationCenter,因为它的代码更少,可以立即工作,并且 UIKit 用户都知道).

    (I prefer using .addTarget for #1.2 rather than going through NotificationCenter, as it's less code, worked immediately, and it is well known to users of UIKit).

    这是一个显示此工作的更新示例:

    Here is an updated example that shows this working:

    import SwiftUI
    import Combine
    
    // Example view showing that `model.text` and `textField.text`
    //     stay in sync with one another
    struct CustomTextFieldDemo: View {
        @ObservedObject var model = Model()
    
        var body: some View {
            VStack {
                // The model's text can be used as a property
                Text("The text is "(model.text)"")
                // or as a binding,
                TextField(model.placeholder, text: $model.text)
                    .disableAutocorrection(true)
                    .padding()
                    .border(Color.black)
                // or the model itself can be passed to a CustomTextField
                CustomTextField().environmentObject(model)
                    .padding()
                    .border(Color.black)
            }
            .frame(height: 100)
            .padding()
        }
    }
    

    型号

    class Model: ObservableObject {
        @Published var text = ""
        var placeholder = "Placeholder"
    }
    

    查看

    struct CustomTextField: UIViewRepresentable {
        @EnvironmentObject var model: Model
    
        func makeCoordinator() -> CustomTextField.Coordinator {
            Coordinator(model: model)
        }
    
        func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
            let textField = UITextField()
    
            // Set the coordinator as the textField's delegate
            textField.delegate = context.coordinator
    
            // Set up textField's properties
            textField.text = context.coordinator.model.text
            textField.placeholder = context.coordinator.model.placeholder
            textField.autocorrectionType = .no
    
            // Update model.text when textField.text is changed
            textField.addTarget(context.coordinator,
                                action: #selector(context.coordinator.textFieldDidChange),
                                for: .editingChanged)
    
            // Update textField.text when model.text is changed
            // The map step is there because .assign(to:on:) complains
            //     if you try to assign a String to textField.text, which is a String?
            // Note that assigning textField.text with .assign(to:on:)
            //     does NOT trigger a UITextField.Event.editingChanged
            let sub = context.coordinator.model.$text.receive(on: RunLoop.main)
                             .map { Optional($0) }
                             .assign(to: UITextField.text, on: textField)
            context.coordinator.subscribers.append(sub)
    
            // Become first responder
            textField.becomeFirstResponder()
    
            return textField
        }
    
        func updateUIView(_ textField: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
            // If something needs to happen when the view updates
        }
    }
    

    查看协调员

    extension CustomTextField {
        class Coordinator: NSObject, UITextFieldDelegate, ObservableObject {
            @ObservedObject var model: Model
            var subscribers: [AnyCancellable] = []
    
            // Make subscriber which runs textDidChange closure whenever model.text changes
            init(model: Model) {
                self.model = model
                let sub = model.$text.receive(on: RunLoop.main).sink(receiveValue: textDidChange)
                subscribers.append(sub)
            }
    
            // Cancel subscribers when Coordinator is deinitialized
            deinit {
                for sub in subscribers {
                    sub.cancel()
                }
            }
    
            // Any code that needs to be run when model.text changes
            var textDidChange: (String) -> Void = { text in
                print("Text changed to "(text)"")
                // * * * * * * * * * * //
                // Put your code here  //
                // * * * * * * * * * * //
            }
    
            // Update model.text when textField.text is changed
            @objc func textFieldDidChange(_ textField: UITextField) {
                model.text = textField.text ?? ""
            }
    
            // Example UITextFieldDelegate method
            func textFieldShouldReturn(_ textField: UITextField) -> Bool {
                textField.resignFirstResponder()
                return true
            }
        }
    }
    

    原答案

    听起来你有几个目标:

    Original Answer

    It sounds like you have a few goals:

    1. 使用 UITextField 以便您可以使用像 .becomeFirstResponder()
    2. 这样的功能
    3. 在文本更改时执行操作
    4. 通知其他 SwiftUI 视图文本已更改

    我认为您可以使用单个模型类和 UIViewRepresentable 结构来满足所有这些要求.我以这种方式构建代码的原因是,您有一个单一的真实来源(model.text),它可以与其他采用 String绑定<字符串>.

    I think you can satisfy all these using a single model class, and the UIViewRepresentable struct. The reason I structured the code this way is so that you have a single source of truth (model.text), which can be used interchangeably with other SwiftUI views that take a String or Binding<String>.

    class MyTextFieldModel: NSObject, UITextFieldDelegate, ObservableObject {
        // Must be weak, so that we don't have a strong reference cycle
        weak var textField: UITextField?
    
        // The @Published property wrapper just makes a Combine Publisher for the text
        @Published var text: String = "" {
            // If the model's text property changes, update the UITextField
            didSet {
                textField?.text = text
            }
        }
    
        // If the UITextField's text property changes, update the model
        @objc func textFieldDidChange() {
            text = textField?.text ?? ""
    
            // Put your code that needs to run on text change here
            print("Text changed to "(text)"")
        }
    
        // Example UITextFieldDelegate method
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            textField.resignFirstResponder()
            return true
        }
    }
    

    查看

    struct MyTextField: UIViewRepresentable {
        @ObservedObject var model: MyTextFieldModel
    
        func makeUIView(context: UIViewRepresentableContext<MyTextField>) -> UITextField {
            let textField = UITextField()
    
            // Give the model a reference to textField
            model.textField = textField
    
            // Set the model as the textField's delegate
            textField.delegate = model
    
            // TextField setup
            textField.text = model.text
            textField.placeholder = "Type in this UITextField"
    
            // Call the model's textFieldDidChange() method on change
            textField.addTarget(model, action: #selector(model.textFieldDidChange), for: .editingChanged)
    
            // Become first responder
            textField.becomeFirstResponder()
    
            return textField
        }
    
        func updateUIView(_ textField: UITextField, context: UIViewRepresentableContext<MyTextField>) {
            // If something needs to happen when the view updates
        }
    }
    

    如果你不需要上面的#3,你可以替换

    If you don't need #3 above, you could replace

    @ObservedObject var model: MyTextFieldModel
    

    @ObservedObject private var model = MyTextFieldModel()
    

    演示

    这是显示所有这些工作的演示视图

    Demo

    Here's a demo view showing all this working

    struct MyTextFieldDemo: View {
        @ObservedObject var model = MyTextFieldModel()
    
        var body: some View {
            VStack {
                // The model's text can be used as a property
                Text("The text is "(model.text)"")
                // or as a binding,
                TextField("Type in this TextField", text: $model.text)
                    .padding()
                    .border(Color.black)
                // but the model itself should only be used for one wrapped UITextField
                MyTextField(model: model)
                    .padding()
                    .border(Color.black)
            }
            .frame(height: 100)
            // Any view can subscribe to the model's text publisher
            .onReceive(model.$text) { text in
                    print("I received the text "(text)"")
            }
    
        }
    }
    

    这篇关于如何使用 Combine 来跟踪 UIViewRepresentable 类中的 UITextField 更改?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆