Post

Replies

Boosts

Views

Activity

Reply to Xcode 14: Publishing changes from within view updates
I may have a slightly different opinion about whether it's a huge thing, not to allow changes of the published properties directly from views ;) In my opinion, this could also be considered a code smell if the pattern should follow MVVM principles. When we employ the MVVM pattern, we do not have writable published properties, only readable. Changes are notified to the view model via functions. These "intents" then get processed by the view model which in turn may change the published value, or it may not. The published value is a function of the view model, that is, it's the view model which determines the view's state, not the view. However, even when a view model determines the published value, it still must not change it during the view updates, otherwise the same warning would occur. There is one pattern which may alleviate the issue: For any practical reasons, a ViewModel should be an actor. A MainActor would be suitable. So, we start with our ViewModel to be a MainActor: @MainActor final class SomeViewModel: ObservableObject {     @Published private(set) var state: State = .init() ... Notice, that the published value state needs to have an initial value, and it's setter is private. In order to let a view express an intent, which may change the state, we define a function: @MainActor final class SomeViewModel: ObservableObject {     @Published private(set) var state: State = .init() nonisolated func updateValue(_ value: T) { ... } Note, the function updateValue(_:) is declared nonisolated. In order to make it work with the main actor though, we need to dispatch onto the main thread. This will be accomplished with another private helper function, which will be executed on the actor: nonisolated func updateValue(_ value: T) { Task { await _updateValue(value) } } private func _updateValue(_ value: T) { let newValue = someLogic(value) self.state.value = newValue } This has two desired side effects: First we dispatch on the main thread (_updateValue(_:) which is required by the actor And because of the dispatching we ensure (or strongly assume) that the view update phase is finished when this will get executed (thus, no warnings anymore) Note: the function someLogic(_: T) should be a pure, synchronous function, which determines the logic, i.e. the new state, based on the current state and the input. Now in a View we can call updateValue(:_) from anywhere, for example: Button { viewModel.updateValue(false) } Note: the action function is NOT declared to be executed on the main thread, even though it will be executed on the main thread. An isolated main actor function would require the closure to be dispatched explicitly onto the main thread. However, this function is nonisolated - thus, we can call it from any thread. Of course, we also can construct a binding in the body of a parent view and pass this along to child views: let binding: Binding<Bool > = .init( get { viewModel.state.value } set { value in viewModel.updateValue(value) } ) So, with this "pattern" we get some desired benefits. We also "fix" this issue due to the dispatching.
Sep ’22