I am trying to create a UIView(A) containing 2 custom Views (B) in it
View B is setup using Autolayout Constraints and made in Interface Builder, including the constraints. A is added in the Nib of the viewController
B
- UIImageView(Leading = 10, Trailing = 10, AlignVertically)
- UITextField(Leading = 10, Trailing = 10, AlignVertically)
ViewController
A(300x300, AlignHorizontally, AlignVertically)
In the ViewController I have A to be fixed at 300x300 and
B1 and B2 has its Leading, Trailing, Top and Bottom pinned at 0. (which should make B1 and B2 to be 300x150, forgive me if I miss something out)
When loading View B I use the following code to load its Nib:
override func awakeAfterUsingCoder(aDecoder: NSCoder!) -> AnyObject! {
if self.subviews.count == 0 {
let bundle = NSBundle(forClass: self.dynamicType)
var view = bundle.loadNibNamed("B", owner: nil, options: nil)[0] as B
view.setTranslatesAutoresizingMaskIntoConstraints(false)
let constraints = self.constraints()
self.removeConstraints(constraints)
view.addConstraints(constraints)
return view
}
return self
}
But when I try to run this I get the following warning including a crash:
The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x7f897ad1acc0 V:[TestProject.B:0x7f897af73840(300)]>
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView _viewHierarchyUnpreparedForConstraint:] to debug.
I have also tried adding the view as a property of View B and use the following code to add it to B
NSBundle.mainBundle().loadNibNamed("B", owner: self, options: nil)
self.addSubview(self.viewOfB);
the result of this is the view is added to the viewController, but it is not adopting any of the AutoLayoutConstraints from its own Nib.
Right now I have no idea how to add this view to the viewController's view including the constraints. What am I doing wrong? Is there a better way to do this?
PS: View A used to be custom too.
PPS: I am using Swift to do this, but I'm sure solutions in Objective-C works as well.
First of all the error you are getting is because
you can't swap or move constraints between your views
the views must already be added in the hierarchy before adding new constraints
the constraints are usually added on the parent view that holds the views (or common ancestor).
(for more details see AutoLayout Guide)
I would suggest having view A (CustomView : UIView) loading the contents (B Views) from the nib file and adding them as it's subviews.
The trick is that you need to layout you B views inside a content view so that your nib file has only one root object in it.
This way, when you add the content view as the view A subview, all constraints will transfer over(similar with how UITableViewCells use their contentView).
Important Note: The content view should NOT be of the class type CustomView. If you want to drag outlets and actions just declare the File's Owner object of the class CustomView and link them there.
Your final view hierarchy should look like this:
MainView (ViewController's view)
View A
Content View
B1 View
B2 View
This way view A has it's layout configured in the ViewController's storyboard/xib and you B views will use the constraints from the nib file related to the content view.
So the only thing let is to make sure is that the content view will always have the same size with view A.
class CustomView: UIView
{
#IBOutlet var _b1Label: UILabel!
#IBOutlet var _b2Button: UIButton!
func loadContentView()
{
let contentNib = UINib(nibName: "CustomView", bundle: nil)
if let contentView = contentNib.instantiateWithOwner(self, options: nil).first as? UIView
{
self.addSubview(contentView)
// We could use autoresizing or manually setting some constraints here for the content view
contentView.translatesAutoresizingMaskIntoConstraints = true
contentView.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
contentView.frame = self.bounds
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
loadContentView()
}
required init(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder);
loadContentView()
}
}
The problem is: constraints retrieved from self are referring to self.
Instead, you have to make new constraints referring to view and with same other properties. like this:
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]];
}
/* if you also have to move subviews into `view`.
for(UIView *subview in self.subviews) {
[view addSubview:subview];
}
*/
[view addConstraints:constraints];
And, I think you should copy a few more properties from self.
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
Sorry for Obj-C code, I copied from this answer :)
Related
I have a custom view and xib. I use this custom view in one of my storyboard's controller views.
I have a use case where I want to be able to hide the custom view (and bring its height to zero). Right now, I set the height in the interface builder and set constraints to the superview's edges:
As you can see, I want its height to be 84 everywhere.
Now here is my custom view's class:
import UIKit
#IBDesignable
class BannerView: UIView {
#IBOutlet var contentView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override func awakeFromNib() {
super.awakeFromNib()
initialize()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
initialize()
contentView?.prepareForInterfaceBuilder()
}
func initialize() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "BannerView", bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func hide() {
// Hide the view and set its height to zero here
}
}
But, now I'm confused... should I also be setting a height constraint on the custom view when I load it into one of my storyboards? Or should its height be 84 everywhere and I shouldn't have to specify it any further?
Also, how would I hide the custom view and set its height to zero in the above hide() function?
There are several ways to do this... here's one.
Give the content of your xib constraints to make its height 84-pts. You haven't shown your xib's layout, but I'll assume you know how to do that.
Then, when you add BannerView to your main view (I'm guessing that's what you're doing), embed it in a Vertical UIStackView with these properties:
Now, when you set bannerView.isHidden = true, the stack view automatically removes it from the height calculations, resulting in the stack view having a height of Zero.
Setting bannerView.isHidden = false will then re-display the banner view along with its height.
As you want the view's height to be 84 everywhere I think you should add a height constraint outlet and set the value 84.
Set the constraint value to 0 to hide the view (I highly suggest not to hide some view by doing this).
I'm trying to create custom reusable view, lets say QuestionView. Now I use this class to be extended by my QuestionView, so it loads view from my xib, then this view added as subview to self. It works ok in case if my view has constant height and width, but I need kind of this layout
This view's file's owner set to QuestionView.
I have label on top which connected with top, left and right via constraints but it's flexible in terms of height - label is multiline. Yes/No buttons view connected to bottom of label, left and right of superview and has constant height. Details view connected to bottom of buttons view, to left and to right, has constant height. So my QuestionView has flexible height. If I change text of label to 2 lines for example, my view should be stretched.
I have ViewController xib, where I put generic view and set its class to QuestionView.
I just add this view as subview of QuestionView so I think there is a problem with constraints between view and subview, I should add them? I tried to add left, right, top, bottom constraints between them with translatesAutoresizingMaskIntoConstraints set to false but anyway got strange (similar to height from xibs) superview(QuestionView) height, subview height is ok in runtime.
So what am I doing wrong here? Do I need to bind subview height to superview height somehow differently?
UPD. Here is screenshot in runtime, gray view is it's size in runtime, should be stretched to TextField bottom. Now it looks like it was false effect of ok subview height in runtime.
Here is my code now
import UIKit
protocol NibDefinable {
var nibName: String { get }
}
#IBDesignable
class NibLoadingView: UIView, NibDefinable {
var containerView: UIView!
var nibName: String {
return String(self.dynamicType)
}
override init(frame: CGRect) {
super.init(frame: frame)
nibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
nibSetup()
}
private func nibSetup() {
//clipsToBounds = true
containerView = loadViewFromNib()
containerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(containerView)
addConstraint(.Top)
addConstraint(.Left)
addConstraint(.Bottom)
addConstraint(.Right)
}
private func addConstraint(attribute: NSLayoutAttribute) {
self.addConstraint(NSLayoutConstraint(item: self,
attribute: attribute,
relatedBy: .Equal,
toItem: containerView,
attribute: attribute,
multiplier: 1,
constant: 0.0
))
}
private func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let nibView = nib.instantiateWithOwner(self, options: nil).first as! UIView
return nibView
}
}
First, you have to add a constraint between QuestionView and bottom of it's superview
Second, looks like problem might be with QuestionView's superview and its parent constraints.
UPD
the best tool to nail those kind of bugs - Reveal
Here is what I wanna do:
My storyboard contains a UIViewController with just a few controlls. It's height is less than half a screen. For simplicity, let's call it the DataViewController.
The DataViewController is embedded inside other UIViewControllers through a "Container View".
Although "Container View" displays the DataViewController, it still needs to have an explicit height set. Otherwise interface builder complains about ambiguous constraints.
Now, how can I tell "Container View" that it's size should be what's required by DataViewController? I.e. without setting a hard coded, explicit height in Interface Builder (which, I fear, would break the layout if the font size changes)?
Or in other words: How do you size/position embedded UIViewControllers in Interface Builder?
If you want the childViewContainer to be responsible for its width and height, here is a way to do it :
Set width and height constraints in your Parent View Controller(s) and select remove at build time. They are just here so interface builder stops complaining about missing constraints.
(You can) Change your Child View Controller simulated size from fixed to freeform in your storyboard size inspector tab.
Here is the trick : In your Parent View Controller(s) viewDidLoad disable translatesAutoresizingMaskIntoConstraints on the first subviews, and simply redefine by yourself the containerView constraints to its subview - root view of your Child View Controller - like bellow :
Objective-C
- (void)viewDidLoad {
[super viewDidLoad];
self.containerView.subviews[0].translatesAutoresizingMaskIntoConstraints = NO;
}
#pragma mark - Constraints
- (void)updateViewConstraints {
[super updateViewConstraints];
[self initConstraints];
}
- (void)initConstraints {
if (!self.didSetConstraints) {
self.didSetConstraints = YES;
self.containerView.subviews[0].translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *views = #{#"subview" : self.containerView.subviews[0]};
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[subview]|" options:0 metrics:nil views:views]];
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[subview]|" options:0 metrics:nil views:views]];
}
}
Swift
override func viewDidLoad() {
super.viewDidLoad()
containerView.subviews[0].translatesAutoresizingMaskIntoConstraints = false
}
// Mark: Constraints
override func updateViewConstraints() {
super.updateViewConstraints()
initConstraints()
}
func initConstraints() {
if !didSetConstraints {
didSetConstraints = true
containerView.subviews[0].translatesAutoresizingMaskIntoConstraints = false
let views = ["subview": containerView.subviews[0]]
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[subview]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[subview]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
}
}
Now your child View Controller (linked to the container view) is fully responsible for its content size. Thus, you must set constraints that let the root view calculates its size to satisfies the constraints it holds.
If you are embedding with a segue, just do:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let viewController = segue.destination as? EmbeddedViewController {
viewController.view.translatesAutoresizingMaskIntoConstraints = false
}
}
in your content view controller set the preferredContentSize (In your example DataViewController) like the following
override func viewDidLoad() {
super.viewDidLoad()
preferredContentSize = CGSize(width: 400, height: 200)
}
When the child gets added to the parentViewController, the parentViewController gets notified about the prefered size. Following function gets called on the parent
func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer)
In this method you can then change the container to the prefered size of the child.
For example change the height to the prefered height of the child/content
override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
// heightConstraint is a IBOutlet to your NSLayoutConstraint you want to adapt to height of your content
heigtConstraint.constant = container.preferredContentSize.height
}
I typically like to create and design my uiviews in interface builder. Sometimes I need to create a single view in a xib that can be reused in multiple view controllers in a storyboard.
Reuse and render a xib in a storyboard.
Tested with Swift 2.2 & Xcode 7.3.1
1 ---- Create a new UIView named 'DesignableXibView'
File > New > File > Source > Cocoa Touch Class > UIView
2 ---- Create a matching xib file named 'DesignableXibView'
File > New > File > User Interface > View
3 ---- Set the file owner of the of the xib
select the xib
select file's owner
set custom class to 'DesignableXibView' in the Identity Inspector.
Note: Do not set the custom class of the view on the xib. Only the File Owner!
4 ---- DesignableXibView's Implementation
// DesignableXibView.swift
import UIKit
#IBDesignable
class DesignableXibView: UIView {
var contentView : UIView?
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
func xibSetup() {
contentView = loadViewFromNib()
// use bounds not frame or it'll be offset
contentView!.frame = bounds
// Make the view stretch with containing view
contentView!.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(contentView!)
}
func loadViewFromNib() -> UIView! {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: String(self.dynamicType), bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
5 ---- Test your reuseable view in a storyboard
Open your storyboard
Add a view
Set that view's Custom Class
wait a sec ... BOOM!!
NEW! updated answer with ability to render directly in the storyboard (and swift!)
Works in Xcode 6.3.1
Create a new UIView named 'ReuseableView'
File > New > File > Source > Cocoa Touch Class > UIView
Create a matching xib file named 'ReuseableView'
File > New > File > User Interface > View
Set the file owner of the of the xib
select the xib
select file's owner
set custom class to 'ReusableView' in the Identity Inspector.
Note: Do not set the custom class of the view on the xib. Only the File Owner!
Make an outlet from the view in the ReuseableView.xib to your ReuseableView.h interface
Open Assistant Editor
Control + Drag from the view to your interface
Add initWithCoder implementation to load view and add as a subview.
- (id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self) {
// 1. load the interface
[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil];
// 2. add as subview
[self addSubview:self.view];
// 3. allow for autolayout
self.view.translatesAutoresizingMaskIntoConstraints = NO;
// 4. add constraints to span entire view
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[view]|" options:0 metrics:nil views:#{#"view":self.view}]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[view]|" options:0 metrics:nil views:#{#"view":self.view}]];
}
return self;
}
Test your reuseable view in a storyboard
Open your storyboard
Add a view
Set that view's Custom Class
Run and observe!
Swift 3&4 Update to the accepted answer
1. Create a new UIView named 'DesignableXibView'
File > New > File > Source > Cocoa Touch Class > UIView
2. Create a matching xib file named 'DesignableXibView'
File > New > File > User Interface > View
3. Set the file owner of the of the xib
Select the "DesignableXibView.xib" > "File's Owner" > set "Custom Class" to 'DesignableXibView' in the Identity Inspector.
Note: Do not set the custom class of the view on the xib. Only the
File Owner!
4. DesignableXibView's Implementation
import UIKit
#IBDesignable
class DesignableXibView: UIView {
var contentView : UIView!
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
func xibSetup() {
contentView = loadViewFromNib()
// use bounds not frame or it'll be offset
contentView.frame = bounds
// Make the view stretch with containing view
contentView.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
// Adding custom subview on top of our view
addSubview(contentView)
}
func loadViewFromNib() -> UIView! {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}
5 Test your reusable view in a storyboard
Open your storyboard
Add a view
Set that view's Custom Class
The initWithCoder function in swift 2 if anybody is having trouble translating it:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
UINib(nibName: String(self.dynamicType), bundle: NSBundle.mainBundle()).instantiateWithOwner(self, options: nil)
self.addSubview(view)
self.view.translatesAutoresizingMaskIntoConstraints = false
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: NSLayoutFormatOptions.AlignAllCenterY , metrics: nil, views: ["view": self.view]))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: NSLayoutFormatOptions.AlignAllCenterX , metrics: nil, views: ["view": self.view]))
}
For anyone trying to adapt accepted answer (by #Garfbargle) to Objective-C
Just converting Swift to Objective-C isn't enough to make it work. I've had hard time to allow live rendering in Storyboard.
After translating the whole code, the view is well loaded when running on device (or simulator), but the live rendering in Storyboard doesn't work. The reason for this is that I used [NSBundle mainBundle] whereas Interface Builder hasn't got access to mainBundle. What you have to use instead is [NSBundle bundleForClass:self.classForCoder]. BOOM, live rendering works now !
Note : if you have having issues with Auto Layout, try disabling Safe Area Layout Guides in the Xib.
For your convenience, I leave my whole code here, so that you just have to copy/paste (for all process, follow original answer) :
BottomBarView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface BottomBarView : UIView
#end
BottomBarView.m
#import "BottomBarView.h"
#interface BottomBarView() {
UIView *contentView;
}
#end
#implementation BottomBarView
-(id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self xibSetup];
}
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self xibSetup];
}
return self;
}
-(void) xibSetup {
contentView = [self loadViewFromNib];
contentView.frame = self.bounds;
contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:contentView];
}
-(UIView*) loadViewFromNib {
NSBundle *bundle = [NSBundle bundleForClass:self.classForCoder]; //this is the important line for view to render in IB
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:bundle];
UIView *view = [nib instantiateWithOwner:self options:nil][0];
return view;
}
#end
Tell me if you encounter some issues but it should almost work out of the box :)
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:
Import BFWControls framework
Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell)
That's it!
It even works with IBDesignable to render 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
Tom đź‘Ł
If someone is interested, here's the Xamarin.iOS version of the code Step 4 of #Garfbargle
public partial class CustomView : UIView
{
public ErrorView(IntPtr handle) : base(handle)
{
}
[Export("awakeFromNib")]
public override void AwakeFromNib()
{
var nibObjects = NSBundle.MainBundle.LoadNib("CustomView", this, null);
var view = (UIView)Runtime.GetNSObject(nibObjects.ValueAt(0));
view.Frame = Bounds;
view.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
AddSubview(rootView);
}
}
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 viewcontroller. But how can one load a view from a external xib in a storyboard and is it even possible? If thats not the case, what other alternatives are availble to suit the situation abouve?
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.
For a while Christopher Swasey's approach was the best approach I had found. I asked a couple of the senior devs on my team about it and one of them had the perfect solution! It satisfies every one of the concerns that Christopher Swasey so eloquently addressed and it doesn't require boilerplate subclass code(my main concern with his approach). There is one gotcha, but other than that it is fairly intuitive and easy to implement.
Create a custom UIView class in a .swift file to control your xib. i.e. MyCustomClass.swift
Create a .xib file and style it as you want. i.e. MyCustomClass.xib
Set the File's Owner of the .xib file to be your custom class (MyCustomClass)
GOTCHA: leave the class value (under the identity Inspector) for your custom view in the .xib file blank. So your custom view will have no specified class, but it will have a specified File's Owner.
Hook up your outlets as you normally would using the Assistant Editor.
NOTE: If you look at the Connections Inspector you will notice that your Referencing Outlets do not reference your custom class (i.e. MyCustomClass), but rather reference File's Owner. Since File's Owner is specified to be your custom class, the outlets will hook up and work propery.
Make sure your custom class has #IBDesignable before the class statement.
Make your custom class conform to the NibLoadable protocol referenced below.
NOTE: If your custom class .swift file name is different from your .xib file name, then set the nibName property to be the name of your .xib file.
Implement required init?(coder aDecoder: NSCoder) and override init(frame: CGRect) to call setupFromNib() like the example below.
Add a UIView to your desired storyboard and set the class to be your custom class name (i.e. MyCustomClass).
Watch IBDesignable in action as it draws your .xib in the storyboard with all of it's awe and wonder.
Here is the protocol you will want to reference:
public protocol NibLoadable {
static var nibName: String { get }
}
public extension NibLoadable where Self: UIView {
public static var nibName: String {
return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
}
public static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
func setupFromNib() {
guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
}
}
And here is an example of MyCustomClass that implements the protocol (with the .xib file being named MyCustomClass.xib):
#IBDesignable
class MyCustomClass: UIView, NibLoadable {
#IBOutlet weak var myLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFromNib()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupFromNib()
}
}
NOTE: If you miss the Gotcha and set the class value inside your .xib file to be your custom class, then it will not draw in the storyboard and you will get a EXC_BAD_ACCESS error when you run the app because it gets stuck in an infinite loop of trying to initialize the class from the nib using the init?(coder aDecoder: NSCoder) method which then calls Self.nib.instantiate and calls the init again.
Assuming that you've created an xib that you want to use:
1) Create a custom subclass of UIView (you can go to File -> New -> File... -> Cocoa Touch Class. Make sure "Subclass of:" is "UIView").
2) Add a view that's based on the xib as a subview to this view at initialization.
In Obj-C
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super initWithCoder:aDecoder]) {
UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:#"YourXIBFilename"
owner:self
options:nil] objectAtIndex:0];
xibView.frame = self.bounds;
xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview: xibView];
}
return self;
}
In Swift 2
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
xibView.frame = self.bounds
xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(xibView)
}
In Swift 3
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
xibView.frame = self.bounds
xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addSubview(xibView)
}
3) Wherever you want to use it in your storyboard, add a UIView as you normally would, select the newly added view, go to the Identity Inspector (the third icon on the upper right that looks like a rectangle with lines in it), and enter your subclass's name in as the "Class" under "Custom Class".
I've always found the "add it as a subview" solution unsatisfactory, seeing as it screws with (1) autolayout, (2) #IBInspectable, and (3) outlets. Instead, let me introduce you to the magic of awakeAfter:, an NSObject method.
awakeAfter lets you swap out the object actually woken up from a NIB/Storyboard with a different object entirely. That object is then put through the hydration process, has awakeFromNib called on it, is added as a view, etc.
We can use this in a "cardboard cut-out" subclass of our view, the only purpose of which will be to load the view from the NIB and return it for use in the Storyboard. The embeddable subclass is then specified in the Storyboard view's identity inspector, rather than the original class. It doesn't actually have to be a subclass in order for this to work, but making it a subclass is what allows IB to see any IBInspectable/IBOutlet properties.
This extra boilerplate might seem suboptimal—and in a sense it is, because ideally UIStoryboard would handle this seamlessly—but it has the advantage of leaving the original NIB and UIView subclass completely unmodified. The role it plays is basically that of an adapter or bridge class, and is perfectly valid, design-wise, as an additional class, even if it is regrettable. On the flip side, if you prefer to be parsimonious with your classes, #BenPatch's solution works by implementing a protocol with some other minor changes. The question of which solution is better boils down to a matter of programmer style: whether one prefers object composition or multiple inheritance.
Note: the class set on the view in the NIB file remains the same. The embeddable subclass is only used in the storyboard. The subclass can't be used to instantiate the view in code, so it shouldn't have any additional logic, itself. It should only contain the awakeAfter hook.
class MyCustomEmbeddableView: MyCustomView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
}
}
⚠️ The one significant drawback here is that if you define width, height, or aspect ratio constraints in the storyboard that don't relate to another view then they have to be copied over manually. Constraints that relate two views are installed on the nearest common ancestor, and views are woken from the storyboard from the inside-out, so by the time those constraints are hydrated on the superview the swap has already occurred. Constraints that only involve the view in question are installed directly on that view, and thus get tossed when the swap occurs unless they are copied.
Note that what is happening here is constraints installed on the view in the storyboard are copied to the newly instantiated view, which may already have constraints of its own, defined in its nib file. Those are unaffected.
class MyCustomEmbeddableView: MyCustomView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!
for constraint in constraints {
if constraint.secondItem != nil {
newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
} else {
newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
}
}
return newView as Any
}
}
instantiateViewFromNib is a type-safe extension to UIView. All it does is loop through the NIB's objects until it finds one that matches the type. Note that the generic type is the return value, so the type has to be specified at the call site.
extension UIView {
public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
if let objects = bundle.loadNibNamed(nibName, owner: nil) {
for object in objects {
if let object = object as? T {
return object
}
}
}
return nil
}
}
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 UIViews. Check out Apple developer document.
Having UIView as File's owner leads to these undesirable consequences.
You are forced to use 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.
You can only have one UIView per Xib. An Xib is supposed to have multiple UIViews.
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.
I think about alternative for using XIB views to be using View Controller in separate storyboard.
Then in main storyboard in place of custom view use container view with Embed Segue and have StoryboardReference to this custom view controller which view should be placed inside other view in main storyboard.
Then we can set up delegation and communication between this embed ViewController and main view controller through prepare for segue. This approach is different then displaying UIView, but much simpler and more efficiently (from programming perspective) can be utilised to achieve the same goal, i.e. have reusable custom view that is visible in main storyboard
The additional advantage is that you can implement you logic in CustomViewController class and there set up all delegation and view preparation without creating separate (harder to find in project) controller classes, and without placing boilerplate code in main UIViewController using Component. I think this is good for reusable components ex. Music Player component (widget like) that is embeddable in other views.
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.
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
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:
Import BFWControls framework
Change your superclass from 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:
create the view controller in the storyboard
set the class of the controller to the controllerB
delete the view of the controllerB in the storyboard
override load view in controllerA to:
*
- (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];
}
}
class BYTXIBView: UIView {
var nibView: UIView?
// MARK: - init methods
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonSetup()
}
func commonSetup() {
guard let nibView = loadViewFromNib() else { return }
nibView.frame = bounds
nibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(nibView)
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let className = type(of: self).className
let view = bundle.loadNibNamed(className, owner: self, options: nil)?.first as? UIView
return view
}
}