Reusable nib for multiple classes - ios

I have a case in which I would like to use one NIB (for UITableViewCell to be precise) for multiple classes. I have tried to define outlets as 'File owner' as then instantiate the cell, but I always receive crash, where xCode telling me that class is not key/value compliant for some outlet.
So my question is: is it possible to use one NIB, define outlets and then instantiate nib & class in a way, that Outlets are linked to the class?
The code I have tried to use:
let cell = UINib(nibName: "SampleTableViewCell", bundle: nil).instantiate(withOwner: DummyTableViewCell.self, options: nil).first as? SampleTableViewCell
The SampleTableViewCell nib has UITableViewCell defined for cell class (default) and FileOwner as NSObjet (default again). All IBOutlet are linked as FileOwner.

Related

How do you make and use an XIB file for a table view cell, rather than just using a cell "in" the table view in storyboard?

In a normal storyboard, table views come "with" embedded cells. Simply set "Dynamic Prototypes" to 1 (or more):
You then use the those cells in your view controller simply with
...cellForRowAt
let cell = tableView.dequeueReusableCell(
withIdentifier:"YourCellClassID", for: indexPath) as! YourCellClass
Where YourCellClass is the class of the table view cell,
and the string "YourCellClassID" is just
the "Identifier" set in the Attributes Inspector (nowadays the **5th button)** of the storyboard.
(Be careful to NOT use the "restoration identifier" on the 4th button, identity inspector, which sounds the same but is unrelated.)
But what if you want to use an XIB file?
Instead of using one of the prototypes "in" the table view in storyboard?
If you use an XIB file, you can use the same cell in different tables.
How to do it?
Step 1, to make a cell-style XIB file
In current (2020) Xcode there's no direct button to create a table view cell -style XIB file.
The secret is
create a 'Cocoa Touch Class', select UITableViewCell
and ALSO select 'create XIB file'
You now have a cell-style XIB file, TesteCell.xib in the example.
(Feel free to delete TesteCell.swift if you don't need it.)
Step 2, add the register#UINib code in viewDidLoad
If you now try this:
let cell = tableView.dequeueReusableCell(
withIdentifier: "TesteCellID",
for: indexPath) as! TesteCell
In fact it does not work.
In viewDidLoad, you must add
override func viewDidLoad() {
super.viewDidLoad()
// We will be using an XIB file, rather than
// just a cell "in" the table view on storyboard
tableView.register(
UINib(nibName: "filename without suffix", bundle: nil),
forCellReuseIdentifier: "identifier in attributes inspector"
)
}
In the example
override func viewDidLoad() {
super.viewDidLoad()
// We will be using an XIB file...
tableView.register(
UINib(nibName: "TesteCell", bundle: nil),
forCellReuseIdentifier: "TesteCellID")
}
And that's how you do it.
Summary
New file - cocoa touch class - subclass of table cell - do select 'also create XIB file'
In viewDidLoaed, register the XIB file using,
The file name (no suffix) and the identifier
You're done, you can now refer to the cell in the normal way in dequeueReusableCell

UIViewController xib file containing a collectionview with a custom cell

I have a xib file containing an UIViewController called FullScreenGalleryVC. It contains a UICollectionView that contains a custom UICollectionViewCell, which identifier is fullscreen_cell.
When I want to create a new FullScreenGalleryVC, I call FullscreenGalleryVC.display(from: self).
Here's the func:
class func display(from sourcevc:UIViewController){
let vc=UINib(nibName: "FullscreenGalleryVC", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! FullscreenGalleryVC
sourcevc.present(vc, animated: true, completion: nil)
}
I get this error:
could not dequeue a view of kind: UICollectionElementKindCell with
identifier fullscreen_cell - must register a nib or a class for the
identifier or connect a prototype cell in a storyboard
It works fine if I put the custom cell in another xib file and call register(nibClass, forCellWithReuseIdentifier: cellId).
It works fine if I don't put this FullScreenGalleryVC class in a separate xib file (and keep it in my main storyboard).
But I use this class from both the app and an action extension so that's why I'd like to use a common file instead of duplicating everything. Is there a way to do that or do I imperatively have to put the custom cell in a different xib file to make it work?
If you a xib file for FullScreenGalleryVC controller then you should use register(nibClass, forCellWithReuseIdentifier: cellId) to tell the collection view how to create a new cell of the given type.
let cellNib = UINib(nibName: "FullScreenGalleryCell", bundle: .main)
collectionView.register(cellNib, forCellWithReuseIdentifier: "fullscreen_cell")
If you use a storyboard, then it's already known to the collection view with the specified cellId.
In my opinion, you can use a storyboard for FullScreenGalleryVC and reuse the same for other classes.
Present a viewController from a specific storyboard like,
let storyboard = UIStoryboard(name: "YourStoryboardName", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "storyboardId")
self.present(controller, animated: true, completion: nil)

load xib view in storyboard. why do we have to do self.addSubview(nibView) ?

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view) // My question
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: self.className, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
i want customXibView initial with nib .look like self = loadViewFromNib(), not by self.addSubview(nibView) .
I found that many of the answers are all done self.addSubview(nibView). In fact, look our view hierarchy,we will found a extra view.
This is my view hierarchy
Is there any way to instance xib to storyboard, not by self.addSubview(nibView).
if you don't understand my question,look this Custom UIView from Xib - requirements, good practices. I hope some one help me.
There are three scenarios:
You have CustomView class with all of its UI defined
programmatically in its drawRect method.
You have CustomView class with its UI defined in a subview of storyboard scene with its class set to 'CustomView'.
You have CustomView class with its UI defined in a separate xib.
Now if you want to include your CustomView in as a subview in your viewcontroller, the method differs depending how your class is defined in the first instance. Secondly how do you want to include it in you viewcontroller.
Adding CustomView with scenario 1:
Initialise the CustomView instance in your viewDidLoad method and add it to your viewcontroller's view as a subview.
Adding CustomView with condition 2:
You don't have to do anything in this scenario. Storyboards will initizlize your class for you. You can do additional setup in init(coder) method that will be called when you view is initialized.
Adding CustomView with condition 3:
The proper way to initialise a view with an xib is to initialise it programmatically via UINib's inistantiateWithOwner method. Then add the view as a subview in the viewDidLoad method of your viewcontroller.
What you are doing (or what I can guess seeing your image hierarchy) is that you have changed the class of your UIView to your custom class. Now since you have a separate xib, you are initialising it in init(coder) method and adding it as a subview to the view that was defined in and initialised by the storyboard. Hence you end up with an extra container view.
If you don't want the extra view, add your custom view programmatically.
OR
(If possible) copy the view defined in xib and place it in the viewcontroller dock and then link the view with an outlet and add it in viewDidLoad method.
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 (i.e. no need for self.addSubview(view), which is your question).
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 👣
Its not necessary that you have to add the xib view as subview. Think about the usage, as per my understanding there are tow scenarios
Suppose you have a view called customXibView class and its xib, now in the xib select the view and and from the right side panel select [identity inspector] option and check, under [custom class] by default its class is UIView. So you can keep it as UIView and set the file owner as customXibView and load it from xib as UIView instance as you are doing now. So, now the question is if you allocate customXibView object manually or in some other xib how it will show your custom components which are in your view xib. So in that case in your customXibView's init method you have to load the UIView xib and add it to customXibViewobject's view.
Another case where you can set the xib's view [custom class] as your class customXibView in this case when you call the below method
func loadViewFromNib() -> customXibView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: self.className, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? customXibView
}
You directly get an object of customXibView with UI loaded no need to add it to any other view. But in this case if you allocate the customXibView class object your UI will not be loaded if you don't do what what you are doing now. If any doubt plz comment.

xib with UICollectionView - not key value coding compliant

I have a Custom UIView with an XIB. This custom UIView has a UICollectionView which is connected to an IBOutlet. In the view setup, the UICollectionView is initialised properly and is not nil.
However in the cellForItemAtIndexPath method, I get this error:-
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key selectorCollectionView.'
If I remove the datasource and delegate, I do not get any error. If I add them Iget an error at this line:-
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "selectorCell", for: indexPath) as! SelectorCell
Please help!
Edit: I have attached screenshots of my setup
I have uploaded the project here too http://www.fast-files.com/getfile.aspx?file=148623
Maybe a sync issue. Happens sometimes:
Try cut outlets loose and reconnect them.
Make sure Collection Reusable View identifier is defined in xib file:
Make sure collection-view cell's custom class is define in xib file:
EDIT:
I dug into your project and here are my findings
UICollectionView should be init' in awakeFromNib method (override it):
override func awakeFromNib() {
super.awakeFromNib()
let cellNib = UINib(nibName: String(describing: "SelectorCell"), bundle: nil)
selectorCollectionView.register(cellNib, forCellWithReuseIdentifier: "selectorCell")
selectorCollectionView.dataSource = self
selectorCollectionView.delegate = self
}
loadViewFromNib should be looking like this (Remove collection view init'):
func loadViewFromNib() -> UIView {
let nib = UINib(nibName: "SelectorView", bundle: nil)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
Subclass SelectorTableViewCell likewise.
class SelectorTableViewCell: UITableViewCell {
#IBOutlet weak var selectorView: SelectorView!
}
In Main.storyboard - custom class UITableViewCell to SelectorTableViewCell and connect SelectorView inside contentView to 'SelectorTableViewCell''s outlet.
That's it i think. Here is the project:
You can try the following:
Click on File Owner at the left panel of YourCell.xib. Then go to Attributes inspector and check whether there is any class specified in Class field. If not - no help from me here. If yes - remove it and tap Enter.
In YourCell.xib sequentially click on all subviews and delete all the outlets in Connections Inspector. No need to delete them in YourCell.swift.
Once your cell's File Owner and outlets are removed, we're ready to reconnect the outlets. Just drag every one as usually to an existing corresponding IBOutlet in YourCell.swift.
As a result, when you click on YourCell at the left panel of YourCell.xib and go to Connections Inspector, you'll see there are all the outlets connected. Like so:
And if you check connections for a particular view in YourCell, you should see that it's outlet is connected to YourCell (StaticSlot in my case).
This helped me and hope you'll get the same.
In our case, the problem was making the wrong connection in Storyboard. Click on the custom class in Storyboard -- not the File Owner -- then connect the IB outlet to the variable in the custom class.
if try to register you custom class and cell ID
self.collectionView.register(nib, forCellWithReuseIdentifier: "Cell")
self.collectionView.register(CustomClassCell.self, forCellWithReuseIdentifier: "Cell")

Register a cell in programmatically instanciated Table View Controller

I am getting this error:
'NSInternalInconsistencyException', reason: 'unable to dequeue a cell with identifier NavigationNodeCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
I am using two cells with the same class "NavigationItemCell" and two identifiers "NavigationNodeCell" and "NavigationLinkCell".
And I create a cell with e.g.:
let cell: NavigationItemCell = self.tableView.dequeueReusableCellWithIdentifier("NavigationNodeCell", forIndexPath: indexPath) as! NavigationItemCell
This problem has been there before me, e.g. Assertion failure in dequeueReusableCellWithIdentifier:forIndexPath:
As I understand (e.g. the anser of Sebastian Borggrewe) it should be enough to register a UITableViewCell in the story board with its custom class and an identifier.
That's exactly what I did and I am still getting this error.
I tried to use two different classes but it did not solve the error.
I also tried to register my cells with nib but there I am running into other issues (label is nil).
I've found that this problem probably occures because I instanciate a new table view controller programmatically.
var newController = MyTableViewController()
Does it mean I will have to register nibs anyway? Is it a problem that I will have to register the same class twice? And how do I avoid the problem of the nil label?
You should create an instance of your view controller with storyboard ID.
Replace the line var newController = MyTableViewController() with the following code
let storyboard = UIStoryboard(name: "YOUR_STORYBOARD_NAME", bundle: nil)
let newController = storyboard.instantiateViewController(withIdentifier: "VIEWCONTROLLER_ID_FROM_STORYBOARD")
Edit:
It's also possible to use:
self.storyboard!.instantiateViewControllerWithIdentifier("VIEWCONTROLLER_ID_FROM_STORYBOARD")
if the view controller is instantiated from the storyboard, and second view controller is in the same storyboard
If you create your table view controller with an initializer (instead of storybaord.instantiateViewController(withIdentifier:)), then your table view controller will not know about the cells you put in the storyboard. If you want to use an initializer, then you will need to manually register the custom cell class in your controller's viewDidLoad.
class MyTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad
tableView.register(NavigationNodeCell.self, forCellReuseIdentifier: "NavigationNodeCell")
}
}

Resources