Enable or disable menu items on different view controllers in cocoa app? - cocoa

I have 3 view controllers say main1, main2 and child. I have added a menu item, on click of that it should open child view controller as modal.
Whenever user is in main1 VC, menu item should be enabled. If user in main2 VC, menu should be disabled. Right now I’ve added modal segue between menu item and child VC.
I followed following approaches to disable, but they are not working.
Method 1:
In main2 VC, I’ve added
func validateUserInterfaceItem(_ anItem: NSValidatedUserInterfaceItem) -> Bool {
return false
}
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
return false
}
Method 2:
override func viewDidLoad() {
super.viewDidLoad()
let mainMenu = NSApplication.shared().mainMenu!
let appMenu = mainMenu.item(at: 0)!.submenu
appMenu?.item(withTitle: someMenuTitle)?.isEnabled = false
}

If you use a modal segue it will be always activated.
To enable/disable dependent on the presented view controller I would add an action to the view controller to open the view controller manualy as modal. The menu item has to be connected to the action (openModalViewController) with the first responder.
#IBAction func openModalViewController(_ sender: AnyObject) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: "MyViewController") as! NSViewController
presentAsModalWindow(viewController)
}
Consider there must be at least one view able to get the first responder in main1/main2 that the menu item will activate. If this is not the case you would have to implement acceptsFirstResponder for the corresponding view.
override var acceptsFirstResponder: Bool{
return true
}
To implement validateUserInterfaceItem would be not required in this case, only if you want to control activation/deactivation dependent on an additional state as in the example below.
extension ViewController: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(delete(_:)) {
return tableView.selectedRow < 0 ? false : true
}
return true
}
}

Related

Implementing NSSegmentedControl in NSToolbar to control NSTabViewController

In my macOS application, I'm trying to replicate the Photos.app implementation of NSSegmentedControl in NSToolbar to control an NSTabViewController. For reference, here's what that looks like:
So, my approach was as follows:
Hide the default NSTabView header using the Interface Builder
Programmatically add an NSToolbar
Insert NSSegmentedControl as an NSToolbarItem.
Use a #selector to listen for changes to NSSegmentedControl.
Here's the current implementation:
class WindowController: NSWindowController, NSToolbarDelegate {
// MARK: - Identifiers
let mainToolbarIdentifier = NSToolbar.Identifier("MAIN_TOOLBAR")
let segmentedControlIdentifier = NSToolbarItem.Identifier("MAIN_TABBAR")
// MARK: - Properties
var tabBar: NSSegmentedControl? = NSSegmentedControl(labels: ["One", "Two"], trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(didSwitchTabs))
var toolbar: NSToolbar?
var tabBarController: NSTabViewController?
// MARK: - Life Cycle
override func windowDidLoad() {
super.windowDidLoad()
self.toolbar = NSToolbar(identifier: mainToolbarIdentifier)
self.toolbar?.allowsUserCustomization = false
self.toolbar?.delegate = self
self.tabBar?.setSelected(true, forSegment: 0)
self.tabBarController = self.window?.contentViewController as? NSTabViewController
self.tabBarController?.selectedTabViewItemIndex = 0
self.window?.toolbar = self.toolbar
}
// MARK: - NSToolbarDelegate
public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
var toolbarItem: NSToolbarItem
switch itemIdentifier {
case segmentedControlIdentifier:
toolbarItem = NSToolbarItem(itemIdentifier: segmentedControlIdentifier)
toolbarItem.view = self.tabBar
case NSToolbarItem.Identifier.flexibleSpace:
toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
default:
fatalError()
}
return toolbarItem
}
public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
// MARK: - Selectors
#objc func didSwitchTabs(sender: Any) {
let segmentedControl = sender as! NSSegmentedControl
if (segmentedControl.selectedSegment == 0) {
self.tabBarController?.selectedTabViewItemIndex = 0
} else if (segmentedControl.selectedSegment == 1) {
self.tabBarController?.selectedTabViewItemIndex = 1
}
}
}
And, here it is in action:
Now, I am new to macOS development and this feels like it's a very complicated and convoluted way of solving this problem. Is there an easier way I could achieve the same thing ? Perhaps somehow in Interface Builder ? What could be done to improve here ? What have I done wrong ?
Thanks for your time.
For anybody implementing NSSegmentedControl on the toolbar and it did not trigger IBAction, I got the same problem and pay my half-day to resolve this.
The problem is I connect my segmented with the NSWindowController class.
To fix this, create a subclass of NSWindow, set that class to base class of window on your storyboard, then create #IBOutlet #IBAction link to NSWindow. Remember, link it with NSWindowController will not work.

NSOutlineView how to show blue outline during right click

Everyone discussed about how to get rid of the blue outline during right click... but me.
Instead, I'm trying to display the blue outline.
I didn't get any outline when I right clicked my outline view row. The menu appeared but the outline wasn't. You can see that the blue outline is not visible in this picture below:
Below is what I'm trying to achieve.
Update
This is how I implemented the NSMenu. I subclassed the NSOutlineView and made a new protocol to override NSOutlineViewDelegate.
This idea was to make it simple by letting the NSOutlineView ask the NSMenu for each item, so we can implement different menu for each item. It works but the blue outline view doesn't show up during right click.
KRMenuOutlineView.swift
import Cocoa
#objc protocol KRMenuOutlineViewDelegate: NSOutlineViewDelegate {
// This method will ask NSMenu for each item in outline view
func outlineView(_ outlineView: KRMenuOutlineView, menuFor item: Any, event: NSEvent) -> NSMenu?
}
class KRMenuOutlineView: NSOutlineView {
override var delegate: NSOutlineViewDelegate? {
didSet {
if let newValue = delegate {
/*
* Swift doesn't support overriding inherited properties with different type
* like Objective C Does, therefore we need internal delegate.
*/
internalDelegate = unsafeBitCast(newValue, to: KRMenuOutlineViewDelegate.self)
} else {
internalDelegate = nil
}
}
}
private var internalDelegate: KRMenuOutlineViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
let point = self.convert(event.locationInWindow, from: nil)
if let item = self.item(atRow: self.row(at: point)) {
return self.internalDelegate?.outlineView(self, menuFor: item, event: event)
}
return super.menu(for: event)
}
}
Then, I use it in my view controller like this:
KRTreeViewController.swift
extension KRTreeViewController: KRMenuOutlineViewDelegate {
func outlineView(_ outlineView: KRMenuOutlineView, menuFor item: Any, event: NSEvent) -> NSMenu? {
let menu = NSMenu(title: "Contextual Menu")
menu.delegate = self
let key = String(utf16CodeUnits: [unichar(NSBackspaceCharacter)], count: 1) as String
let deleteMenuItem = menu.addItem(withTitle: "Delete",
action: #selector(didClickMenuItem(_:)),
keyEquivalent: key)
deleteMenuItem.representedObject = myItem
deleteMenuItem.target = self
return menu
}
#objc fileprivate func didClickMenuItem(_ menuItem: NSMenuItem) {
// ...
}
}
How to properly show a context menu:
If you have created your menu using a storyboard:
First, go to the storyboard and add the menu to the viewController that contains the outlineView.
Then make it an #IBOutlet so you can reference it later.
In a method like viewDidLoad(), add the menu to the outlineView by calling
outlineView.menu = myMenu
where myMenu can either be the one you created in Interface Builder or in code.
You can run the app now and should see the blue outline around the cell.
The problem now is that you don't know which cell the user has clicked.
To fix this, set yourself as the delegate of myMenu and adopt the NSMenuDelegate protocol.
func menuNeedsUpdate(_ menu: NSMenu) {
let row = self.outlineView.clickedRow
guard row != -1 else { return }
for item in menu.items {
item.representedObject = row
}
}
Here you can do whatever you need. This implementation sets the rowIndex as the representedObject of each menu item. Keep in mind that this only works on static outlineViews (ones that don't change in the background) and menus which only go one level deep.
You could also store the index or object represented by the cell (if the outlineView is not static) in a local variable.

Conditionally showing NSViewController at app launch

I'm developing an OSX app where I show first a login/register window if the user hasn't logged in yet.
After login success I show my main view controller.
If the user is already logged in (a token is stored), then the app has to launch directly with the main view controller.
I'm new to OSX development, I googled for this kind of scenario but couldn't find anything.
So I went up with what I think should work. It works sometimes, sometimes I get a blank window.
In the storyboard I let the Main Menu and the Window Controller. I removed the "contains" segue to my main view controller.
In AppDelegate, I put this:
func applicationDidFinishLaunching(aNotification: NSNotification) {
if loggedIn {
self.showViewController(NSStoryboard.mainViewController())
} else {
let loginController = NSStoryboard.loginViewController()
loginController.delegate = self
self.showViewController(loginController)
}
}
private func showViewController(viewController: NSViewController) {
if let mainWindow = NSApplication.sharedApplication().mainWindow {
mainWindow.contentViewController = viewController
} else {
print("Error: No main window!")
}
}
About half of the times the window is empty and I see in the console "Error: No main window!". I thought maybe I can use applicationDidBecomeActive but this is called basically when it comes to the foreground and this is not what I need.
Further, the times when it works, and I log in, then I want to show the main view controller:
func onLoginSuccess() {
self.showViewController(NSStoryboard.mainViewController())
}
And here I also get "Error: No main window!" (always) and nothing happens.
The docs say following about mainWindow being nil:
The value in this property is nil when the app’s storyboard or nib file has not yet finished loading. It might also be nil when the app is inactive or hidden.
But why is the storyboard not finished loading or the app inactive when I'm launching it? And on login success the app is definitely active and in the foreground and the main window is always nil.
What am I doing wrong? How can I implement this workflow? Alternatively I could create a "parent" view controller, have that one connected to the window in the storyboard, and add the login or main view controller as nested view controllers to that. But don't really like having to add a do nothing view controller.
I'm using XCode 7(beta 4), Swift 2, OSX 10.10.4
Edit:
The NSStoryboard methods come from an extension, it looks like this:
extension NSStoryboard {
private class func mainStoryboard() -> NSStoryboard { return NSStoryboard(name: "Main", bundle: NSBundle.mainBundle()) }
private class func signupStoryboard() -> NSStoryboard { return NSStoryboard(name: "LoginRegister", bundle: NSBundle.mainBundle()) }
class func mainViewController() -> ViewController {
return self.mainStoryboard().instantiateControllerWithIdentifier("MainViewController") as! ViewController
}
class func loginViewController() -> LoginViewController {
return self.signupStoryboard().instantiateControllerWithIdentifier("LoginViewController") as! LoginViewController
}
class func registerViewController() -> RegisterViewController {
return self.signupStoryboard().instantiateControllerWithIdentifier("RegisterViewController") as! RegisterViewController
}
}
To put the solution we found in the comments as an answer:
Apparently NSApplication.sharedApplication().mainWindow is a different window than my main window in the storyboard.
So, I created an NSWindowController subclass and assigned it to the window in the storyboard, using the identity inspector.
Then I moved the logic I had in app delegate to this NSWindowController. It looks like this:
class MainWindowController: NSWindowController, LoginDelegate {
override func windowDidLoad() {
if loggedIn {
self.onLoggedIn()
} else {
let loginController = NSStoryboard.loginViewController()
loginController.delegate = self
self.contentViewController = loginController
}
}
func onLoggedIn() {
self.contentViewController = NSStoryboard.mainViewController()
}
func onLoginSuccess() {
self.onLoggedIn()
}
}
* Thanks Lucas Derraugh for pointing me in the right direction!
enum Storyboards: String {
case main = "Main"
case settings = "Settings"
func instantiateVC<T>(_ identifier: T.Type) -> T? {
let storyboard = NSStoryboard(name: rawValue, bundle: nil)
guard let viewcontroller = storyboard.instantiateController(withIdentifier: String(describing: identifier)) as? T else { return nil}
return viewcontroller
}
}
//Need to use like this
//Make sure Storyboard Id and class-name are the same
if let windowController = Storyboards.main.instantiateVC(IDMainController.self) {
windowController.showWindow(nil)
//----- OR -----
self.contentViewController = windowController
} else {
print("Cannot find IDMainController")
}

Keep both NSSplitViewController's child controllers in first responder chain

I've a document based app with an NSSplitViewController as the main window's content view controller.
The left pane contains a custom view with controller, which implements some menu commands.
The right pane contains a standard NSTableView with controller.
When the app starts the menu commands work as expected, but as soon as anything inside the right table view is selected, the menu commands get disabled.
How can I make sure that the view controller of the left pane remains inside the first responder chain?
I tried hooking up the menu commands directly to the correct view controller, but IB does not allow connections to another scene in a storyboard. I can only connect to objects in the same scene.
Regards,
Remco Poelstra
Connect to First Responder.
You can have all child view controllers respond to actions by implementing -[NSResponder supplementalTargetForAction:sender:] in your NSSplitViewController subclass:
- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
id target = [super supplementalTargetForAction:action sender:sender];
if (target != nil) {
return target;
}
for (NSViewController *childViewController in self.childViewControllers) {
target = [NSApp targetForAction:action to:childViewController from:sender];
if (![target respondsToSelector:action]) {
target = [target supplementalTargetForAction:action sender:sender];
}
if ([target respondsToSelector:action]) {
return target;
}
}
return nil;
}
In Swift 4 you can do the following:
override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
for childViewController in childViewControllers {
if childViewController.responds(to: action) {
return childViewController
} else {
guard let supplementalTarget = childViewController.supplementalTarget(forAction: action, sender: sender) else {
continue
}
return supplementalTarget
}
}
return super.supplementalTarget(forAction: action, sender: sender)
}

How do I get the return key to perform the same action as a button press in Swift?

I want to know how you allow an action to be made by either pressing the return key on the software keyboard or by tapping a UIButton.
The UI button is already set up to perform an IBAction.
How do I also allow users to press the return key on the keyboard to perform the same action?
Make sure your class extends the UITextFieldDelegate protocol
SomeViewControllerClass : UIViewController, UITextFieldDelegate
You can perform action as follows:
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
//textField code
textField.resignFirstResponder() //if desired
performAction()
return true
}
func performAction() {
//action events
}
UPDATE
If your deployment target is iOS 9.0 or later, you can connect the “Primary Action Triggered” event of your text field to an action, like this:
ORIGINAL
Make your view controller adopt the UITextFieldDelegate protocol.
Set your text field's delegate to your view controller.
Implement textFieldShouldReturn: to call your action.
Swift 4.2 :
Other approach for the textfield created programmatically and doesn't need delegate :
MyTextField.addTarget(self, action: #selector(MyTextFielAction)
, for: UIControl.Event.primaryActionTriggered)
And then perform your action like below :
func MyTextFielAction(textField: UITextField) {
//YOUR CODE can perform same action as your UIButton
}
If your deployment target is iOS 9.0 or later, you can connect the “Primary Action Triggered” event of your text field to an action, like this:
I was not able to get the "Primary Action Triggered" to work as suggested. I used "Editing Did End" and that works for now Screenshot of Editing Did End
Here is a complete example, with both:
button-action to write and also to clear label and text when pressing button repeatedly it alternates both actions
return-in-keyboard when pressing key it triggers action and also resigns first responder
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField1: UITextField!
#IBOutlet weak var label1: UILabel!
var buttonHasBeenPressed = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
textField1.delegate = self
}
#IBAction func buttonGo(_ sender: Any) {
performAction()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
performAction()
return true
}
func performAction() {
buttonHasBeenPressed = !buttonHasBeenPressed
if buttonHasBeenPressed == true {
label1.text = textField1.text
} else {
textField1.text = ""
label1.text = ""
}
}
}

Resources