on
SwiftUI - Connecting the models
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)
}