Reputation: 2798
I am new to interface builder and I would like to have a screen which contains a 3x3 grid of a UIView each of which contain a UIImageView, and 4 UILabels.
My logic is probably flawed but the way I am trying to achieve this was to:
Is it possible to load a nib into another nib graphically from within InterfaceBuilder, if so how do I do it? Do I drag a "standard" UIView onto the canvas and then change the class to be a MyUIView?
Or do I need to do this all programmatically within the MyGridViewController.m with something like:
for (int i=0; i<9; i++)
{
NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:@"MyUIView" owner:self options:nil];
[myViewArray addObject:[ nibViews objectAtIndex: 1]];
}
The only other way I have gotten this to work was to have a single Nib and put 9 UIImageViews and 36 UILabels but this obviously is a pain when I want to change something as I need to update each one of the 3x3 "cells". I thought it would be easier to change it in one file and all 9 would be updated.
Upvotes: 2
Views: 3328
Reputation: 16661
"Yes you (almost) can."
I do it in my projects using Interface Builder.
The only flaw is that you see a white area to represent the 'nested nibs' in Interface Builder. Let's say that, in the mean time (I main waiting for Apple to add this feature in XCode), the solution I present here is acceptable.
First read this: https://blog.compeople.eu/apps/?p=142
Then, if you do ARC, follow these instructions and grab my UIVIew+Util category included here.
For ARC, you will have to allow this 'self' assignation. (https://blog.compeople.eu/apps/?p=142 state that it's not needed, but it is. If you do not, you will get some 'messages send to deallocated instance')
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];
}
}
}
Tadaaaa :-)
Credits goes to https://stackoverflow.com/users/45018/yang for initial solution. I just improved it.
Upvotes: 2
Reputation: 395
What I've done, is:
1) For the owning view, I put in the subview as an empty view with the class name of the custom view as a placeholder. This will create the custom view, but none of its subviews or connections will be made as these are defined in the custom view's nib.
2) I then manually load a new copy of the custom view using its nib and replace the placeholder custom view in the parent.
Use:
NSView *newView = (Code to load the view using a nib.)
newView.frame = origView.frame;
NSView *superView = origView.superview;
[superView replaceSubview:origView with:newView];
Ugh!
Upvotes: 0
Reputation: 5043
If you create your view with the 1 image view and 4 labels in a nib (I'll call this view the "grid cell"), you can create another nib for your grid and drag in 9 instances of your grid cell. The 9 instances will only show up as placeholders (blank views) in IB, but they'll work when you run the app.
This article tells you how to do it. Check out the section called "A reusable subview". And since you're making a grid with 9 identical cells, it might make sense to use AQGridView -- see the section "A reusable AQGridViewCell".
Upvotes: 1
Reputation: 17812
Here's a slightly more dynamic way to do something like this.
I didn't really want to be pulling in the nibs and sifting through their objects wherever I wanted to use them in code, seemed messy. Also, I wanted to be able to add them in interface builder.
1) Create your custom subclass of UIView (lets call it myView) 2) Create your nib and call it myViewNIB
Add these two methods (would be smarter to have them in a superclass UIView and subclass that)
- (UINib *)nib {
NSBundle *classBundle = [NSBundle bundleForClass:[self class]];
return [UINib nibWithNibName:[self nibName] bundle:classBundle];
}
- (NSString *)nibName {
NSString *className = NSStringFromClass([self class]);
return [NSString stringWithFormat:@"%@NIB", className];
}
The so-called magic is that last line there where it returns a nibName by appending NIB to the current class.
Then for your init method, which is initWithCoder since you want to use this in interface builder (you could make it more robust so it can be used programmatically too by also setting up initWithFrame):
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code
NSArray *nibObjects = [[self nib] instantiateWithOwner:nil options:nil];
UIView *view = [nibObjects objectAtIndex:0];
[self addSubview:view];
}
return self;
}
So this requires that myViewNIB's first (and probably only) member is a view. What this won't give you is a way to set things like labels programmatically inside the nib, at least not easily. Without looping through the views and looking for tags I'm not sure how else you'd do that.
Anyway. At this point you can drag out a new UIView in IB, and set the class to myView. myView will automatically search for our associated myViewNIB.xib and add its view as a subview.
Seems there should be a better way to do this.
Upvotes: 0
Reputation: 17811
You cannot do it in Interface Builder.
What I would probably do is to make a custom view, say MyGridItem, that loads MyUIView.nib as a subview when it awakes, and then use 9 of them in your MyGridView.nib.
Be careful with awakeFromNib, as it can be called twice if the view is involved in the loading of two different nibs (eg, if MyGridItem is the owner when loading MyGridView.nib, then MyGridItem awakeFromNib will be called once when it is loaded as part of loading MyUIView.nib, and once when it loads the MyGridView.nib.
Also, since you're loading the nib 9 times, you may want to cache the nib once using
NSNib* theNib = [[NSNib alloc] initWithNibNamed:@"MyGridItem" bundle:nil];
load it with:
if ( [theNib instantiateNibWithOwner:self topLevelObjects:NULL] ) {
You then may want to deallocate it after you've loaded all nine subviews.
Upvotes: 3
Reputation: 566
AFAIK, there's no way to load a NIB within a NIB. What I would do, in your case, is add the UILabels programmatically in MyUIView.
Upvotes: 0