简体   繁体   English

如何使用 Combine 跟踪 UIViewRepresentable class 中的 UITextField 更改?

[英]How can I use Combine to track UITextField changes in a UIViewRepresentable class?

I have created a custom text field and I'd like to take advantage of Combine.我创建了一个自定义文本字段,我想利用 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 结构中。

My CustomTextField struct conforms to UIViewRepresentable.我的 CustomTextField 结构符合 UIViewRepresentable。 Inside this struct, there is a NSObject class called Coordinator and it conforms to UITextFieldDelegate.在这个结构中,有一个名为 Coordinator 的 NSObject class,它符合 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.我已经在使用其他 UITextField 委托方法,但找不到与我的自定义修饰符完全一样的方法。 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.无论如何,我觉得最好将这个新的自定义 textFieldDidChange 方法放在 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.我的 CustomTextField 用于 SwiftUI 视图,并附有我的自定义修饰符。 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.我想在我的协调器 class 中使用它,以及我的 UITextFieldDelegate 方法。

This is my CustomTextField这是我的 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.我一直在观看有关 Combine 的视频,我想开始在我正在构建的新应用程序中使用它。 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.我想将一个名为 textFieldDidChange 的 function 添加到我的协调器 class 中,每次我的文本字段发生更改时都应该触发它。 It must utilise Combine.它必须使用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.我已经将 model 和协调器折叠成一个 class,虽然它适用于我的示例,但并不总是可行或可取的。 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.如果 model 和 coordinator 不能相同,则不能依赖 model 属性的 didSet 方法来更新 textField。 So instead, I'm making use of the Combine publisher we get for free using a @Published variable inside our model.因此,我正在使用我们的 model 中的@Published变量免费获得的 Combine 发布者。

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通过使model.texttextField.text保持同步来建立单一的事实来源

    1. Use the publisher provided by the @Published property wrapper to update textField.text when model.text changesmodel.text更改时,使用@Published属性包装器提供的发布者更新textField.text

    2. Use the .addTarget(:action:for) method on textField to update model.text when textfield.text changes使用textField上的.addTarget(:action:for)方法在textfield.text更改时更新model.text

  2. Execute a closure called textDidChange when our model changes.当我们的 model 发生变化时,执行一个名为textDidChange的闭包。

(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). (我更喜欢将.addTarget用于#1.2 而不是通过NotificationCenter ,因为它的代码更少,可以立即工作,而且 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 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()使用UITextField这样您就可以使用.becomeFirstResponder()类的功能
  2. Perform an action when the text changes文本更改时执行操作
  3. Notify other SwiftUI views that the text has changed通知其他 SwiftUI 视图文本已更改

I think you can satisfy all these using a single model class, and the UIViewRepresentable struct.我认为您可以使用单个 model class 和UIViewRepresentable结构来满足所有这些要求。 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.text ),它可以与其他采用StringBinding<String>的 SwiftUI 视图互换使用。

Model 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如果您不需要上面的#3,您可以替换

@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)\"")
        }

    }
}

I also needed to use a UITextField in SwiftUI, so I tried the following code:我还需要在 SwiftUI 中使用 UITextField,所以我尝试了以下代码:

struct MyTextField: UIViewRepresentable {
  private var placeholder: String
  @Binding private var text: String
  private var textField = UITextField()

  init(_ placeholder: String, text: Binding<String>) {
    self.placeholder = placeholder
    self._text = text
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(textField: self.textField, text: self._text)
  }

  func makeUIView(context: Context) -> UITextField {
    textField.placeholder = self.placeholder
    textField.font = UIFont.systemFont(ofSize: 20)
    return textField
  }

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

  class Coordinator: NSObject {
    private var dispose = Set<AnyCancellable>()
    @Binding var text: String

    init(textField: UITextField, text: Binding<String>) {
      self._text = text
      super.init()

      NotificationCenter.default
        .publisher(for: UITextField.textDidChangeNotification, object: textField)
        .compactMap { $0.object as? UITextField }
        .compactMap { $0.text }
        .receive(on: RunLoop.main)
        .assign(to: \.text, on: self)
        .store(in: &dispose)
    }
  }
}

struct ContentView: View {
  @State var text: String = ""

  var body: some View {
    VStack {
      MyTextField("placeholder", text: self.$text).padding()
      Text(self.text).foregroundColor(.red).padding()
    }
  }
}

I'm a little confused with what you're asking because you're talking about UITextField and SwiftUI.我对你的问题有点困惑,因为你在谈论UITextField和 SwiftUI。

What about something like this?这样的事情呢? It doesn't use UITextField instead it uses SwiftUI's TextField object instead.它不使用UITextField而是使用 SwiftUI 的TextField object 代替。

This class will notify you whenever there's a change to the TextField in your ContentView .只要ContentView中的TextField发生更改,此 class 就会通知您。

class CustomModifier: ObservableObject {
    var observedValue: String = "" {
        willSet(observedValue) {
            print(observedValue)
        }
    }
}

Ensure that you use @ObservedObject on your modifier class and you'll be able to see the changes.确保在修改器 class 上使用@ObservedObject ,您将能够看到更改。

struct ContentView: View {
    @ObservedObject var modifier = CustomModifier()

    var body: some View {
        TextField("Input:", text: $modifier.observedValue)
    }
}

If this is completely off track with what you're asking then can I suggest the following article, which may help?如果这与您的要求完全不同,那么我可以建议以下文章,这可能会有所帮助吗?

https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2 https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何制作通用 UIViewRepresentable 结构? - How can I make a generic UIViewRepresentable struct? 如何在 SwiftUI 中的 UIViewRepresentable 中更改 UITextField 大小 - How to change UITextField size inside UIViewRepresentable in SwiftUI 如何将 ObservableObject 与 UIViewRepresentable 一起使用 - How to use ObservableObject with UIViewRepresentable 如何从 NSViewRepresentable / UIViewRepresentable 的协调器 class 中访问 EnvironmentObject? - How can I access an EnvironmentObject from within a NSViewRepresentable / UIViewRepresentable 's Coordinator class? 如何检查 UITextField 何时更改? - How do I check when a UITextField changes? 如何使用didSet更改UITextField(IBOutlet)的文本? - How can I use didSet to change the text of a UITextField (IBOutlet)? 如何重用 UIViewRepresentable UIKit 组件? - How do I reuse a UIViewRepresentable UIKit component? 如何触发 UIViewRepresentable 的 updateUIView? - How do I trigger updateUIView of a UIViewRepresentable? 在 SwiftUI 中,如何访问 UIViewRepresentable 中 .foregroundColor 和其他修饰符的当前值? - In SwiftUI, how can I access the current value of .foregroundColor and other modifiers inside my UIViewRepresentable? 如何在没有固定宽度的情况下将文本包装在 UILabel 中(通过 UIViewRepresentable)? - How can I get text to wrap in a UILabel (via UIViewRepresentable) without having a fixed width?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM