Xcode - How IBOutlets and Nib files work? - ios

I'm for the first time using nib files. I mean xib and the corresponding swift class.
Here is my swift class:
import UIKit
#IBDesignable class LittleVideoView: UIView {
var view: UIView!
var nibName: String = "LittleVideoView"
// MARK: Views
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var clicksLabel: UILabel!
#IBOutlet weak var channelNameLabel: UILabel!
#IBOutlet weak var thumbnailImageView: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
addSubview(view)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
From the editor view I created some IBOutlets as you can see. Everything works properly.
There is just something I don't understand. I am programmatically loading the nib in this class, so why can I create IBOutlets while Xcode doesn't really knows that I will really load the correct nib file? Shortly, I don't understand how IBOutlets can work in this case. So, how will Xcode correclty link the loaded UIView in the setup() method with the IBOutlets ?

Nib files have a "File's Owner" type which the editor uses to list available outlets and actions. However when the nib is loaded at runtime that type is not enforced. Outlet's are connected using -setValue:forKey: under the assumption that the "Owner" passed to instantiateWithOwner is compatible with any outlet bindings defined in the nib.
One xib File with Multiple "File's Owner"s
If you have an IBOutlet, but not a property, is it retained or not?

Related

Instantiating a class and using properties makes app crash

I'm building a keyboard extension.
In my program I have a view controller(1) and a view(2) class which I use for a xib file .
1)
class KeyboardViewController: UIInputViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "view1", bundle: nil)
let objects = nib.instantiate(withOwner: self, options: nil)
view = objects[0] as? UIView
}
class View1: UIView {
#IBOutlet weak var someLabel: UILabel!
#IBOutlet weak var someButton: UIButton!
}
I wanted to instantiate the view class inside of my view controller so, following apple's documentation I did this:
class KeyboardViewController: UIInputViewController {
let v1 = View1()
let v2 = View2()
}
The problem is that whenever I try to call inside of my view did load something like:
v1.someLabel.text = "something"
and I run my app, for some reasons it doesn't work and eventually crashes.
Important things: the views are connected to two different .xib files and I'm working on a custom keyboard extension.
I'm sure I'm missing something in the instantiation but I can't find out what it is, I see other developers on git hub doing exactly the same as I do but running their apps gives no problem... So what am I missing out? If you can please send me more documentation about it as well...
Edit:
In both my view classes I'm doing the following to initialize them:
class View1: UIView {
#IBOutlet weak var someLabel: UILabel!
#IBOutlet weak var someButton: UIButton!
init(label: UILabel, button: UIButton) {
self.someLabel = label
self.someButton = button
super.init(frame: CGRect(x: 0, y: 0, width: 330, height: 200))
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
But then in my main view controller, when i call let v1 = View1(), it becomes:
let v1 = View1(coder: NSCoder)
And I'm having a hard time figuring out what to put in the parameter field
If it's crash, I think that your UITextField in your View1 is null. This field is init in your class or it refers to a UINib or UIStoryboard ?
To fix this, try init child elements in your custom UIView as well.
I found a solution by myself:
Go to your xib file, in the file's owner section set the custom class on View1; As per the view section, set the custom class to UIView;
In your class View1: UIView add a view outlet from your xib file ie:
#IBOutlet var view: UIView!
Paste this in your class View1: UIView
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
Bundle.main.loadNibNamed("view1", owner: self, options: nil)
addSubview(view)
view.frame = self.bounds
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
//Add any other setup here
}
Now create another xib file, make sure it's file's Owner is KeyboardViewController. Add an UIView to it and make sure its class is of type View1.
Now go to class KeyboardViewController: UIInputViewController and link the view of type View1 in your code. Right under it write weak var v : View1!
In viewDidLoad()you can eventually write view = View1()
You're done!

Initializing a xib on a UIViewController's subview

I'm using DZNEmptySet to load when there's nothing to display in a UITableView. It works fine for DZNEmptySet's methods like titleForEmptyDataSet, imageForEmptyDataSet, but not the one I want to use (which is customViewForEmptyDataSet).
When I try to load the xib into the scrollView.frame, Xcode's memory starts bloating in 30 megabyte increments and the app hangs. I know I'm at fault, but I don't know what I'm mucking up.
I've looked at many answers here and tutorials on other sites, but I can't find a solution that works for this circumstance (which I think is pretty simple). Any feedback on that front would be greatly appreciated.
Here's customViewForEmptyDataSet on MyViewController
// App hangs on this and memory bloats in 30 megabyte increments.
func customViewForEmptyDataSet(scrollView: UIScrollView!) -> UIView! {
return EmptySetView(frame: scrollView.frame)
}
Here's the class for my EmptySetView that I'm trying to initialize:
import UIKit
class EmptySetView: UIView {
var view: UIView!
// These are connected to a xib
#IBOutlet weak var backgroundImageView: UIImageView!
#IBOutlet weak var viewLabel: UILabel!
#IBOutlet weak var viewTextView: UITextView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(self.view)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass:self.dynamicType)
let nib = UINib(nibName: "EmptySetView", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
Update
At Matt's suggestion, I investigated recursion I was experiencing and discovered source of the problem.
You do not specify the UIView class on the View for your xib. Instead, you click the yellow cube titled File's Owner and specify the UIView there, leaving the class field empty on the View in the Document Outline panel.
After cleaning caches and rebuilding, I can get the view to load in the hierarchy with this code called on MyViewController:
func customViewForEmptyDataSet(scrollView: UIScrollView!) -> UIView! {
let emptySetView = EmptySetView(frame: scrollView.frame)
return emptySetView
}
The EmptySetView xib and MyViewController don't know about each other until the xib is loaded, so you'll need to deal with layout constraints.
My guess is that in the nib you are loading the top level view is itself an EmptySetView. This is causing a recursion. You start by instantiating the EmptySetView in code:
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
In setup(), you load the nib. But this causes an EmptySetView to be instantiated again, from the nib. This time, the other initializer is called:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
But setup() loads the nib, so we are now going around in circles, trying to nest an infinite number of nib-loaded views inside one another like matrushka dolls.

Is it possible to use IBDesignable with UICollectionViewCells and live render them in storyboard?

I know how to use #IBDesignable with custom views.
but is it possible to use IBDesignable for cells and render them in storyboard?
for example: i have a collectionViewController in storyboard, and added a uiCollectionCell and specified class as my customCellClass.
p.s: i know for using Xibs in collecionViews and tableViews we have to call method registerNib:forReuseIdentifer in code (and i am doing it). just wondered, is it possible to see it's rendered view in storyboard or not.
p.s2: i found this and it works perfectly with UIViews, but don't know how to make it work with CollectionCells and TableCells. :(
Yes. Here is what I found with Xcode 10.1 and iOS 12. Adding #IBDesignable to the custom subclass of UICollectionViewCell did work intermittently, but this works more reliably:
Add #IBDesignable to a custom subclass of UIView
Override layoutSubviews(), and define the appearance there
Optionally, if you want to define dummy data for IB only, override prepareForInterfaceBuilder()
Add that custom view to your prototype UICollectionViewCell in Interface Builder
You should see Interface Builder "build" the views and draw your change in your customer view (I find this unreliable, too. If nothing happens and Menu / Editor / Automatically Refresh Views is checked, make some other change in Interface Builder)
Example Class
#IBDesignable
class Avatar: UIView {
// Despite not being used for views designed in Interface Builder, must still be defined for custom UIView subclasses with #IBDesignable, or IB will report errors
override init(frame: CGRect) {
super.init(frame: frame)
}
// Used when a view is designed inside a view controller scene in Interface Builder and assigned to this custom UIView subclass
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.bounds.width / 2
self.backgroundColor = UIColor.gray
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.backgroundColor = UIColor.blue
}
}
Yep. Here's how I did it.
First make sure the File Owner of NIB file is set to your custom cell class. Check this
Override the prepareForInterfaceBuilder method and add the contentView from NIB file in the contentView of prototype cell. This is what it looks like.
// ArticleTableViewCell.swift
import UIKit
#IBDesignable
class ArticleTableViewCell: UITableViewCell {
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var authorImage: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var dateCreatedLabel: UILabel!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
addNIBContentView(toView: contentView)
}
private func addNIBContentView() {
let view = loadContentViewFromNib()
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.addSubview(view)
}
private func loadContentViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
// Make sure your NIB file is named the same as this class, or else
// Put the name of NIB file manually (without the file extension)
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}
Now set the custom class of the prototype cell in your table view to the one above and refresh the views.
Editor > Refresh All Views
If it still does not show up. Just clear the build folder and refresh all views again.
Product > Clean Build Folder
I made a handy extension for myself to reuse the last 2 functions in all UITableViews/UICollectionViews
// ViewExtensions.swift
import UIKit
extension UIView {
func addNIBContentView(toView contentView: UIView? = nil) {
let view = loadContentViewFromNib()
// Use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if let contentView = contentView {
contentView.addSubview(view)
} else {
addSubview(view)
}
}
private func loadContentViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
// Make sure your NIB file is named the same as it's class
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}
// ArticleTableViewCell.swift
import UIKit
#IBDesignable
class ArticleTableViewCell: UITableViewCell {
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var authorImage: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var dateCreatedLabel: UILabel!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
addNIBContentView(toView: contentView)
}
}
After lots of testing and working with the library I came up with this:
you should not add TableViewCell or CollectionViewCells inside .nib files, instead you have to add simple View. I'm not sure if it's gonna show up inside storyboard or not (haven't checked it yet) but it makes errors go away. Now you can even use autoLayout for self sizing cells.

Cannot instantiate UIView from nib. "warning: could not load any Objective-C class information"

I get "could not load any Objective-C class information. This will significantly reduce the quality of type information available." warning in the console while initializing an instance of this class:
#IBDesignable
class SystemMessage: UIView{
#IBOutlet weak var lbl_message: UILabel!
var view: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup(){
view = loadViewFromNib()
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
addSubview(view)
}
func loadViewFromNib() -> UIView{
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "SystemMessage", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
Execution stops on line let view = nib.instantiateWithOwner... with "Thread 1: EXC_BAD_ACCESS(code=2...)"
What could be the possible reason behind this?
Found the solution. It all comes to understanding of how xibs work.
What I did was that I set class for both view and File's Owner and connected all the outlets from the View rather than from the File's owner.
This seems like you are going the long way round instantiating a view. You have a view of class SystemMessage which instantiates a nib of name SystemMessage and then inserts that as a view :/
The simplest way to do this is to set the root view in your Xib to be of type SystemMessage
Then you connect your outlets to the view that you just gave the right type
This means that you can lose have your code and end up with
import UIKit
#IBDesignable
class SystemMessage: UIView {
#IBOutlet weak var lbl_message: UILabel!
static func loadViewFromNib() -> SystemMessage {
return NSBundle(forClass: self).loadNibNamed("SystemMessage", owner: nil, options: nil).first as! SystemMessage
}
}
This just gives you an easy way to instantiate your view from code with SystemMessage.loadViewFromNib(). File's Owner is probably being set incorrectly in this instance

Subviews of custom UIView are equal to nil even after view is loaded

I'm trying to render a custom view. The problem is that even after the view is loaded, its subviews are still equals to nil.. So not showing and impossible to configure. The custom view is setup through interface builder and all the outlets are linked to the properties you can see below.
Here is the custom UIView code :
import UIKit
class BadgeView : UIView
{
#IBOutlet weak var progressCircleView: CircleProgressView!
#IBOutlet weak var progressionValue: UILabel!
#IBOutlet weak var name: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Tell me if you need more informations.
How do you initialise BadgeView. If you are using storyboard or xib, the view should be initialised like this:
let nib = NSBundle.mainBundle().loadNibNamed("BadgeView", owner: self, options: nil)
let badgeView = nib[0] as! BadgeView

Resources