Implementing a Calendar-style sidebar

On iOS 14, the sidebar in the Calendar app appears to be the primary controller of a split view controller of style UISplitViewControllerStyleDoubleColumn.

The state of the Calendar sidebar is controlled by three bar button items: the Calendars button, the Inbox button, and the Today button.

When the sidebar is visible, these buttons appear in the navigation bar of the sidebar. When the sidebar is closed, the three buttons animate to the secondary controller's navigation bar.

How do you specify the buttons such that they animate on show/hide?

Steve
Answered by Frameworks Engineer in 616051022
Calendar provides their own UI and control of the UISplitViewControllers that they use—all with API (at least the UISVC behavior is all API), though the wrapped internal UISVC is definitely pushing the envelope.

In order to have sidebars on both sides they have a wrapped UISVC with primaryEdge = trailing in the secondary column of their main UISVC. (This is the delicate thing that they do, and you might run into trouble that we can't help you with).

Then, they have presentsWithGesture = NO on both UISVC's. This suppresses the default displayModeButtonItem. (-initWithStyle: UISVC takes much more control over the button item than before.) They don't want gesture-driven sidebar behavior, but if they did they would have had to add their own gestures.

I forget what navigation controller is providing their navigation bar (or bars), but they put their own bar button items in there, with their own actions, which ultimately call -showColumn:/hideColumn: as they want.

For coordinating animations, immediately after sending -show/hideColumn: you can request the UISVC's transitionCoordinator and use the alongside API to coordinate animations or completions with the UISVC's show/hide animation.

The answer does *not* appear to be to subclass UISplitViewController and provide a custom bar button item in displayModeButtonItem as the method does not appear to be called.

Code Block objective-c
- (UIBarButtonItem *)displayModeButtonItem
{
    UIButton *calendar = [UIButton systemButtonWithImage:[UIImage systemImageNamed:@"calendar.badge.exclamationmark"] target:self action:@selector(toggleCalendarAction:)];
    UIButton *inboxButton = [UIButton systemButtonWithImage:[UIImage systemImageNamed:@"tray"] target:self action:@selector(toggleInboxAction:)];
    UIButton *todayButton = [UIButton systemButtonWithImage:[UIImage systemImageNamed:@"list.bullet"] target:self action:@selector(toggleTodayAction:)];
    UIStackView *buttonGroup = [[UIStackView alloc] initWithArrangedSubviews:@[calendar, inboxButton, todayButton]];
    return [[UIBarButtonItem alloc] initWithCustomView:buttonGroup];
}


Accepted Answer
Calendar provides their own UI and control of the UISplitViewControllers that they use—all with API (at least the UISVC behavior is all API), though the wrapped internal UISVC is definitely pushing the envelope.

In order to have sidebars on both sides they have a wrapped UISVC with primaryEdge = trailing in the secondary column of their main UISVC. (This is the delicate thing that they do, and you might run into trouble that we can't help you with).

Then, they have presentsWithGesture = NO on both UISVC's. This suppresses the default displayModeButtonItem. (-initWithStyle: UISVC takes much more control over the button item than before.) They don't want gesture-driven sidebar behavior, but if they did they would have had to add their own gestures.

I forget what navigation controller is providing their navigation bar (or bars), but they put their own bar button items in there, with their own actions, which ultimately call -showColumn:/hideColumn: as they want.

For coordinating animations, immediately after sending -show/hideColumn: you can request the UISVC's transitionCoordinator and use the alongside API to coordinate animations or completions with the UISVC's show/hide animation.

Frameworks Engineer, thanks for your answer. I am wondering about this as well.

Could you please elaborate on the animation part of your answer? UIBarButtonItem (assuming that's what the calendar app is using) are not views and cannot be animated. Maybe we should animate some certain subview of the navigation bar? That doesn't seem like a good idea either. Or perhaps it is animating on some private view that devs don't have access to?

It would be great if you could provide some sample code snippets. Thanks!
I was able to get something working using UISplitViewControllerDelegate and including the function below. You'll also need to set splitViewController?.presentsWithGesture = false to disable the automatic button assignment

Code Block
    func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
        let pvc = (svc.viewController(for: .secondary) as? UINavigationController)?.topViewController
        if let addButton = self.addButton, let backtimeButton = self.backtimeButton, let sidebarButton = self.sidebarButton, let sidebarButtonOn = self.sidebarButtonOn {
            switch displayMode {
            case .oneBesideSecondary, .oneOverSecondary:
                pvc?.navigationItem.setLeftBarButtonItems([], animated: true)
                navigationItem.setLeftBarButtonItems([addButton, backtimeButton, sidebarButtonOn], animated: true)
            default:
                pvc?.navigationItem.setLeftBarButtonItems([addButton, backtimeButton, sidebarButton], animated: true)
                navigationItem.setLeftBarButtonItems([], animated: true)
            }
        }
    }


This does not animate as nicely as the Calendar app, I too would be curious how the official solution is implemented. The toggle button they use to show/hide the sidebar is also somewhat challenging to reproduce as the UIBarButtonItem doesn't have the option to set as selected like a standard UIButton. This solution does at least seem to transfer the buttons to the sidebar when appropriate like the calendar app.
Implementing a Calendar-style sidebar
 
 
Q