The problem: Created a view controller in a xib with a table view. But, the table view acts very weirdly and doesn't look like the table view we use in the storyboard.
My plan was to populate this table view with a custom table view cell which I have created in another xib file. But, sadly, it doesn't work as I have expected because everything was off and those cells are instantiated, I know my custom cells work because it has worked in my other view controller that was created in the storyboard:
I wanted a way to design my view controllers so I can just instantiate them when I need, my reasoning behind this is I don't want to have a very populated storyboard. Now I know that I can't use a table view in a xib file like how we use it in a storyboard. Is there a work around to this? do I need another storyboard to achieve this?
Task 1. Load UIViewcontroller form Xib
We can load a UIViewcontroller form Xib instead of Storyboard. We can use following procedure:
1. Create a UIViewcontroller.
XCode File -> New -> File -> Cocoa Touch Class -> Fill Class with your class name , subclass of with UIViewController , check Also create Xib file, language Swift -> Next - Create.
Example: ViewControllerFromXib
2. Override init().
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
{
super.init(nibName: "ViewControllerFromXib", bundle: Bundle.main)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
}
3. Open newly created Controller
let controller = ViewControllerFromXib.init()
self.present(controller, animated: true, completion: nil)
In this above way, We can load a UIViewcontroller from XIB.
Task 2. Create a tableview & populate it's cell using custom xib
1. Create a Custom UItableViewCell
XCode File -> New -> File -> Cocoa Touch Class -> Fill Class with your cell name , subclass of with TableViewCell , check Also create Xib file, language Swift -> Next - Create.
Example: CustomTableViewCell
1.Register UItableViewCell for Your TableView.
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Item"
self.tableView.register(UINib(nibName: "CustomTableViewCell", bundle:Bundle.main), forCellReuseIdentifier: "CustomTableViewCell");
}
2. Implement UITableViewDataSource into Your Viewcontroller
extension ViewControllerFromXib:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath) as! CustomTableViewCell
return cell
}
}
2. Implement UITableViewDelegate into Your Viewcontroller.
extension ViewControllerFromXib:UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat{
return UITableViewAutomaticDimension;
}
}
Yes, you can use as many storyboards as you would like in an application.
I try to setup 1 storyboard per workflow. However some less storyboard enthusiastic developers use 1 storyboard per view controller.
To get the initial view controller from a storyboard named "MyStoryboard":
let storyboard = UIStoryboard(name: "MyStoryboard", bundle: nil)
let viewController = storyboard.instantiateInitialViewController()!
or to get a view controller with the identifier "MyViewController".
let storyboard = UIStoryboard(name: "MyStoryboard", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "MyViewController")!
There are two basic ways for a view controller to access a second view controller in another storyboard.
In the storyboard, use a Storyboard Reference to the second view controller, then use a show segue to push the second view controller.
In the view controller's source, create the second view controller using instantiateInitialViewController() or instantiateViewController(withIdentifier:) and push it onto the navigation controller.
You can have many storyboards in the same project.
I usually like to have 1 storyboard per "main" screen, which is easy to instantiate programmatically, and can be linked in the IB as well.
As for your problem, i'd suggest your custom uitableviewcell be created in a .xib file. As an independent view. This way on your code you can just register it as the "reusable cell" for any view controller you want regardless of the storyboard that contains it.
Related
I have a UITableViewController where I want to add a UIButton in the section header.
I saw in calendar app this button, and I wanted to know if this is part of a class I can reuse, or if I need to write my own class to add it.
You need to use your own custom class for this case.
Because UITableViewHeaderFooterView does not containing any kind of buttons itself.
So, better go with your customClass.
You have to create a xib file and create it class file.
Add your button and create its outlet.
Register your xib same in you viewDidLoad()
For example:
let headerNib = UINib(nibName: "yourNIBname", bundle: nil)
tableView.register(headerNib, forHeaderFooterViewReuseIdentifier: "reuseIdentifer")
Then add the following tableview delegate
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "reuseIdentifer") as! youNIBClassName
headerView.yourButton.addTarget(self, action: #selector(hideAllTapped(withSender:)), for: .touchUpInside)
return headerView
}
You will get the action event in your hideAllTapped() function
#objc func hideAllTapped(withSender sender: UIButton) {
// do your action
}
I have a UITableView in a HomeViewController. This UITableView has one customized UITableViewCell in it.
I want to use the same UITableViewCell in ListViewController by dequeueing it. Below code does not work.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let homeVC: HomeViewController = storyboard.instantiateViewController(withIdentifier: "HomeViewController") as! HomeViewController
if let homeTableView = homeVC.tableView {
// Above homeVC.tableView is becoming nil.
// Would like to dequeue here.
}
Since I cannot see your HomeViewController code, I am assuming the tableView property is linked from the Interface Builder as #IBOutlet weak var tableView: UITableView!. This means that that tableView is not instantiated until the view actually appears on the screen. To prove this, try placing a breakpoint inside your HomeViewConntrollers viewDidLoad() method, and notice that it is never reached.
To solve your problem, I would add a table view into your ListViewController, and copy the custom table cell from HomeViewController into your table view. Then create a custom UITableViewCell, and in IB assign both cells in HomeViewController and ListViewController as your custom table view cell.
You can create a xib file and a custom UITableViewCell class for the cell you want to reuse in multiple view controllers. Then, in the xib's identity inspector, define the custom type of the cell as your custom class. Then, register the cell class in each view controller by calling register function on the tableView instance in viewDidLoad. Then dequeue your cell by implementing UITableViewDataSource's tableView(_:cellForRowAt:).
I have problems with my custom cell file in tableview. I managed to get it done using the out comment line shown below, but the performance was really bad when it had 10+ cells.
UsingdequeueReusableCell leads to this error:
'NSInternalInconsistencyException', reason: 'unable to dequeue a cell with identifier DiveNewsShort - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
which is strange, because I do register the nib in viewDidLoad(). I hope you can help me, I am getting frustrated by this.
class ProfilTableView: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "DiveNewsShort", bundle: nil), forCellReuseIdentifier: "DiveNewsShort")
tableView.register(DiveNewsShort.self, forCellReuseIdentifier: "DiveNewsShort")
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let cell = Bundle.main.loadNibNamed("DiveNewsShort", owner: self, options: nil)?.first as! DiveNewsShort
// This one works as expected
let cell = tableView.dequeueReusableCell(withIdentifier: "DiveNewsShort", for: indexPath) as! DiveNewsShort
// This one does not
return cell }
Update:
I managed to get rid of the error by adding the register function in the cellForRowAt function, but I don't think that this is a efficient way actually. It should work within the vieDidLoad shouldn't it?
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
tableView.register(UINib(nibName: "DiveNewsShort", bundle: nil), forCellReuseIdentifier: "DiveNewsShort")
let cell = tableView.dequeueReusableCell(withIdentifier: "DiveNewsShort", for: indexPath) as! DiveNewsShort
return cell }
You don't need this line:
tableView.register(DiveNewsShort.self, forCellReuseIdentifier: "DiveNewsShort")
You already have registered the nib file one line before.
There are three ways to register cells for reuse/dequeuing:
You are programmatically creating the cells, in which case you register the class in viewDidLoad.
You are using a NIB, in which case you register the NIB in viewDidLoad.
You are using storyboard cell prototypes, in which case you don't have to register anything. The storyboard does all of this for you.
Since you are using NIBs, you should remove the registering of the class and only register the NIB. And you should do this in viewDidLoad. This process is outlined in https://stackoverflow.com/a/28490468/1271826 as well as in Reinier's answer.
Looking at your MCVE, your problem was a result of a more fundamental mistake, where you had a UIViewController trying to use another view controller, which was a UITableViewController, to manage the table. But UITableViewController has its own UITableView and won't use the one that you have an #IBOutlet for, so you were registering the NIB for a table view you weren't seeing. There were a ton of other issues here (e.g. if you really want a view controller within a view controller, you have to do view controller containment calls, etc.), but the simplest solution was to excise this separate UITableViewController from the project and when this was fixed, it works precisely as we described. See https://github.com/robertmryan/Divers for a working version of your MCVE.
You also didn't hook up the outlets for the other two controls in your cell (the switch and slider). Thus, if you changed either of those two controls and then scrolled, the cells are reused and you see the changed UIKit control that was done for some other cell, but was subsequently reused. To fix that, your custom UITableViewCell subclass should have outlets for all controls, and cellForRowAt must set values for all of these outlets. You also need some mechanism for the cell to inform the view controller when the switch and slider have changed and update the model accordingly, so when cellForRowAt was later called for that row, it would know the state of that CellData to set the control appropriately. A common solution for this is to use the protocol-delegate pattern. See the above GitHub repo, which illustrates this pattern, too.
I have build this protocol for help me in this process
protocol CBNibInstanceableCellProtocol {
static func getCellXib() -> UINib?
static func getReuseIdentifier() ->String
}
and in your class you have to implement those methods like here
//example implementation
extension CBUsersAttendanceEmptyCell : CBNibInstanceableCellProtocol
{
static func getCellXib() -> UINib?
{
if Bundle.main.path(forResource: "CBUsersAttendanceEmptyCell", ofType: "nib") != nil
{
return UINib(nibName: "CBUsersAttendanceEmptyCell", bundle: nil)
}
return nil
}
static func getReuseIdentifier() ->String
{
return "CBUsersAttendanceEmptyCell"
}
}
then in your viewDidLoad you must do something like this
//example code
self.collectionView.register(CBUsersAttendanceAvatarCell.getCellXib(), forCellWithReuseIdentifier: CBUsersAttendanceAvatarCell.getReuseIdentifier())
self.collectionView.register(CBUsersAttendanceCountCell.getCellXib(), forCellWithReuseIdentifier: CBUsersAttendanceCountCell.getReuseIdentifier())
self.collectionView.register(CBUsersAttendanceEmptyCell.getCellXib(), forCellWithReuseIdentifier: CBUsersAttendanceEmptyCell.getReuseIdentifier())
in your cellForRow
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CBUsersAttendanceCountCell.getReuseIdentifier(), for: indexPath) as? CBUsersAttendanceCountCell
{
return cell
}
You must have defined the class for you view in your xib, this is very important check this pictures
Hope this helps
First sorry for my english.
I have a UISplitViewController and i replaced the template master UITableViewController with an UIViewController put a UITableView and UIContainerView what contains a second static UITAbleViewController for settings one of this settings cell has another detail UITableViewController to change its value. on iPhone works well but on iPad with that splitviewcontroller this settings value table shows over on the splitviewcontroller detail view. its good too but i can't hide anymore. (On iPhone has a back button and just works well)
I couldn't find the solution.
My question is how can i hide that detail view to show the original detail view of the split view?
Here are my 3 pics from storyboard, the initial view and when the settings values are opens(that can't be hide n iPad)
Any advice would be nice. Thanks
Storyboard
Ipad screenshots
Finally i made an easy solution. Delete show detail storyboard segue on settings tableviewcontroller and define didSelectRowAtIndexPath for custom push
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//i want only the third row to perform an action
if indexPath.row == 2 {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let groupListViewController = mainStoryboard.instantiateViewControllerWithIdentifier("GroupListViewController") as! GroupListViewController
self.navigationController!.pushViewController(groupListViewController, animated: true)
}
}
This way i can push any view to the split view left or right side cos both have navigation controller. After this i defined the same function on the pushed tableviewcontroller fire some action and go back. It works like a charm on iPad and iPhone too.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.appDelegate.client.selectedGroupIndex = indexPath.row
//The main master viewcontroller of split view
let vehicleListViewController = self.navigationController?.viewControllers.first as! VehicleListViewController
//settingsviewcontroller that embedded in master viewcontroller
let vehicleListSettingsViewController = vehicleListViewController.childViewControllers.first as! VehicleListSettingsViewController
//call a custom function on embedded settings viewcontroller to change selected value
vehicleListSettingsViewController.changeGroup(indexPath.row)
//refresh splitviewcontroller's master tableview
vehicleListViewController.tableView.reloadData()
//Easy go back to masterview
self.navigationController?.popViewControllerAnimated(true)
}
How can I add a view of a child view controller to a custom UITableViewCell? I can add the view like this inside cellForRowAtIndexPath:
self.addChildViewController(controlsViewController)
cell!.cellView.addSubview(controlsViewController.view)
controlsViewController.didMoveToParentViewController(self)
But when the cell disappears, I need to remove this child view controller. I'm not really sure how to do that. Is there a better way to go about this?
Do it via delegation. I have done on collection view ,you can do it on tableview too. follow the below steps
1 .In your custom cell class create a delegateHandler and override your awakeFromNib() method. eg
protocol BibleReadingSliderProtocol: class {
func addThisViewControllerAsChild(audioViewController :AudioViewController)
}
class BibleReadingSliderCollectionCell: UICollectionViewCell {
#IBOutlet weak var containerView: UIView!
var audioVC = AudioViewController()
weak var bibleReadingSliderDelegate:BibleReadingSliderProtocol?
override func awakeFromNib() {
super.awakeFromNib()
print("Awake call from cell")
// Initialization code
let storyboard = UIStoryboard(name: "Main", bundle: nil)
audioVC = storyboard.instantiateViewController(withIdentifier: "AudioViewController") as! AudioViewController
audioVC.view.frame = self.containerView.bounds
self.containerView.addSubview(audioVC.view)
if self.bibleReadingSliderDelegate != nil {
self.bibleReadingSliderDelegate?.addThisViewControllerAsChild(audioViewController: audioVC)
}
}
}
In your ViewController where you are using this custome cell (either tableview or collection view) define the delegate hander
func addThisViewControllerAsChild(audioViewController: AudioViewController) {
self.addChildViewController(audioViewController);
}
And Dont forget to set your delegate to this viewcontroller in cellForItemAt/cellForRowAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let imageSliderCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! BibleReadingSliderCollectionCell
imageSliderCollectionViewCell.bibleReadingSliderDelegate = self
return imageSliderCollectionViewCell
}
Don't misunderstand MVC. Not every view in the world needs to have its own personal view controller! A main view has a view controller, but a button in that main view does not have its own personal view controller; it simply talks to the main view's view controller.
The same is true of this view. Views can come and go very easily; do not add the heavyweight burden of an additional view controller when you don't need to! Just grab the view (somehow) and stick it into the cell's contentView or remove it from the cell's contentView in cellForRowAtIndexPath:, just like any other view - but manage it using your table view controller or table view data source / delegate or whatever is in charge here. Don't add an extra view controller to the story just for the sake of this one little view. That's likely to be a bad use of view controllers.