I'm having a difficult time wrapping my head around loading views with Interface Builder and NSViewController.
My goal is to have a view which meets the following description: Top bar at the top (like a toolbar but not exactly) which spans the entire width of the view, and a second "content view" below. This composite view is owned by my NSViewController subclass.
It made sense to use Interface Builder for this. I have created a view nib, and added to it two subviews, laid them out properly (with the top bar and the content view). I've set File's Owner to be MyViewController, and connected outlets and such.
The views I wish to load in (the bar and the content) are also in their own nibs (this might be what's tripping me up) and those nibs have their Custom Class set to the respective NSView subclass where applicable. I'm not sure what to set as their File's Owner (I'm guessing MyController as it should be their owner).
Alas, when I init an instance of MyViewController none of my nibs actually display. I've added it to my Window's contentView properly (I've checked otherwise), and actually, things sort of load. That is, awakeFromNib gets sent to the bar view, but it does not display in the window. I think I've definitely got some wires crossed somewhere. Perhaps someone could lend a hand to relieve some of my frustration?
EDIT some code to show what I'm doing
The controller is loaded when my application finishes launching, from the app delegate:
MyController *controller = [[MyController alloc] initWithNibName:#"MyController" bundle:nil];
[window setContentView:[controller view]];
And then in my initWithNibName I don't do anything but call to super for now.
When breaking out each view into its own nib and using NSViewController, the typical way of handling things is to create an NSViewController subclass for each of your nibs. The File's Owner for each respective nib file would then be set to that NSViewController subclass, and you would hook up the view outlet to your custom view in the nib. Then, in the view controller that controls the main window content view, you instantiate an instance of each NSViewController subclass, then add that controller's view to your window.
A quick bit of code - in this code, I'm calling the main content view controller MainViewController, the controller for the "toolbar" is TopViewController, and the rest of the content is ContentViewController
//MainViewController.h
#interface MainViewController : NSViewController
{
//These would just be custom views included in the main nib file that serve
//as placeholders for where to insert the views coming from other nibs
IBOutlet NSView* topView;
IBOutlet NSView* contentView;
TopViewController* topViewController;
ContentViewController* contentViewController;
}
#end
//MainViewController.m
#implementation MainViewController
//loadView is declared in NSViewController, but awakeFromNib would work also
//this is preferred to doing things in initWithNibName:bundle: because
//views are loaded lazily, so you don't need to go loading the other nibs
//until your own nib has actually been loaded.
- (void)loadView
{
[super loadView];
topViewController = [[TopViewController alloc] initWithNibName:#"TopView" bundle:nil];
[[topViewController view] setFrame:[topView frame]];
[[self view] replaceSubview:topView with:[topViewController view]];
contentViewController = [[ContentViewController alloc] initWithNibName:#"ContentView" bundle:nil];
[[contentViewController view] setFrame:[contentView frame]];
[[self view] replaceSubview:contentView with:[contentViewController view]];
}
#end
Should not MainViewController be a subclass of NSWindowController? And the outlets in the class connected to view elements in the main Window in MainMenu.xib?
Let's hope old threads are still read...
Related
I've got plenty of experience with iOS, but Cocoa has me a bit confused. I read through several Apple docs on Cocoa but there are still details that I could not find anywhere. It seems the documentation was written before the NSDocument-based Xcode template was updated to use NSViewController, so I am not clear on how exactly I should organize my application. The template creates a storyboard with an NSWindow, NSViewController.
My understanding is that I should probably subclass NSWindowController or NSWindow to have a reference to my model object, and set that in makeWindowControllers(). But if I'd like to make use of the NSViewController instead of just putting everything in the window, I would also need to access my model there somehow too. I notice there is something called a representedObject in my view controller which seems like it's meant to hold some model object (to then be cast), but it's always nil. How does this get set?
I'm finding it hard to properly formulate this question, but I guess what I'm asking is:how do I properly use NSViewController in my document-based application?
PS: I understand that NSWindowController is generally meant to managing multiple windows that act on one document, so presumably if I only need one window then I don't need an NSWindowController. However, requirements might change and having using NSWindowController may be better in the long run, right?
I haven't dived into storyboards but here is how it works:
If your app has to support 10.9 and lower create custom of subclass NSWindowController
Put code like this into NSDocument subclass
- (void)makeWindowControllers
{
CustomWindowController *controller = [[CustomWindowController alloc] init];
[self addWindowController:controller];
}
If your app has multiple windows than add them here or somewhere else (loaded on demand) but do not forget to add it to array of document windowscontroller (addWindowController:)
If you create them but you don't want to show all the windows then override
- (void)showWindows
{
[controller showWindow:nil]
}
You can anytime access you model in your window controller
- (CustomDocument *)document
{
return [self document];
}
Use bindings in your window controller (windowcontroller subclass + document in the keypath which is a property of window controller)
[self.textView bind:#"editable"
toObject:self withKeyPath:#"document.readOnly"
options:#{NSValueTransformerNameBindingOption : NSNegateBooleanTransformerName}];
In contrast to iOS most of the views are on screen so you have to rely on patterns: Delegation, Notification, Events (responder chain) and of course MVC.
10.10 Yosemite Changes:
NSViewController starting from 10.10 is automatically added to responder chain (generally target of the action is unknown | NSApp sendAction:to:from:)
and all the delegates such as viewDidLoad... familiar from iOS are finally implemented. This means that I don't see big benefit of subclassing NSWindowCotroller anymore.
NSDocument subclass is mandatory and NSViewController is sufficient.
You can anytime access you data in your view controller
- (CustomDocument *)document
{
return (CustomDocument *)[[NSDocumentController sharedDocumentController] documentForWindow:[[self view] window]];
//doesn't work if you do template approach
//NSWindowController *controller = [[[self view] window] windowController];
//CustomDocument *document = [controller document];
}
If you do like this (conforming to KVC/KVO) you can do binding as written above.
Tips:
Correctly implement UNDO for your model objects in Document e.g. or shamefully call updateChangeCount:
[[self.undoManager prepareWithInvocationTarget:self] deleteRowsAtIndexes:insertedIndexes];
Do not put code related to views/windows into your Document
Split your app into multiple NSViewControllers e.g.
- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:AAPLListWindowControllerShowAddItemViewControllerSegueIdentifier]) {
AAPLListViewController *listViewController = (AAPLListViewController *)self.window.contentViewController;
AAPLAddItemViewController *addItemViewController = segue.destinationController;
addItemViewController.delegate = listViewController;
}
}
Previous code is called on windowcontroller with viewcontroller as delegate (again possible only after 10.10)
I always prefer to use multiple XIBs rather than one giant storyboard/XIB. Use following subclass of NSViewController and always inherit from it:
#import <Cocoa/Cocoa.h>
#interface MyViewController : NSViewController
#property(strong) IBOutlet NSView *viewToSubstitute;
#end
#import "MyViewController.h"
#interface MyViewController ()
#end
#implementation MyViewController
- (void)awakeFromNib
{
NSView *view = [self viewToSubstitute];
if (view) {
[self setViewToSubstitute:nil];
[[self view] setFrame:[view frame]];
[[self view] setAutoresizingMask:[view autoresizingMask]];
[[view superview] replaceSubview:view with:[self view]];
}
}
#end
Add a subclass of MyViewController to the project with XIB. Rename the XIB
Add NSViewController Object to the XIB and change its subclass name
Change the loading XIB name to name from step 1
Link view to substitute to the view you want to replace
Check example project Example Multi XIB project
Inspire yourself by shapeart or lister or TextEdit
And a real guide is to use Hopper and see how other apps are done.
PS: You can add your views/viewcontroller into responder chain manually.
PS2: If you are beginner don't over-architect. Be happy with the fact that your app works.
I'm relatively new to this myself but hopefully I can add a little insight.
You can use the view controllers much as you would in ios. You can set outlets and targets and such. For NSDocument-based apps you can use a view controller or the window controller but I think for most applications you'll end up using both with most of the logic being in the view controller. Put the logic wherever it makes the most sense. For example, if your nsdocument can have multiple window types then use the view controller for logic specific to each type and the window controller for logic that applies to all the types.
The representedObject property is primarily associated with Cocoa bindings. While I am beginning to become familiar with bindings I don't have enough background to go into detail here. But a search through the bindings programming guide might be helpful. In general bindings can take the place of a lot of data source code you would need to write on ios. When it works it's magical. When it doesn't work it's like debugging magic. It can be a challenge to see where things went wrong.
Let me add a simple copy-pastable sample for the short answer category;
In your NSDocument subclass, send self to the represented object of your view controller when you are called to makeWindowControllers:
- (void) makeWindowControllers
{
NSStoryboard* storyboard = [NSStoryboard storyboardWithName: #"My Story Board" bundle: nil];
NSWindowController* windowController = [storyboard instantiateControllerWithIdentifier: #"My Document Window Controller"];
MyViewController* myController = (id) windowController.contentViewController;
[self addWindowController: windowController];
myController.representedObject = self;
}
In you MyViewController subclass of NSViewController, overwrite setRepresentedObject to trap it's value, send it to super and then make a call to refresh your view:
- (void) setRepresentedObject: (id) representedObject
{
super.representedObject = representedObject;
[self myUpdateWindowUIFromContent];
}
Merci, bonsoir, you're done.
I'm missing something fundamental about NSView. I have a Cocoa Application with an Objective C class named DataSource that is just a regular class, it's not in the nib. The data source has a single instance variable, an NSColor *, and it has a getter and setter.
The view class instantiates the DataSource in awakeFromNib:
- (void)awakeFromNib{
NSLog(#"awakeFromNib");
ds = [[DataSource alloc] init];
}
and then queries the DataSource for the color to use in drawRect. It works fine. I also implement
- (void)mouseDown:(NSEvent *) anEvent;
in the view class, change the color of the DataSource, and then call
[self setNeedsDisplay:YES];
and it also works as I expect when I click in the custom view.
But if I hook up a button in the nib, wired to this IBAction in the view class:
- (IBAction)buttonPushed:(id) sender {
NSLog(#"buttonPushed");
[ds setData:[NSColor cyanColor]];
[self setNeedsDisplay:YES];
}
the data source updates, but drawRect is never called, despite setNeedsDisplay. In my more complicated version, if I click in the view (in a way that doesn't change the color), I will then get the update (caused by the button). Something is delaying drawing. How can I fix this?
Update: There is no controller and there are no outlets. The NSView subclass contains buttonPushed. The data source updates immediately upon button push, but drawing is delayed, despite calling setNeedsDisplay:YES from the view class. Drawing is delayed indefinitely, unless something else happens to trigger it.
Where is the IBAction located? Are you using some view controller? Is the NSView an outlet in that controller?
I created a NSWindow xib file that I want to open on click of a button in another window.
Now, to control the behavior of the NSWindow, I dragged an object from Library in xib and changed it to subclass of NSWindowController (i.e. ListingWindowController) that I defined in XCode.
Similarly I also created a subclass of NSViewController (i.e. ListingViewController) to manage the NSView inside the NSWindow. To do this, I dragged NSViewController from Library in xib and changed its class to ListingViewController.
#class ListingViewController;
#interface ListingWindowController : NSWindowController {
IBOutlet ListingViewController *listingVC;
}
#property (nonatomic, retain) IBOutlet ListingViewController *listingVC;
#end
I connected window and listingVC of my window controller in IB.
Now to invoke this window on click of a button in my launch (first) window, I create the window controller using initWithWindowNibName like this..
- (IBAction) pushConnect:(id)sender {
NSLog(#"Connect pushed.");
if (wc == nil) {
wc = [[ListingWindowController alloc] initWithWindowNibName:#"ListingWindow" owner:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(closeWindow:) name:NSWindowWillCloseNotification object:nil];
[wc showWindow:sender];
}
}
The problem is that despite all the bindings done in IB for the view controllers of upcoming window/view, the window and listingVC comes out to be (null), (null) even after the new window has loaded (below code).
- (void)windowDidLoad {
[super windowDidLoad];
NSLog(#"windowDidLoad = %#, %#", self.window, self.listingVC);
}
Please help why the connections are not working. I'm banging my head against this problem for quite a while now.
PS: I'm coming from iOS programming background. So, I'm assuming the Mac's window/view controller behave similar to iOS UIViewControllers.
TIA..
Note that:
wc = [[ListingWindowController alloc] initWithWindowNibName:#"ListingWindow" owner:self];
means that self (it’s not clear what self is from your question) is the owner of ListingWindow.nib. This means that self is the one who keeps outlets to objects in that nib file, and self is responsible for releasing the top-level objects in the nib file. This also means that you’re creating an instance of ListingWindowController in your code and another instance inside your nib file since you’ve dragged an object of class ListingWindowController onto the nib file.
This is not how it’s supposed to be.
In the vast majority of cases, a window (view) controller loads a nib file and becomes its owner. It has a window (view) outlet that must be linked to a top-level window (view) in the nib file. Being the nib file’s owner, it must have been created before the nib file is loaded.
In order to achieve this for your window controller, you need to set the file’s owner class to ListingWindowController. You must not drag an object cube and instantiate the window controller inside the nib file. The window controller is the owner of the nib file, so it must exist before the nib file is loaded. You must also link the window outlet in file’s owner to the top-level window object in the nib file so that the window controller is aware of what window it should manage.
Having done that, use:
wc = [[ListingWindowController alloc] initWithWindowNibName:#"ListingWindow"];
instead of:
wc = [[ListingWindowController alloc] initWithWindowNibName:#"ListingWindow" owner:self];
since wc is supposed to be the owner of the nib file.
View controllers work similarly. They’re created before loading the nib file, are responsible for loading a nib file that contains a view as a top-level object, are that nib file’s owner, and have a view outlet that must be linked to that top-level view.
It’s not clear from your question whether you have a separate nib file for the view. If you don’t, then using a subclass of NSViewController is not needed at all — you could use a subclass of NSObject instead. If you insist on using NSViewController to manage a view that’s not loaded from a separate nib file, then you should override -loadView so that you get a reference to the view by some means other than loading it from a nib file, and sending it -setView: so that it is aware of the view it’s supposed to be managing.
Recommended reading: Nib Files in the Resource Programming Guide, NSWindowController class reference, NSViewController class reference.
I have to upgrade one existing application and neeed to split its existing UI into separate NIBs. I have planning to start with creating separate NIBs and NSViewController for all my splitted UIs. Now the problem is my NSViewController didn't respond on keyboard TAB and SHFIT+TAB events, I simply wants my NSViewController to set focus on appropriate child control in my dynamically loaded NSViewController view when user click TAB or SHIFT+TAB.
Thanks,
EDITED :
Following is my requirement.
I have three sub views which i need to load dynamically and switch using NSPopupButton in my MainWindow placeholder NSBox.
For checking i have created new cocoa app and added one NSPopupButton and NSBox to Window and join the outlets for NSPopupButton and NSBox.
Second, I have created three new NSViewController's with three different NIB's containing separate custom view containing two or three NSTextField's child controls.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
}
In the main app delegate function i am adding all the three NSViewController's to an array and later swapping of views using replaceSubview to replace views in placeholder NSBox.
I have added following code in all the three NSViewController's, but i am still not getting focus on child controls by pressing TAB or SHIFT+tab keys.
- (void)loadView {
[super loadView];
// store the responder that’s right after the view in the responder chain
NSResponder *nextResponder = [[self view] nextResponder];
// set the view controller (self) as the next responder after the view
[[self view] setNextResponder:self];
// set the stored responder as the next responder after the view controller
[self setNextResponder:nextResponder];
}
Even though NSViewController inherits from NSResponder, Cocoa doesn’t automatically add NSViewController instances to the responder chain. You need to do it yourself.
One possible solution is to insert your view controller into the responder chain between the view it is controlling and the next responder of that view. For instance, in your view controller implementation,
- (void)loadView {
[super loadView];
// store the responder that’s right after the view in the responder chain
NSResponder *nextResponder = [[self view] nextResponder];
// set the view controller (self) as the next responder after the view
[[self view] setNextResponder:self];
// set the stored responder as the next responder after the view controller
[self setNextResponder:nextResponder];
}
When creating a nib, I have 2 types to create, a WindowNib or a ViewNib. I see the difference is, the window nib has a window, and a view.
How do I load a view nib into another window? Do I have to create a WindowController and add a window then load the nib in the window?
Typically, you should have a Controller for each NIB. So in the case of a Window NIB you would have an NSWindowController subclass as the File's Owner. Similarly for a View NIB you would have an NSViewController subclass as the File's Owner. In the case you present the NSWindowController subclass would instantiate the NSViewController subclass (passing it the appropriate View NIB) and then attach the NSViewController's view property to the window's view hierarchy.
Example in your Window Controller's awakFromNib method you would have the following:
- (void) awakeFromNib {
_viewController = [[MyViewController alloc] initWithNibName: #"MyView" bundle: nil];
[[[self window] contentView] addSubview: [_viewController view]];
}
You could also place this code in the windowDidLoad method of your NSWindowController subclass.