I'm creating an app where it simply lives in the menu bar, however I'd like a full-sized normal window to pop up if the user is not logged in, I have made a little pop over window which is sufficient for my main app to go into:
The code I have used to achieve this:
class AppDelegate: NSObject, NSApplicationDelegate{
var statusItem: NSStatusItem?
var popOver = NSPopover()
func applicationDidFinishLaunching(_ notification: Notification) {
let menuView = MenuView().environmentObject(Authentication())
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSViewController()
popOver.contentViewController?.view = NSHostingView(rootView: menuView)
popOver.contentViewController?.view.window?.makeKey()
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let MenuButton = statusItem?.button{
MenuButton.image = NSImage(systemSymbolName: "gearshape.fill", accessibilityDescription: nil)
MenuButton.action = #selector(MenuButtonToggle)
}
if let window = NSApplication.shared.windows.first {
window.close()
}
}
#objc func MenuButtonToggle(sender: AnyObject? = nil){
if popOver.isShown{
popOver.performClose(sender)
}
else{
if let menuButton = statusItem?.button{
NSApplication.shared.activate(ignoringOtherApps: true)
self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.minY)
}
}
}
#objc func closePopover(_ sender: AnyObject? = nil) {
popOver.performClose(sender)
}
#objc func togglePopover(_ sender: AnyObject? = nil) {
if popOver.isShown {
closePopover(sender)
} else {
MenuButtonToggle(sender: sender)
}
}
}
I make the popover view inside the AppDelegate, I'd like to either render this (with the icon in the menu bar) or just a normal macOS window (without the icon in the menu bar). Then have the ability to switch between the two easily via something like this:
if session != nil{
// show menu bar style
else{
// show window view to log in
}
I think you can reference the demo
Create a reference to an instance of NSWindowController in your AppDelegate class.
private var mainVC: MainViewController?
func showMainWindow() {
if mainVC == nil {
mainVC = MainViewController.create()
mainVC?.onWindowClose = { [weak self] in
self?.mainVC = nil
}
}
mainVC?.showWindow(self)
}
The MainviewController is like following:
class MainViewController: NSWindowController {
var onWindowClose: (() -> Void)?
static func create() -> MainViewController {
let window = NSWindow()
window.center()
window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
window.title = "This is a test main title"
let vc = MainViewController(window: window)
// Use your SwiftUI here as the Main Content
vc.contentViewController = NSHostingController(rootView: ContentView())
return vc
}
override func showWindow(_ sender: Any?) {
super.showWindow(sender)
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(self)
window?.delegate = self
}
}
extension MainViewController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
onWindowClose?()
}
}
In Swift using macOS:
By removing #NSApplicationMain (and making a subclass of NSWindowController) in AppDelegate I create the main window programmatically, without using storyboards, etc.:
//#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var viewController: NSViewController!
var windowController: NSWindowController!
func configMainWindow(_ viewController: NSViewController) {
window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [NSWindow.StyleMask.closable, NSWindow.StyleMask.titled, NSWindow.StyleMask.resizable, NSWindow.StyleMask.miniaturizable],
backing: NSWindow.BackingStoreType.buffered,
defer: false)
window.title = "My App"
window.setFrameAutosaveName("My App")
window.center()
window.isOpaque = false
window.isMovableByWindowBackground = true
window.backgroundColor = NSColor.white
window.makeKeyAndOrderFront(nil)
window.contentViewController = viewController
windowController = WindowController(window: window)
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
viewController = ViewController()
configMainWindow(viewController)
}
}
The windowController attaches a toolbar, statusBar and menuBar:
(Only the menuBar is loaded from a NIB. A class MainMenuAction handles the menu choices.)
class WindowController: NSWindowController, NSWindowDelegate {
var toolbarController = ToolbarController()
var statusBarController = StatusBarController()
var mainMenuAction: MainMenuAction?
override init(window: NSWindow?) {
super.init(window: window)
window?.toolbar = toolbarController.toolbar
window?.delegate = self
var topLevelObjects: NSArray? = []
Bundle.main.loadNibNamed("MainMenu", owner: self, topLevelObjects: &topLevelObjects)
NSApplication.shared.mainMenu = topLevelObjects?.filter { $0 is NSMenu }.first as? NSMenu
self.mainMenuAction = MainMenuAction.shared
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func windowDidLoad() {
if let window = window {
if let view = window.contentView {
view.wantsLayer = true
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.backgroundColor = .white
}
}
}
}
Additionally, I needed to add a main.swift file:
(thanks for reminding me, apodidae)
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
I tried:
let vc = NSViewController()
let win = configWindow(vc, windowWidth: 420, windowHeight: 673)
let wc = NSWindowController(window: win)
wc.window?.present
vc.view.window?.contentViewController = vc
Where I copied the method configMainWindow from AppDelegate, to create configWindow that allowed me the specify size and the vc.
But how can I open a new window (from some method in a new class - programmatically) with a custom size and style?
Please provide a code example.
The following demo creates a second window called by a method in a custom class. It may be run in Xcode by adding a 'swift.main' file and replacing AppDelegate with the following code:
import Cocoa
class Abc : NSObject {
var panel: NSPanel!
func buildWnd2() {
let _panelW : CGFloat = 200
let _panelH : CGFloat = 200
panel = NSPanel(contentRect:NSMakeRect(9300, 1300, _panelW, _panelH), styleMask:[.titled, .closable, .utilityWindow],
backing:.buffered, defer: false)
panel.isFloatingPanel = true
panel.title = "NSPanel"
panel.orderFront(nil)
}
}
let abc = Abc()
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
#objc func myBtnAction(_ sender:AnyObject ) {
abc.buildWnd2()
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
// **** Button **** //
let myBtn = NSButton (frame:NSMakeRect( 100, 100, 175, 30 ))
myBtn.bezelStyle = .rounded
myBtn.autoresizingMask = [.maxXMargin,.minYMargin]
myBtn.title = "Build Second Window"
myBtn.action = #selector(self.myBtnAction(_:))
window.contentView!.addSubview (myBtn)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()
I am trying to make custom keyboard extension. Keyboard buttons showing nicely but no action triggered! Here is Buttons UI:
struct MyKeyButtons: View {
let data: [String] = ["A", "B", "C"]
var body: some View {
HStack {
ForEach(data, id: \.self) { aData in
Button(action: {
KeyboardViewController().keyPressed()
}) {
Text(aData)
.fontWeight(.bold)
.font(.title)
.foregroundColor(.purple)
.padding()
.border(Color.purple, width: 5)
}
}
}
}
}
The KeyboardViewController code here:
import SwiftUI
class KeyboardViewController: UIInputViewController {
#IBOutlet var nextKeyboardButton: UIButton!
override func updateViewConstraints() {
super.updateViewConstraints()
// Add custom view sizing constraints here
}
override func viewDidLoad() {
super.viewDidLoad()
let child = UIHostingController(rootView: MyKeyButtons())
//that's wrong, it must be true to make flexible constraints work
// child.translatesAutoresizingMaskIntoConstraints = false
child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(child.view)
addChild(child)//not sure what is this for, it works without it.
// Perform custom UI setup here
self.nextKeyboardButton = UIButton(type: .system)
self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), for: [])
self.nextKeyboardButton.sizeToFit()
self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = false
self.nextKeyboardButton.addTarget(self, action: #selector(handleInputModeList(from:with:)), for: .allTouchEvents)
self.view.addSubview(self.nextKeyboardButton)
self.nextKeyboardButton.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.nextKeyboardButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
}
override func viewWillLayoutSubviews() {
self.nextKeyboardButton.isHidden = !self.needsInputModeSwitchKey
super.viewWillLayoutSubviews()
}
override func textWillChange(_ textInput: UITextInput?) {
// The app is about to change the document's contents. Perform any preparation here.
}
override func textDidChange(_ textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
var textColor: UIColor
let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.dark {
textColor = UIColor.white
} else {
textColor = UIColor.black
}
self.nextKeyboardButton.setTitleColor(textColor, for: [])
}
//==================================
func keyPressed() {
print("test--- clicked! ")
//textDocumentProxy.insertText("a")
(textDocumentProxy as UIKeyInput).insertText("a")
}
}
For more info see the GitHub project: https://github.com/ask2asim/KeyboardTest1
I am having difficult to add a custom NSToolbarItem to my existing toolbar.
NSToolbar was created in NSWindowController, then I have a function to populate toolbar items programmatically, code as:
public func populateFileToolbarItem(_ toolbar: NSToolbar) -> Void{
let itemId = NSToolbarItem.Identifier("FILE_OPEN")
let index = toolbar.items.count
var toolbarItem: NSToolbarItem
toolbarItem = NSToolbarItem(itemIdentifier: itemId)
toolbarItem.label = String("File")
toolbarItem.paletteLabel = String("Open File")
toolbarItem.toolTip = String("Open file to be handled")
toolbarItem.tag = index
toolbarItem.target = self
toolbarItem.isEnabled = true
toolbarItem.action = #selector(browseFile)
toolbarItem.image = NSImage.init(named:NSImage.folderName)
toolbar.insertItem(withItemIdentifier: itemId, at: index)
}
Then I called this function to add the toolbar item to an existing toolbar in windowController
.......
populateFileToolbarItem((self.window?.toolbar)!)
self.window?.toolbar?.insertItem(withItemIdentifier: NSToolbarItem.Identifier.flexibleSpace, at: (self.window?.toolbar?.items.count)!)
self.window?.toolbar?.insertItem(withItemIdentifier: NSToolbarItem.Identifier.print, at: (self.window?.toolbar?.items.count)!)
print("after toolbaritems were inserted into toolbar. \(String(describing: self.window?.toolbar?.items.count))")
......
The console print out shows, there are only two toolbar items were added to toolbar.
.......
after toolbaritems were inserted into toolbar. Optional(2)
And there is no custom item shows in the toolbar.
Any one has experience, please advise!
To add/remove items from the toolbar, you need the toolbar delegate: NSToolbarDelegate.
Here is a template for the implementation I'm using (probably more than you want).
Boilerplate code to create toolbar items of various types:
struct ToolbarIdentifiers {
static let mainToolbar = NSToolbar.Identifier(stringLiteral: "MainToolbar")
static let navGroupItem = NSToolbarItem.Identifier(rawValue: "NavGroupToolbarItem")
static let shareItem = NSToolbarItem.Identifier(rawValue: "ShareToolBarItem")
static let addItem = NSToolbarItem.Identifier(rawValue: "AddToolbarItem")
static let statusItem = NSToolbarItem.Identifier(rawValue: "StatusToolbarItem")
static let filterItem = NSToolbarItem.Identifier(rawValue: "FilterToolbarItem")
static let sortItem = NSToolbarItem.Identifier(rawValue: "SortToolbarItem")
static let cloudUploadItem = NSToolbarItem.Identifier(rawValue: "UploadToolbarItem")
static let cloudDownloadItem = NSToolbarItem.Identifier(rawValue: "DownloadToolbarItem")
static let leftButtonItem = NSToolbarItem.Identifier(rawValue: "leftButtonToolbarItem")
static let rightButtonItem = NSToolbarItem.Identifier(rawValue: "rightButtonToolbarItem")
static let hideShowItem = NSToolbarItem.Identifier(rawValue: "hideShowToolbarItem")
}
// Base toolbar item type, extended for segmented controls, buttons, etc.
struct ToolbarItem {
let identifier: NSToolbarItem.Identifier
let label: String
let paletteLabel: String
let tag: ToolbarTag
let image: NSImage?
let width: CGFloat
let height: CGFloat
let action: Selector?
weak var target: AnyObject?
var menuItem: NSMenuItem? = nil // Needs to be plugged in after App has launched.
let group: [ToolbarItem]
init(_ identifier: NSToolbarItem.Identifier, label: String = "", tag: ToolbarTag = .separator, image: NSImage? = nil,
width: CGFloat = 38.0, height: CGFloat = 28.0,
action: Selector? = nil, target: AnyObject? = nil, group: [ToolbarItem] = [], paletteLabel: String = "") {
self.identifier = identifier
self.label = label
self.paletteLabel = paletteLabel
self.tag = tag
self.width = width
self.height = height
self.image = image
self.action = action
self.target = target
self.group = group
}
}
// Image button -- creates NSToolbarItem
extension ToolbarItem {
func imageButton() -> NSToolbarItem {
let item = NSToolbarItem(itemIdentifier: identifier)
item.label = label
item.paletteLabel = label
item.menuFormRepresentation = menuItem // Need this for text-only to work
item.tag = tag.rawValue
let button = NSButton(image: image!, target: target, action: action)
button.widthAnchor.constraint(equalToConstant: width).isActive = true
button.heightAnchor.constraint(equalToConstant: height).isActive = true
button.title = ""
button.imageScaling = .scaleProportionallyDown
button.bezelStyle = .texturedRounded
button.tag = tag.rawValue
button.focusRingType = .none
item.view = button
return item
}
}
// Segmented control -- creates NSToolbarItemGroup containing multiple instances of NSToolbarItem
extension ToolbarItem {
func segmentedControl() -> NSToolbarItemGroup {
let itemGroup = NSToolbarItemGroup(itemIdentifier: identifier)
let control = NSSegmentedControl(frame: NSRect(x: 0, y: 0, width: width, height: height))
control.segmentStyle = .texturedSquare
control.trackingMode = .momentary
control.segmentCount = group.count
control.focusRingType = .none
control.tag = tag.rawValue
var items = [NSToolbarItem]()
var iSeg = 0
for segment in group {
let item = NSToolbarItem(itemIdentifier: segment.identifier)
items.append(item)
item.label = segment.label
item.tag = segment.tag.rawValue
item.action = action
item.target = target
control.action = segment.action // button & container send to separate handlers
control.target = segment.target
control.setImage(segment.image, forSegment: iSeg)
control.setImageScaling(.scaleProportionallyDown, forSegment: iSeg)
control.setWidth(segment.width, forSegment: iSeg)
control.setTag(segment.tag.rawValue, forSegment: iSeg)
iSeg += 1
}
itemGroup.paletteLabel = paletteLabel
itemGroup.subitems = items
itemGroup.view = control
return itemGroup
}
}
// Text field -- creates NSToolbarItem containing NSTextField
extension ToolbarItem {
func textfieldItem() -> NSToolbarItem {
let item = NSToolbarItem(itemIdentifier: identifier)
item.label = ""
item.paletteLabel = label
item.tag = tag.rawValue
let field = NSTextField(string: label)
field.widthAnchor.constraint(equalToConstant: width).isActive = true
field.heightAnchor.constraint(equalToConstant: height).isActive = true
field.tag = tag.rawValue
field.isSelectable = false
item.view = field
return item
}
}
// Menu item -- creates an empty NSMenuItem so that user can click on the label
// definitely a work-around till we implement the menus
extension ToolbarItem {
mutating func createMenuItem(_ action: Selector) {
let item = NSMenuItem()
item.action = action
item.target = target
item.title = label
item.tag = tag.rawValue
self.menuItem = item
}
}
/*
* Create specialized toolbar items with graphics, labels, actions, etc
* Encapsulates implementation-specific details in code, because the table-driven version was hard to read.
*/
struct InitializeToolbar {
}
extension InitializeToolbar {
static func navGroupItem(_ action: Selector, segmentAction: Selector, target: AnyObject) -> ToolbarItem {
var group = [ToolbarItem]()
group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "BackToolbarItem"), label: "Prev", tag: .navPrev,
image: NSImage(named: NSImage.goBackTemplateName), action: segmentAction, target: target))
group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "FwdToolbarItem"), label: "Next", tag: .navNext,
image: NSImage(named: NSImage.goForwardTemplateName), action: segmentAction, target: target))
let item = ToolbarItem(ToolbarIdentifiers.navGroupItem, tag: .navGroup, width: 85, height: 28,
action: action, target: target, group: group, paletteLabel: "Navigation")
return item
}
}
extension InitializeToolbar {
static func hideShowItem(_ action: Selector, segmentAction: Selector, target: AnyObject) -> ToolbarItem {
var group = [ToolbarItem]()
group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "HideLeftItem"), label: "", tag: .leftButton,
image: NSImage(named: "leftButton"), action: segmentAction, target: target))
group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "HideRightItem"), label: "", tag: .rightButton,
image: NSImage(named: "rightButton"), action: segmentAction, target: target))
let item = ToolbarItem(ToolbarIdentifiers.hideShowItem, tag: .hideShow, width: 85, height: 28,
action: action, target: target, group: group, paletteLabel: "Hide/Show")
return item
}
}
extension InitializeToolbar {
static func addItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.addItem, label: "Add", tag: .add, image: NSImage(named: NSImage.addTemplateName), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func shareItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.shareItem, label: "Share", tag: .share, image: NSImage(named: NSImage.shareTemplateName), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func filterItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.filterItem, label: "Filter", tag: .filter, image: NSImage(named: "filter"), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func sortItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.sortItem, label: "Sort", tag: .sort, image: NSImage(named: "sort"), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func cloudDownloadItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.cloudDownloadItem, label: "Down", tag: .cloudDownload, image: NSImage(named: "cloudDownload"), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func cloudUploadItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.cloudUploadItem, label: "Up", tag: .cloudUpload, image: NSImage(named: "cloudUpload"), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func leftButtonItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.leftButtonItem, label: "", tag: .leftButton, image: NSImage(named: "leftButton"), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func rightButtonItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
let item = ToolbarItem(ToolbarIdentifiers.rightButtonItem, label: "", tag: .rightButton, image: NSImage(named: "rightButton"), action: action, target: target)
return item
}
}
extension InitializeToolbar {
static func textItem() -> ToolbarItem {
return ToolbarItem(ToolbarIdentifiers.statusItem, label: "Watch This Space", tag: .status, width: 300, height: 24)
}
}
Here is the toolbar class, which implements the initializer and delegate:
/*
* Initializer builds a specialized toolbar.
*/
enum ToolbarTag: Int {
case separator = 1
case navGroup
case navPrev
case navNext
case add
case share
case filter
case sort
case cloudDownload
case cloudUpload
case leftButton
case rightButton
case hideShow
case status
}
class Toolbar: NSObject, NSToolbarDelegate, Actor {
var actorDelegate: ActorDelegate?
var identifier: NSUserInterfaceItemIdentifier?
var toolbarItemList = [ToolbarItem]()
var toolbarItemIdentifiers: [NSToolbarItem.Identifier] { return toolbarItemList.map({ $0.identifier }) }
var toolbarDefaultItemList = [ToolbarItem]()
var toolbarDefaultItemIdentifiers: [NSToolbarItem.Identifier] { return toolbarDefaultItemList.map({ $0.identifier }) }
// Delegate toolbar actions
#objc func controlSentAction(_ sender: Any) {
guard let control = sender as? NSControl else { return }
guard let tag = ToolbarTag(rawValue: control.tag) else { return }
actorDelegate?.actor(self, initiator: control, tag: tag, obj: nil)
}
#objc func segmentedControlSentAction(_ sender: Any) {
guard let segmented = sender as? NSSegmentedControl else { return }
guard let tag = ToolbarTag(rawValue: segmented.tag(forSegment: segmented.selectedSegment)) else { return }
actorDelegate?.actor(self, initiator: segmented, tag: tag, obj: nil)
}
// These don't get called at the moment
#objc func toolbarItemSentAction(_ sender: Any) { ddt("toolbarItemSentAction") }
#objc func menuSentAction(_ sender: Any) { ddt("menuSentAction") }
// Toolbar initialize
init(_ window: Window) {
super.init()
identifier = Identifier.View.toolbar
let toolbar = NSToolbar(identifier: ToolbarIdentifiers.mainToolbar)
toolbar.centeredItemIdentifier = ToolbarIdentifiers.statusItem
// Build the initial toolbar
// Text field
toolbarItemList.append(ToolbarItem(.flexibleSpace))
toolbarItemList.append(InitializeToolbar.textItem())
toolbarItemList.append(ToolbarItem(.flexibleSpace))
// Show/Hide
toolbarItemList.append(InitializeToolbar.hideShowItem(#selector(toolbarItemSentAction), segmentAction: #selector(segmentedControlSentAction), target: self))
// Save initial toolbar as default
toolbarDefaultItemList = toolbarItemList
// Also allow these, just to demo adding
toolbarItemList.append(InitializeToolbar.cloudDownloadItem(#selector(controlSentAction), target: self))
toolbarItemList.append(InitializeToolbar.sortItem(#selector(controlSentAction), target: self))
toolbar.allowsUserCustomization = true
toolbar.displayMode = .default
toolbar.delegate = self
window.toolbar = toolbar
}
deinit {
ddt("deinit", caller: self)
}
}
/*
* Implement NSToolbarDelegate
*/
extension Toolbar {
// Build toolbar
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
guard let item = toolbarItemList.firstIndex(where: { $0.identifier == itemIdentifier }) else { return nil }
switch toolbarItemList[item].identifier {
case ToolbarIdentifiers.navGroupItem, ToolbarIdentifiers.hideShowItem:
return toolbarItemList[item].segmentedControl()
case ToolbarIdentifiers.addItem, ToolbarIdentifiers.shareItem, ToolbarIdentifiers.sortItem, ToolbarIdentifiers.filterItem, ToolbarIdentifiers.cloudUploadItem, ToolbarIdentifiers.cloudDownloadItem,
ToolbarIdentifiers.leftButtonItem, ToolbarIdentifiers.rightButtonItem:
return toolbarItemList[item].imageButton()
case ToolbarIdentifiers.statusItem:
return toolbarItemList[item].textfieldItem()
default:
return nil
}
} // end of toolbar
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarDefaultItemIdentifiers;
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarItemIdentifiers
}
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return []
}
func toolbarWillAddItem(_ notification: Notification) {
}
func toolbarDidRemoveItem(_ notification: Notification) {
}
} // End of extension
Initial Toolbar:
Customization drop-down, which Cocoa does for you:
After adding cloud button:
Hope this is helpful.
Added to clarify 4/28/2019:
My Toolbar class is not an NSToolbar subclass. Its initializer gets passed a reference to the window, so that at the end it sets the window's toolbar to the toolbar it creates:
init(_ window: Window) {
super.init()
identifier = Identifier.View.toolbar
**** stuff removed for clarity ****
let toolbar = NSToolbar(identifier: ToolbarIdentifiers.mainToolbar)
toolbar.allowsUserCustomization = true
toolbar.displayMode = .default
toolbar.delegate = self
window.toolbar = toolbar
}
Perhaps this is confusing semantics, but it creates the toolbar and acts as the toolbar delegate, as you can see in the extension.
The "Actor" protocol is part of my coordination framework, not important to constructing the toolbar itself. I would have had to include the entire demo app to show that, and I assume that you have your own design for passing toolbar actions to your controllers/models.
This app is Xcode 10.2/Swift 5, although I don't think it uses any new Swift 5 features.
How Toolbars Work
To create a toolbar, you must create a delegate that provides important information:
A list of default toolbar identifiers. This list is used when reverting to default, and constructing the initial toolbar. The default set of toolbar items can also be specified using toolbar items found in the Interface Builder library.
A list of allowed item identifiers. The allowed item list is used to construct the customization palette, if the toolbar is customizable.
The toolbar item for a given item identifier.
For example add a flexibleSpace, print and custom item:
class MyWindowController: NSWindowController, NSToolbarDelegate {
var toolbarIdentifier = NSToolbarItem.Identifier("FILE_OPEN")
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.print, toolbarIdentifier]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.print, toolbarIdentifier]
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == toolbarIdentifier {
let toolbarItem = NSToolbarItem(itemIdentifier: toolbarIdentifier)
toolbarItem.label = String("File")
toolbarItem.paletteLabel = String("Open File")
toolbarItem.toolTip = String("Open file to be handled")
toolbarItem.isEnabled = true
toolbarItem.target = self
toolbarItem.action = #selector(browseFile)
toolbarItem.image = NSImage.init(named:NSImage.folderName)
return toolbarItem
}
else {
return NSToolbarItem(itemIdentifier: itemIdentifier)
}
}
}
It is also possible to add some or all standard and/or custom items in IB.
I have a custom view subclassing NSView, which is just an NSStackView containing a label, slider, a second label and a checkbox. The slider and checkbox are both configured to report changes to the view (and eventually, via a delegate to a ViewController):
fileprivate extension NSTextField {
static func label(text: String? = nil) -> NSTextField {
let label = NSTextField()
label.isEditable = false
label.isSelectable = false
label.isBezeled = false
label.drawsBackground = false
label.stringValue = text ?? ""
return label
}
}
#IBDesignable
class Adjustable: NSView {
private let sliderLabel = NSTextField.label()
private let slider = NSSlider(target: self, action: #selector(sliderChanged(_:)))
private let valueLabel = NSTextField.label()
private let enabledCheckbox = NSButton(checkboxWithTitle: "Enabled", target: self, action: #selector(enabledChanged(_:)))
var valueFormatter: (Double)->(String) = { String(format:"%5.2f", $0) }
...
#objc func sliderChanged(_ sender: Any) {
guard let slider = sender as? NSSlider else { return }
valueLabel.stringValue = valueFormatter(slider.doubleValue)
print("Slider now: \(slider.doubleValue)")
delegate?.adjustable(self, changedValue: slider.doubleValue)
}
#objc func enabledChanged(_ sender: Any) {
guard let checkbox = sender as? NSButton else { return }
print("Enabled now: \(checkbox.state == .on)")
delegate?.adjustable(self, changedEnabled: checkbox.state == .on)
}
}
Using InterfaceBuilder, I can add one instance of this to a ViewController by dragging in a CustomView and setting it's class in the Identity Inspector. Toggling the checkbox or changing the slider will have the desired effect.
However, if I have multiple instances then in the target-action functions self will always refer to the same instance of the view, rather than the one being interacted with. In other words, self.slider == sender is only true in sliderChanged for one of the sliders. While I can get the correct slider value via sender, I cannot update the correct label as self.valueLabel is always the label in the first instance of the custom view.
Incidentally, #IBDesignable and the code intended to support it have no effect so there's something I'm missing there too - Interface Builder just shows empty space.
The whole file:
import Cocoa
fileprivate extension NSTextField {
static func label(text: String? = nil) -> NSTextField {
let label = NSTextField()
label.isEditable = false
label.isSelectable = false
label.isBezeled = false
label.drawsBackground = false
label.stringValue = text ?? ""
return label
}
}
protocol AdjustableDelegate {
func adjustable(_ adjustable: Adjustable, changedEnabled: Bool)
func adjustable(_ adjustable: Adjustable, changedValue: Double)
}
#IBDesignable
class Adjustable: NSView {
var delegate: AdjustableDelegate? = nil
private let sliderLabel = NSTextField.label()
private let slider = NSSlider(target: self, action: #selector(sliderChanged(_:)))
private let valueLabel = NSTextField.label()
private let enabledCheckbox = NSButton(checkboxWithTitle: "Enabled", target: self, action: #selector(enabledChanged(_:)))
var valueFormatter: (Double)->(String) = { String(format:"%5.2f", $0) }
#IBInspectable
var label: String = "" {
didSet {
sliderLabel.stringValue = label
}
}
#IBInspectable
var value: Double = 0 {
didSet {
slider.doubleValue = value
valueLabel.stringValue = valueFormatter(value)
}
}
#IBInspectable
var enabled: Bool = false {
didSet {
enabledCheckbox.isEnabled = enabled
}
}
#IBInspectable
var minimum: Double = 0 {
didSet {
slider.minValue = minimum
}
}
#IBInspectable
var maximum: Double = 100 {
didSet {
slider.maxValue = maximum
}
}
#IBInspectable
var tickMarks: Int = 0
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
setup()
}
override func prepareForInterfaceBuilder() {
setup()
}
override func awakeFromNib() {
setup()
}
private func setup() {
let stack = NSStackView()
stack.orientation = .horizontal
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(sliderLabel)
stack.addArrangedSubview(slider)
stack.addArrangedSubview(valueLabel)
stack.addArrangedSubview(enabledCheckbox)
sliderLabel.stringValue = label
slider.doubleValue = value
valueLabel.stringValue = valueFormatter(value)
slider.minValue = minimum
slider.maxValue = maximum
slider.numberOfTickMarks = tickMarks
// Make the slider be the one that expands to fill available space
slider.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 249), for: .horizontal)
sliderLabel.widthAnchor.constraint(equalToConstant: 60).isActive = true
valueLabel.widthAnchor.constraint(equalToConstant: 60).isActive = true
addSubview(stack)
stack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
stack.topAnchor.constraint(equalTo: topAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
#objc func sliderChanged(_ sender: Any) {
guard let slider = sender as? NSSlider else { return }
valueLabel.stringValue = valueFormatter(slider.doubleValue)
print("Slider now: \(slider.doubleValue)")
delegate?.adjustable(self, changedValue: slider.doubleValue)
}
#objc func enabledChanged(_ sender: Any) {
guard let checkbox = sender as? NSButton else { return }
print("Enabled now: \(checkbox.state == .on)")
delegate?.adjustable(self, changedEnabled: checkbox.state == .on)
}
}
The solution, as described in the question linked by Willeke, was to ensure init had completed before referencing self. (I'm slightly surprised the compiler allowed it to be used in a property initialiser)
Wrong:
private let slider = NSSlider(target: self, action: #selector(sliderChanged(_:)))
private let enabledCheckbox = NSButton(checkboxWithTitle: "Enabled", target: self, action: #selector(enabledChanged(_:)))
Right:
private lazy var slider = NSSlider(target: self, action: #selector(sliderChanged(_:)))
private lazy var enabledCheckbox = NSButton(checkboxWithTitle: "Enabled", target: self, action: #selector(enabledChanged(_:)))