Preparations
Note that in Beta 2 you need to manually import a special bridging header if you want to use NSToolbar
. Here's how to do it.
The best way to insert toolbars is to use the new UIScene
API that Apple introduced with iOS 13. The gist is that you use a SceneDelegate
and in the scene:willConnectToSession:options
method, you can modify a window scene's titlebar
by setting a new NSToolbar
on it.
If your project doesn't have a SceneDelegate
yet, here's a brief primer on how to set it up.. This delegate is - as far as I'm aware - required to support toolbars.
Modifying the Scene
The first step is to modify the scene(willConnectTo:options:)
method in your SceneDelegate
to check if we're running UIKitForMac
- after all, iOS doesn't have toolbars - and then make sure that our scene is actually a UIWindowScene
:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(UIKitForMac)
#endif
}
UIWindowScene
objects have a titlebar
property on macOS. These UITitleBar
objects are currently not documented, but the headers expose a toolbar
property that you can use to assign a NSToolbar
to your window:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(UIKitForMac)
// Create a new toolbar (see below)
let newToolbar = makeMyFancyToolbar()
// Insert it into the titlebar
windowScene.titlebar.toolbar = self
// Hide the window title, looks nicer
windowScene.titlebar.titleVisibility = .hidden
#endif
}
So this is how we insert an actual toolbar into our window. The last remaining step is to create such a toolbar, so how does that work?
Creating a Toolbar
This is actually a multi-step process as toolbars are quit versatile. You begin by instantiating a new toolbar and configuring it accordingly:
// Create the toolbar
let toolbar = NSToolbar(identifier: \"MyFancyToolbar\")
// Assign the delegate
toolbar.delegate = self
As you can see, the configuration is just two lines of code (for now) but toolbars employ a delegate that you can leverage to configure them in much more detail. Lets see how.
Implementing the NSToolbarDelegate protocol
First, you need an object that can conform to your toolbar delegate. For simplicity, lets just take our SceneDelegate
. However, this can be any object and for more complex setups could be, for example, your own MyToolbarController
class.
#if targetEnvironment(UIKitForMac)
extension SceneDelegate: NSToolbarDelegate {
}
#endif
Again, make sure this code only compiles on macOS. Next, lets implement the bare minimum needed to display a toolbar.
Toolbars are versatile and allow your users to configure them and modify their contents. Our toolbar will just display a button on the right side. Toolbars identify their contents via toolbar identifiers of type NSToolbarItem.Identifier
. Each item in the toolbar has a custom identifier. This makes it easy for the system to understand which items are in the toolbar, which can be customized, removed, etc. So in order for us to add a button, we need an identifier for our button:
private let OurButtonToolbarIdentifier = NSToolbarItem.Identifier(rawValue: \"OurButton\")
#if targetEnvironment(UIKitForMac)
extension SceneDelegate: NSToolbarDelegate {
}
#endif
Next, we need to implement two methods that are called on the delegate to figure out what to display in the toolbar:
toolbarDefaultItemIdentifiers
: This returns the identifiers for the items that should be in the toolbar by defaulttoolbarAllowedItemIdentifiers
: This returns the identifiers that are currently allowed in the toolbar
We will implement default first:
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace,
OurButtonToolbarIdentifier]
}
}
Here, we're returning an array of two items. The last is our OurButtonToolbarIdentifier
. The first is a "flexible space" identifier that will consume as much horizontal space as possible, thus pushing our button all the way to the right.
For simplicity, the second method, toolbarAllowedItemIdentifiers
just calls the first one.
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace,
OurButtonToolbarIdentifier]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarDefaultItemIdentifiers(toolbar)
}
}
Now that we told the toolbar how to display itself, we just need to create those items (in our case just our button):
extension SceneDelegate: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if (itemIdentifier == OurButtonToolbarIdentifier) {
let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add,
target: self,
action: #selector(self.myFancyAction(sender:)))
let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem)
return button
}
return nil
}
Again, this is simple enough. We just create a UIBarButtonItem
, wrap it into a NSToolbarItem
and return it. There're a lot of options here that we will not go into, but reading the headers and the documentation will help you along if you need more complex setups.
If you want to hide your existing iOS / UIKit toolbar, too, have a look at this section.
If you want to display a nice segmented control your toolbar, like this:
There's an explanation on how to do this in the following section.
Here's the whole code SceneDelegate
in one section:
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(UIKitForMac)
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = makeMyFancyToolbar()
}
#endif
}
private func makeMyFancyToolbar() -> NSToolbar {
let toolbar = NSToolbar(identifier: \"MyToolbar\")
toolbar.delegate = self
return toolbar
}
@objc func myFancyAction(sender: UIBarButtonItem) {
print(\"Button Pressed\")
}
}
#if targetEnvironment(UIKitForMac)
private let OurButtonToolbarIdentifier = NSToolbarItem.Identifier(rawValue: \"OurButton\")
extension SceneDelegate: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if (itemIdentifier == OurButtonToolbarIdentifier) {
let barButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(myFancyAction(sender:)))
let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButton)
return button
}
return nil
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, OurButtonToolbarIdentifier]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarDefaultItemIdentifiers(toolbar)
}
}
#endif