Sidebar in iPad iOS 14, explained.
Side bars are how we should create a hierarchical navigation system in iPad for non compact mode environments in iOS 14.
Ok , but what is actually a side bar? is just a view controller that has a collection view that uses the new side bar appearance list layout.
(Don’t want to read your stuff 😡, give me the code)
Since iOS 14 Apple introduced UICollectionLayoutListConfiguration an incredible improvement for UICollectionViews
that allows us use a set of defined configurations for our collection views.
You create a sidebar by using a sidebar-style list and placing it in the primary column of a split view. For related guidance, see Split Views.
Apply the correct appearance to a sidebar. To create a sidebar, use the sidebar appearance of a collection view list layout. For developer guidance, see UICollectionLayoutListConfiguration.Appearance.
Ok, let’s build one, we are going to use all the awesome new collection view API’s available, including new cell registration. If you did not seen it yet I highly recommend watch advances in collection views from WWDC 2020 by Steve Breen.
When working with collection views in iOS 14 you pretty much always need to implement 3 important pieces.
1- Provide a Layout.
2- Configure the Diffable data source, cell and headers/footers registration is included on this step.
3- Apply the Snapshot.
Before we start with those 3 steps, we need to create models that will be displayed on each cell. Here is a tip, you can create a shared view model to define the navigation tab items that you can use in your tab bar controller and your side bar, it will look like this…
(yes we will also add a tab bar controller for the splitView UISPlitViewController.Column
.compact
later on this post.)
// MARK:- ViewModelenum TabsViewModel: String, CaseIterable {
// 1
case home
case search
case camera
case notifications
case profile/// Return:- the tab bar icon using SF Symbols// 2
var icon: UIImage? {
switch self {
case .home: return UIImage(systemName: "house.fill")
case .search: return UIImage(systemName: "magnifyingglass")
case .camera: return UIImage(systemName: "plus.app")
case .notifications: return UIImage(systemName: "suit.heart")
case .profile: return UIImage(systemName: "person")
}
}/// Return:- the tab bar title// 3
var title: String { rawValue }/// Return:- the master/primary `topViewController`, it instantiates a view controller using a convenient method for `UIStoryboards`.// 4
var primaryViewController: UIViewController {
switch self {
case .home: return HomeViewController()
case .search: return SearchViewController()
case .camera: return CameraViewController()
case .notifications: return NotificationsViewController()
case .profile: return UserProfileViewController()
}
}
}
1- Create cases that represents the main navigation hierarchy of your app.
2- Provide an icon image, SF Symbols are awesome for this.
3- Provide a title for each case, this will be needed for the side bar.
4- Create a convenient way to provide a primaryViewController
for each case.
Ok now that we have a view model we can start with our sidebar, just create a view controller and inside view did load…
override func viewDidLoad() { super.viewDidLoad() configureCollectionViewLayout() // 1 Configure the layout configureDataSource() // 2 configure the data Source applyInitialSnapshots() // 3 Apply the snapshots.}
Let’s see what is inside those methods:
Step 1: Configure the Layout.
private func configureCollectionViewLayout() {
collectionView.collectionViewLayout = createLayout()
}private func createLayout() -> UICollectionViewLayout {let sectionProvider = { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in// 1
let section = NSCollectionLayoutSection.list(using: .init(appearance: .sidebar), layoutEnvironment: layoutEnvironment)//2
let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .estimated(200))let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize,elementKind: SideBarViewController.sectionHeaderElementKind, alignment: .top)section.boundarySupplementaryItems = [sectionHeader]return section}
// 3
return UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)}
1- New UICollectionLayoutListConfiguration.Appearance .sideBar
yes, thats it. 🔥
2- Define the estimated size for the section header.
3- Return an instance of UICollectionViewCompositionalLayout.
Step 2: Configure the Diffable data source.
private func configureDataSource() {/// 1 - header registration let headerRegistration = UICollectionView.SupplementaryRegistration<CollectionReusableView<ProfileInfoView>>(elementKind: "Header") {(supplementaryView, string, indexPath) in
supplementaryView.subView.configureWith(UserProfileViewModel.stub)}/// 2 - data source dataSource = UICollectionViewDiffableDataSource<Section, TabsViewModel>(collectionView: collectionView) {(collectionView, indexPath, item) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: self.configuredOutlineCell(), for: indexPath, item: item)}/// 4- data source supplementaryViewProvider dataSource.supplementaryViewProvider = { view, kind, index in return self.collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: index)}}// 3
private func configuredOutlineCell() -> UICollectionView.CellRegistration<UICollectionViewListCell, TabsViewModel> { UICollectionView.CellRegistration<UICollectionViewListCell, TabsViewModel> { cell, indexPath, item in
var content = cell.defaultContentConfiguration()
content.text = item.title
content.image = item.icon
content.imageProperties.tintColor = .white
cell.contentConfiguration = content
cell.accessories = [.disclosureIndicator()]
}
}
1- Register a header for the section, the element kind string will define if should be displayed as header or footer.
2- Define the Diffable data source, and configure the cell using dequeConfiguredReusableCell
.
3- If you did not saw WWDC Modern cell configuration yet, take a look!
4- The suppleMentaryViewProvider
configures the content for your header/footer.
Step 3: Apply the Snapshot.
private func applyInitialSnapshots() {//1
let sections = Section.allCases// 2
var snapshot = NSDiffableDataSourceSnapshot<Section, TabsViewModel>()
snapshot.appendSections(sections)
dataSource.apply(snapshot, animatingDifferences: false) var outlineSnapshot =
// 3
NSDiffableDataSourceSectionSnapshot<TabsViewModel>()
outlineSnapshot.append(TabsViewModel.allCases)
dataSource.apply(outlineSnapshot, to: .list, animatingDifferences: false)
}
1- Section
is an enum defined in the view controller.
2- Define the snapshot for the overall data source.
3- New! NSDiffableDataSourceSectionSnapshot
in iOS 14:
A section snapshot represents the data for a single section in a collection view or table view. Through a section snapshot, you set up the initial state of the data that displays in an individual section of your view, and later update that data.
Thats it! now we have a working side bar that looks like this…
Now, remember that we said at the beginning that we will be displaying a tab bar controller as main navigation system for compact mode? well if you worked with iPad apps before iOS 14 you know the pain that achieving that can be, fortunately Apple introduced an easy way to do so just by providing a view controller for .compact
in the setViewControllers
method, this is all you need to do in your SceneDelegate
file…
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {guard let _ = (scene as? UIWindowScene) else { return }// 1
let sideBar = SideBarViewController.instantiate(from: "Main")
let supplementaryViewController = SupplementaryViewController()
let secondaryViewController = SecondaryViewController()//2
let tabBarController = TabBarController()// 3
let splitViewController = UISPlitViewController(style: .tripleColumn)
splitViewController.setViewController(sideBar, for: .primary)
splitViewController.setViewController(supplementaryViewController, for: .supplementary)
splitViewController.setViewController(secondaryViewController, for: .secondary)
// 4
splitViewController.setViewController(tabBarController, for: .compact)window?.rootViewController = splitViewController
window?.makeKeyAndVisible()}
1- Create the set of view controllers for each UISPlitViewController.Column
case.
2- An instance of UITabBarController
that may looks like this, or however you want…
final class TabBarController: UITabBarController { override func viewDidLoad() {
super.viewDidLoad()
// 1
viewControllers = TabsViewModel.allCases.map {
let rootNavigationController =
UINavigationController(rootViewController: $0.primaryViewController)
rootNavigationController.tabBarItem.image = $0.icon
return rootNavigationController
}
}
}
3- Now you can instantiate your split view controller with 2 or 3 columns using the UISPlitViewController.Style
initializer.
4- Providing any kind of view controller for compact width is extremely easy! 🤯
When your app changes from regular to compact width it will adapt automatically…
A few things to consider when implementing a side bar for iPad…
- Use a sidebar to organize information at the app level.
- Whenever possible, let people customize the contents of a sidebar.
- Don’t prevent people from hiding the sidebar.
- Keep titles in the sidebar clear and concise.
- In general, refrain from exposing more than two levels of hierarchy within a sidebar.
Lastly, is worth to mention that although display a view controller for compact mode is extremely easy, there is a catch, we need to make our app restores its navigation state so the user lands in the proper view controller when app traits changes.
Thats a whole another story and I am currently learning about that :) , here are some resources if you want to investigate further…
Here is the code of this post.
😎