Building a SwiftUI app using MVVM architecture — Part 5: Connecting the models with the UI using MVVM
The MVVM architecture is a perfect fit for SwiftUI as one can make a view model conform the ObservableObject and mark its underlying properties as @Published so that views always reflect the latest model changes.
Focusing on these four @Published properties from the Headlines app.
@Published var preferences: UserPreferences
@Published var headlines: [Headlines] = []
@Published var isLoading = true
@Published var keyword: String = “”
The @Published attribute is a property wrapper which allows access to a Publisher type using either the `$` syntax or the projectedValue struct property.
@propertyWrapper public struct Published<Value> { /// Initialize the storage of the Published property as well as the corresponding `Publisher`.
public init(initialValue: Value) {…} /// The property that can be accessed with the `$` syntax and allows access to the `Publisher`
public var projectedValue: Published<Value>.Publisher { mutating get }}
New values assigned to the @Published `keyword` property can be then processed using the underlying Publisher type.
self.$keyword // or self._keyword.projectedValue
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.sink { self.sortArticles(byKeyword: $0) }
.store(in: &cancellable)
The `sink` method creates a subscriber and immediately receive values from the upstream publisher enabling us to sort the articles by keyword.
Notice how the `$` syntax or `projectedValue` property from @Published is similar to the @State ones however both provide with different types —respectively a Publisher type and a Binding type — .
Our views, while observing these @Published properties from our ObservableObject, will actually react to any new values emitted by these underlying publishers through the `objectWillChange` synthesizer.
This shows how tightly related SwiftUI and Combine are.
When the user select new preferences, the `UserPreferences` object is mutated accordingly and changes are automatically reflected to the UI using the @ObservedObject property from the views.
struct PreferencesView: View {
@ObservedObject var viewModel: HeadlinesViewModel {...}
}
Data is retrieved from the web-service and assigned to the headlines property triggering a new UI update. In the meantime, `isLoading` is set to false which hides the loader and presents the latest data on screen.
func fire() { let data = webService.fetch(preferences: preferences).map { value -> [Headlines] in let headlines = value.map { (section, result) -> Headlines in
let isFavorite = self.preferences.categories
.first(where: { $0.name == section })?
.isFavorite return Headlines(
name: section, isFavorite: isFavorite ?? false,
articles: result.articles
)
} return headlines.sortedFavorite() }.handleEvents(receiveSubscription: { _ in
self.isLoading = true
}, receiveOutput: { _ in
self.isLoading = false
})
.receive(on: DispatchQueue.main)
.assign(to: \HeadlinesViewModel.headlines, on: self) cancellable?.insert(data)
}
— Flow
Views use an @ObservedObject property to observe the view model through its @Published properties. Whenever these properties are mutated, the UI is updated.
Continue with Part 6: Integrating UIKit.