In 2019, Apple introduced another powerful API alongside the Combine framework, diffable data sources. Diffable data sources make it almost trivial to build performant table and collection views. In this episode, I show you why diffable data sources work well with the Combine framework. I won't cover diffable data sources in detail in this series, but the API is straightforward and easy to pick up.
Creating a Data Source
The WeekViewController class is the data source of its table view. That is a common pattern. A diffable data source also acts as the data source of a table or collection view. The class we use in this episode is UITableViewDiffableDataSource and conforms to the UITableViewDataSource protocol. This means the week view controller should no longer be the data source of its table view.
Open Main.storyboard and navigate to the Week View Controller scene. Select the table view and open the Connections Inspector on the right. Remove the week view controller as the table view's data source.

We need to decide which object owns the diffable data source. The view controller seems the most obvious choice, but I prefer to make the view model the owner of the diffable data source. Why that is becomes clear in a few moments.
Open WeekViewModel.swift and add an import statement for the Combine framework at the top.
import UIKit
import Combine
final class WeekViewModel: WeatherViewModel {
...
}
Declare a private, variable property, dataSource, of type UITableViewDiffableDataSource?. UITableViewDiffableDataSource is a generic type. It defines a type for section identifiers and a type for item identifiers. Because the table view displays a single section, we can keep things simple. We use Int to identify sections and WeatherDayData to identify items.
import UIKit
import Combine
final class WeekViewModel: WeatherViewModel {
// MARK: - Properties
private var dataSource: UITableViewDiffableDataSource<Int, WeatherDayData>?
...
}
The section identifier type and the item identifier type need to conform to the Hashable protocol. Int already meets this requirement. WeatherDayData doesn't. Open WeatherDayData.swift and conform WeatherDayData to the Hashable protocol.
import Foundation
struct WeatherDayData: Decodable, Hashable {
...
}
Return to WeekViewModel.swift and implement a helper method with name bind(to:). It accepts a UITableView instance as its only argument.
// MARK: - Public API
func bind(to: tableView: UITableView) {
}
In bind(to:), the week view model creates an instance of the UITableViewDiffableDataSource class, passing in the table view as the first argument of the initializer. The second argument is a cell provider, a closure that accepts three arguments, the table view, an index path, and an item identifier. The return type of the closure is UITableViewCell?. The diffable data source executes the closure every time it needs a table view cell.
In the cell provider, we ask the table view to dequeue a table view cell for the given index path and cast it to WeatherDayTableViewCell. This should never fail so we throw a fatal error if the table view isn't able to dequeue a WeatherDayTableViewCell instance.
We create a WeatherDayViewModel object using the item identifier, a WeatherDayData object, and pass it to the table view cell's configure(with:) method. The closure returns the table view cell.
// MARK: - Public API
func bind(to: tableView: UITableView) {
// Create Table View Diffable Data Source
dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, weatherDayData -> UITableViewCell? in
guard let cell = tableView.dequeueReusableCell(withIdentifier: WeatherDayTableViewCell.reuseIdentifier, for: indexPath) as? WeatherDayTableViewCell else {
fatalError("Unable to Dequeue Weather Day Table View Cell")
}
cell.configure(with: WeatherDayViewModel(weatherDayData: weatherDayData))
return cell
}
}
With the diffable data source created, it is time to put it to use. Declare a private, variable property, subscriptions, of type Set<AnyCancellable> and set its initial value to an empty set.
private var subscriptions: Set<AnyCancellable> = []
Define a method with name start(). In this method, the view model subscribes to weatherDataStatePublisher and applies the compactMap operator. The closure of the compactMap operator returns the value of the dailyData property of the WeatherData object. The view model invokes the sink(receiveValue:) method to subscribe to the resulting publisher. The magic happens in the closure that is passed to the sink(receiveValue:) method.
func start() {
weatherDataStatePublisher
.compactMap { $0.weatherData?.dailyData }
.sink { [weak self] weatherData in
}
.store(in: &subscriptions)
}
To update the table view, we need to apply a snapshot to the diffable data source. As the name suggests, a snapshot describes the data the table view should display. We create an instance of the NSDiffableDataSourceSnapshot class. Like UITableViewDiffableDataSource, NSDiffableDataSourceSnapshot is a generic type. It defines a type for section identifiers and a type for item identifiers. Both types match those of the diffable data source the snapshot is applied to.
// Create Diffable Data Source Snapshot
var snapshot = NSDiffableDataSourceSnapshot<Int, WeatherDayData>()
We need to populate the snapshot with data. We append the sections to the snapshot by invoking its appendSections(_:) method. This method accepts an array of section identfiers. Because the table view displays a single section, we pass an array with 0 to appendSections(_:).
// Create Diffable Data Source Snapshot
var snapshot = NSDiffableDataSourceSnapshot<Int, WeatherDayData>()
// Append Sections
snapshot.appendSections([0])
We provide the items for each section by invoking the appendItems(_:) method. This method accepts an array of item identifiers as its first argument and a section identfier as its second argument. Because the table view displays a single section, we can omit the second argument.
// Create Diffable Data Source Snapshot
var snapshot = NSDiffableDataSourceSnapshot<Int, WeatherDayData>()
// Append Sections
snapshot.appendSections([0])
// Append Items
snapshot.appendItems(weatherData)
What is left is applying the snapshot to the data source. The data source takes care of the rest. We invoke the apply(_:animatingDifferences:completion:) method, passing in the snapshot. We pass false as the second argument to disable animations and ignore the third argument, a completion handler.
func start() {
weatherDataStatePublisher
.compactMap { $0.weatherData?.dailyData }
.sink { [weak self] weatherData in
// Create Diffable Data Source Snapshot
var snapshot = NSDiffableDataSourceSnapshot<Int, WeatherDayData>()
// Append Sections
snapshot.appendSections([0])
// Append Items
snapshot.appendItems(weatherData)
// Apply Snapshot
self?.dataSource?.apply(snapshot, animatingDifferences: false)
}
.store(in: &subscriptions)
}
Open WeekViewController.swift and invoke the view model's start() method in viewDidLoad().
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Setup Bindings
setupBindings()
// Setup View
setupView()
// Start View Model
viewModel?.start()
}
Navigate to the setupBindings() method. Invoke the view model's bind(to:) method, passing in the week view controller's table view.
// MARK: - Helper Methods
private func setupBindings() {
// Bind to Table View
viewModel?.bind(to: tableView: tableView)
}
Cleaning Up the Week View Controller
Because the week view model decides when it is appropriate to fetch weather data, it no longer makes sense to support pull-to-refresh. In WeekViewController.swift, remove the setupRefreshControl() method and its reference in setupView(). We also remove thedidRefresh(_:)` method.
// MARK: - View Methods
private func setupView() {
// Configure Message Label
messageLabel.text = "Cloudy was unable to fetch weather data."
}
The WeekViewControllerDelegate protocol is no longer needed and we can also delete the delegate property.
Because the view model's diffable data source acts as the table view's data source, the WeekViewController class no longer needs to conform to the UITableViewDataSource protocol or implement any of its methods.
Cleaning Up the Week View Model
The week view controller no longer asks its view model what it needs to display in its table view. This means we can remove the numberOfSections and numberOfDays computed properties and the viewModel(for:) method in WeekViewModel.swift.
The week view model no longer holds on to the weather data its view controller needs to display. Every time weatherDataStatePublisher emits weather data, the week view model creates a snapshot and applies it to its data source. This means we can remove the weatherData property in WeekViewModel.swift.
Cleaning Up Root View Controller
Open RootViewController.swift. The root view controller no longer acts as the delegate of the week view controller. Navigate to the prepare(for:sender:) method and remove the line on which the root view controller sets itself as the delegate of the week view controller.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let viewModel = viewModel, let identifier = segue.identifier else {
return
}
switch identifier {
...
case Segue.weekView:
guard let destination = segue.destination as? WeekViewController else {
fatalError("Unexpected Destination View Controller")
}
// Update Week View Controller
self.weekViewController = destination
self.weekViewController.viewModel = WeekViewModel(weatherDataStatePublisher: viewModel.weatherDataStatePublisher)
...
}
}
We also remove the extension we created to conform the RootViewController class to the WeekViewControllerDelegate protocol.
Cleaning Up the Root View Model
Open RootViewModel.swift and remove the refreshData() method. We no longer need it.
Build and Run
Build and run the application to make sure the week view controller displays the weather data the root view model fetches from the weather service.

What's Next?
We drastically simplified the implementation of Cloudy by leveraging two modern technologies, Combine and diffable data sources. The week view model no longer holds on to state. It passes the weather data it receives from weatherDataStatePublisher to its data source. The week view controller simply displays a table view. It is the diffable data source that populates the table view.