How to access NSViewController from app delegate? - swift

In my macOS application I have menu items, which are replicated also in main UI. Application is consisted of main window with its delegate and single view along with its view controller. In app delegate I capture menu item click action, then I need to send this event to my view controller in order to take appropriate actions and also update main UI.
Question is how to access my view controller (NSViewController) from app delegate?

If you have window as an IBOutlet you can do
var rootViewController: MyViewController? {
return window!.contentViewController as? MyViewController
}
func sendPressed(_ sender: Any?) {
rootViewController?.sendPressed()
}
If you don‘t have a window variable you can get it through
NSApplication.shared.orderedWindows.first!

Actually you don't need a reference to the controller. There is First Responder
Declare the IBActions
In Interface Builder connect the menu items to the First Responder (red cube) of the target object and choose the appropriate action.
The action will be executed in the first object of the responder chain which implements the action.

Related

Segue from SKScene and back again cause game to "freeze"

I'm developing a SpriteKit game with two different scenes (MainMenu.sks and Settings.sks). I also have a UIViewController.
From the MainMenu the user can tap "Settings" which causes the app to load the Settings.sks scene and present it.
It's done using this code:
// Move to Settings.sks
let transition = SKTransition.flipVertical(withDuration: 1.0)
let loadScene = SettingsScene(fileNamed: "SettingsScene")
loadScene?.scaleMode = self.scaleMode
self.view?.presentScene(loadScene!, transition: transition)
From the Settings.sks scene the user can tap on the SKLabelNode "Change username" which causes the app to load the UIViewController by performing a segue.
The UIViewController contains a UITextField and a UIButton. The UIButton takes the user back to MainMenu.sks.
However, when I'm doing so the it's like a new scene of MainMenu is placed upon the other causing the app to sort of "freeze" and not responding to any touch gestures. I can see from the nodeCount that the nodes are doubled.
A workaround is loading the UIViewController directly from the MainMenu.sks (also via a segue). By doing so, the app works just fine.
I'm suspecting the problem to be the way I load Settings.sks.
What am I missing? Maybe unloading the entire SKView before exiting?
Performing a segue will present a new view on top of the stack. By using a segue to return to the main menu, the app will create a new main menu on top of the UIViewController. I can't say for certain why your app freezes. But I would guess that there are some properties or functions that don't work properly when a new main menu is created and presented from your UIViewController.
I recommend unwinding the segue used to reach the UIViewController instead. This way you can return to the previous view and conserve memory.
You'll need to write an IBAction in your destination view, the main menu in this case:
#IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
}
Then ctrl-drag from your button in the UIViewController to the exit icon in storyboard. You should see an option to use the IBAction unwindToMainMenu that you just wrote. This will create an unwind segue.
A complete guide to unwind segues and multiple examples can be found in this answer: What are Unwind segues for and how do you use them?

How to detect a click from an NSToolbarItem inside an NSWindowController?

I have a toolbar on my macOS app, developed in Swift. The toolbarItem is dragable onto the NSWindowController, and I can setup an IABAction function, I just have a print in the function at the moment. And when I click on the button nothing happen the click does not seem to be recognised as an action ?
I had a few more line of code in the function but deleted it and now have just the print("test") line.
#IBAction func exportCsvClicked(_ sender: NSToolbarItem) {
print("test") }
No output observed, so I'd love to get "test" in the console when I click on this button.
Here is a list of the connections associated with the toolbarItem.
I found a way to get around the fact that the IBAction from an NSToolbarItem does not recognise the click on this item:
1/I made a customSegue from the item to the main window controller (it can go anywhere)
2/The prepare for segue function posts a notification to the notification saying that the item has been clicked.
3/The main view controller observes the notification and presents, either has a popup or a sheet (I got two buttons), the view that I have setup in the storyboard (referencing with the storyboardID). I found that you need to pass on all the necessary variable to setup the view from the main view Controller, and that there was issue of code in the viewDidLoad function of the sheet/popup view not running, I am suspecting that they might be different instances.

Open URL on Menu Item Click

I need to Open a URL when the user clicks on a Menu Item.
Currently I'm using the following code
url = URL(string: "https://www.example.com/test")
NSWorkspace.shared().open(url)
This works fine from a Button.But I cannot connect a Segue from the Menu Item to the view controller inorder to setup an IOAction,so that I can write the necessary code.
How can I solve this problem? Please advice.
Segues are for opening new views or windows from your app. You should implement your function as ordinary #IBAction in your view controller or app delegate, and connect action under Sent Actions in the Connections Inspector with the First Responder in the storyboard scene. You will find your action method there. Cocoa touch will automatically enable or disable the menu item weather the view controller is in the responder chain. If you implement the action method in the app delegate the menu item should always be enabled, because the delegate is always in the responder chain.
#IBAction func openURL(_ sender: AnyObject) {
let url = URL(string: "https://www.example.com/test")
NSWorkspace.shared().open(url)
}
Implement the method as IBAction in the view controller.
Connect the NSMenuItem to First Responder (red cube) of the Application Scene and select the method.
If there are multiple implementations of the method the framework executes the first in the responder chain.

Get document object with current focus from NSCollectionView

I have a (subclassed) NSCollectionView open, containing multiple text views. Each of the text views is mapped to a (subclassed) NSDocument object. (The idea is to use the document architecture's save functions but not its windowing functions, because I need multiple documents in the same window and the traditional document architecture doesn't allow that.)
Now, there's a function I'd like the user to be able to call from the main menu that will affect their currently selected document. That is: the document is currently visible in a text view with current focus, and the menu command should make an alteration to that document. But the sender of the menu command is just the menu. When the window controller handles the command from the menu, how can I tell it what the currently selected document is?
This is what the responder chain is for.
Since you're using NSCollectionView, you probably already have a subclass of NSCollectionViewItem. If not, create one. Implement your action method in this subclass. Example:
class DocumentItem: NSCollectionViewItem {
var document: MyDocument? {
return representedObject as? MyDocument
}
#IBAction func doThatThing(sender: AnyObject?) {
Swift.print("This is where I do that thing to \(document)")
}
// #IBOutlets and whatnot here...
}
You may need to set this as the custom class of your NSCollectionViewItem in your xib or storyboard.
Next, if your cell view (the view owned by your NSCollectionViewItem) isn't a custom subclass of NSView already, you should make it a custom subclass. You must override acceptsFirstResponder to return true:
class DocumentCellView: NSView {
override var acceptsFirstResponder: Bool { return true }
// #IBOutlets and whatnot here...
}
Make sure you set this as the custom class of your cell view in your storyboard or xib.
Finally, connect the action of your menu item to doThatThing: on First Responder:
Here's how it works:
Because the cell view now returns true for acceptsFirstResponder, when the user clicks a cell view in the collection view, the system will make it the first responder (the start of the responder chain).
When a view has a view controller, it makes that view controller the next responder after itself in the responder chain (if you are on OS X 10.10 Yosemite or later). Your cell view has a view controller: the item object you return from outlineView:itemForRepresentedObjectAtIndexPath:. (NSCollectionViewItem is a subclass of NSViewController, so your custom item is a view controller.)
When the user clicks the menu item, the menu item asks NSApplication to send its action along the responder chain, starting with the first responder. The first responder is the cell view, but it doesn't respond to the doThatThing: message. So NSApplication asks the view for its nextResponder, which is an instance of your NSCollectionViewItem subclass. That object does respond to doThatThing:, so NSApplication sends doThatThing: to your item object (with the NSMenuItem object as the sender argument) and doesn't check the rest of the responder chain.

Segue to UIViewController in different UINavigationController

I have the following setup:
I have a UIViewController embedded in a UINavigationController. I have a second UIViewController embedded in a second UINavigationController. I need to call the second UIViewController in two ways. I have two buttons set up on the first UIViewController. The first button has a segue created by control-click-drag between the button and the second UINavigationController. When I click the button, the second view controller displays. The second button calls the created segue using performSegueWithIdentifier like this:
#IBAction func segueTwoButton(sender: AnyObject) {
performSegueWithIdentifier("ShowSecondView", sender: self)
}
In both cases the second view controller displays correctly.
But, I have some information that I want to pass. So in the prepareForSegue function, I have this:
let vc = segue.destinationViewController as! SecondViewController
This fails, because the segue is going through the second UINavigationController to get to the second UIViewController.
I've come up with two ways of handling this.
I can use the following in the prepareForSegue
let vc = segue.destinationViewController.childViewControllers[0] as! SecondViewController
I can add a UINavigationController class and then pipe it through there, but that seems like a lot of work to do this.
Both of these ways work, but is this the best way, or is there some other way that I should consider handling it?
Second question - Assuming that I do it this way and put a UINavigationController class in so that I can pass the information along, what is the proper way to unwind? I can create an unwind segue that will go straight from the second view controller, back to the first, but is it acceptable to do it this way? Or would I need to unwind to the UINavigationController class and then to the first view controller?
I'm not sure why you decided to embed another navigation controller within another. Generally, if you embed a navigation controller one time that will suffice for the rest after you've set up your other segues. So first thing's first delete that second nav controller.
Secondly, from experience, I would always avoid control dragging from anything other the view controller itself. Otherwise, you get duplicate calls to perform segues. So go ahead and delete all remnants of the prior segues you created with the buttons. Instead, Control drag from the top yellow view controller icon to the next view controller. make sure you name the segue arrow so that you can properly performsegueWithIdentifier
Now you should have First View directly segued to the Second View. This will be easier to pipe or funnel information through the segue function exactly like you had coded above
let vc = segue.destinationViewController as! SecondViewController
Global Variable ( will replace segue funneling information )
Lets take this as an example. Say I wanted to recored the user's name on the first view controller.
// First View Controller
private let userName: String
// After that we can create a new object called NameSingleton
class NameSingleton {
// NameSingleton.swift
var UserName:String = " " // Since its global this value will change
static let sharedinstance = NameSingleton()
}
// Now hop back over to your first ViewController
// before you trigger your segue assign whatever data you need to your
// singleton. In this case, a name.
// First View Controller.swift
#IBAction func segueTwoButton(sender: AnyObject) {
NameSingleton.sharedInstance.UserName = "Hi"
performSegueWithIdentifier("ShowSecondView", sender: self)
}
// Now if you go to the second view controller and access it, the name
// will be stored in the singleton. (Great for moving huge pieces info
// (i.e. structs, objects, lists)