Interface Builder: How to load view from nib file - objective-c

I have a MyCustomView subclassed from NSView designed in a .xib.
I would like to insert this view into some of my other xib's round my application. How should I do this? If i drag a custom view and change the class to MyCustomView, but that does not load my xib-file. Can this only be done programmatically or is there a way to do this inside interface builder?
EDIT1:
Here is a very small demo-project:
http://s000.tinyupload.com/index.php?file_id=09538344018446482999
It contains the default MainMenu xib and my CustomView xib. I would like my CustomView.xib to be displayed inside the custom view added to my MainMenu.xib -- using as less code as possible.

For loading the view you need to add on your window:-
Created custom class of view inheriting to NSViewController
#import <Cocoa/Cocoa.h>
#interface NewViewController : NSViewController
#end
#import "NewViewController.h"
#implementation NewViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Initialization code here.
}
return self;
}
#end
Your xib name is yourview.xib
- (void)windowDidLoad {
NSViewController *yourVC = [[NewViewController alloc] initWithNibName:#"NewViewController" bundle:nil];
[[[self window] contentView] addSubview:[yourVC view]];
}

Sounds like you need a container view. But I think you will have to use storyboard for it to be doable in interface builder.

Use a view controller as it will handle nib loading for you and provide a place to hook up IBOutlet and IBActions in a reusable way.
In your app delegate or whatever controller create an instance of your view controller.
Ask your view controller to load its view.
Cast the return type to your view class name.
Then keep a reference to your view controller and possibly the view.
Tell whatever view to add your view as a subview.
Add any layout constraints.
( you can build out very generic constraints to add themselves in your view or view controller by overriding viewDidMoveToSuperview or viewDidMoveToWindow when superview or window are not nil. Use the same to remove your constraints. )
Oddly you remove a view by telling it to remove itself from its superview.

I'd advise just doing it programmatically:
Add a View to your main xib/storyboard and set the custom class to your custom view's class
In your xib for your custom view, set the File's Owner class to your custom view's class
Hook up any IBOutlets, etc. as needed
Make a __strong property/ivar for holding a reference to the top level NSView of the xib
Implement initFromFrame in your custom view's class roughly as follows:
#interface CustomView ()
{
__strong NSView *nibView;
}
#end
#implementation CustomView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSArray *nibObjects;
[[NSBundle mainBundle] loadNibNamed:#"CustomView" owner:self topLevelObjects:&nibObjects];
nibView = nibObjects[1];
[self addSubview:nibView];
}
return self;
}
The IBOutlet are connected up immediately after the loadNibNamed call, so you can do further initialization from there.
Another option is to do things purely programmatically:
1. In your custom xib, set the root View's class to your custom class
2. Implement awakeFromNib in your custom class to perform initialization
3. Call loadNibNamed: on your custom xib and programmatically add it to the user interface without interface builder.

Related

Custom view created with Interface Builder does not render when called in other views

I have an xib for the main window, and I created a custom view in the following steps:
Create a new class which inherits from NSView.
MyView.h:
#import <Cocoa/Cocoa.h>
IB_DESIGNABLE
#interface MyView : NSTableCellView
#end
MyView.m:
#import "MyView.h"
#implementation MyView
- (void)awakeFromNib {
NSLog(#"Post view awaking from nib.");
}
#end
Create a new xib, and set the root view's class to the class created above. And design in that xib.
Set outlets from the xib to the class.
And I tried to use this custom view in the main window in the following steps:
Drag a custom view to the main window's xib.
Set the class of that custom view to the class created above.
But nothing renders. From the log, I can see that code in awakeFromNib from the custom view class is executed. When I set the class to be IB_DESIGNABLE, the view gets empty in the main window's xib, different from what I designed.
I tried to set the file owner of the custom view's xib to the custom class, but nothing changed.
I guess the problem is that, the custom view's xib file is not actually loaded. When I googled it, there seem to be few references on this exact topic. So, how should I actually achieve this goal? I.e., design a view in IB, implement its methods in a class, associate these two, and expose it just like a system view for use in other xibs?
UPDATE:
I found a tutorial and realized what I lack (for correctly rendering the view when built). I have to add an outlet from the view in the xib to the view class:
#property (nonatomic, strong) IBOutlet NSView *view;
, and then load it in the (id)initWithCoder:(NSCoder *)coder method.
[[NSBundle mainBundle] loadNibNamed:#"MyView" owner:self topLevelObjects:nil];
[self addSubview:self.view];
But the view still won't render in the interface builder.
Your guess is correct: the xib is not being loaded. The nib loader doesn't know about your custom view's nib. The nib framework doesn't provide a facility for defining that connection, so you need to write code to load the xib.
Here's what I'd do. Add a contentView property to your custom view:
#interface MyView ()
#property (nonatomic, strong, readwrite) IBOutlet NSView *contentView;
#end
In your custom view's nib, set the custom class of the root view back to NSView and disconnect all the (no-longer-valid) outlet connections from it. Set the custom class of File's Owner to your custom class name (e.g. MyView). Connect the root view to File's Owner's contentView outlet, and connect all the other outlets from File's Owner to the appropriate objects in the nib.
Then implement awakeFromNib in your custom view subclass to load the nib and add the content view as a subview:
#implementation MyView {
BOOL hasLoadedOwnNib: 1;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self loadOwnNibIfNeeded];
}
- (void)loadOwnNibIfNeeded {
if (hasLoadedOwnNib) {
return;
}
hasLoadedOwnNib = YES;
[[NSBundle bundleForClass:self.class] loadNibNamed:NSStringFromClass(self.class) owner:self topLevelObjects:nil];
self.contentView.frame = self.bounds;
self.contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[self addSubview:self.contentView];
}
#end
Note that you have to be careful not to allow infinite recursion. When your app loads the main window's nib, it will create an instance of MyView and (eventually) send it awakeFromNib. Then, in awakeFromNib, MyView loads its own nib, where it is the File's Owner. The nib loader sends awakeFromNib to File's Owner, and this will happen while you're already in -[MyView awakeFromNib]. If you don't check for this, you get a stack overflow due to unbounded recursion.
You aren't providing any code, but here are some sanity checks:
Are you specifying the nib name correctly? In iOS its caps sensitive, but I don't think it is for you.
Check the package, is the nib actually there? Make sure it is part of the target you are building.
Could also be a frame issue. Make sure your auto-resizing parameters are set up correctly and that everything is in frame.
Another check you can do is set your IBOutlets to the actual frame of a UIView (or other) that you are interested in. Then in awakeFromNib, you can make sure their frame exists, or that they exist at all.

Unable to set content in NSPopover

I'm showing an NSPopover in an NSView, originating from a point on an NSBezierPath. I'm able to show the popover without a problem, but I can't seem to set the string value of the two text fields in it. The popover and the content view are both a custom subclass of NSPopover and NSViewController, respectively. The NSPopover subclass is also the NSPopover's delegate, although I don't implement any delegate methods, so I'm not sure I even need to do that.
Here is my subclass of NSViewController:
#import <Cocoa/Cocoa.h>
#interface WeightPopoverViewController : NSViewController
#end
#import "WeightPopoverViewController.h"
#interface WeightPopoverViewController ()
#end
#implementation WeightPopoverViewController
- (id)init {
self = [super initWithNibName:#"WeightPopoverViewController" bundle:nil];
if (self) {
}
return self;
}
#end
And my subclass of NSPopover:
#import <Cocoa/Cocoa.h>
#interface WeightPopoverController : NSPopover <NSPopoverDelegate> {
NSTextField *dateLabel;
NSTextField *weightLabel;
}
#property (strong) IBOutlet NSTextField *dateLabel;
#property (strong) IBOutlet NSTextField *weightLabel;
#end
#import "WeightPopoverController.h"
#implementation WeightPopoverController
#synthesize weightLabel;
#synthesize dateLabel;
#end
This is the code in my NSView subclass that opens up the popover:
#interface WeightGraphViewController () {
WeightPopoverController *popover;
WeightPopoverViewController *vc;
}
...
-(void)mouseEntered:(NSEvent *)theEvent {
// initialize the popover and its view controller
vc = [[WeightPopoverViewController alloc] init];
popover = [[WeightPopoverController alloc] init];
// configure popover
[popover setContentViewController:vc];
[popover setDelegate:popover];
[popover setAnimates:NO];
// set labels
for (id key in (id)[theEvent userData]) {
[popover.weightLabel setStringValue:[(NSDictionary*)[theEvent userData] objectForKey:key]];
[popover.dateLabel setStringValue:key];
}
// set the location
(redacted, irrelevant)
// show popover
[popover showRelativeToRect:rect ofView:[self window].contentView preferredEdge:NSMaxYEdge];
}
-(void)mouseExited:(NSEvent *)theEvent {
[popover close];
popover = nil;
}
In WeightPopoverViewController.xib, I've set the File's Owner to WeightPopoverViewController and connected the view to the custom NSView. In this xib I also have an Object set to WeightPopoverController with the dateLabel and weightLabel connected to their text fields and the contentViewController set to File's Owner.
I think where I am going wrong is likely related to how I have configured my class / instance variables for the NSPopover, but from the research I've done and documentation I've read I can't seem to crack where I've gone wrong. Any help would be appreciated.
UPDATE:
I removed the NSPopover subclass from code and from IB. I put my outlets in my NSViewController and connected them in IB. However, I'm still not able to set the string values. The following won't compile with the error "Property 'weightLabel' not found on object of type NSPopover*'".
#interface WeightGraphViewController () {
NSPopover *popover;
...
}
-(void)mouseEntered:(NSEvent *)theEvent {
vc = [[WeightPopoverViewController alloc] init];
popover = [[NSPopover alloc] init];
[popover setContentViewController:vc];
[popover.dateLabel setStringValue:#"test"];
}
I have the property definition exactly as I had it in my NSPopover subclass, but now in my NSViewController. This is actually what I had before, and since I wasn't able to set the properties from the NSViewController, I figured I needed to do it through a subclass of NSPopover. This is why I thought I am having an issue with how I have configured my class / instance variables.
You seem to be creating two popovers, one in code (popover = [[WeightPopoverController alloc] init]) and one in Interface Builder (In this xib I also have an Object set to WeightPopoverController). Have a think about what you’re trying to achieve.
I would also advise against subclassing NSPopover. I believe this is causing confusion and is unnecessary. Instead, put the outlets to your dateLabel and weightLabel in the popover’s content view controller.
I've experienced something that I think is similar. The root problem is that the "outlets" connecting your view (XIB) to your controller are not initialized until after the view has been displayed. If the controller tries to set properties on any UI controls in the view before the popover has been opened, those changes are ignored (since all the controls will be nil).
Luckily, there's an easy solution (as mentioned in this answer): just invoke the view getter on your controller, and it will force the view to initialize sooner.
In other words:
popover = [NSPopover new];
myController = [[MyViewController alloc] initWithNibName:#"MyView" bundle:nil];
popover.contentViewController = myController;
[myController view]; // force view to initialize
...set some values on myController... // works because view is now loaded
[popover showRelativeToRect: ...];

Subclassing UIViewController, viewDidLoad called repeatedly

I subclassed UIViewController as STViewController and noticed that classes inheriting from STViewController have their viewDidLoad method being called repeatedly. Ultimately crashing the app. STViewController is basically a blank implementation at this point. I am subclassing as shown below:
#import "STViewController.h"
#interface WelcomeViewController : STViewController {
STViewController.h
#import <UIKit/UIKit.h>
#interface STViewController : UIViewController
{
}
#end
STViewController.m
#import "STViewController.h"
#implementation STViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)loadView
{
// Implement loadView to create a view hierarchy programmatically, without using a nib.
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
viewDidLoad() from WelcomeViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// hide the buttons
[[self signUp] setHidden: YES];
[[self logIn] setHidden: YES];
}
You are overriding loadView, but your implementation is empty, and you're not assigning a view. Remove the loadView override.
From UIViewController Class Reference (emphasis mine):
You should never call this method directly. The view controller calls
this method when the view property is requested but is currently nil.
If you create your views manually, you must override this method and
use it to create your views. If you use Interface Builder to create
your views and initialize the view controller—that is, you initialize
the view using the initWithNibName:bundle: method, set the nibName and
nibBundle properties directly, or create both your views and view
controller in Interface Builder—then you must not override this
method.
The default implementation of this method looks for valid nib
information and uses that information to load the associated nib file.
If no nib information is specified, the default implementation creates
a plain UIView object and makes it the main view.
If you override this method in order to create your views manually,
you should do so and assign the root view of your hierarchy to the
view property. (The views you create should be unique instances and
should not be shared with any other view controller object.) Your
custom implementation of this method should not call super.

Custom UINavigationController Implementation

I've got a TabBarController in my app and in it I've got a few NavigationControllers. I've got all this stuff simply made in interface builder.
Now I want to implement my custom navigationcontroller so I created a class:
#import <UIKit/UIKit.h>
#interface DetailNavigationController : UINavigationController
#end
#implementation DetailNavigationController
- (id)initWithRootViewController:(UIViewController *)rootViewController
{
self = [super initWithRootViewController:rootViewController];
if (self) {
NSLog(#"I work!");
}
return self;
}
- (void) dealloc {
[super dealloc];
}
#end
And in Interface builder I added this class as Custom Class to the navigation controller I want. Now when I start the app and select the tab with this navigation controller it works but the initWithRootController is not called. I guess it's completely ignoring the class and runs as default navigation controller.
Do I need to specify anything more in interface builder or do I need to specify this controller programatically somewhere in tabbar delegate?
Thanks.
If you're creating the navigation controller in interface builder, you will need to override initWithCoder:, not initWithRootViewController:. The xib contains an instantiated version of your object, with the root view controller already set.

Loading custom UIView in UIViewController's main view

I have subclassed UIView and created a NIB that controls the main logic for my application.
Hoping the view will scale nicely, I want to use it for both the iPhone and iPad versions of the app.
On the iPhone the view will cover the full screen. On the iPad the view will cover only part of the screen.
I have read that you shouldn't use UIViewControllers to control only part of the screen. So, I am trying to embed the custom UIView in the main UIViewController's view using IB.
How can this be done?
After a lot of trial and error I found a solution based on an approach explained in the following question, answered by Brian Webster.
The solution was originally suggested for a Cocoa environment. I hope it is valid in an iOS environment as well.
Create the main view controller with a NIB-file. In the NIB, the File's Owner should correspond to the class of your main view controller.
Create a custom view controller with a NIB-file. In this NIB, the File's Owner should correspond to the class of your custom view controller.
Create a custom view controller property in your main view controller class.
Create an UIView property in the main view controller class. It will hold your custom view controller's view. Define it as an IBOutlet, so it can be linked in the NIB.
Drop a UIView in your main view controller's NIB. Link it to the main view controller's view IBOutlet. It will be used as a placeholder for the custom view.
In the main view controller's viewDidLoad method, load the custom view controllers NIB, determine the custom view's frame size and copy the view in the main view controller's view.
Here is some code:
MainViewController.h
#interface MainViewController : UIViewController {
CustomViewController *customViewController;
UIView *customView;
}
#property (nonatomic, retain) CustomViewController *customViewController;
#property (nonatomic, retain) IBOutlet UIView *customView;
#end
MainViewController.m
- (void)viewDidLoad {
CustomViewController *controller = [[CustomViewController alloc] initWithNibName:#"CustomViewController" bundle:nil];
self.customViewController = controller;
[controller release];
customViewController.view.frame = customView.frame;
customViewController.view.autoresizingMask = customView.autoresizingMask;
[customView removeFromSuperview];
[self.view addSubview:customViewController.view];
self.customView = customViewController.view;
[super viewDidLoad];
}
Add an IBOutlet propertyfor your custom UIView to the UIViewController, and additional outlets for any subviews you wish to access.
Go to Interface Builder, select the "File's Owner" object in your NIB and in the Inspector go the rightmost tab set its class to match your UIViewController's class.
Connect the IBOutlet from step one on the "File's Owner" to your custom UIView.
In XCode, when you need to load your view, do something like this:
--
[[NSBundle mainBundle] loadNibNamed:#"MyNib" owner:self options:0];
self.myCustomView.frame=self.view.bounds; // make view fill screen - customize as necessary
[self.view addSubview:self.myCustomView];
When you load the NIB, the outlet(s) you set up in step 1 will be populated with the objects loaded from your NIB.