os x equivalent to ios's rootViewController - cocoa-touch

What would be the OS X equivalent of ios' (Ruby style) :
#window.rootViewController = NSViewController.alloc.initWithNibName(nil, bundle: nil)
There are two aspects :
dealing with the absence of rootViewController in os x
doing the equivalent of initWithNibName(nil, bundle: nil), that fails on os x
I'm trying to build a window in code (without nib)... and follow along Pragmatic's Programmer Guide to RubyMotion (written for iOS).

In my case, I had a single view controller owned by the window, so the following code worked for me:
let viewController = NSApplication.sharedApplication().keyWindow!.contentViewController as! MyViewController
where MyViewController is my NSViewController subclass.

There is no concept of a "rootViewController" in OS X applications. This is because applications are made up of one or more windows and/or a menu bar, none of which is the "root".
You can, however, find the window controller starting from a view controller:
[[[self view] window] delegate];
If you are looking to call custom methods on the window controller, it is probably better to create a delegate so that assumptions about the controller don't get you into trouble.
The equivalent of NSViewController.alloc.initWithNibName(nil, bundle: nil) would be:
[[NSViewController alloc] initWithNibName:nil bundle:nil];
Method calls get nested in square brackets, methods with multiple parameters are simply label1:parameter1 label2:parameter2...
Usually, though, you would have a custom NSViewController subclass that you would instantiate instead.

If you want to reset rootViewController like in iOS you can use this code:
let storyBoard = NSStoryboard(name: NSStoryboard.Name(rawValue:"Main"), bundle: Bundle.main)
let homeViewController = storyBoard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "SIHome")) as! HomeViewController
NSApp.keyWindow?.contentViewController = homeViewController
NSApp is a shortcut for NSApplication.shared()
rootViewController is called contentViewController in OSX

Related

Equivalent Storyboard function for Nib

I'm trying to convert a project on macOS that uses Storyboards to instantiate a ViewController through a delegate, although I'm running into some difficulty trying to convert it to use a Nib instead.
Currently the storyboard version of the code uses an App Delegate which is associated with two View Controllers. When a button is clicked the front window animates and flips over revealing another (back) window. The code that instantiates the View Controller is:
mainWindow = [NSApplication sharedApplication].windows[0];
secondaryWindow = [[NSWindow alloc]init];
[secondaryWindow setFrame:mainWindow.frame display:false];
// the below is what I'm not sure of - how to reference nib instead of storyboard?
NSStoryboard *mainStoryboard = [NSStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]];
NSViewController *vc = [mainStoryboard instantiateControllerWithIdentifier:#"BackViewController"];
[secondaryWindow setContentViewController:vc];
I'm not sure the proper way to reference a nib instead of a storyboard in the example shown above. The project I'm trying to convert is located here. I'm really hoping someone can help, thank you!
This is pretty easy to do. Just make an NSViewController subclass (or an NSWindowController subclass if you want it to control a whole window) for each of the two views. For each view, override -init and have it call super's implementation of -initWithNibName:bundle: with the name of the view's nib file:
#implementation MyViewController
- (instancetype)init {
self = [super initWithNibName:#"MyViewController" bundle:nil];
if (self == nil) {
return nil;
}
return self;
}
Note that if you can require a sufficiently recent version of macOS (I think it's 10.11 and higher off the top of my head, but it's possible that I could be off by a version or so), you don't even have to do this much, because NSViewController will automatically look for a nib file with the same name as the class.
Anyway, now you should be able to instantiate a MyViewController and insert its view into your view hierarchy, and manipulate it the same way you'd manipulate any other view:
MyViewController *vc = [MyViewController new];
[someSuperview addSubview:vc.view];
If you want to do windows instead, you can make an NSWindowController subclass instead of NSViewController. NSWindowController is slightly more annoying to use, since its initializers that take nib names are all convenience initializers, whereas the designated initializer just takes an NSWindow. So if you're using, say, Swift, you can't do it the way I did it above with NSViewController. Objective-C, of course, generally lets you do whatever you want, so you actually can get away with just calling super's -initWithWindowNibName:owner:, and I won't tell anyone, wink wink, nudge nudge. However, to be stylistically correct, you probably should just call -initWithWindow: passing nil, and then override windowNibName and owner:
#implementation MyWindowController
- (instancetype)init {
self = [super initWithWindow:nil];
if (self == nil) {
return nil;
}
return self;
}
- (NSNibName)windowNibName {
return #"MyWindowController";
}
- (id)owner {
return self;
}
This should get you a window controller that you can just initialize with +new (or +alloc and -init if you prefer), then call its -window property and manipulate the window as normal.

Calling a view in macOS from outside a view controller

My app uses in app purchases, and if the user is found to not have the premium version, then the class handling the function they tried to use will call on a method from MKStoryKit, a class that makes dealing with StoryKit easier. MKStoryKit is not a view or a view controller, it's simply for utility. The view I want to call is a custom view which provides them with the features of the premium version and gives the user a few options, while looking a little bit prettier than just a system alert. On my iOS app, after declaring the view from the storyboard (this view has no segues going to it) using:
NSString *storyboardName = #"Main";
NSString *viewID = #"TermsOfUseQuery";
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
UIViewController *Terms = [storyBoard instantiateViewControllerWithIdentifier:viewID];
I call the following line which simply presents the previously declared UIViewController "Terms" on top of whatever is currently happening:
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:Terms animated:YES completion:nil];
I was wondering if there was a similar method for macOS, i've been unable to find it. I am aware that, generally, anything "UI" on iOS becomes "NS" and I've already accounted for that, like declaring NSViewController.
As an alternative, I've managed to call the view from inside the main view controller using:
let board:NSStoryboard = NSStoryboard(name: "Main", bundle: nil)
let viewtest:NSViewController = board.instantiateController(withIdentifier: "termsOfUseVC") as! NSViewController
self.presentViewControllerAsModalWindow(viewtest)
This has the effect that I am looking for, however it has to be achieved from MKStoryKit (which is an Objective-C class). I was wondering if anyone had a solution for this, either to just display the view as a modal window at any time (preferred, effectively what that single line does) or to call it as a modal window on top of the current "top" view? Thanks to everyone!
I found a way to do it, posting this for future reference in case anyone else needs this in the future, or if anyone else tries using Google for this:
[[NSApplication sharedApplication].keyWindow.windowController.contentViewController presentViewControllerAsModalWindow:Terms];
This will present a view from the storyboard from the most recent Window of your application, though that "from" part is not too relevant as this will show the View as its own window. If you chose to use:
[[NSApplication sharedApplication].keyWindow.windowController.contentViewController presentViewControllerAsSheet:Terms];
Then this would present it as a slidedown from the last window, this is where the window's identity, if you have multiple, matters. The "Terms" present in both lines is a NSViewController that I've shown how to declare in my original answer (Except this is NS instead of UI, macOS vs iOS difference). The actual declaration is below for the sake of clarity:
NSString *storyboardName = #"Main";
NSStoryboard *storyBoard = [NSStoryboard storyboardWithName:storyboardName bundle:nil];
NSViewController *Terms = [storyBoard instantiateControllerWithIdentifier:#"termsOfUseVC"];
Hope this helps someone.

Initializing another window using storyboard for OS X

I have created a Cocoa application in Xcode6 which uses storyboards. As a template, Xcode provides a window for the application. I want to add a second window to show when the program is first loaded. So basically, there will be two windows showing up.
I have put a window controller on Main.storyboard where the first window also resides. However, I couldn't find the way to show this second window when the program starts. Could you please help?
Thank you.
In your Storyboard, select your second Window Controller. In the identity inspector, specify a name for this window controller, e.g secondWindowController
Then, in your app delegate, set up a property for the window controller:
#property NSWindowController *myController;
In your applicationDidFinishLaunching: method implementation, create a reference to the Storyboard. This way you get access your window controller from the storyboard.
After that, the only thing left to do is to display the window by sending your window controller the showWindow: method.
#import "AppDelegate.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
#synthesize myController;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSStoryboard *storyBoard = [NSStoryboard storyboardWithName:#"Main" bundle:nil]; // get a reference to the storyboard
myController = [storyBoard instantiateControllerWithIdentifier:#"secondWindowController"]; // instantiate your window controller
[myController showWindow:self]; // show the window
}
#end
Swift 3 version:
var myWindowController: NSWindowController!
override init() {
super.init()
let mainStoryboard = NSStoryboard.init(name: "Main", bundle: nil)
myWindowController = mainStoryboard.instantiateController(withIdentifier: "myWindowControllerStoryboardIdentifier") as! NSWindowController
myWindowController.showWindow(self)
}
Make sure you define myWindowController outside the function or else the window won't show up.
It's actually better to do this in init() (of AppDelegate) as you may need it earlier on.
Swift 5:
The project setup in XCode 13 has entirely changed. There is no longer an example of how to connect to the storyboard from the AppDelegate. Instead, they are hardcoding a NSWindow. I still find Storyboards useful, hence the below should come in handy. Remember to name your WindowController in Storyboard as mainWindowController.
let mainStoryboard = NSStoryboard.init(name: NSStoryboard.Name("Main"), bundle: nil)
var monitorcontroler = mainStoryboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("mainWindowController")) as! NSWindowController
monitorcontroler.showWindow(self)
swift 4 version :
var monitorcontroler: NSWindowController!
override init() {
super.init()
let mainStoryboard = NSStoryboard.init(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
monitorcontroler = mainStoryboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "moniteur")) as! NSWindowController
monitorcontroler.showWindow(self)
}

Confusion with UINavigationControllers in SplitViewController

I am setting up an iPad app that uses a SplitViewController. In my app delegate I have the following in didFinishLaunchingWithOptions:
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *leftNavController = [splitViewController.viewControllers objectAtIndex:0];
LeftViewController *leftViewController = (LeftViewController*)[leftNavController topViewController];
DetailViewController *detailViewController = [splitViewController.viewControllers objectAtIndex:1];
NSLog(#"Detail View Ctrl >> %#", [detailViewController class]);
When I run the app, the NSLog statement returns "UINavigationController" when DetailViewController is actually a subclass of UIViewController. However, in XCode, code completion shows all the methods that are implemented in the DetailViewController subclass. Any ideas? Thanks!
I think your DetailViewController is actually embedded inside a UINavigationController, and your fourth line is in error. Take a look instead at the topViewController for the second view controller inside your split view controller, much like you do for the LeftViewController.
The reason Xcode is continuing to suggest completion for DetailViewController methods is because you've given it that type. Code completion doesn't rely on runtime behavior (how could it?) – instead, it relies on static analysis of the code that you type. If you tell Xcode that something is a DetailViewController, it'll believe you and autocomplete based on that information.

Cocoa-Touch internals: How does the view know its controller?

If you add a subview to a view or add a view to a window, how does iOS know which controller this view belongs too?
Easy example:
Have a UIView without UIViewController and add it to the window [window addSubView:myView] --> it will not rotate.
Now use a UIViewController, have it implement shouldAutoRotateToInterfaceOrientation: and add the controller's view to the window: [window addSubView:myController.view] --> magically, the view will adjust to interface orientation.
But look at the code: in both cases a UIView was added. How can iOS possibly be aware that in the second case a UIViewController was involved?
I'm interested in how this is done internally. My best guess is that UIViewController.view is a setter which adds the controller to an internal array of controllers or assigns itself to some internal variable which holds the currently active controller.
Simple. Look in UIView.h. It's right there. Each UIView has a pointer back to a UIViewController (which is apparently referred to as the "viewDelegate").
Dave DeLong is correct (and gets +1) as it is clearly defined UIView.h as #package so anything in UIKit can access it.
Here is an example of accessing that variable for educational purposes only (obviously you will not do this in a real application).
SomeAppDelegate.m
#synthesize navigationController=_navigationController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Add the navigation controller's view to the window and display.
self.window.rootViewController = self.navigationController;
[self.window makeKeyAndVisible];
//DO NOT TRY THIS AT HOME
UIView *mynavview = self.navigationController.view;
//Guaranteed _viewDelegate atleast in iOS 4.3
Ivar ivar = class_getInstanceVariable([UIView class], "_viewDelegate");
UIViewController *controller = object_getIvar(mynavview, ivar);
NSLog(#"controller = self.navigationController? %#", controller == self.navigationController ? #"Yes" : #"No");
return YES;
}
UIViewController has a private class method (called controllerForView:, I believe) which is used to find the view's controller. Internally, there is probably a table used to connect the two together, and this method simply finds the proper location in that table and returns its value. When the result is nil, the default implementation will be used (don't rotate).
If you want to be sure about the name of the method, set a breakpoint in -[UIView becomeFirstResponder], tap on a text field, and step through the code until it shows up in the call stack. I suggest using becomeFirstResponder because it is easier to control than most things which get the view controller.