Teabyte

Mobile app development for iOS and Swift

Empty state with diffable datasource

2021-03-14

During the last week I was heavily working with UICollectionViewDiffableDataSources at work. We are currently creating an app which contains a lot of collection views which are constantly updating and changing. So diffable datasources were a perfect fit. From the UI department we got the task to implement empty state views for all of our lists. So I sat down and was searching for a way to directly bake in the empty state view inside the the diffable datasource without having the need to handle the state anywhere else. In the following I want to provide you with my findings and the code I came up with, to enhance a UICollectionViewDiffableDataSource with an empty state view.

Code

The following code block demonstrates my solution to have an empty state view directly tied to an UICollectionViewDiffableDataSource.

/// A diffable data source fir UICollectionViews that is capable of displaying an UIView if the datasource does not contain any item
class EmptyableDiffableDataSource<S, T>: UICollectionViewDiffableDataSource<S, T> where S: Hashable, T: Hashable {
 
    private var collectionView: UICollectionView
    private var emptyStateView: UIView?
 
    /// Creates a diffable data source with the specified cell provider, and connects it to the specified collection view. Additionally an `UIView` can be specified which is shown when the data source is empty
    ///
    /// - Parameters:
    ///   - collectionView: The initialized collection view object to connect to the diffable data source.
    ///   - cellProvider: A closure that creates and returns each of the cells for the collection view from the data the diffable data source provides.
    ///   - emptyStateView: An UIView that is displayed when the data source is empty. If nil is provided, the default view for the collection view is shown
    convenience init(collectionView: UICollectionView,
                     cellProvider: @escaping UICollectionViewDiffableDataSource<S, T>.CellProvider,
                     emptyStateView: UIView? = nil
    ) {
        self.init(collectionView: collectionView, cellProvider: cellProvider)
 
        self.collectionView = collectionView
        self.emptyStateView = emptyStateView
    }
 
    override func apply(_ snapshot: NSDiffableDataSourceSnapshot<S, T>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) {
        super.apply(snapshot, animatingDifferences:animatingDifferences, completion: completion)
 
        guard let emptyView = emptyStateView else {
            return
        }
 
        if snapshot.itemIdentifiers.isEmpty {
 
            // Add and show empty state view
            emptyView.translatesAutoresizingMaskIntoConstraints = false
            collectionView.addSubview(emptyView)
            NSLayoutConstraint.activate([
                emptyView.topAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.topAnchor),
                emptyView.trailingAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.trailingAnchor),
                emptyView.bottomAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.bottomAnchor),
                emptyView.leadingAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.leadingAnchor)
            ])
        } else {
            NSLayoutConstraint.deactivate([
                emptyView.topAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.topAnchor),
                emptyView.trailingAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.trailingAnchor),
                emptyView.bottomAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.bottomAnchor),
                emptyView.leadingAnchor.constraint(equalTo: collectionView.layoutMarginsGuide.leadingAnchor)
            ])
            // Remove and hide empty state view
            emptyStateView?.removeFromSuperview()
        }
    }
}

Explanation

  • Line 2: Extend the base UICollectionViewDiffableDataSource class
  • Line 3-4: Keep references to the UICollectionView on which the data source is applied and a potential empty state view
  • Line 13-21: Initialize the base class with the super.init call and set the view references
  • Line 23-24: Override the base class apply method with a custom one which handles the empty state view
  • Line 26-28: If no empty view was supplied, do not calculate anything and just return the method
  • Line 30: If the itemIdentifiers of the snapshot are empty, show the empty state view by activating the auto layout constraints and adding it as a subview to the collectionView, referenced in the init
  • Line 41: If the item itemIdentifiers of the snapshot are not empty, deactivate corresponding auto layout constraints and remove the empty state view from the superview.

Usage

The usage is rather simple. The only difference from calling the standard initializer is, that you can provide an optional UIView to be displayed when the datasource is empty.

 let dataSource = EmptyableDiffableDataSource<CollectionViewListSection, CollectionViewListCell>(
    collectionView: aCollectionView,
    cellProvider: aCellProviderClosure,
    emptyStateView: anEmptyView
)

Conclusion

With this small custom UICollectionViewDiffableDataSource class we are now able to define any UIView instance to be shown, when the datasource does not contain any items.

I hope you found this small tip useful. If you have suggestions or question don't hesitate to reach out to me !

See you next time 👋