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)
}