Mac Catalyst Title Bar Window Tabs
How to setup NSTitleBar style tabs for a UIKit app on mac with Catalyst
To start with let's take a simple tab bar based app with a label in the UIViewController
's' telling us which view we are looking at.
If we just take this app and compile and run it for mac Catalyst this is the result we get.
This works... but feels a lot like running the app in the iOS simulator.
You probably want to change the style, to either the side tabs, like the AppStore app, or titlebar tabs, like the Calendar app.
Most of the time I want the tabs in the titlebar. (if you want the sidebar style, that's UISplitViewController
+ .primaryBackgroundStyle = .sidebar
)
Putting Tabs in the Title Bar
To do that we need to customise the window's tilebar. In iOS, the application window (as in multi-window support, not UIWindow
or NSWindow
) is now controlled from the UISceneDelegate
.
In the SceneDelegate we setup an NSToolbar
, set the SceneDelegate
as the NSToolbar
delegate and then hide both the title bar's title itself, after assigning the toolbar, & the tab bar.
Inside the SceneDelegate class add:
Objective-C (ObjC)
SceneDelegate.m
// SceneDelegate.m
#if TARGET_OS_MACCATALYST
#import <AppKit/AppKit.h>
@interface SceneDelegate () <NSToolbarDelegate>
#else
@interface SceneDelegate ()
#endif
// -[SceneDelegate scene:willConnectToSession:options:] {
#if TARGET_OS_MACCATALYST
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"mainToolbar"];
toolbar.centeredItemIdentifier = @"mainTabsToolbarItem";
toolbar.delegate = self;
[(UIWindowScene*)scene titlebar].toolbar = toolbar;
[(UIWindowScene*)scene titlebar].titleVisibility = UITitlebarTitleVisibilityHidden;
_tabbarController.tabBar.hidden = YES;
#endif
Swift
SceneDelegate.swift
// SceneDelegate.swift
// SceneDelegate.scene(_:willConnectTo:options:) {
#if targetEnvironment(macCatalyst)
let toolbar = NSToolbar(identifier: "mainToolbar")
toolbar.centeredItemIdentifier = NSToolbarItem.Identifier(rawValue: "mainTabsToolbarItem")
toolbar.delegate = self
windowScene.titlebar?.titleVisibility = .hidden
windowScene.titlebar?.toolbar = toolbar
tabBarController.tabBar.isHidden = true
#endif
Most of the NSToolbar
stuff is actually configured via the NSToolbarDelegate
methods.
Here we are: specifying which items can be added to the toolbar toolbarDefaultItemIdentifiers:
& toolbarAllowedItemIdentifiers:
; setting up a new NSToolbarItemGroup
item, specifying the segment control in the center of the title bar; and then .
ObjC
// SceneDelegate.m
#pragma mark - NSToolbarDelegate
#if TARGET_OS_MACCATALYST
// NSToolbarItemIdentifier is just an NSString*
- (nullable NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSToolbarItemIdentifier)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
{
if ([itemIdentifier isEqualToString:@"mainTabsToolbarItem"]) {
NSToolbarItemGroup *group = [NSToolbarItemGroup groupWithItemIdentifier:itemIdentifier
titles:@[@"First", @"Second"]
selectionMode:NSToolbarItemGroupSelectionModeSelectOne
labels:@[@"First view", @"Second view"]
target:self
action:@selector(toolbarGroupSelectionChanged:)];
[group setSelectedIndex:0];
return group;
}
return nil;
}
/* Returns the ordered list of items to be shown in the toolbar by default. If during initialization, no overriding values are found in the user defaults, or if the user chooses to revert to the default items this set will be used. */
- (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar{
return @[@"mainTabsToolbarItem"];
}
/* Returns the list of all allowed items by identifier. By default, the toolbar does not assume any items are allowed, even the separator. So, every allowed item must be explicitly listed. The set of allowed items is used to construct the customization palette. The order of items does not necessarily guarantee the order of appearance in the palette. At minimum, you should return the default item list.*/
- (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar{
return [self toolbarDefaultItemIdentifiers:toolbar];
}
-(void)toolbarGroupSelectionChanged:(NSToolbarItemGroup*)sender{
self.tabbarController.selectedIndex = sender.selectedIndex;
}
#endif
Swift
// SceneDelegate.swift
// MARK: - NSToolbarDelegate
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem?
{
if (itemIdentifier.rawValue == "mainTabsToolbarItem")
{
let group = NSToolbarItemGroup.init(itemIdentifier: itemIdentifier,
titles: ["First", "Second"],
selectionMode: .selectOne,
labels: ["First view", "Second view"],
target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
return nil
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier(rawValue: "mainTabsToolbarItem")]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return self.toolbarDefaultItemIdentifiers(toolbar)
}
@objc func toolbarGroupSelectionChanged(_ sender: NSToolbarItemGroup) {
tabBarController.selectedIndex = sender.selectedIndex
}
If you get the error message:
ERROR: Declaration of 'NSToolbarItemGroup' must be imported from module 'AppKit.NSToolbarItemGroup' before it is required
This means you haven't imported the Appkit headers. The fix is to @include AppKit;
or #import <AppKit/AppKit.h>
.
Before and after:
Looking at the result it might seem that the labels
part has been ignored. However, if you turn on customisation of the toolbar, or remove the hiding of the title in the titlebar you can see where it is used.
Example Projects
Made with Xcode 11.4.1 targeting iOS 13.0