Project on GitHub
Swift 5
Xcode 11
It is many years that we use UI data source APIs like numberOfSections, numberOfItemsInSection, and cellForItemAt to drive table and collection views. This technique is very simple and flexible as we can feed a complex UI with only a two-dimensional array. But it was not the whole story. In practice, when the data comes from complicated data sources like Core Data and web services, synchronising the data between the data source and the UI turns to a difficult job. Probably you have encountered this error message in your career many times:
***Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (10) must be equal to the number of sections contained in the collection view before the update (10), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted).***
Xcode Debugger
To solve the above problem, Apple introduced the “Diffable Data Source” API in WWDC19. To understand how it works, you should watch this WWDC video and read the interesting sample project that Apple engineers used to explain the new API. The architecture of Apple’s sample project is MVC to merely focus on the Diffable Data Source mechanics. So I decided to convert the architecture of the WiFi sections of the app to the clean architecture to show how to use Diffable Data Source with clean architecture.
WiFi Settings Original Structure
The WiFi part of the app consists of WiFiSettingsViewController that drives the UI and asks for data from WiFiController. The WiFiController acts as an interactor, so to keep changes as simple as possible I won’t change its name. So hereafter the WiFiController would be our interactor, and in another words, Use Case.
As you can see, the WiFiSettingsViewController and the WiFiController have been coupled together tightly. We want to have a clean architectrue that every modules are loosely coupled.
WiFi Settings Clean Architecture
As you see in the above architecture, all classes depends on either protocols or structs. So all classes are lossely coupled to each other.
From a higher view point, the View Module depends on the Interactor and Presenter modules, the Presenter depends on the Interactor and the Interactor does not depend on anything.
WiFi Interactor
The WiFi interactor is responsible for detecting available networks and reporting the current state of connected network and WiFi signal. As this is a sample project, all these data are generated on a random basis in the WiFiController class but in a real project, providing these information is regarded as detailed implementation and must be provided by service providers to the interactor. So for the sake of simplicity we have omitted the implementation of the service provider module, but if it was available, the structure would be as below:
WiFi Presenter
The responsibility of presenter is to convert the Network instances, receiving from interactor, into proper NetworkViewModel instances and deliver them to the view controller. One of the goals of this architecture is delivering data to the view controller in a way that the view controller can represent it directly without needing to do any processing on it.
WiFi View Controller
This is where the diffable data source happens. Note that the diffable data source is a technique to drive the UI in UIKit, so it should no go beyound the UI scope. This scope here is WiFiSettingsViewController. This view controller is the place that all UIKit dependent codes should happen.
If take a look at that view controller, you will see that it is very clean, small and stright forward. The view controller implements the presenter callback as below:
extension WiFiSettingsViewController: WifiPresenterDelegate {
func wifiPresenterDidUpdate(_: WifiPresenter, wifiEnabled: Bool, configItem: NetworkViewModel, connectedNetwork: NetworkViewModel?, avaialbleNetworks: [NetworkViewModel]?) {
currentSnapshot = NSDiffableDataSourceSnapshot<Section, NetworkViewModel>()
currentSnapshot.appendSections([.config])
currentSnapshot.appendItems([configItem])
if let connectedNetwork = connectedNetwork {
currentSnapshot.appendItems([connectedNetwork])
}
if let availableNetworks = avaialbleNetworks {
currentSnapshot.appendSections([.networks])
currentSnapshot.appendItems(availableNetworks)
}
self.dataSource.apply(currentSnapshot, animatingDifferences: !firstShow && self.isVisible)
firstShow = false
}
}
There are two simple notes in the above code. First, I check for the first show, to show the list of available WiFis without animation. and the second, again to show the table view without animations, if the view controller is not visible. Recently, we encounter a warning message in the debugger that says we are displaying the contents of a table view while it is not visible. Showing the table view without animation is a workaround for that.
I hope you enjoy it. Feel free to ask your questions and leave your comments below.
Project on GitHub
Swift 5
Xcode 11