Yang Meyer
Yang Meyer

Reputation: 5699

Pluggable custom-view Nibs (Nib-in-a-Nib): Memory leak – why?

Our current best-practice for custom views is:

  1. Build the custom view in a Nib.
  2. In the view controller, programmatically load the Nib, get the custom view from the array of loaded objects (we do this in a UIView category method +loadInstanceFromNib).
  3. Add custom view as subview, set its frame.

What we actually want is to "embed" the custom-view Nib inside the view-controller Nib. Failing that, at least we'd like to add and position a custom-view instance inside the view-controller Nib (without seeing its contents).

We have come very close with the following solution:

@implementation CustomView

static BOOL loadNormally;

- (id) initWithCoder:(NSCoder*)aDecoder {
    id returnValue = nil;
    if (loadNormally) { // Step 2
        returnValue = [super initWithCoder:aDecoder];
        loadNormally = !loadNormally;
    } else {            // Step 1
        loadNormally = !loadNormally;
        returnValue = [CustomView loadInstanceFromNib];
    }
    return returnValue;
}

- (id) initWithFrame:(CGRect)frame {
    loadNormally = YES;
    self = (id) [[CustomView loadInstanceFromNib] retain];
    self.frame = frame;
    return self;
}
// ...
@end

If we instantiate the custom view programmatically, we use -initWithFrame:, which will load the view from the Nib (which will call -initWithCoder: and go right to the if-branch labeled "Step 2"), set its frame, and set its retain count to 1.

However if we instantiate the custom view inside a view-controller Nib, the (admittedly rather ugly) static loadNormally variable is initially NO: We start in "Step 1", where we load and return the instance loaded from its Nib, after making sure that we will forthwith use the "normal" if-branch of -initWithCoder:. Loading from the custom-view Nib means that we come back into -initWithCoder:, this time with loadNormally==YES, i.e. we let the Nib loading mechanism do its job and return the custom-view instance.

Results, in summary:

Upvotes: 4

Views: 1709

Answers (3)

MeloS
MeloS

Reputation: 7938

There is an alternative way to do this:

say you use View1 in your Interface Builder, then you create another view called View2, View2 has a corresponding View2.xib file, you have linked the outlets in View2.m and View2.xib.

Then, in View1.m, write this:

-(void)awakeFromNib
{
    NSArray *topObjects = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil];
    self.subContentView = topObjects.firstObject]
    [self addSubview:self.subContentView];
}

With this, you can use View1 in places where you need to put your custom view in Interface Builder, thus make View1 reusable in Interface Builder without writing any more code.

Upvotes: 0

Pascal
Pascal

Reputation: 16631

Yang's answer is great... but 'messages send to deallocated instance' can still occurs. I solved this problem by using 'self' assignation.

So if you use ARC, you will have to allow this 'self' assignation. (read https://blog.compeople.eu/apps/?p=142 for more info)

To achieve this in an ARC project, add the '-fno-objc-arc' flag compiler setting on your file. Then do NO-ARC coding in this file (like dealloc setting nils, calling super dealloc, etc..)

Also, client nib's viewcontroller should use strong property to hold the instance returned by awakeFromNib. In the case of my sample code, the customView is referenced like this:


@property (strong, nonatomic) IBOutlet CustomView* customView;


I finally added some other improvements to properties handling and nib loading using copyUIPropertiesTo: and loadNibNamed defined in my UIView+Util category.

So awakeAfterUsingCoder: code is now

#import "UIView+Util.h"
...
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder
{
    // are we loading an empty “placeholder” or the real thing?
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);

    if (theThingThatGotLoadedWasJustAPlaceholder)
    {
        CustomView* customView = (id) [CustomView loadInstanceFromNib];
        // copy all UI properties from self to new view!
        // if not, property that were set using Interface buider are lost!
        [self copyUIPropertiesTo:customView];

        [self release];
        // need retain to avoid deallocation
        self = [customView retain];
    }
    return self;
}

The UIView+Util category code is

@interface UIView (Util)
   +(UIView*) loadInstanceFromNib;
   -(void) copyUIPropertiesTo:(UIView *)view;
@end

along with its implementation

#import "UIView+Util.h"
#import "Log.h"

@implementation UIView (Util)

+(UIView*) loadInstanceFromNib
{ 
    UIView *result = nil; 
    NSArray* elements = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner: nil options: nil];
    for (id anObject in elements)
    { 
        if ([anObject isKindOfClass:[self class]])
        { 
            result = anObject;
            break; 
        } 
    }
    return result; 
}

-(void) copyUIPropertiesTo:(UIView *)view
{
    // reflection did not work to get those lists, so I hardcoded them
    // any suggestions are welcome here

    NSArray *properties =
    [NSArray arrayWithObjects: @"frame",@"bounds", @"center", @"transform", @"contentScaleFactor", @"multipleTouchEnabled", @"exclusiveTouch", @"autoresizesSubviews", @"autoresizingMask", @"clipsToBounds", @"backgroundColor", @"alpha", @"opaque", @"clearsContextBeforeDrawing", @"hidden", @"contentMode", @"contentStretch", nil];

    // some getters have 'is' prefix
    NSArray *getters =
    [NSArray arrayWithObjects: @"frame", @"bounds", @"center", @"transform", @"contentScaleFactor", @"isMultipleTouchEnabled", @"isExclusiveTouch", @"autoresizesSubviews", @"autoresizingMask", @"clipsToBounds", @"backgroundColor", @"alpha", @"isOpaque", @"clearsContextBeforeDrawing", @"isHidden", @"contentMode", @"contentStretch", nil];

    for (int i=0; i<[properties count]; i++)
    {
        NSString * propertyName = [properties objectAtIndex:i];
        NSString * getter = [getters objectAtIndex:i];

        SEL getPropertySelector = NSSelectorFromString(getter);

        NSString *setterSelectorName =
            [propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]];

        setterSelectorName = [NSString stringWithFormat:@"set%@:", setterSelectorName];

        SEL setPropertySelector = NSSelectorFromString(setterSelectorName);

        if ([self respondsToSelector:getPropertySelector] && [view respondsToSelector:setPropertySelector])
        {
            NSObject * propertyValue = [self valueForKey:propertyName];

            [view setValue:propertyValue forKey:propertyName];
        }
    }    
}

Upvotes: 1

Yang Meyer
Yang Meyer

Reputation: 5699

We ended up with a nicer way, which involves overriding -awakeAfterUsingCoder: in our custom view, replacing the object loaded from the view-controller Nib with the one loaded from the "embedded" Nib (CustomView.xib).

I wrote up how we embed custom-view Nibs inside other Nibs in an extensive blog post.

The code goes something like this:

// CustomView.m
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder {
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
    if (theThingThatGotLoadedWasJustAPlaceholder) {
        // load the embedded view from its Nib
        CustomView* theRealThing = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([CustomView class]) owner:nil options:nil] objectAtIndex:0];

        // pass properties through
        theRealThing.frame = self.frame;
        theRealThing.autoresizingMask = self.autoresizingMask;

        [self release];
        self = [theRealThing retain];
    }
    return self;
}

Upvotes: 3

Related Questions