Cocoa - Adding a menuContext programmatically in to ScrollView / CollectionView - swift

How can I add a menuContext programmatically to ScrollView / CollectionView?
This is my code but when I right-click the menu does not appear.
private var selectedPatients: [XxPatient] = []
private var contextMenu: NSMenu {
let rightClickMenu = NSMenu()
// edit info patient
let editPatientItem = NSMenuItem()
editPatientItem.title = LocalizationUtility.localizedString("contextMenu.editPatientInfo")
editPatientItem.target = self
editPatientItem.action = #selector(didSelectContextItem(_ :))
editPatientItem.tag = 1
rightClickMenu.addItem(editPatientItem)
return rightClickMenu
}
#objc func didSelectContextItem(_ sender: NSMenuItem) {
if sender.tag == 1 {
// edit info patient
openEditPatientInfo(with: selectedPatients.first)
}
}
private func openEditPatientInfo(with patient: XxPatient?) {
let next = HomeNewPatientController.instantiateFromStoryboard()
next?.patient = patient
// next?.delegate = self
let navigationController = DSNavigationController(withRootViewController: next)
UIUtility.presentModalController(navigationController, from: self)
}

Related

NSStatusItem menu Action not firing

The NSStatusItem menu shows correctly but when I click on menu the action not fire.
Since the app is compatible with Mac Catalyst, I created framework then passing Framework to main app in order to show the menu of NSStatusItem, it work correctly but I have the issue for action of the menu that doesn't work.
Here is my code:
#objc class AppKitController: NSObject {
var statusBarItem: StatusBarItemControler!
override init() {
super.init()
print("[AppKitController] Loaded successfully")
self.statusBarItem = StatusBarItemControler()
self.statusBarItem.updateTitle()
self.statusBarItem.updateMenu()
}
}
class StatusBarItemControler {
let item: NSStatusItem
init() {
self.item = NSStatusBar.system.statusItem(
withLength: NSStatusItem.variableLength
)
let statusBarMenu = NSMenu(title: "APPMenu")
self.item.menu = statusBarMenu
}
func updateTitle() {
let title = "AppMenu"
print("Update title")
DispatchQueue.main.async {
if let button = self.item.button {
button.title = "\(title)"
button.target = self
}
}
}
func updateMenu() {
if let statusBarMenu = self.item.menu {
statusBarMenu.autoenablesItems = false
statusBarMenu.removeAllItems()
statusBarMenu.addItem(NSMenuItem.separator())
statusBarMenu.addItem(NSMenuItem.separator())
self.createPreferencesSection()
}
}
func createPreferencesSection() {
self.item.menu!.addItem(
withTitle: "Open",
action: #selector(openPrefecencesWindow),
keyEquivalent: ",")
self.item.menu!.addItem(
withTitle: "Quit",
action: #selector(quit),
keyEquivalent: "q")
}
#objc func openPrefecencesWindow(_: NSStatusBarButton?) {
print("Open preferences window")
}
#objc func quit(_: NSStatusBarButton?) {
print("Open preferences window")
}
}
Thank you #Alexander, I have found the solution and it works.
class AppKitController: NSObject,NSApplicationDelegate,NSWindowDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
override init() {
super.init()
NSApplication.shared.delegate = self
NSApplication.shared.mainWindow?.delegate = self
statusItem.button?.title = "Your_App_Name"
statusItem.menu = createMenu()
print("[AppKitController] Loaded successfully")
}
func createMenu() -> NSMenu{
let menu = NSMenu()
let openMenuItem = menu.addItem(withTitle: "Open", action: #selector(openMenu), keyEquivalent: "")
openMenuItem.target = self
return menu
}
#objc func openMenu(_ sender:Any?){
print("Open menu called")
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
print("Window should close")
return false
}
}

Why is my UIViewController not showing up in my popup card?

I wanted to create a pop up for one of my UIViewController and found this repo on GitHub.
It is working fine with my InfoViewController which only has 4 UILabels (I think this might be the problem that it is not showing up when you use reusable cells)
But somehow it is not working with my StructureNavigationListViewController and I do not know why.
I call the didTapCategory method in my MainViewController where the StructureNavigationController should pop up but I only see the dimming view (which is weird cause the tap recognizer and pan gestures are working fine but no content is showing up)
In my MainViewController I set up the popup like before:
#IBAction func didTapCategory(_ sender: UIBarButtonItem) {
let popupContent = StructureNavigationListViewController.create()
let cardpopUp = SBCardPopupViewController(contentViewController: popupContent)
cardpopUp.show(onViewController: self)
}
In my StructureNavigationListViewController I set up the table view and the pop up:
public var popupViewController: SBCardPopupViewController?
public var allowsTapToDismissPopupCard: Bool = true
public var allowsSwipeToDismissPopupCard: Bool = true
static func create() -> UIViewController {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "StructureNavigationListViewController") as! StructureNavigationListViewController
return vc
}
#IBOutlet var tableView: UITableView!
var structures = Variable<[Structure]>([])
public var treeSource: StructureTreeSource?
let disposeBag = DisposeBag()
var depthDictionary : [String : Int] = [:]
public override func viewDidLoad() {
structures.asObservable()
.bind(to:tableView.rx.items) {(tableView, row, structure) in
let cell = tableView.dequeueReusableCell(withIdentifier: "StructureNavigationCell", for: IndexPath(row: row, section: 0)) as! StructureNavigationCell
cell.structureLabel.text = structure.name
cell.spacingViewWidthConstraint.constant = 20 * CGFloat(self.depthDictionary[structure.id]!)
return cell
}.disposed(by:disposeBag)
_ = tableView.rx.modelSelected(Structure.self).subscribe(onNext: { structure in
let storyBoard = UIStoryboard(name:"Main", bundle:nil)
let plansViewCtrl = storyBoard.instantiateViewController(withIdentifier: "PlansViewController2") as! PlansViewController2
self.treeSource?.select(structure)
plansViewCtrl.treeSource = self.treeSource
plansViewCtrl.navigationItem.title = structure.name
self.show(plansViewCtrl, sender: self)
if let mainVC = self.parent as? ProjectOverViewTabController2 {
mainVC.addChildView(viewController: plansViewCtrl, in: mainVC.scrollView)
}
})
showList()
}
func showList() {
if treeSource == nil {
treeSource = StructureTreeSource(projectId:GlobalState.selectedProjectId!)
}
//The following piece of code achieves the correct order of structures and their substructures.
//It is extremely bad designed and rather expensive with lots of structures and should
//therefore be refactored!
if let strctrs = getStructures() {
var sortedStructures : [Structure] = []
while(sortedStructures.count != strctrs.count) {
for strct in strctrs {
if let _ = sortedStructures.index(of: strct) {
continue
} else {
depthDictionary[strct.id] = getDepthOfNode(structure: strct, depth: 1)
if let structures = getStructures() {
if let parent = structures.first(where: {$0.id == strct.parentId}) {
if let index = sortedStructures.index(of: parent) {
sortedStructures.insert(strct, at: index+1)
}
} else {
sortedStructures.insert(strct, at: 0)
}
}
}
}
}
structures.value = sortedStructures
tableView.reloadData()
}
}
func getDepthOfNode(structure: Structure, depth: Int) -> Int {
if(structure.parentId == nil || structure.parentId == "") {
return depth
} else {
if let structures = getStructures() {
if let parent = structures.first(where: {$0.id == structure.parentId}) {
return getDepthOfNode(structure: parent, depth: depth + 1)
}
}
}
return -1
}
private func getStructures() -> Results<Structure>? {
do {
if let projectId = GlobalState.selectedProjectId {
return try Structure.db.by(projectId: projectId)
}
} catch { Log.db.error(error: error) }
return nil
}
}
Lot of code here. Sorry..
Is it because I call the create() method after the viewDidLoad() dequeues the cells?
It's hard to tell what is the problem, since you left no information about where didTapCategory is supposed to be called, but maybe it has something to do with your modelSelected subscription being prematurely released?
Edit:
As posted here: https://stackoverflow.com/a/28896452/11851832 if your custom cell is built with Interface Builder then you should register the Nib, not the class:
tableView.registerNib(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCellIdentifier")

How to add a custom NSToolbarItem to an existing toolbar programmatically

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.

Target-Action problems with custom view built from standard views

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(_:)))

Swift 2.2 selector in NSMenuItem

I have a simple one file menu bar app in swift:
import Cocoa
class StatusBarApp : NSObject {
func buildMenu() {
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
statusItem.title = "StatusBarApp"
let menu = NSMenu()
let aboutMenuItem = NSMenuItem()
aboutMenuItem.title = "About"
aboutMenuItem.target = self
aboutMenuItem.action = #selector(about)
menu.addItem(aboutMenuItem)
statusItem.menu = menu
}
func about() {
print("XXX")
}
}
NSApplication.sharedApplication()
StatusBarApp().buildMenu()
NSApp.run()
I can't make the "About" menu bar item to connected to the about() function. When I run the app, the "About" item is disabled.
How do I pass the selector to menu item action in Swift 2.2? Thanks
The selector is supposed to have a parameter (the NSMenuItem instance)
aboutMenuItem.action = #selector(StatusBarApp.about(_:))
...
func about(sender : NSMenuItem) {
print("XXX")
}
Edit:
The solution is to run the app as full Cocoa app including its delegate.
I added a second menu item to terminate the app.
import Cocoa
class StatusBarApp : NSObject, NSApplicationDelegate {
var statusItem : NSStatusItem!
func applicationDidFinishLaunching(aNotification: NSNotification) {
statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
statusItem.title = "StatusBarApp"
let menu = NSMenu()
let aboutMenuItem = NSMenuItem(title:"About", action:#selector(StatusBarApp.about(_:)), keyEquivalent:"")
aboutMenuItem.target = self
let quitMenuItem = NSMenuItem(title:"Quit", action:#selector(StatusBarApp.quit(_:)), keyEquivalent:"")
quitMenuItem.target = self
menu.addItem(aboutMenuItem)
menu.addItem(quitMenuItem)
statusItem.menu = menu
}
func about(sender : NSMenuItem) {
print("XXX")
}
func quit(sender : NSMenuItem) {
NSApp.terminate(self)
}
}
NSApplication.sharedApplication()
let statusBarApp = StatusBarApp()
NSApp.delegate = statusBarApp
NSApp.run()
update action
aboutMenuItem.action = Selector("about")
and add
aboutMenuItem.enabled = true
Consider this:
import Cocoa
class StatusBarApp : NSObject {
func buildMenu() {
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
statusItem.title = "StatusBarApp"
let menu = NSMenu()
let aboutMenuItem = NSMenuItem()
aboutMenuItem.title = "About"
aboutMenuItem.target = self
aboutMenuItem.action = #selector(about)
menu.addItem(aboutMenuItem)
statusItem.menu = menu
}
func about() {
print("XXX")
}
}
let app = StatusBarApp()
NSApplication.sharedApplication()
app.buildMenu()
NSApp.run()