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

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

问题描述

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

我的CustomTextField结构符合UIViewRepresentable.在此结构内部,有一个名为Coordinator的NSObject类,它符合UITextFieldDelegate.

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

这是我的自定义修饰符

  private让textFieldDidChange = NotificationCenter.default.publisher(用于:UITextField.textDidChangeNotification).map {$ 0.object as!UITextField}struct CustomModifer:ViewModifier {func body(content:Content)->一些视图{内容.tag(1).onReceive(textFieldDidChange){数据在//做一点事}}} 

我的CustomTextField用于SwiftUI视图,并附加了我的自定义修饰符.只要文本字段发生更改,我就可以执行操作.修改器还使用Combine.它很好用,但我不希望此功能采用修饰符的形式.我想在Coordinator类中使用它,以及UITextFieldDelegate方法.

这是我的CustomTextField

  struct CustomTextField:UIViewRepresentable {var isFirstResponder:Bool = false@EnvironmentObject var authenticationViewModel:AuthenticationViewModelfunc makeCoordinator()->协调员{返回协调器(authenticationViewModel:self._authenticationViewModel)}类协调器:NSObject,UITextFieldDelegate {var didBecomeFirstResponder =假@EnvironmentObject var authenticationViewModel:AuthenticationViewModelinit(authenticationViewModel:EnvironmentObject< AuthenticationViewModel>){self._authenticationViewModel = authenticationViewModel}//限制可在字段中输入的字符数func textField(_ textField:UITextField,shouldChangeCharactersIn范围:NSRange,replaceString string:String)->布尔{让currentText = textField.text ??"后卫让stringRange = Range(range,in:currentText)else {返回false}让updateText = currentText.replacingCharacters(in:stringRange,with:string)返回updatedText.count< = 14}/*我想将我的textFieldDidChange方法放在这里*//* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */func textFieldDidEndEditing(__ textField:UITextField){textField.resignFirstResponder()textField.endEditing(true)}}func makeUIView(context:Context)->UITextField {让textField = UITextField()textField.delegate = context.coordinatortextField.placeholder = context.coordinator.authenticationViewModel.placeholdertextField.font = .systemFont(ofSize:33,权重:.bold)textField.keyboardType = .numberPad返回textField}func updateUIView(_ uiView:UITextField,context:Context){让textField = uiViewtextField.text = self.authenticationViewModel.text}}struct CustomTextField_Previews:PreviewProvider {静态var预览:某些视图{CustomTextField().previewLayout(.fixed(宽度:270,高度:55)).previewDisplayName(自定义文本字段").previewDevice(.none)}} 

我一直在观看有关Combine的视频,我想在我正在开发的新应用中开始使用它.我确实认为在这种情况下使用是正确的做法,但仍然不确定如何实现这一目标.我真的很喜欢一个例子.

总结一下:

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

预先感谢

解决方案

更新后的答案

在查看了您的最新问题之后,我意识到我的原始答案可能需要进行一些清理.我已经将模型和协调器分解为一个类,尽管以我的示例为例,但这并不总是可行或理想的.如果模型和协调器不能相同,则不能依赖模型属性的didSet方法来更新textField.因此,相反,我使用的是我们在模型内部使用的 @Published 变量免费获得的Combine发布者.

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

  1. 通过使 model.text textField.text 保持同步

    ,使真相单一

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

    2. textfield上使用 textField 上的 .addTarget(:action:for)方法更新 model.text .text 更改

  2. 当我们的模型更改时,执行一个名为 textDidChange 的闭包.

(我更喜欢将#code..addTarget 用于#1.2,而不是通过 NotificationCenter ,因为它的代码较少,可立即使用,并且UIKit用户众所周知)

这是一个更新的示例,显示了此工作原理:

演示

 导入SwiftUI进口联合收割机//示例视图显示了"model.text"和"textField.text"//保持同步struct CustomTextFieldDemo:查看{@ObservedObject var模型= Model()var body:some View {VStack {//模型的文本可用作属性文本(文本为\" \(model.text)\"))//或作为绑定,TextField(model.placeholder,文本:$ model.text).disableAutocorrection(true).填充().border(彩色.黑色)//或将模型本身传递给CustomTextFieldCustomTextField().environmentObject(model).填充().border(彩色.黑色)}.frame(高度:100).填充()}} 

型号

 类模型:ObservableObject {@Published var text ="var占位符=占位符"} 

查看

  struct CustomTextField:UIViewRepresentable {@EnvironmentObject var模型:模型func makeCoordinator()->CustomTextField.Coordinator {协调员(型号:model)}func makeUIView(上下文:UIViewRepresentableContext< CustomTextField>)->UITextField {让textField = UITextField()//将协调器设置为textField的委托textField.delegate = context.coordinator//设置textField的属性textField.text = context.coordinator.model.texttextField.placeholder = context.coordinator.model.placeholdertextField.autocorrectionType = .no//更改textField.text时更新model.texttextField.addTarget(context.coordinator,动作:#selector(context.coordinator.textFieldDidChange),:.editingChanged)//更改model.text时更新textField.text//出现地图步骤是因为.assign(to:on :)抱怨//如果您尝试将一个String分配给textField.text,这是一个String?//请注意,使用.assign(to:on :)分配textField.text//不触发UITextField.Event.editingChanged让sub = context.coordinator.model.$ text.receive(on:RunLoop.main).map {可选($ 0)}.assign(至:\ UITextField.text,on:textField)context.coordinator.subscribers.append(sub)//成为第一响应者textField.becomeFirstResponder()返回textField}func updateUIView(_ textField:UITextField,context:UIViewRepresentableContext< CustomTextField>){//如果在视图更新时需要发生某些事情}} 

View.Coordinator

 扩展名CustomTextField {类协调器:NSObject,UITextFieldDelegate,ObservableObject {@ObservedObject var模型:模型var订阅者:[AnyCancellable] = []//使订户在model.text更改时运行textDidChange闭包init(model:Model){self.model =模型让sub = model.$ text.receive(on:RunLoop.main).sink(receiveValue:textDidChange)subscriptions.append(sub)}//取消初始化协调器时取消订阅者取消初始化{对于订阅者中的子用户{sub.cancel()}}//当model.text更改时需要运行的任何代码var textDidChange:(String)->无效= {文字在print(文本更改为\" \(文本)\")//* * * * * * * * * * *////将您的代码放在这里////* * * * * * * * * * *//}//更改textField.text时更新model.text@objc func textFieldDidChange(_ textField:UITextField){model.text = textField.text ??"}//示例UITextFieldDelegate方法func textFieldShouldReturn(_ textField:UITextField)->布尔{textField.resignFirstResponder()返回真}}} 

原始答案

听起来您有几个目标:

  1. 使用 UITextField ,因此您可以使用 .becomeFirstResponder()
  2. 之类的功能
  3. 文本更改时执行操作
  4. 通知其他SwiftUI视图文本已更改

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

型号

  class MyTextFieldModel:NSObject,UITextFieldDelegate,ObservableObject {//必须很弱,以免我们没有强大的参考周期弱var textField:UITextField?//@Published属性包装器只是为文本创建一个Combine Publisher@Published var text:String =" {//如果模型的text属性更改,请更新UITextFielddidSet {textField?.text =文字}}//如果UITextField的text属性更改,请更新模型@objc func textFieldDidChange(){text = textField?.text ??"//将需要在文本更改上运行的代码放在此处print(文本更改为\" \(文本)\")}//示例UITextFieldDelegate方法func textFieldShouldReturn(_ textField:UITextField)->布尔{textField.resignFirstResponder()返回真}} 

查看

  struct MyTextField:UIViewRepresentable {@ObservedObject变量模型:MyTextFieldModelfunc makeUIView(上下文:UIViewRepresentableContext< MyTextField>)->UITextField {让textField = UITextField()//给模型一个对textField的引用model.textField = textField//将模型设置为textField的委托textField.delegate =模型//TextField设置textField.text = model.texttextField.placeholder =此UITextField中的类型"//更改时调用模型的textFieldDidChange()方法textField.addTarget(model,action:#selector(model.textFieldDidChange),针对:.editingChanged)//成为第一响应者textField.becomeFirstResponder()返回textField}func updateUIView(_ textField:UITextField,context:UIViewRepresentableContext< MyTextField>){//如果在视图更新时需要发生某些事情}} 

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

  @ObservedObject变量模型:MyTextFieldModel 

使用

  @ObservedObject私有var模型= MyTextFieldModel() 

演示

这是一个演示视图,显示了所有这些工作

  struct MyTextFieldDemo:查看{@ObservedObject var模型= MyTextFieldModel()var body:some View {VStack {//模型的文本可用作属性文本(文本为\" \(model.text)\"))//或作为绑定,TextField(在此TextField中键入",文本:$ model.text).填充().border(彩色.黑色)//但模型本身仅应用于一个包装的UITextFieldMyTextField(model:模型).填充().border(彩色.黑色)}.frame(高度:100)//任何视图都可以订阅模型的文本发布者.onReceive(model.$ text){print(我收到了文字\" \(文字)\")}}} 

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.

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

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.

Here is my custom modifier

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

             }
    }
}

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.

This is my 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)
    }
}

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.

To summarise:

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.

Thanks in advance

解决方案

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. Make a single source of truth by keeping model.text and textField.text in sync

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

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

  2. Execute a closure called textDidChange when our model changes.

(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:

Demo

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

Model

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

View

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

View.Coordinator

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. Use a UITextField so you can use functionality like .becomeFirstResponder()
  2. Perform an action when the text changes
  3. Notify other SwiftUI views that the text has changed

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

Model

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

View

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

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

@ObservedObject var model: MyTextFieldModel

with

@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天全站免登陆