4
votes

I have a NIB file, with some class (SomeClassView : UIView) set as the custom class of the top level view of the NIB. SomeClass has IBOutlets that I use to hookup subviews of SomeClass that I lay out in Interface Builder.

I instantiate SomeClass like this:

- (id)initWithFrame:(CGRect)frame {
    self = [[[[NSBundle mainBundle] loadNibNamed:@"SomeClassView" owner:nil options:nil] objectAtIndex:0] retain]; 
    // "SomeClassView" is also the name of the nib
    if (self != nil) {
        self.frame = frame;
    }
    return self;
}

Now say I subclass SomeClass with SubClassView. I add a method to SubClassView called -(void)foo:

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self != nil) {
        [self foo];
    }
    return self;
}

At this point I get a runtime error: * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SomeClassView foo:]: unrecognized selector sent to instance 0xAAAAAA'

It seems as though "self" within initWithFrame of SubClassView is still set to the super, SomeClassView. A quick hack fix to work around this is to change the isa pointer within SubClassView initWithFrame:

- (id)initWithFrame:(CGRect)frame {
    Class prevClass = [self class];
    self = [super initWithFrame:frame];
    if (self != nil) {
        isa = prevClass;
        [self foo]; // works now
    }
 }

It's not an ideal workaround, since I have to update isa each time I subclass, or there could even be different init methods which I'll also have to update.

1) Ideally, is there an easy way to fix this, purely by code? Maybe with the way I'm setting self = the loaded nib?

2) Is there an alternative architecture that works better for what I'm trying to do? One example is to set the owner to self, but then you'd have to set all the property/outlet mappings manually.

2

2 Answers

3
votes

Swapping isa pointers is a problem if your subclasses have instance variables other than those declared in SomeClassView. Note that your nib file has an object of type SomeClassView, which means that, upon loading the nib file, the nib loader will allocate an object of that type and unmarshall it from the nib file. Changing the isa pointer to [SubViewClass class] temporarily won’t make it an object of type SubViewClass per se since what the nib loader allocates is a SomeClassView object.

That said, I don’t think there’s a reliable and automatic way to use nib files containing objects whose types need to be changed upon nib loading.

What you could do is to have your SomeClassView objects declare a delegate conforming to some protocol. This protocol would define methods for behaviour in SomeClassView that can potentially be extended. For instance,

@protocol SomeClassViewDelegate
@optional
- (void)someClassViewDidAwakeFromNib:(SomeClassView *)someClassView;
@end

Instead of subclassing SomeClassView, you’d have arbitrary objects performing whatever custom behaviour you currently have in SubClassView. For instance,

@interface SubClassViewBehaviour : NSObject <SomeClassViewDelegate>
…
@end

@implementation SubClassViewBehaviour
- (void)someClassViewDidAwakeFromNib:(SomeClassView *)someClassView {
    // whatever behaviour is currently in -[SubClassView foo]
}
@end

A SubClassViewBehaviour object would be created in code and set as the nib file’s owner upon loading the nib file, or any other IB proxy object for that matter. SomeClassView would have a delegate outlet connected to file’s owner/proxy object, and it’d invoke the delegate methods in the appropriate places. For instance,

@implementation SomeClassView
- (void)awakeFromNib {
    SEL didAwakeFromNib = @selector(someClassViewDidAwakeFromNib:);
    if ([[self delegate] respondsToSelector:didAwakeFromNib]) {
        [[self delegate] performSelector:didAwakeFromNib withObject:self];
    }
}
@end

One further remark: your code currently leaks a view object since two objects are being instantiated: one via +alloc in your code and another one via nib loading. You’re assigning the latter to self, hence the one created via +alloc is leaking. Also, I believe you’ve missed a call to super in your third code snippet.

0
votes

Rather than do this from within the subclass itself why not ensure it is the right class when you first instantiate it from outside the class:

SubClass *instance = [[SubClass alloc] initWithNibName:@"SomeClassView" bundle:nil];