У меня есть View в SwiftUI, который содержит форму с несколькими входами. Входные данные обернуты с помощью Label, TextField и Validator в собственные пользовательские представления. Может быть два типа этого входного представления:
1), содержащее TextField (то есть SwiftUI),
2), содержащее оболочку UIKit вокруг UITextView или UITextField
Вот примеры кода :
class ViewModel: ObservableObject {
@Published var details: String = "" {
didSet {
print("Details: \(details)")
}
}
@Published var detailsValidation = Validation(onEditing: true) {
didSet {
print("Validation: \(detailsValidation.errors)")
}
}
}
struct FormView: View {
@ObservedObject var viewModel : ViewModel
func makeNameInput() -> some View {
LabeledInput(label: "Name", input: self.$viewModel.name, rules: FormRules.name, validation: $viewModel.nameValidation, onEditingChanged: { isEditing in
self.avoider.editingField = 0
})
.textContentType(.name)
.keyboardType(.default)
.autocapitalization(.words)
.avoidKeyboard(tag: 0)
.frame(height: 80)
}
func makeDescriptionInput() -> some View {
LabeledTextArea(label: "Description", input: self.$viewModel.description, rules: FormRules.description, validation: self.$viewModel.descriptionValidation, onEditingChanged: { isEditing in
self.avoider.editingField = 3
})
.keyboardType(.default)
.autocapitalization(.sentences)
.avoidKeyboard(tag: 3)
.frame(height: 150)
}
}
LabeledTextArea выглядит примерно так
struct LabeledTextArea: View {
// MARK: - Properties
let label: String
let dividerColor: Color
let dividerHidden: Bool
// MARK: - Binding
@Binding var input: String
// MARK: - Actions
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
// MARK: - Validation
@ObservedObject var validator : FieldValidator<String>
// MARK: - State
//@State private var isEditing = true
init(label: String, input: Binding<String>, rules: [Rule<String>] = [], validation: Binding<Validation>? = nil, dividerColor: Color = Color("FormGray"), dividerHidden: Bool = false, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = { }) {
print("LabeledTextArea init")
self.label = label
self._input = input
self.dividerColor = dividerColor
self.dividerHidden = dividerHidden
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
self.validator = FieldValidator(input: input, rules: rules, validation: validation ?? .constant(Validation()))
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
if label.isNotEmpty {
Text("\(label.uppercased())")
.font(.custom("AvenirNext-Regular", size: 11))
.foregroundColor(!self.validator.validation.isEdited ? Color("LabelBlue")
: self.validator.validation.isValid ? Color("GreenText") : Color(.red))
}
TextView(text: $validator.value, /*isEditing: $isEditing, */ font: UIFont(name: "AvenirNext-Regular", size: 15)!, textColor: UIColor(named: "InputBlue")!, isEditable: true, isSelectable: true, isScrollingEnabled: true, isUserInteractionEnabled: true, onEditingChanged: { editing in
if self.validator.validation.onEditing {
self.validator.validateField()
}
self.onEditingChanged(editing)
self.validator.validation.isEdited = true
//self.isEditing.toggle()
self.validator.objectWillChange.send()
}, onCommit: {
self.validator.validateField()
self.validator.validateFieldAsync()
self.onCommit()
})
.frame(maxHeight: 120)
if dividerHidden == false {
Divider().background(!self.validator.validation.isEdited ? dividerColor
: self.validator.validation.isValid ? Color("GreenText") : Color(.red) )
}
}
.padding(.horizontal, 24)
.padding(.top, 10)
.onReceive(self.validator.objectWillChange) { _ in
print("Validator CHANGE!")
}
}
}
Оболочка TextView -
@available(iOS 13.0, *)
struct TextView: UIViewRepresentable {
@ObservedObject private var keyboardEvents = KeyboardEvents()
@Binding var text: String
private var isEditing: Binding<Bool>?
let onEditingChanged: (Bool) -> Void
let onCommit: () -> Void
/// .....
func updateUIView(_ textView: UITextView, context: Context) {
//textView.text = text
if let isEditing = isEditing {
if isEditing.wrappedValue {
textView.becomeFirstResponder()
} else {
textView.resignFirstResponder()
}
}
}
final class Coordinator: NSObject, UITextViewDelegate {
private let parent : TextView
init(_ parent: TextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
parent.onEditingChanged(true)
}
func textViewDidBeginEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.isEditing?.wrappedValue = true
}
parent.onEditingChanged(true)
}
func textViewDidEndEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.isEditing?.wrappedValue = false
}
parent.onEditingChanged(false)
parent.onCommit()
}
}
}
У меня есть TabView, и я использую это представление в одной вкладке, и код прекрасно работает. Затем я использую его в другой вкладке, но есть NavigationView и доступ к нему со страницы сведений (глубокая навигация). И тот же FormView не работает должным образом, проверка не работает, а модель представления не хранит значения из пользовательских входных данных в оболочке UIKit! Входы от SwiftUI работают как положено.
После нескольких часов отладки я обнаружил, что это представление отображается 3 раза, а ViewModel создается 3 раза, и что интересно, этот LabeledInput, LabeledTextArea размещается внутри того же FormView в методах onEditingChanged (я полагаю, также в случае $ viewModel.name, $ viewModel.description используют совершенно разные экземпляры ViewModel (ссылки). Я не знаю, как в SwiftUI возможно, чтобы дочерние представления внутри родительского представления, имеющие @ObservedObject, ссылались на общее количество разных экземпляров, вызывая неправильное хранение данных привязки viewmodal и view не обновляются корректно на модели представления @ Обновления содержимого опубликованных свойств.
Я отлаживал ссылки на эти модели представления на этапе создания, а затем в обратных вызовах onEditingChanged в LabeledInputs.
Здесь вот что я получаю:
--------------------------------
(lldb) po self
<NewDealViewModel: 0x12dd1fc00>
NewDealViewModel init
(lldb) po self
<NewDealViewModel: 0x12dca6890>
NewDealViewModel init
(lldb) po self
<NewDealViewModel: 0x13064f630>
NewDealViewModel init
// Text Area Input
(lldb) po self
▿ _viewModel : ObservedObject<NewDealViewModel>
▿ wrappedValue : <NewDealViewModel: 0x12dd1fc00>
(lldb) po self.viewModel
<NewDealViewModel: 0x12dd1fc00>
// SwiftUI Input
po self.viewModel
<NewDealViewModel: 0x13064f630>
Как видите, LabeledInput на основе SwiftUI ссылается на последнюю созданную модель представления, в то время как LabeledTextArea на основе UIKit ссылается на f Первый экземпляр модели представления!
И это не работает только в том случае, если NavigationLink (destintation: FormView ()) помещен в View, который помещается в стек NavigationView (как второе представление), помещенный в TabView, тогда как он работает нормально, когда это NavigationLink (назначение: FormView ()) помещено в root представление NavigationView в TabView на другой вкладке!
Я решил эту проблему после нескольких часов отладки очень странным использованием оболочки свойств @State со ссылочным типом ObservableObject.
Я использовал вид оболочки с таким кодом:
struct FormViewWrapper : View {
// MARK: - Observed
@State var viewModel = ViewModel()
// MARK: - Binding
@Binding var added: String?
init(added: Binding<String?> = .constant(nil)) {
self._added = added
}
var body: some View {
FormView(viewModel: viewModel, added: $added)
}
}