#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 и что будет добавлено в будущем? Я пытаюсь оценить, насколько это банально и следует ли мне каким-то образом провести рефакторинг, чтобы избежать такого рода решений.