I want to use a view throughout multiple viewcontrollers in a storyboard. Thus, I thought about designing the view in an external xib so changes are reflected in every viewc
Best solution currently is to just use a custom view controller with its view defined in a xib, and simply delete the "view" property that Xcode creates inside the storyboard when adding the view controller to it (don't forget to set the name of the custom class though).
This will make the runtime automatically look for the xib and load it. You can use this trick for any kind of container views, or content view.
Although the top most popular answers works fine, they are conceptually wrong. They all use File's owner
as connection between class's outlets and UI components. File's owner
is supposed to be used only for top-level objects not UIView
s. Check out Apple developer document.
Having UIView as File's owner
leads to these undesirable consequences.
contentView
where you are supposed to use self
. It’s not only ugly, but also structurally wrong because the intermediate view keeps data structure from conveying it’s UI structure. It's like going against the concept of declarative UI.There's elegant way to do it without using File's owner
. Please check this blog post. It explains how to do it the right way.
Here's the answer you've wanted all along. You can just create your CustomView
class, have the master instance of it in a xib with all the subviews and outlets. Then you can apply that class to any instances in your storyboards or other xibs.
No need to fiddle with File's Owner, or connect outlets to a proxy or modify the xib in a peculiar way, or add an instance of your custom view as a subview of itself.
Just do this:
UIView
to NibView
(or from UITableViewCell
to NibTableViewCell
)That's it!
It even works with IBDesignable to refer your custom view (including the subviews from the xib) at design time in the storyboard.
You can read more about it here: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155
And you can get the open source BFWControls framework here: https://github.com/BareFeetWare/BFWControls
And here's a simple extract of the NibReplaceable
code that drives it, in case you're curious:
https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4
Tom
This solution can be used even if your class does not have the same name as the XIB. For example, if you have a base view controller class controllerA which has a XIB name controllerA.xib and you subclassed this with controllerB and want to create an instance of controllerB in a storyboard, then you can:
*
- (void) loadView
{
//according to the documentation, if a nibName was passed in initWithNibName or
//this controller was created from a storyboard (and the controller has a view), then nibname will be set
//else it will be nil
if (self.nibName)
{
//a nib was specified, respect that
[super loadView];
}
else
{
//if no nib name, first try a nib which would have the same name as the class
//if that fails, force to load from the base class nib
//this is convenient for including a subclass of this controller
//in a storyboard
NSString *className = NSStringFromClass([self class]);
NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
UINib *nib ;
if (pathToNIB)
{
nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
}
else
{
//force to load from nib so that all subclass will have the correct xib
//this is convenient for including a subclass
//in a storyboard
nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
}
self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
}
}
Solution for Objective-C according to steps described in Ben Patch's response.
Use extension for UIView:
@implementation UIView (NibLoadable)
- (UIView*)loadFromNib
{
UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
xibView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:xibView];
[xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
[xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
[xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
[xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
return xibView;
}
@end
Create files MyView.h
, MyView.m
and MyView.xib
.
First prepare your MyView.xib
as Ben Patch's response says so set class MyView
for File's owner instead of main view inside this XIB.
MyView.h
:
#import <UIKit/UIKit.h>
IB_DESIGNABLE @interface MyView : UIView
@property (nonatomic, weak) IBOutlet UIView* someSubview;
@end
MyView.m
:
#import "MyView.h"
#import "UIView+NibLoadable.h"
@implementation MyView
#pragma mark - Initializers
- (id)init
{
self = [super init];
if (self) {
[self loadFromNib];
[self internalInit];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self loadFromNib];
[self internalInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self loadFromNib];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self internalInit];
}
- (void)internalInit
{
// Custom initialization.
}
@end
And later just create your view programatically:
MyView* view = [[MyView alloc] init];
Warning! Preview of this view will not be shown in Storyboard if you use WatchKit Extension because of this bug in Xcode >= 9.2: https://forums.developer.apple.com/thread/95616
My full example is here, but I will provide a summary below.
Layout
Add a .swift and .xib file each with the same name to your project. The .xib file contains your custom view layout (using auto layout constraints preferably).
Make the swift file the xib file's owner.
Code
Add the following code to the .swift file and hook up the outlets and actions from the .xib file.
import UIKit
class ResuableCustomView: UIView {
let nibName = "ReusableCustomView"
var contentView: UIView?
@IBOutlet weak var label: UILabel!
@IBAction func buttonTap(_ sender: UIButton) {
label.text = "Hi"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
}
Use it
Use your custom view anywhere in your storyboard. Just add a UIView
and set the class name to your custom class name.