STEP1. Replacing self
from Storyboard
Replacing self
in initWithCoder:
method will fail with following error.
'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Instead, you can replace decoded object with awakeAfterUsingCoder:
(not awakeFromNib
). like:
@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
@end
STEP2. Preventing recursive call
Of course, this also causes recursive call problem. (storyboard decoding -> awakeAfterUsingCoder:
-> loadNibNamed:
-> awakeAfterUsingCoder:
-> loadNibNamed:
-> ...)
So you have to check current awakeAfterUsingCoder:
is called in Storyboard decoding process or XIB decoding process.
You have several ways to do that:
a) Use private @property
which is set in NIB only.
@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
and set "User Defined Runtime Attributes" only in 'MyCustomView.xib'.
Pros:
Cons:
- Simply does not work:
setXib:
will be called AFTER awakeAfterUsingCoder:
b) Check if self
has any subviews
Normally, you have subviews in the xib, but not in the storyboard.
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if(self.subviews.count > 0) {
// loading xib
return self;
}
else {
// loading storyboard
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
}
Pros:
- No trick in Interface Builder.
Cons:
- You cannot have subviews in your Storyboard.
c) Set a static flag during loadNibNamed:
call
static BOOL _loadingXib = NO;
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if(_loadingXib) {
// xib
return self;
}
else {
// storyboard
_loadingXib = YES;
typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
_loadingXib = NO;
return view;
}
}
Pros:
- Simple
- No trick in Interface Builder.
Cons:
- Not safe: static shared flag is dangerous
d) Use private subclass in XIB
For example, declare _NIB_MyCustomView
as a subclass of MyCustomView
.
And, use _NIB_MyCustomView
instead of MyCustomView
in your XIB only.
MyCustomView.h:
@interface MyCustomView : UIView
@end
MyCustomView.m:
#import "MyCustomView.h"
@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In Storyboard decoding path.
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
@end
@interface _NIB_MyCustomView : MyCustomView
@end
@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In XIB decoding path.
// Block recursive call.
return self;
}
@end
Pros:
- No explicit
if
in MyCustomView
Cons:
- Prefixing
_NIB_
trick in xib Interface Builder
- relatively more codes
e) Use subclass as placeholder in Storyboard
Similar to d)
but use subclass in Storyboard, original class in XIB.
Here, we declare MyCustomViewProto
as a subclass of MyCustomView
.
@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In storyboard decoding
// Returns MyCustomView loaded from NIB.
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
owner:nil
options:nil] objectAtIndex:0];
}
@end
Pros:
- Very safe
- Clean; No extra code in
MyCustomView
.
- No explicit
if
check same as d)
Cons:
- Need to use subclass in storyboard.
I think e)
is the safest and cleanest strategy. So we adopt that here.
STEP3. Copy properties
After loadNibNamed:
in 'awakeAfterUsingCoder:', You have to copy several properties from self
which is decoded instance f the Storyboard. frame
and autolayout/autoresize properties are especially important.
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
// copy layout properities.
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
// copy autolayout constraints
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in self.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == self) firstItem = view;
if(secondItem == self) secondItem = view;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
// move subviews
for(UIView *subview in self.subviews) {
[view addSubview:subview];
}
[view addConstraints:constraints];
// Copy more properties you like to expose in Storyboard.
return view;
}
FINAL SOLUTION
As you can see, this is a bit of boilerplate code. We can implement them as 'category'.
Here, I extend commonly used UIView+loadFromNib
code.
#import <UIKit/UIKit.h>
@interface UIView (loadFromNib)
@end
@implementation UIView (loadFromNib)
+ (id)loadFromNib {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
owner:nil
options:nil] objectAtIndex:0];
}
- (void)copyPropertiesFromPrototype:(UIView *)proto {
self.frame = proto.frame;
self.autoresizingMask = proto.autoresizingMask;
self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in proto.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == proto) firstItem = self;
if(secondItem == proto) secondItem = self;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
for(UIView *subview in proto.subviews) {
[self addSubview:subview];
}
[self addConstraints:constraints];
}
Using this, you can declare MyCustomViewProto
like:
@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
MyCustomView *view = [MyCustomView loadFromNib];
[view copyPropertiesFromPrototype:self];
// copy additional properties as you like.
return view;
}
@end
XIB:
Storyboard:
Result: