SwiftUI: ошибка с ObservableObject во View, дочерние представления имеют разные ссылки на Observable Object

У меня есть 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, ссылаются на совокупность различных экземпляров, в результате чего данные привязки хранятся в неправильном представлении, а представление не обновляется правильно при обновлении содержимого модели представления @Published properties.

Я отлаживал ссылки на эти модели просмотра на этапе создания, а затем в обратных вызовах 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 ссылается на первую созданную модель представления!

И это не работает только в том случае, если NavigationLink(destintation: FormView()) помещается внутри View, который помещается в стек NavigationView (как второе представление), помещенный внутри TabView, в то время как он работает нормально, когда этот NavigationLink(destination: FormView()) размещается в корневом представлении 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)
    }
}

0 ответов

Другие вопросы по тегам