Вычисленное свойство из дочерней модели представления не обновляет родительскую модель представления @ObservedObject

#ios #swift #mvvm #swiftui #combine

Вопрос:

У меня есть родительское представление и дочернее представление, каждое со своими собственными моделями представлений. Родительское представление вводит модель представления дочернего представления.

Родительское представление неправильно реагирует на изменения в вычисляемом свойстве дочернего isFormInvalid элемента (дочернее представление реагирует).

@Published не может быть добавлен в вычисляемое свойство, и другие вопросы/ответы, которые я видел в этой области, не были сосредоточены на наличии отдельных моделей представления, как это делает этот вопрос. Я хочу, чтобы отдельные модели представлений повышали тестируемость, так как дочернее представление может стать довольно сложной формой.

Вот файл для минимального воспроизведения проблемы:

 import SwiftUI

extension ParentView {
    final class ViewModel: ObservableObject {
        @ObservedObject var childViewViewModel: ChildView.ViewModel

        init(childViewViewModel: ChildView.ViewModel = ChildView.ViewModel()) {
            self.childViewViewModel = childViewViewModel
        }
    }
}

struct ParentView: View {
    @ObservedObject private var viewModel: ViewModel

    init(viewModel: ViewModel = ViewModel()) {
        self.viewModel = viewModel
    }

    var body: some View {
        ChildView(viewModel: viewModel.childViewViewModel)
        .navigationBarTitle("Form", displayMode: .inline)
        .toolbar {
            ToolbarItem(placement: .confirmationAction) {
                addButton
            }
        }
    }

    private var addButton: some View {
        Button {
            print("======")
            print(viewModel.childViewViewModel.$name)
        } label: {
            Text("ParentIsValid?")
        }
        .disabled(viewModel.childViewViewModel.isFormInvalid) // FIXME: doesn't work, but the actual fields work in terms of two way updating
    }
}

struct ParentView_Previews: PreviewProvider {
    static var previews: some View {
        let childVm = ChildView.ViewModel()
        let vm = ParentView.ViewModel(childViewViewModel: childVm)

        NavigationView {
            ParentView(viewModel: vm)
        }
    }
}

// MARK: child view

extension ChildView {
    final class ViewModel: ObservableObject {

        // MARK: - public properties

        @Published var name = ""
        
        var isFormInvalid: Bool {
            print("isFormInvalid")
            return name.isEmpty
        }
    }
}

struct ChildView: View {
    @ObservedObject private var viewModel: ViewModel
    
    init(viewModel: ViewModel = ViewModel()) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        Form {
            Section(header: Text("Name")) {
                nameTextField
            }
            Button {} label: {
                Text("ChildIsValid?: (String(!viewModel.isFormInvalid))")
            }
            .disabled(viewModel.isFormInvalid)
        }
    }
    
    private var nameTextField: some View {
        TextField("Add name", text: $viewModel.name)
            .autocapitalization(.words)
    }
}

struct ChildView_Previews: PreviewProvider {
    static var previews: some View {
        let vm = ChildView.ViewModel()
        ChildView(viewModel: vm).preferredColorScheme(.light)
    }
}
 

Спасибо!

Комментарии:

1. Не используйте вычисляемое свойство для isFormValid того, чтобы сделать его @Published свойством, и используйте didSet для name установки свойство isFormValid .

2. Однако как это будет работать с несколькими областями?

3. Вам нужно создать validate функцию в своей модели и вызывать ее всякий раз, когда какие-либо свойства изменяются.

Ответ №1:

Вычисленные свойства не вызывают никаких обновлений. Именно свойство изменено на @Publised запускает обновление, когда это происходит, вычисленное свойство переоценивается. Это работает так, как ожидалось, что вы можете увидеть в своем ChildView . Проблема, с которой вы сталкиваетесь, заключается в том, что ObservableObject s на самом деле не предназначены для цепочки (обновление до дочернего не вызывает обновления для родителя. Вы можете обойти этот факт, повторно опубликовав обновление от ребенка: у вас есть подписка на ребенка, и каждый раз, когда он запускает вручную триггер на родителе. objectWillChange objectWillChange

 extension ParentView {
    final class ViewModel: ObservableObject {
        @ObservedObject var childViewViewModel: ChildView.ViewModel
        private var cancellables = Set<AnyCancellable>()

        init(childViewViewModel: ChildView.ViewModel = ChildView.ViewModel()) {
            self.childViewViewModel = childViewViewModel
            childViewViewModel
                .objectWillChange
                .receive(on: RunLoop.main)
                .sink { [weak self] _ in
                    self?.objectWillChange.send()
                }
                .store(in: amp;cancellables)
        }
    }
}
 

Комментарии:

1. Это работает, спасибо. В какой степени это «работает против/вокруг фреймворка», и вы думаете, что это то, чего не хватает SwiftUI/Combine и что будет добавлено в будущем? Я пытаюсь оценить, насколько это банально и следует ли мне каким-то образом провести рефакторинг, чтобы избежать такого рода решений.