So I'm a newbie and trying some reusability. I've a class called SingleButtonFooterView which subclasses UIView and UI is done in an .xib.
Now I want to use this in a UITableViewCell. I've tried almost all possible solutions nothing is working for me.
Code for the class:
class SingleButtonFooterView: UIView {
#IBOutlet weak var button: Button50!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
let view = UINib(nibName: "SingleButtonFooterView", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView
view.frame = self.bounds
self.addSubview(view)
}
override func awakeFromNib() {
super.awakeFromNib()
button.backgroundColor = .clear
button.layer.cornerRadius = 5
button.layer.masksToBounds = true
button.titleLabel?.font = UIFont.futura(with: .medium, size: 16)
button.setTitleColor(.white, for: .normal)
self.contentMode = .scaleAspectFit
self.layer.masksToBounds = true
}
}
Now for cellForRowAt:
let cellId = "SingleButtonFooterView"
var cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
if cell == nil {
cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: cellId)
let subView = SingleButtonFooterView(frame: cell.frame)
cell.contentView.attachViewWithConstraints(subView)
let _ = subView.viewLoadedFromNibAttached(name: cellId)
}
return cell
and in viewDidLoad() of my VC class.
tableView.register(UINib(nibName: "SingleButtonFooterView", bundle: nil), forCellReuseIdentifier: "SingleButtonFooterView")
In short
-> I want to use a UIView class (UI done using interface builder -> .xib) in a UITableViewCell
If I can suggest you something just do it "by the book".
Create a custom cell and .xib. Then you can do whatever you want with the UIViews (remember that you can create your own class and put it into xib by changing the class here:
Having awakeFromNib and Init in one class it's somehow a code smell because either you use .xib or code to create a view.
Remember that adding subviews from the VC it's always risky because you need to take care of recycle, what means that this subview may stay and be not wanted unless you handle this situation.
Remember about prepareForReuse() to handle cells' recycle.
You have two issues here:
1)
Bundle.main.loadNibNamed(name, owner: self, options: nil) in viewLoadedFromNibAttached (if you use the exact same code from the other question)
and you have the same nib load in commonInit.
You have to decide where to put it, IMO you can get rid of
let subView = SingleButtonFooterView(frame: cell.frame)
cell.contentView.attachViewWithConstraints(subView)
let _ = subView.viewLoadedFromNibAttached(name: cellId)
and put that part in the cell (this way you can easily maintain the livecycle of the SingleButtonFooterView
2) Guess: the File owner of the xib is empty or using wrong class, that's why commonInit and required init?(coder aDecoder: NSCoder) are causing infinite loop
EDIT:
Step 1: subclass UITableViewCell, lets call it SingleButtonCell
Step 2: custom view (SingleButtonFooterView in your case). NOTE! File's owner should be the class itself -> SingleButtonFooterView
#IBOutlet var contentView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
loadXib()
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadXib()
}
private func loadXib() {
Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)
addSubview(contentView)
contentView.frame = bounds
}
This is what I use to load views from .xib. contentView is the outlet of the main view in the .xib file
Step 3: Two options here:
Option 3.1: (easier to maintain IMO)
With the SingleButtonCell you create it's own .xib file
3.1.1: Add view in the cell's xib (SingleButtonCell.xib) and change it's class to SingleButtonFooterView
Option 3.2: Do not create cell xib. Instead instantiate the view (SingleButtonFooterView) inside the cell and add it as subview (add constraints if you want). Here you have to be careful where to instantiate the view, because there is a chance to add it multiple times
What I can see is that dequeueReusableCell(withIdentifier:for:) never returns nil. So the code in your nil-check will never be called. You may be thinking of the "old" dequeueReusableCell(withIdentifier:) which can return nil and can be used similar to how you do.
Since it's never nil, you need an additional parameter, say "hasBeenInitialized" to keep track of if you have added your custom UIView yet or not.
// In loading viewDidLoad, register cellId for type for tableView
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
if !hasBeenInitialized {
// Load view from xib
// Add view as subview to cell contentview
// Add surrounding constraints
hasBeenInitialized = true
}
return cell
Related
I request apologies in advance in the case this question is very elemental or the answer is obvious.
I have a custom xib, that works very well when used with the storyboard interface builder. The custom xib is implemented like the classical samples you can find across the internet:
I have a CustomView.swift class
a CustomView.xib file.
The FileOwner of the CustomView.xib file is set to the CustomView.class. Then this xib file has a couple of outlets for the views used in the xib. Something like the following:
#IBDesignable
class CustomView: UIView {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
required init?(coder aDecoder: NSCoder) {
//When loaded from storyboard.
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
//When loaded from code
super.init(frame: frame)
commonInit()
}
func commonInit() {
if let view = Bundle.main.loadNibNamed(self.nibName, owner: self, options: nil)?.first as? UIView {
view.frame = self.bounds
self.addSubview(view)
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
}
func renderViews()
//A lot of stuff is done here.
}
}
As said, this works very well when using the Storyboard designer to insert the custom xib in the layout of an UIController. But I need to use the same xib on another place, and I need to instantiate and insert it programmatically in a container view multiple times.
I tried different approaches to instantiate the xib and add it as a subview of another view, for example:
class AnotherView: UIView
(...)
func instantiateXib(){
let view = CustomView()
self.addSubView(view)
}
}
or
class AnotherView: UIView
(...)
func instantiateXib(){
let nib = UINib(nibName: "CustomView", bundle: nil).instantiate(withOwner: nil, options: nil)
let view = nib.first as! CustomView
self.addSubView(view)
}
}
or many other ways that I found across the internet, but all of them end with an infinite loop, because the method init?(coder aDecoder: NSCoder) of the xib, calls the commonInit method that instantiates again an instance of the xib, and so on.
I suspect the solution is obvious, but I'm struggling to find it.
May you point me in the right direction? Thank you in advance!
Overview
I created a custom view class and an associated nib. When I try to load it in another view controller, it loads it but the constraints are removed.
What I found
Before marking this as a duplicate... I read plenty of questions on StackOverflow of people having the same issues but the ones in swift were either unanswered either non-working. I tried everything.
What I'm doing
I have a view controller (which happens to be a keyboardviewcntroller but on a regular one I still have the problem) with an associated nib and a custom uiView with its associated nib.
KeyboardViewController:
I associated with my Keyboard view controller a zip file. His file owner is KeyboardViewController.
In my KeyboardViewController I have the basic code you get by creating a keyboard target and then I added the custom view outlet before the viewDidLoad
#IBOutlet weak var vieww: Custom!
and inside of my viewDidLoad I loaded my nib
let nib = UINib(nibName: "nibbb", bundle: nil)
let objects = nib.instantiate(withOwner: self, options: nil)
view = objects[0] as? UIView
Custom view
My custom view has an associated nib too. Its file owner is Custom (my new class). In the nib I put a button and a label as shown in the picture below and I added some constraints.
In the Custom class, I put the following code which includes outlets and the init of the class.
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
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("Custom", owner: self, options: nil)
addSubview(label)
label.frame = self.bounds
label.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(button)
button.frame = self.bounds
button.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
}
This is the output i get:
and of course, it's not what I wanted.
Question
How do I load a custom UIView nib from another view controller without having my constraints being messed up?
The reason your constraints are not working as expected is because in the commonInit function sets both the label and button to be the same size as the view is.
You should declare a UIView property and then replace the content of commonInit function:
var contentView: UIView!
private func commonInit() {
if let nib = Bundle.main.loadNibNamed("Custom", owner: self, options:nil)?.first as? UIView {
contentView = nib
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.frame = bounds
addSubview(contentView)
}
}
In my project I am trying to create a custom UIView from a XIB file. I followed few tutorials and arrived at below code to load
import UIKit
class StorePricing: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
private func setupView() {
let view = self.loadViewFromXib()
view.frame = self.bounds
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.addSubview(view)
}
private func loadViewFromXib() -> 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
}
}
When I add this custom view in another view my app crashed and I noticed the init call is called in a indefinite loop. The call hierarchy is as follows in my custom view
Call to init coder
Call to setupView()
Call to loadViewFromXib()
nib.instantiate calls init coder and the loop becomes indefinite
Any suggestions on how to solve this issue?
if your xib file contains an instance of your view, then then it's loaded, it will call init(coder:), which will then load the xib file again, and the cycle will restart. I would either remove instances of your view from the xib file, or don't call setupView() from within init(coder:)
Your setupView() (executed in init(coder:)) loads the nib again which fires init with coder again causing infinite recursion.
Do not instantiate the nib inside of the init(coder:). If you want to configure the view after loading it do it for example in awakeFromNib method.
You are probably getting the infinite recursion if, in your .xib file, for your new view (StorePricing) your Custom Class class type is set to your new custom class name (StorePricing). It should be set to UIView. To understand what' going on, when nib.instantiate(...) is reading the .xib and comes across the Custom Class name for your view, it calls required init?(coder aDecoder: NSCoder) to create it, and then, around it goes.
Set the File’s Owner Custom Class to your custom class. This will allow you to establish Referencing Outlets from the .xib file into your code file.
Set the File’s Owner Class to your custom class.
This is because every time you when you call setupView() you add sub view. so it happens again every time. for that what i had done you can see below. Hope will help you.
class TableBackGroundView: UIView {
// Here is my common view handled
var view: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
func xibSetup() {
let nib = UINib(nibName: "TableBackGroundView", bundle: nil)
view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(view)
}
}
Ive been searching for a while for a simple example on how to reuse views from xib files in my storyboard but all i find was outdated or dosen't solve my problem
the situation is that I has simple:
I have a viewController in my storyboard
I dragged to it two view from the library
I had created a myCustomView.xib and myCustomView.swift file (they are empty now)
I have I button on viewController (so the tree (two view and one button) are setting together on the viewController in the storyboard)
the question is: I want one view to be loaded dynamically on app launch and the other one to be loaded on button click
an other question: how can I connect myCustomView to that viewController
thank you
I've implemented an extension for UIView:
extension UIView {
static func createInstance<T: UIView>(ofType type: T.Type) -> T {
let className = NSStringFromClass(type).components(separatedBy: ".").last
return Bundle.main.loadNibNamed(className!, owner: self, options: nil)![0] as! T
}
}
In this way, wherever you can load your custom view in this way:
func override viewDidLoad() {
super.viewDidLoad()
let customView = UIView.createInstance(ofType: CustomView.self)
self.view.addSubview(customView)
}
Add bellow code in your custom view class
class MyCustomView: UIView {
#IBOutlet var contentView: UIView! // take view outlet
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!)
layoutIfNeeded()
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
print("layoutIfNeeded")
}
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)[0] as! UIView
return view
}
}
Add this class as superclass view in storyboard.
Question: Why is UILabel nil here (code attached) in init after creating UIView programmatically from XIB? (refer to code below) That is the line label1.text = "TBC - It was updated" throws an error
Background: I want to programmatically create multiple custom views, multiple GCDateView's in this case. I want to use a XIB file to layout the custom view with an associated class to finalise customisations programmatically too, hence here I have a GCDateView.swift and a GCDateView.xib file.
Aside: As a 2nd aside question I note the view I create within the GCDateView from the xib file can't be directly allocated to be the main view (e.g. at the end of init I can't say self = gcDateViewView). Perhaps I need a separate question for this.
From within parent controller/view:
let dv = GCDateView()
GCDateView:
import UIKit
class GCDateView : UIView {
#IBOutlet weak var label1: UILabel!
func commonInit() {
// Programmtically use XIB file
if self.subviews.count == 0 {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "GCDateView", bundle: bundle)
let gcDateViewView : UIView = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
gcDateViewView.frame = self.bounds
gcDateViewView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(gcDateViewView)
}
**label1.text = "TBC - It was updated" // ** ERROR: label1 was Nil ****
}
override init(frame: CGRect) {
NSLog("GCDateView: Init frame")
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
NSLog("GCDateView: Init decoder")
super.init(coder: aDecoder)
commonInit()
}
}
IBOutlet instances are not initialized in init?(coder aDecoder: NSCoder).
This process is done in separate step.
There is method awakeFromNib where it's guaranteed that all IBOutlet instances are initialized:
override func awakeFromNib() {
super.awakeFromNib()
label1.text = "TBC - It was updated" // Won't crash.
}
To solve your second problem (i.e. avoid adding another instance of self type as self subview) I recommend to create class method that will create new instance of GCDateViewby loading it from xib.
Here is updated code:
import UIKit
class GCDateView : UIView {
#IBOutlet weak var label1: UILabel!
class func loadFromXIB() -> GCDateView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "GCDateView", bundle: bundle)
let gcDateView = nib.instantiate(withOwner: self, options: nil)[0] as! GCDateView
gcDateView.frame = self.bounds
gcDateView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
gcDateView.label1.text = "TBC - It was updated"
return gcDateView
}
}
Usage:
let dateView = GCDateView.loadFromXIB()