Обновленный ответ
Просмотрев ваш обновленный вопрос, я понял, что мой оригинальный ответ может использовать некоторую очистку. Я свел модель и координатора в один класс, который, хотя и работал для моего примера, не всегда выполним или желателен. Если модель и координатор не могут быть одинаковыми, вы не можете полагаться на метод didSet свойства модели для обновления textField. Поэтому вместо этого я использую издателя Combine, который мы бесплатно получаем, используя переменную @Published
внутри нашей модели.
Ключевые вещи, которые нам нужно сделать:
Создайте единый источник правды, поддерживая синхронизацию model.text
и textField.text
Используйте издателя, предоставляемого оболочкой свойства @Published
, для обновления textField.text
приmodel.text
изменения
Используйте метод .addTarget(:action:for)
для textField
, чтобы обновить model.text
, когда textfield.text
изменится
Выполнить замыкание под названием 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()
}
}
Модель
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
}
}
}
Оригинальный ответ
Похоже, у вас есть несколько целей:
- Использованиеa
UITextField
, чтобы вы могли использовать такие функции, как .becomeFirstResponder()
- Выполнить действие при изменении текста
- Уведомить другие представления SwiftUI об изменении текста
Я думаю, что вы можете удовлетворить все эти требования, используя один класс модели и структуру UIViewRepresentable
. Причина, по которой я структурировал код таким образом, заключается в том, что у вас есть один источник правды (model.text
), который можно использовать взаимозаменяемо с другими представлениями SwiftUI, которые принимают String
или 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 выше, вы можете заменить
@ObservedObject var model: MyTextFieldModel
на
@ObservedObject private var model = MyTextFieldModel()
Demo
Вот демонстрационный вид, показывающий всю эту работу
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)\"")
}
}
}