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

Our current best practice for custom views :

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

In fact, we want the custom view Nib to โ€œinsertโ€ into the Nib controller for viewing. Otherwise, we would like to add and place an instance of a custom view inside the Nib view-controller (without viewing its contents).

We come close to 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 create an instance of the user view programmatically, we use -initWithFrame: which will load the view from Nib (which will call -initWithCoder: and go to the if-branch labeled "Step 2"), set its frame, and set its save value to 1.

However, if we create an instance of the user view inside the View-Control Nib, the initially (admittedly, pretty ugly) static variable loadNormally initially NO : we start with "Step 1", where we load and return the loaded instance from its Nib, making sure that we will use the "normal" if-branch -initWithCoder: Downloading from a custom Nib view means that we are returning to -initWithCoder: this time with loadNormally==YES , that is, we allow the Nib loading engine to complete its task and return an instance of the custom view.

Results in brief:

  • Good: THIS WORKS !!! We have โ€œplug-inโ€ custom views in Interface Builder!
  • Bad: Ugly static variable ...: - /
  • Ugly: An instance of the user view is happening! Here I will be happy for your help - I do not understand why. Any ideas?
+4
source share
3 answers

We ended up with a better way that involves overriding -awakeAfterUsingCoder: in our user view, replacing the object loaded from the Nib View controller with the one loaded from the "built-in" Nib (CustomView.xib).

I wrote how we insert custom widgets inside other Nibs into an extensive blog post.

The code looks 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; } 
+3
source

Ian's answer is great ... but "messages are sent to the freed instance" can still happen. I solved this problem using the "self" assignment.

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

To achieve this in your ARC project, add the flag compiler option '-fno-objc-arc' to your file. Then do NO-ARC encoding in this file (e.g. noalloc setting noils, calling super dealloc, etc.)

In addition, the nib client view controller must use a strong property to store the instance returned by awakeFromNib. In the case of my sample code, customView is referenced as follows:


@property ( strong , non-nuclear) IBOutlet CustomView * customView;


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

So awakeAfterUsingCoder: code 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; } 

Category Code UIView + Util

 @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]; } } } 
+1
source

There is an alternative way:

let's say that you use View1 in your Interface Builder , then you create another view called View2 , View2 has the corresponding View2.xib file, you linked the outputs in View2.m and View2.xib .

Then in View1.m write the following:

 -(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 where you need to put your custom view in Interface Builder , thereby making View1 reusable in Interface Builder without writing code.

0
source

All Articles