19
votes

How do I correctly load an object that is a subclass of NSView using a Xib?

I want it to be loaded dynamically not from the beginning so I made a MyView.Xib From MyDocument.m I did:

MyView *myView = [[MyView alloc] initWithFrame:...];
[NSBundle loadNibNamed:@"MyView" owner:myView];
[someview addSubview:myView];
...

and the background is fine (it calls drawrect: and it's drawn as expected) but all the buttons I put on the xib won't appear. I have checked, and they are loaded BUT their superview is not the same object as myView.

Why is this? I think I am missing something in the Xib but I don't know exactly what. In other words: How do I make sure that the root view in my xib is the same object as file's owner?

I wish there was something similar to this for the mac:

NSArray* nibViews =  [[NSBundle mainBundle] loadNibNamed:@"MyView" owner:self options:nil]; //in iOS this will load the xib
MyView* myView = [ nibViews objectAtIndex:1];
[someview addSubview:myView];
...

Thanks in advance.

EDIT

I think I have realised the origin of the problem...(??)

In MyView class I have various IBOutlet which are connected correctly in IB that is why they load just fine (I can refer them). However there is no IBOutlet for the top view. So when NSBundle loads the nib, the top view gets assigned to some other object. I thought this will happen if I set my top view in IB to be from class:MyView and put myView as the owner in [NSBundle loadNibNamed:@"MyView" owner:myView]; but it seems not to be the case. What am I doing wrong?

5
Try moving them to front maybe it draws it in the back and can't be seen beacause of the backgroundRadu
I just tried... that is not happening ;(nacho4d
In your nib file, is there a top-level view whose class is MyView? Are there other top-level objects in the nib file? And what’s the class of the file’s owner?user557219
In my nib: File's Owner class is MyView the top view in it is currently of class NSView but I have tried also MyView. Neither workednacho4d

5 Answers

34
votes

Note that a nib file contains:

  • One or more top-level objects. For example, an instance of MyView;
  • A file’s owner placeholder. This is an object that exists prior to the nib file being loaded. Since it exists before the nib file is loaded, it cannot be the same as the view in the nib file.

Scenario 1: NSNib

Let’s consider that your nib file has only one top-level object whose class is MyView, and the file’s owner class is MyAppDelegate. In that case, considering that the nib file is loaded in an instance method of MyAppDelegate:

NSNib *nib = [[[NSNib alloc] initWithNibNamed:@"MyView" bundle:nil] autorelease];
NSArray *topLevelObjects;
if (! [nib instantiateWithOwner:self topLevelObjects:&topLevelObjects]) // error

MyView *myView = nil;
for (id topLevelObject in topLevelObjects) {
    if ([topLevelObject isKindOfClass:[MyView class]) {
        myView = topLevelObject;
        break;
    }
}

// At this point myView is either nil or points to an instance
// of MyView that is owned by this code

It looks like the first argument of -[NSNib instantiateNibWithOwner:topLevelObjects:] can be nil so you wouldn’t have to specify an owner since it seems that you aren’t interested in having one:

if (! [nib instantiateWithOwner:nil topLevelObjects:&topLevelObjects]) // error

Although this works, I wouldn’t rely on it since it’s not documented.

Note that you have ownership of the top level objects, hence you are responsible for releasing them when they’re no longer needed.

Scenario 2: NSViewController

Cocoa provides NSViewController to manage views; usually, views loaded from a nib file. You should create a subclass of NSViewController containing whichever outlets are needed. When editing the nib file, set the view outlet and whichever other outlets you may have defined. You should also set the nib file’s owner so that its class is this subclass of NSViewController. Then:

MyViewController *myViewController = [[MyViewController alloc] initWithNibName:@"MyView" bundle:nil];

NSViewController automatically loads the nib file and sets the corresponding outlets when you send it -view. Alternatively, you can force it to load the nib file by sending it -loadView.

6
votes

You can refer to this category

https://github.com/peterpaulis/NSView-NibLoading-/tree/master

+ (NSView *)loadWithNibNamed:(NSString *)nibNamed owner:(id)owner class:(Class)loadClass {

    NSNib * nib = [[NSNib alloc] initWithNibNamed:nibNamed bundle:nil];

    NSArray * objects;
    if (![nib instantiateWithOwner:owner topLevelObjects:&objects]) {
        NSLog(@"Couldn't load nib named %@", nibNamed);
        return nil;
    }

    for (id object in objects) {
        if ([object isKindOfClass:loadClass]) {
            return object;
        }
    }
    return nil;
}
1
votes

I found the answer by @Bavarious very useful, but it still needed some squirrelling on my part to make it work for my use case - load one of several xib's view definitions into an existing "container" view. Here's my workflow:

1) In the .xib, create the view in IB as expected.

2) DO NOT add an object for the new view's viewController, but rather

3) Set the .xib's File's Owner to new view's viewController

4) Connect any outlets & actions to File's Owner, and, specifically, .view

5) Call loadInspector (see below).

enum InspectorType {
    case None, ItemEditor, HTMLEditor

    var vc: NSViewController? {
        switch self {
        case .ItemEditor:
            return ItemEditorViewController(nibName: "ItemEditor", bundle: nil)
        case .HTMLEditor:
            return HTMLEditorViewController(nibName: "HTMLEditor", bundle: nil)
        case .None:
            return nil
        }
    }
}

class InspectorContainerView: NSView {

    func loadInspector(inspector: InspectorType) -> NSViewController? {
        self.subviews = [] // Get rid of existing subviews.
        if let vc = inspector.vc {
            vc.loadView()
            self.addSubview(vc.view)
            return vc
        }
        Swift.print("VC NOT loaded from \(inspector)")
        return nil
    }
}
0
votes

Here is a way to write the NSView subclass so the view itself comes fully from a separate xib and has everything set up correctly. It relies on the fact that init can change the value of self.

Create a new 'view' xib using Xcode. Set the File Owner's class to NSViewController, and set its view outlet to your target view. Set the class of the target view to your NSView subclass. Layout your view, connect outlets etc.

Now, in your NSView subclass, implement the designated initializers:

- (id)initWithFrame:(NSRect)frameRect
{
    NSViewController *viewController = [[NSViewController alloc] init];
    [[NSBundle mainBundle] loadNibNamed:@"TheNib" owner:viewController topLevelObjects:NULL];

    id viewFromXib = viewController.view;
    viewFromXib.frame = frameRect;
    self = viewFromXib;
    return self;
}

And the same with initWithCoder: so that it will also work when using your NSView subclass in an another xib.

-1
votes
class CustomView: NSView {

@IBOutlet weak var view: NSView!
@IBOutlet weak var textField: NSTextField!

required init(coder: NSCoder) {
    super.init(coder: coder)!

    let frameworkBundle = Bundle(for: classForCoder)
    assert(frameworkBundle.loadNibNamed("CustomView", owner: self, topLevelObjects: nil))
    addSubview(view)
}

}