How to attach a custom menu to the main view in a OS X desktop app in Swift - swift

Given the following GitHub project:
https://github.com/kellyjanderson/swift-custom-menue
How do I link the menu item Custom >> Custom Action, to the function customAction in the ViewController?

Define an outlet of your menu item in AppDelegate
#IBOutlet weak var customMenuItem: NSMenuItem!
In your view controller first get the instance of AppDelegate:
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
Get the instance of the menu item and then bind an IBAction to it:
appDelegate.customMenuItem.action = #selector(customAction(_:))
For e.g., you want to bind an action customAction to your menu item. You can add the following code in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
print(appDelegate.customMenuItem)
appDelegate.customMenuItem.action = #selector(customAction(_:))
}
And then define the IBAction
func customAction(sender: NSMenuItem){
print("Custom Menu Item clicked")
}
Output:
<NSMenuItem: 0x6080000a0720 Custom Action>
Custom Menu Item clicked

So the linked question Why can't I connect my menu to my view controller IBAction? came close but did not provide a concrete solution for me. Here is how I solved this.
First in the ViewController you need to mark your function as an #IBAction
#IBAction func customAction(sender: NSMenuItem){
print("Custom Menu Item clicked")
}
Once that function is marked as an IBAction it will be available in the responder chain; so now you can control-click and drag the menu item to the first responder icon and select your function, in this example customAction.

Related

How to communicate to the window controller from view controller of newly, programmatically created tab?

In my window controller, I implement:
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .above)
window.makeKey()
}
}
In the view controller, I have this code:
let window = self.view.window?.windowController as? WindowController
Also tried:
let window = NSApp.mainWindow?.windowController as? WindowController
If I don't have any tabs, it's able to get the window controller. But on new tabs, it does grab the window controller.
Similarly, I've unsuccessfully tried sending an action to the WindowController:
NSApp.sendAction(#selector(WindowController.pageLabelChange), to: nil, from: label)
Works for the original window, but not for any newly created tabs.
How do the newly created view controller objects communicate with the window controller?
Edit:
For more context in how I am using this code: It's basically a PDFView that's embedded in a window. The window has a tool bar that displays the page number. Using any of the above code, I can set the current page number of the PDFView, but when there's a tab, it does not work. Using the .PDFViewPageChanged notification, I call my func
NSApp.sendAction(#selector(WindowController.pageLabelChange), to: nil, from: pdfView)
Edit 2:
I've created a GitHub with a test project that shows the problem I have. You should be able to see that when you launch the project, the + button will add a number to the textfield in the tab bar. But if you go to the View menu > Add tab, it creates a new tab, but the + does nothing.
You create new window controller on stack so it is destroyed right after return, that is the reason. You need to store somewhere reference to created tab window controller and manage it (the place of storage is up to your app logic).
Here is simple demo that makes code work
Tested with Xcode 11.4 / macOS 10.15.5
final class WindowController: NSWindowController {
#IBOutlet weak var testField: NSTextField!
var tabControllers = [WindowController]() // << storage for child controllers
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .above)
window.makeKey()
tabControllers.append(wc) // keep reference in storage
// TODO: - you are responsible to manage wc, eg: remove
// from storage on window close.
}
}
}

How to reference a View from within a Window Controller?

I'm having a Window Controller with a toolbar. I also have a View Controller containing some views. How do I reference a view from the View Controller within my Window Controller? I'm still learning macOS development and I'm missing the bigger picture how code is structured and classes are meant to interact.
My concrete problem right now is this: Using XCode 9.4.1 I have a window with a toolbar and a button in it. That's how my WindowsController.swift looks like:
import Cocoa
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.titleVisibility = .hidden
}
#IBAction func startExport(_ sender: NSButton) {
print("Start Export")
}
}
In the ViewControllerScene there's a WKWebView that's loading a web page. When the button in the toolbar is pressed, I want to call that Web Views takeSnapshot method. So I need a reference in WindowsController.swift to that Web View, but control-dragging the Web View from the storyboard to WindowsController.swift in the assistant editor doesn't let me create that outlet.
This:
let vc = contentViewController as? ViewController
will take you to your view controller.

Opening window + view from an other view controller

I've got a ViewControllerOne. ViewControllerOne is connected via Ctrl-Drag (in storyboard) to a menu-button mBtn (which means I don't know how it is implemented programmatically).
Clicking on this mBtn, a ViewOne appears (present modal). This ViewOne is bound to ViewControllerOne. ViewOne has a button btnOne.
Clicking on btnOne I want ViewOne to be dismissed and ViewTwo to be shown. ViewTwo belongs to ViewControllerTwo and to WindowControllerTwo.
The WindowControllerTwo-ViewControllerTwo-binding is the standard case as created on a new project.
I have the following code in the IBAction for button btnOne in ViewControllerOne:
#IBAction func onbtnOnePressed(sender: AnyObject){
let m_WindowControllerTwo = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("WindowControllerTwo")) as! NSWindowController // I have no custom class for the window controller as I don't really know what I can use it for ...
let m_ViewTwo = WindowControllerTwo.contentViewController as! ViewControllerTwo // my custom class for ViewTwo
m_ViewTwo.attributeIWantToPassToThisView = self.x // an attribute I want to pass from view a to view b
m_WindowControllerTwo.contentViewController = m_ViewTwo // passing the attribute from a to b
m_WindowControllerTwo.showWindow(self) // this does not work
self.dismiss(nil) // see NOTE
}
This code actually does not work. On debugging it step by step, I'm seeing the window/view flickering but not appearing...
NOTE: I could connect the button btnOne with a ctrl-drag to ViewControllerTwo. This works. But then the current ViewOne does not get dismissed!
Question: What am I doing wrong here? In iOS swift this also works. I don't quite get the WindowController stuff, so I'll need your advice on this.
Instead of this: m_WindowControllerTwo.showWindow(self)
use:
let application = NSApplication.shared()
application.runModal(for: wordCountWindow) //this will present WindowControllerTwo modally.
then to close your present controller add this line: PresentWindowControllerName.close()

Adding views with IBAction to a NSStackView crashes application

I want to use the NSStackView to stack views above each other, I also want them to de able to expand so I can't use the NSCollectionView if i understood it correctly.
So, in storyboard, I've created a NSStackView(embedded in scroll view) in the main view controller and a view controller that I want to fill it with:
The button will fill the stack view with ten views:
#IBOutlet weak var stackView: NSStackView!
#IBAction func redrawStackView(_ sender: Any) {
for i in 0..<10 {
let stackViewItemVC = storyboard?.instantiateController(withIdentifier: "StackViewItemVC") as! StackViewItemViewController
stackViewItemVC.id = i
stackView.addArrangedSubview(stackViewItemVC.view)
}
}
And the ViewController on the right simply looks like this:
class StackViewItemViewController: NSViewController {
var id: Int = -1
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
#IBAction func buttonPressed(_ sender: Any) {
debugPrint("StackViewItemViewController" + id.description + "pressed")
}
Running this small application works fine, every time I press the button ten more stack view items appears. But, when I have the audacity to press one of the buttons to the right the application crashes:
Where am I going wrong?
I have tried to work around the IBAction to verify that this what breaks, and the application will not crash if I subclass the button and make a "buttonDelegate" protocol with a function being called from mouseUp.
I guess the problem is that the viewController objects, which you create in the loop, are released immediately.
Even though the view is attached to the stackView, it's viewController is destroyed.
You can fix this issue by keeping a reference to each viewController.
You can do this by creating a new variable
var itemViewControllers = [StackViewItemViewController]()
and then add each newly created viewController to it:
itemViewController.append(stackViewItemVC)

Menubar with Storyboard - validateMenuItem not get called

I'm trying to setup a menubar Application using storyboard but my validateMenuItem method not get called.
I will try to explain what i did.
First i dragged a Menu Item in my Application Scene. Then one Object for my MenuController. Created a MenuController (MenuController.swift) and filled it with code. Back in my storyboard I set my Menu delegate to MenuController and MenuController Outlet to Menu. (I'm not totally sure whether i have set the delegates correctly.)
When i start the app, the menu icon appears and the first item title is set to test. But when i'm clicking the icon the validateMenuItem method not get called.
MenuController.swift
import Cocoa
class MenuController: NSObject {
var statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1)
#IBOutlet weak var statusMenu: NSMenu!
#IBOutlet weak var item1: NSMenuItem!
override func awakeFromNib() {
print("awakeFromNib")
self.item1.title = "Test"
let icon = NSImage(named: "menubarIcon")
statusItem.image = icon
statusItem.menu = statusMenu
}
override func validateMenuItem(menuItem: NSMenuItem) -> Bool {
print("validateMenuItem")
return true
}
}
Storyboard Menu Delegates
(source: picr.de)
Storyboard MenuController Delegates
(source: picr.de)
Has anybody an idea?
Greets from Austria!
The menu/UI validation mechanism does not query the menu's delegate but uses the item's target to determine the enabled state instead.
If the target is not explicitly set, it walks the responder chain.
To get basic validation, you have to make sure that the following things are setup:
"Auto Enables Items" is checked in Interface Builder (on by default)
You control-dragged the menu's action to the first responder
The menu's action is implemented in a class that is part of the responder chain (e.g. a view controller that manages a set of actions for your UI)
A basic implementation of validateUserInterfaceItem: could look like the following for an item with an action selector called test:
func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool
{
if anItem.action() == Selector("test:") {
print("validating item \(anItem)")
return true
}
return true
}