Calrizer
Calrizer

Reputation: 31

How to add NSToolBarDelegate to a ViewController

I have recently ported an app to Mac Catalyst and I'm trying to configure my app's tool bar to essentially mirror the macOS News and Stocks apps where they have a back button and share button in the toolbar. See below:

Desired Outcome :

enter image description here

This code works when I add it to the AppDelegate but not when I add it to a normal ViewController class.

class ViewController: UIViewController {

override func viewDidLoad() {

        super.viewDidLoad()

        #if targetEnvironment(macCatalyst)

        UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.forEach { windowScene in

            if let titlebar = windowScene.titlebar {
                let toolbar = NSToolbar(identifier: "testToolbar")

                titlebar.toolbar = toolbar
                toolbar.delegate = self
                titlebar.titleVisibility = .hidden

            }

        }

        #endif

    }

}

#if targetEnvironment(macCatalyst)

private let SettingsIdentifier = NSToolbarItem.Identifier(rawValue: "SettingsButton")
private let TitleIdentifier = NSToolbarItem.Identifier(rawValue: "Title")
private let NavigationIdentifier = NSToolbarItem.Identifier(rawValue: "BackButton")


extension ViewController: NSToolbarDelegate {

    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {

            if (itemIdentifier == NavigationIdentifier) {

                let barButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .done, target: self, action: #selector(test))
                let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButton)

                return button

            }

            if (itemIdentifier == SettingsIdentifier) {

                let barButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .done, target: self, action: #selector(test))
                let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButton)

                return button

            }

            if (itemIdentifier == TitleIdentifier) {

                let barButton = UIBarButtonItem(title: "My App", style: .plain, target: self, action: nil)
                let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButton)

                return button

            }

            return nil

        }

        func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {

            return [NavigationIdentifier, NSToolbarItem.Identifier.flexibleSpace, TitleIdentifier, NSToolbarItem.Identifier.flexibleSpace, SettingsIdentifier]

        }

        func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {

            return toolbarDefaultItemIdentifiers(toolbar)

        }

        @objc func test(){

            print("test")

        }

}
#endif


If anyone has any ideas on how to fix or even improve my implementation I will be grateful.

Thanks!

Upvotes: 3

Views: 667

Answers (1)

deniz
deniz

Reputation: 975

I didn't test your sample code - so I will take it as given that trying to initialize a toolbar in a ViewController doesn't work. Frankly, this is not surprising because according to Apple, mac style toolbar should be added during app initialization - preferably within your SceneDelegate class. As Apple states in their tutorial on how to add a toolbar to a MacCatalyst app, this sample app manages its main window in the SceneDelegate class, and that’s where you’ll create the toolbar and add it to the titlebar (excerpt from the tutorial). This appears to be a requirement from Apple and most likely something to do with the timing and the app lifecycle.

Once you add the toolbar during app initialization (either in AppDelegate or SceneDelegate), then you can get a hold of it later in your view controller. For example, you can enable or disable some toolbar buttons depending on the situation. Here is one way of doing it:

class ToolbarDelegate: NSObject {
    #if targetEnvironment(macCatalyst)
    //do this if you want to manipulate the toolbar through your delegate
    //*** OPTIONAL ***
    weak var mainToolbar: NSToolbar?
    #endif
}

#if targetEnvironment(macCatalyst)
extension ToolbarDelegate: NSToolbarDelegate {
   //perform your ToolbarDelegate configuration
   ...
   ...
   //example function that shows how you can directly manipulate the toolbar via your ToolbarDelegate
   func disableToolbarItem(itemIdentifier: NSToolbar.ItemIdentifier) {
      mainToolbar?.items.first(where: { $0.itemIdentifier == itemIdentifier})?.isEnabled = false
   }
}
#endif

In your SceneDelegate do this:

//first configure the tool bar in your SceneDelegate as explained in the tutorial above
//cross posting the most relevant parts for convenience, refer to the tutorial for more info
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    ...
    var toolbarDelegate = ToolbarDelegate()
    ...
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        ...
        ...
        #if targetEnvironment(macCatalyst)
        guard let windowScene = scene as? UIWindowScene else { return }
    
        let toolbar = NSToolbar(identifier: "main")
        toolbar.delegate = toolbarDelegate
        toolbar.displayMode = .iconOnly

        //also pass the toolbar reference to your ToolbarDelegate
        //to be able to control it directly
        //*** OPTIONAL ***
        toolbarDelegate.mainToolbar = toolbar
        
    
        if let titlebar = windowScene.titlebar {
            titlebar.toolbar = toolbar
            titlebar.toolbarStyle = .automatic
        }
    #endif
    }
}

Then add this to your ViewController to be able to access your ToolbarDelegate and manipulate it as you see fit.

class MyViewController: UIViewController {
    ...
    ...
    var toolbarDelegate: ToolbarDelegate? {
        //notice, this approach only works if your app only has one scene
        //if your app supports multiple scenes, modify this code as necessary
        let scene = UIApplication.shared.connectedScenes.first
        if let sd: SceneDelegate = (scene?.delegate as? SceneDelegate) {
            return sd.toolbarDelegate
        } else {
            return nil
        }
    }

    override func viewDidLoad() {
         super.viewDidLoad()
         
         //disables side bar button (if you had it)
         toolbarDelegate?.disableToolbarItem(NSToolbarItem.Identifier.toggleSidebar)
    }
}

Upvotes: 0

Related Questions