0

Why does this init method return an object out of scope?

Using XCode 4.2, base SDK of 4.3, and ARC, I'm trying to load an UIView from a nib (not a UIViewController). I need to not use a UIViewController at all in the process.

After reading this answer to an S.O. question here, it looks like it can be done: How to load a UIView using a nib file created with Interface Builder (The answer by user "MusiGenesis" describes the process)

I created a sub-class of UIView with a single label:

@interface MyView : UIView
@property (unsafe_unretained, nonatomic) IBOutlet UILabel *textLabel;
@end

In the implementation I override initWithFrame:

- (id)initWithFrame:(CGRect)frame
{
    //self = [super initWithFrame:frame];
    self = [JVUIKitUtils initWithNibName:@"MyView" withOwner:self];
    if( self )
    {
        NSLog(@"Created");
    }
    return self;
}

In I.B. I created a file named "MyView.xib" with a single view. It has a label as a sub-view, and I created the label property by dragging it to the h file. MyView nib file setup

And in another file, I created this re-usable static method:

+ (id)initWithNibName:(NSString*)nibName withOwner:(id)uiView
{
    id object = nil;
    NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:nibName owner:uiView options:nil]; // 1 object, out of scope
    for( id tempObject in bundle)
    {
        if( [tempObject isKindOfClass:[uiView class]] ) object = tempObject;
        break;
    }
    return object;
}

As you can see in the following screen shot, the bundle has one object reference, but it's out of scope.

And debugging: MyView is out of scope

This is my code for instantiation:

subView = [[MyView alloc] initWithFrame:CGRectZero]; // ok
NSAssert(subView != nil, @"MyView was nil"); // fail

Any ideas on why the other S.O. poster was able to get it to work but this does not?

Community
  • 1
  • 1
TigerCoding
  • 8,710
  • 10
  • 47
  • 72

2 Answers2

3

The use of the owner seems a bit confusing in the way that you are loading a nib. It appears that you are trying to use the view as both the owner of the nib and the first object in it.

Are you trying to load MyView from your nib (i.e. is the class of the view inside your nib files defined as MyView) or are you trying to load a subview of MyView from the nib?

If the view inside your nib is a MyView, here's how to load it. Create this static method as a category on UIView:

@implementation UIView (NibLoading)

+ (id)viewWithNibName:(NSString*)nibName
{
    NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:nibName owner:nil options:nil];
    if ([bundle count])
    {
        UIView *view = [bundle objectAtIndex:0];
        if ([view isKindOfClass:self])
        {
            return view;
        }
        NSLog(@"The object in the nib %@ is a %@, not a %@", nibName, [view class], self);
    }
    return nil;
}

@end

That will let you load any kind of view from a nib file (the view needs to be the first item defined in the nib). You would create your view like this:

MyView *view = [MyView viewWithNibName:@"MyView"];

If the view inside the nib is not a MyView, but you want to load it as a subview of MyView, with MyView defined as the file's owner in the nib file, do it like this:

@implementation UIView (NibLoading)

- (void)loadContentsFromNibName:(NSString*)nibName
{
    NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil];
    if ([bundle count])
    {
        UIView *view = [bundle objectAtIndex:0];
        if ([view isKindOfClass:[UIView class]])
        {
            //resize view to fit
            view.frame = self.bounds;

            //add as subview
            [self addSubview:view];
        }
        NSLog(@"The object in the nib %@ is a %@, not a UIView", nibName, [view class]);
    }
}

@end

Using that approach, just create your view as normal using initWithFrame, then call loadContentsFromNibName to loa the contents from a nib. You would load your view like this:

MyView *view = [[MyView alloc] initWithFrame:CGRect(0,0,100,100)];
[view loadContentsFromNibName:@"MyView"];
Nick Lockwood
  • 40,865
  • 11
  • 112
  • 103
  • Will try this in a moment by combining it with clairware's answer. – TigerCoding Feb 10 '12 at 09:11
  • You don't need to combine them. They are both slightly different variations of the same approach. Either should work (although mine is less code to implement ;-)) – Nick Lockwood Feb 10 '12 at 09:13
  • The second implementation is what I'm after, will respond in a moment. – TigerCoding Feb 10 '12 at 09:21
  • I tried out the second method, and the nib loads (thank you VERY much). I'm able to print out the text I assign to the label. I'm curious though, why do you use the line [self addSubview:view]; ? It seems like it's adding another subview to the already created uiview. – TigerCoding Feb 10 '12 at 10:01
  • Because when you load the view from the nib, it's not the same as the view that you create with initWithFrame. Nib files actually contain their own copies of objects, which are loaded into the file's owner. When you initialise a view controller with a nib, you are actually loading the view controller's view object from within the nib, but when you initialise a view from a nib, you either have to get the whole object from the nib and return it (as in my first example) or you have to load a subview from the nib and then add that subview to the view you've created (as in my second example). – Nick Lockwood Feb 10 '12 at 11:11
  • I've converted my code to use the first (static) method you proposed, as I don't want the subview effect. But for some reason the bundle loading throws an NSUnknownKey exception (this class is not key value-coding compliant for the key textLabel). However, I created the view with a drag from the label to the file's owner and I can clearly see the connection in the connections inspector. – TigerCoding Feb 10 '12 at 11:30
  • Don't use the file's owner for the first solution. In your nib file, you must set the view in your nib to be of class MyView, and then bind all the outlets from the sub-controls to your view itself, not to the file's owner. This is what I was trying to explain. The file's owner is the class that loads the view, which in the second case is MyView, but in the first case in *nothing* because you are loading and returning the entire view directly from the nib - you aren't loading it *in to* anything. – Nick Lockwood Feb 10 '12 at 14:55
  • Sweeeeet, thank you so much. I'm surprised after all my searching there isn't and easy-to-find tutorial to load a UIView from a nib without a UIViewController. – TigerCoding Feb 10 '12 at 15:03
  • Yeah, it took me a while to get my head around it. – Nick Lockwood Feb 10 '12 at 15:46
0

Your [JVUIKitUtils initWithNibName:@"MyView" withOwner:self] approach is all wrong. When an init method is called, the object is already allocated by the call to [MyView alloc]. The reason why you chain the calls to the super init method is so that this will daisy chain all the way down to the NSObject init method, which simply returns the instance it is.

By setting "self" to the (autoreleased) instance value returned by your JVUIKitUtils, you are effectively setting it to a memory address other than what was allocated by the call to [MyView alloc], which generates the out of scope error.

Instead, don't create the init method your are trying to create. You already have the method to create and initialize your nib-based view in your state method. I would change the name and do something like:

+ (UIView)newViewWithNibName:(NSString*)nibName withClassType:(Class)uiView
{
    id object = nil;

    // you need some object to assign nib file owner to. use an NSObject.
    NSObject* owner = [[NSObject alloc] init];
    NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:nibName owner:owner options:nil]; // 1 object, out of scope
    for( id tempObject in bundle)
    {
        if( [tempObject isKindOfClass:] ) object = [tempObject retain];
        break;
    }
    [owner release];
    return object;
}

and then call it like:

MyView* subView = [JVUIKitUtils viewWithNibName:@"MyView" withClassType:[MyView class]];

I haven't tested this code. Your milage might vary.

kamprath
  • 2,220
  • 1
  • 23
  • 28