This repo demonstrates how UIKIt APIs can be wrapped with a declarative API layer. The UITableView
API is used as an example.
The approach taken in this example allows us to describe table views in terms of simple data models instead of through implementation of UITableViewDataSource
or UITableViewDelegate
.
This improves the UI code in several ways:
- It becomes easier to reason about
- It becomes less redundant
- It becomes easy to test
Using the wraper API from this repo table views can be described in terms of a pure function that produces a description of a table view based on well defined input data:
func tableViewModelForUserList(users: [User]) -> TableViewModel {
return TableViewModel(sections: [
TableViewSectionModel(cells:
users.map { viewModelForUser($0) }
)
])
}
Let's dive right into an example of how the declarative API can be used. This repo provides TableViewModel
, TableViewSectionModel
and TableViewCellModel
to describe what a table view should look like. These types are simple structs.
Below you can see a function tableViewModelForUserList
that creates a TableViewModel
based on a list of users as input:
func tableViewModelForUserList(users: [User], deleteClosure: CommitEditingClosure) -> TableViewModel {
return TableViewModel(sections: [
TableViewSectionModel(cells:
users.map { viewModelForUser($0, deleteClosure: deleteClosure) }
)
])
}
The tableViewModelForUserList
creates a table with one section; within that section it creates an individual cell for each user by calling viewModelForUser
:
func viewModelForUser(user: User, deleteClosure: CommitEditingClosure) -> TableViewCellModel {
// updates the UITableViewCell to represent the data from the TableViewCellModel
func applyViewModelToCell(cell: UITableViewCell, user: Any) {
guard let cell = cell as? UserCell else { return }
guard let user = user as? User else { return }
cell.nameLabel.text = user.username
}
return TableViewCellModel(
cellIdentifier: "UserCell",
applyViewModelToCell: applyViewModelToCell,
commitEditingClosure: deleteClosure,
customData: user
)
}
The beauty of this approach is that the description of the table view is created by pure functions that have a well defined input. This isolates the view code well from the rest of our application code and makes it easy to test that a certain input state results in a certain UI.
To display the TableViewModel
on screen we need to provide it to a TableViewShim
that implements the UITableViewDataSource
and UITableViewDelegate
methods for us:
override func viewDidLoad() {
self.tableViewRenderer = TableViewShim(cellTypes: [
CellTypeDefinition(
nibFilename: "UserCell",
cellIdentifier: "UserCell"
)], tableView: tableView)
self.tableViewRenderer.tableViewModel = tableViewModelForUserList(
users,
deleteClosure: deleteUser
)
}
This setup code provides the TableViewShim
with the definition of where to find the UITableViewCells
, with a UITableView
for which it should provide data and with a description of the TableViewModel
that should be displayed.
With all this in place the table view content will be displayed on screen - without requiring us to implement a large amount of protocol requirements.
Ideally we would simply assign a new TableViewModel
to the TableViewShim
in order to update the UI. In practice this requires quite some effort as we need to compare the old to the new model in order to detect & animate the changes. For this automatic, advanced approach check out my expirement on a truly declarative UI layer that reacts automatically which has support for an auto-updating table view.
For this simple API wrapper we provide the TableViewShim
with a new TableViewModel
whenever our state updates; but we also provide it with the details about which type of change occured so it doesn't need to derive this information itself.
Here's an example of how to add an entry to a user list:
@IBAction func addUser(sender: AnyObject) {
// Update underlying data source
self.users.append(
User(username: NSUUID().UUIDString)
)
// Update UI
self.tableViewRenderer.newViewModelWithChangeset(
tableViewModelForUserList(
users,
deleteClosure: deleteUser
),
changeSet: .Add(NSIndexPath(
forRow: self.users.count - 1,
inSection: 0
))
)
}
It would be great to have a truly declarative view layer on top of UIKit that works like Facebook's React and automatically updates the UI whenever the description of the UI changes. I'm experimenting with this approach in the UILib repository.