Sidebar in iPad iOS 14, explained.

James Rochabrun
Dev Genius
Published in
6 min readJul 1, 2020

--

https://developer.apple.com/design/human-interface-guidelines/ios/bars/sidebars/

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.

From the docs.

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 setViewControllersmethod, 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…

Trait collection changes.

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.

😎

--

--