Say I have a parent view class, that contains at least 1 property:
class BaseView : UIView {
#IBOutlet weak var myLabel: UILabel!
}
This class has a corresponding xib file with an outlet connection made from the xib to the myLabel property.
Now let's say we also have some child classes that inherit from this class:
class ChildView : BaseView {
func setup() {}
}
ChildView has some custom logic but can reuse all of the views from BaseView. It doesn't (or I'd prefer to avoid it having) its own corresponding xib file.
I'd like to be able to do something like this:
let childView = Bundle.main.loadNibNamed(String(describing: BaseView.self), owner: nil, options:nil)?.first as! ChildViewA
but this doesn't work. Neither does:
let childView = ChildView()
Bundle.main.loadNibNamed(String(describing: BaseView.self owner: childView, options: nil)
Is there anyway to allow a child view to inherit from its parent view's xib file in a similar way?
The problem is that the root view in the nib is of type BaseView, so as! ChildViewA fails. Since you don't have access to the NSKeyedUnarchiver that the nib loader uses to unarchive the xib, there is no easy way to substitute your own class during unarchiving.
Here's a workaround.
Do not embed the BaseView itself in the xib. Instead, make the top-level view in the xib be a plain UIView, and set the File's Owner custom class to BaseView. Then delete all of the connections to the top-level view and set them on the File's Owner instead. Also give BaseView a rootViewFromNib outlet, and connect it to the root view.
Then, give BaseView an initializer that loads its nib and adds that rootViewFromNib to itself as a subview, with its frame pinned to the BaseView's own bounds. You can use autoresizing to do it.
In the end, BaseView should look like this:
class BaseView: UIView {
#IBOutlet var myLabel: UILabel!
// other outlets, etc.
#IBOutlet private var rootViewFromNib: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
Bundle(for: BaseView.self).loadNibNamed("BaseView", owner: self, options: nil)
rootViewFromNib.frame = bounds
rootViewFromNib.autoresizingMask = [.flexibleWidth, .flexibleHeight]
rootViewFromNib.translatesAutoresizingMaskIntoConstraints = true
addSubview(rootViewFromNib)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and BaseView.xib should look like this:
Related
Let's say I have SomeViewController: UIViewController, and I have a custom view CustomView: UIView, defined as a XIB, that I want to display. This custom view will be reused in other view controllers and even multiple times in the same view controller.
class CustomView: UIView {
#IBOutlet public var label: UILabel!
}
The way I have always added this view has been:
class UIExamples: UIViewController {
#IBOutlet private var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Assume makeViewFromNib returns the view [0] in the Nib.
let customView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
customView.frame = myView.bounds
myView.addSubview(customView)
}
}
Let's say that later on I want to modify something about the CustomView via a public property label.
I could do it inside viewDidLoad ONLY BECAUSE I have access to customView, but what if I want to change it in some other function? What I have seen is that one would have to do
let customView = myView.subviews[0] as! CustomView
customView.label.text = "some text"
which does not look right.
So, I thought the right way should be this:
class UIExamples: UIViewController {
#IBOutlet public var customView: CustomView! // Now this is always a CustomView type
override func viewDidLoad() {
super.viewDidLoad()
// Assume makeViewFromNib returns the view [0] in the Nib.
customView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
customView.label.text = "some text" // DOES NOT WORK!
}
}
That last line customView.label.text does not work. In fact, the label is not even seen on the screen. What am I doing wrong?
OK, didn't read (or maybe was reading before edit) that you use xib. If ViewController is created from xib with label in it this will be correct way:
set myView class in xib here:
and then connect IBOutlet (remove current one from xib here:
and then from code).
Now myView.label.text = "some text" should work without further issues.
Good luck!
If you create your view from code do it in this manner:
class UIExamples: UIViewController {
#IBOutlet private var myView: CustomView!
override func viewDidLoad() {
super.viewDidLoad()
// Assume makeViewFromNib returns the view [0] in the Nib.
myView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
myView.frame = view.bounds
view.addSubview(myView)
}
}
Because you already have property storing this view in your view controller it's unnecessary to dig inside subviews, it will work like that
myView.label.text = "some text"
And reason for
customView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
customView.label.text = "some text"
isn't working is because it's completely new view that wasn't added to your view controller subviews (also frame wasn't set BTW). And because you changed value of your customView property it's now not pointing to old instance of view, that is present in subviews (you can still see that "old one" but not change it).
But I really recommend to use pointer created once, as correct class to avoid casting. (Or creating view directly in xib / storyboard, otherwise #IBOutlet is not necessary)
Posting my own answer.
Create the XIB file.
Create the UIView subclass Swift file.
Under the XIB file owner's Identify Inspector custom class field, type in the UIView subclass name (your custom view).
Under the XIB file owner's Connections Inspector, make sure all IBOutlets in the Swift file are connected.
Add a view to the view controller and under its Identify Inspector custom class type, specify the custom class name.
Important:
* In your XIB swift file, you have to properly load the XIB content view.
...
/// Initializer used by Interface Builder.
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
/// Initializer used programmatically.
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
...
func configure() {
let contentView = // here use many of the functions available on the internet to
// load a view from a nib.
// Then add this view to the view hierarchy.
addSubview(contentView)
}
I have a subclass of UIView (MyView) that I've hooked up to a NIB file.
class MyView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var tableView: UITableView!
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
let nibName = "MyView"
private func setup() {
let bundle = Bundle.init(for: type(of: self))
bundle.loadNibNamed(nibName, owner: self, options: nil)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.addSubview(contentView)
}
}
This view is simple; containing only one UITableView (or tableView). However, I want to add a UITableViewCell with a UILabel to tableView but the storyboard is not letting me do this.
I understand that MyView is not a view controller and therefore should not (if following an MVC pattern) implement the various table view data source / delate methods, but, still, why can I not add this table view cell to the table view within this custom view?
My aim was to then have some UIViewController subclass that has an instance of MyView, i.e.
var myView = MyView(),
which it then controls the datasource and delegate methods for, i.e.
myView.tableView.dataSource = self.
Finally, I've attached a screenshot showing that I am unable to add this table view cell to the table view.
You cannot add prototype cells to table views in xib files - only in storyboards.
You can create your table view in one xib, and your table view cell in another xib, if you want.
Or, you can create a second storyboard that would contain a table view and it would support cell prototypes.
I have created a custom UIView in MySample.xib. I have added the class MyView to the File Owner of xib.
MyView.swift
class MyView: UIView {
#IBOutlet var view: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
NSBundle.mainBundle().loadNibNamed("MySample", owner: self, options: nil)
self.addSubview(self.view)
}
}
I am now loading this MyView from MyController file like this:
MyController.swift
class MyController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
view.addSubview(MyView())
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Now to display this view, I am using to following code from another controller's UIButton:
presentViewController(MyController(), animated: true, completion: nil)
This does display the view on screen. But the problem is, it doesn't accept any user interaction. In my custom view, I have a UITableView which does display the data but it doesn't scroll or get tapped due to lack of user interaction.
Any idea what I am doing wrong?
There are some unnecessary things in your example.
I am still not sure what are you trying to do, but if you want to add a custom view from xib to your view controller then:
Create a view in a xib file , you don't need to override init , and you can't init view from xib using the default init UIView() , so please remove init method from your MyView class.
In your xib make sure that your view that you see in the IB is of the class type you want to use (i guess MyView class).
In your view controller init the view like this:
class MyController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Get all views in the xib
let allViewsInXibArray = NSBundle.mainBundle().loadNibNamed("MySample", owner: self, options: nil)
//If you only have one view in the xib and you set it's class to MyView class
let myView = allViewsInXibArray.first as! MyView
//Set wanted position and size (frame)
myView.frame = self.view.bounds
//Add the view
self.view.addSubview(myView)
//TODO: set wanted constraints.
}
}
You don't have to re-instantiate this twice
already if you using the design pattern.
It's so simple. Just write:
class MyController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Get all views in the xib
let view = MyView()
self.view.addSubview(myView)
//TODO: set wanted constraints.
}}
And It will work.
Instead of linking xib File's Owner class to MyView, I have to change the class of root view in xib to MyView. Then based on #Oleg Sherman code, it works perfectly with small changes of adding MyView() as owner to get all it's events, otherwise it will throw an error this class is not key value coding-compliant for the key ****.:
let allViewsInXibArray = NSBundle.mainBundle().loadNibNamed("MySample", owner: MyView(), options: nil)
Using File's Owner class to MyView is only required when you have to use the xib in Storyboard.
Not sure if there is a workaround to use File's Owner class to MyView when programmatically loading xib from custom controller like in my original question.
Is it possible to create reusable stack views on the story board that can be used dynamically to be generated at a later time? Sort of a template/widget/component.
I am aware that I can do this with a class but if I am able to visually generate a set of components that can be re-used at a latter time I may be able to let our designers make changes to storyboards directly.
Yes -- you can do this with any UIView. There are many tutorials for this (e.g. http://onedayitwillmake.com/blog/2013/07/ios-creating-reusable-uiviews-with-storyboard/)
The basic idea is to drag one onto Storyboard or XIB, make a custom class for it, then implement the view's awakeFromNib to load it.
Yes.It is.
Create a empty xib and then add a stack view to it.
Then create a class which extends UIStackView.
class stackView: UIStackView {
var contentView : UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
required init(coder: NSCoder) {
super.init(coder: coder)
xibSetup() }
func xibSetup() {
contentView = loadViewFromNib()
contentView.frame = bounds
contentView.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(contentView)
}
func loadViewFromNib() -> UIStackView! {
let view: UIStackView? = Bundle.main.loadNibNamed("stackView", owner: nil, options: nil)?.first as! UIStackView?
return view
}
Create a viewController.Add a stackView to it.In StackView properties, goto 3rd bar which named as custom class, for class name give stackView class name
After creating a custom UIView, how can you use Storyboard to add AutoLayout constraints relating its subviews to views/components outside the custom UIView?
In the example below, SliceView represents a custom UIView. In its .xib file there is a UILabel.
After adding a new UIView and setting its class to SliceView, the new view looks the same in Storyboard. It hasn't inherited the subviews and properties of a SliceView. The two screenshots below illustrate the view hierarchy in the SliceView.xib and how after transforming a UIView into a SliceView, the hierarchy remains the same in Storyboard.
So if you want to add a button that is constrained to 10 pixels above the SliceView label, how can you do that inside Storyboard?
class SliceView: UIView {
#IBOutlet var storyboardView: UIView!
#IBOutlet weak var captionLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if let nibsView = NSBundle.mainBundle().loadNibNamed("SliceView", owner: self, options: nil) as? [UIView] {
let nibRoot = nibsView[0]
self.addSubview(nibRoot)
nibRoot.frame = self.bounds
}
}
}
Storyboard view hierarchy:
SliceView view hierarchy: