Empty View With Diffable Datasource
This post is brought to you by Emerge Tools, the best way to build on mobile.
Among my favorite APIs introduced at W.W.D.C. is without a doubt the new diffable data source for both table and collection views. Replacing a decade old protocol, it brings about a robust way of expressing what should show when, where the truth is and, finally, a way to forgo the death trap that is batch updates.
While rewriting the list view of Spend Stack, I immediately become aware of its benefits:
Diffable data source is just bonkers. Literally ripping out 100s of lines of code I had to do batch updates.
— Jordan Morgan (@JordanMorgan10) January 16, 2020
Just look at the before and after code, look at it š! Free beers for @_breeno and friends next dub dub š» pic.twitter.com/Du17rdxM3t
As far as user experience, batch updates are far superior to the sledgehammer approach of reloadData
, so this is a needed step forward. But, thatās only part of it. Iām personally a believer in showing āempty viewā states, and if you peek into Appleās stock apps - they are too. Hereās an example of a bag with no items in the Apple Store app:
Previously, I used associated objects and the Objective-C Funtime to do the same thing in Spend Stack:
But hey - Iām being all swifty now, right? So Iāve been thinking about how to do the same with my new bestie, diffable data source while forgoing swizzling. Hereās what Iāve got so far:
func createSubscriptions() {
let nc = NotificationCenter.default
// Other subs..
// The one when a list is mutated
let tvAnim = Int(SSTableViewBatchUpdateAnimation)
let listCRUD = nc.publisher(for: .listCRUD)
.compactMap { $0.object as? SSList }
.debounce(for: .milliseconds(tvAnim), scheduler: RunLoop.main)
.sink { [unowned self] list in
self.showEmptyViewIfNeeded()
}
subscriptions.append(listCRUD)
}
This function runs when I initialize my subclassed diffable data source (to provide for things like tableView(_ tableView: UITableView, moveRowAt sourceIndexPath:, to:)
). When Combine fires off the closure, I simply check if the data is empty:
func showEmptyViewIfNeeded() {
guard let view = emptyView, let tv = tableView else { return }
let shouldShow = snapshot().itemIdentifiers.isEmpty && view.superview == nil
if shouldShow {
guard let vc = tv.closestViewController() else { return }
vc.view.addSubview(view)
if let constraints = self.emptyViewConstraints {
view.snp.remakeConstraints { make in
constraints(make)
}
}
} else {
view.removeFromSuperview()
}
}
This all works, but there are some things that need to improve. Notably, there are two core properties on my data source that need to be abstracted out in some fashion:
// Properties on data source object
var emptyView:UIView?
var emptyViewConstraints:((_ make: ConstraintMaker) -> Void)?
// Then later on, when initializing in a view controller or something similar
dataSource.emptyView = SSEmptyStateView(stateText: ss_Localized("list.vc.empty"))
dataSource.emptyViewConstraints = { [unowned self] make in
let lg = self.view.safeAreaLayoutGuide
make.top.equalTo(lg.snp.top)
make.bottom.equalTo(self.toolBar.snp.top)
make.centerX.equalTo(lg.snp.centerX)
make.width.equalTo(lg.snp.width)
}
Currently, the empty data view only works with this particular instance. I already know Iāll need it for the rest of the app, too. As I mentioned above, in Objective-C I have an EmptyDataSetDelegate
protocol which is tacked onto any collection or table view via associated objects.
I could do something similar, I suppose. Here are some thoughts so far.
I could use a protocol so other types could do something similar:
protocol EmptyDataViewProviding {
var emptyView:UIView? { get set }
var emptyViewConstraints:((_ make: ConstraintMaker) -> Void)? { get set }
func showEmptyDataView() -> Void
}
Plus, with protocol extensions I could vend a default implementation.
Next, I could simply extend the type. But without associated objects, Iād lose the two properties I need that describe the view and how it should be constrained.
extension UITableViewDiffableDataSource {
func showEmptyView() {
if snapshot().itemIdentifiers.isEmpty {
// Do stuff
}
}
}
At first, I attempted this route by trying to include the properties using a property wrapper implementation, but thatās not allowed in extensions as far as I can tell:
extension UITableViewDiffableDataSource {
@AssociatedObject var:emptyView?
@AssociatedObject var:emptyViewConstraints:((_ make: ConstraintMaker) -> Void)?
func showEmptyView() {
if snapshot().itemIdentifiers.isEmpty {
// Do stuff
}
}
}
Or, perhaps plain old object oriented programming is the answer? Simply a base type that others extend. Krusty would be so disappointed in me though.
class BaseDiffable : UITableViewDiffableDataSource<SSListTag, SSListItem> {
var emptyView:UIView?
var emptyViewConstraints:((_ make: ConstraintMaker) -> Void)?
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<SSListTag, SSListItem>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) {
super.apply(snapshot, animatingDifferences:animatingDifferences, completion: completion)
if snapshot.itemIdentifiers.isEmpty {
// Show empty view
}
}
}
I canāt decide which I like the most. In the end, my existing Objective-C solution bridges over fine, but if I can avoid swizzling batch updates, I will.
Iāll follow up on Twitter with where I end up.
Until next time āļø.