繁体   English   中英

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

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

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

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

我已经在使用其他 UITextField 委托方法,但找不到与我的自定义修饰符完全一样的方法。 有些方法很接近,但并不完全按照我想要的方式行事。 无论如何,我觉得最好将这个新的自定义 textFieldDidChange 方法放在 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 视图,并附有我的自定义修饰符。 当文本字段发生更改时,我可以做一些事情。 修改器也在使用组合。 它工作得很好,但我不希望这个功能以修饰符的形式出现。 我想在我的协调器 class 中使用它,以及我的 UITextFieldDelegate 方法。

这是我的 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 的视频,我想开始在我正在构建的新应用程序中使用它。 我真的认为在这种情况下使用它是正确的,但仍然不太确定如何实现它。 我真的很感激一个例子。

总结一下:

我想将一个名为 textFieldDidChange 的 function 添加到我的协调器 class 中,每次我的文本字段发生更改时都应该触发它。 它必须使用Combine。

提前致谢

更新的答案

在查看了您更新的问题后,我意识到我的原始答案可能需要一些清理。 我已经将 model 和协调器折叠成一个 class,虽然它适用于我的示例,但并不总是可行或可取的。 如果 model 和 coordinator 不能相同,则不能依赖 model 属性的 didSet 方法来更新 textField。 因此,我正在使用我们的 model 中的@Published变量免费获得的 Combine 发布者。

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

  1. 通过使model.texttextField.text保持同步来建立单一的事实来源

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

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

  2. 当我们的 model 发生变化时,执行一个名为textDidChange的闭包。

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

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

演示

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

看法

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

原始答案

听起来你有几个目标:

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

我认为您可以使用单个 model class 和UIViewRepresentable结构来满足所有这些要求。 我以这种方式构造代码的原因是为了让您拥有单一的事实来源 ( model.text ),它可以与其他采用StringBinding<String>的 SwiftUI 视图互换使用。

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

看法

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,您可以替换

@ObservedObject var model: MyTextFieldModel

@ObservedObject private var model = MyTextFieldModel()

演示

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

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

    }
}

我还需要在 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()
    }
  }
}

我对你的问题有点困惑,因为你在谈论UITextField和 SwiftUI。

这样的事情呢? 它不使用UITextField而是使用 SwiftUI 的TextField object 代替。

只要ContentView中的TextField发生更改,此 class 就会通知您。

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

确保在修改器 class 上使用@ObservedObject ,您将能够看到更改。

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

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

如果这与您的要求完全不同,那么我可以建议以下文章,这可能会有所帮助吗?

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

暂无
暂无

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

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