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

У меня есть 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)
    }
}
...