Question
I'm creating a re-usable custom UITableViewCell and then subclassing it for re-use. How do I set these various subclasses to link to the same xib file?
Background
Let's call my custom UITableViewCell PickerTableViewCell. This cell includes a UIPickerView, as well as all the implementations as to how the picker view looks and behaves. When I want to use this cell, the only thing I need to give it is the data for the picker view. So I subclass PickerTableViewCell, then simply create the data source I need and assign it to the picker view. So far this has all worked well.
Here are the relevant parts of PickerTableViewCell:
class PickerTableViewCell: UITableViewCell {
var picker: UIPickerView! = UIPickerView()
var pickerDataSource: PickerViewDataSource!
override func awakeFromNib() {
super.awakeFromNib()
self.picker = UIPickerView(frame: CGRect(x: 0, y: 40, width: 0, height: 0))
self.picker.delegate = self
self.assignPickerDataSource()
}
// Must be overriden by child classes
func assignPickerDataSource() {
fatalError("Must Override")
}
Here is an example of a subclass:
class LocationPickerTableViewCell: PickerTableViewCell {
override func assignPickerDataSource() {
self.pickerDataSource = LocationPickerDataSource()
self.picker.dataSource = self.pickerDataSource
}
}
Problem
Since I am using these cells all over the place, with different data sources, I created a xib file which defines how the cell looks called PickerTableViewCell.xib, and assign it to the class PickerTableViewCell. In the view controllers I want to use it for, I register the cell with the table view inside viewDidLoad(). Then, inside func tableView(_:, cellForRowAt) I dequeue the subclass I want like this:
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! LocationPickerTableViewCell
return cell
This is where the problem happens. The cell that is created is a PickerTableViewCell, not its subclass LocationPickerTableViewCell. This runs into the fatal error I placed in the parent class which is overriden by the child class.
The only way I have found to solve this is to create a separate xib file for each subclass I want to create, and assign it to the relevant subclass. While this solution does work, it feels wrong to have all of these xib files which are practically identical (except for which class they are assigned to) inside my project.
Is there a way I can overcome this problem, and have all of these cells link to the same single xib file?
Thanks! :)
Add view loaded by xib to UITableViewCell classes in which you want to use it.
Create your xib as per your require design, in your example PickerTableViewCell.xib
Create UITableViewCell sub-classes in which you want to use that view. I am using FirstTableViewCell & SecondTableViewCell for this.
in constructor of table cell load the xib and add it to table cell.
let nib = Bundle.main.loadNibNamed("PickerTableViewCell", owner: nil, options: nil)
if let view = nib?.first as? UIView{
self.addSubview(view)
}
if xib have any #IBOutlet then get them by viewWithTag function and assign to class variables
if let label = self.viewWithTag(1) as? UILabel{
self.label = label
}
override reuseIdentifier var of each tableviewCell subclass with different name
override var reuseIdentifier: String?{
return "FirstTableViewCell"
}
Now You can use these classes where you want, for using this follow below steps:
register this tableviewCell subclass with xib with tableview:
tableView.register(FirstTableViewCell.self, forCellReuseIdentifier:"PickerTableViewCell")
now in cellForRowAt indexPath method use it.
var cell = tableView.dequeueReusableCell(withIdentifier: "FirstTableViewCell", for: indexPath) as? FirstTableViewCell
if cell == nil {
cell = FirstTableViewCell()
}
cell?.label?.text = "FirstTableViewCell"
Don't use subclassing to assign different data sources.
Approach 1: Assign pickerDataSource in tableView(_:cellForRowAt:)
In the table view controller, you need to assign pickerDataSource
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! PickerTableViewCell
cell.pickerDataSource = LocationPickerDataSource()
return cell
}
Handle additional work needed after the assignment of pickerDataSource with a didSet.
class PickerTableViewCell: UITableViewCell {
var picker: UIPickerView! = UIPickerView()
var pickerDataSource: PickerViewDataSource! {
didSet {
self.picker.dataSource = self.pickerDataSource
}
}
…
}
Approach 2: Extend PickerTableViewCell in all the needed ways.
Here instead of subclassing add the needed logic to a uniquely named setup method each defined in their own extension.
extension PickerTableViewCell {
func setupLocationPickerDataSource() {
self.pickerDataSource = LocationPickerDataSource()
self.picker.dataSource = self.pickerDataSource
}
}
then in tableView(_:cellForRowAt:)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! PickerTableViewCell
cell.setupLocationPickerDataSource()
return cell
}
Related
I am trying to register UITableViewCell in viewdidload
self.tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "CustomTableViewCell")
In cellForRowAtIndex
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell") as! CustomTableViewCell
cell.productNameLabel.text = "Product"
cell.productNameLabel.textColor = UIColor.darkGray
return cell
}
Here it is crashing in cell.productNameLabel.text.
What is the purpose of registering cell? why it is crashing?
I want to reload data even if cell or table is not visible.
Crashreport :
See the Apple's comments which answers your query on the purpose of registering cell :
Prior to dequeueing any cells, call this method or the
register(_:forCellReuseIdentifier:) method to tell the table view how
to create new cells. If a cell of the specified type is not currently
in a reuse queue, the table view uses the provided information to
create a new cell object automatically.
This is the standard procedure I apply while working with Custom Cells (if you are using xib) :
Set cell's identifier in Xib's attribute inspector :
Register Xib :
self.tableTasks.register(UINib(nibName: "TaskCell", bundle: nil), forCellReuseIdentifier: "taskCell")
However, if you are not using Xib and creating custom cell using code only, then use registeCell :
self.tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "CustomTableViewCell")
Are you using a xib for this cell? If so, none of the outlets will be connected if you just register the class of the cell. You need to register the actual xib file, so that everything can be connected correctly when the cell is created. Have a look at
-(void)registerNib:(UINib *)nib forCellReuseIdentifier:(NSString *)identifier
https://developer.apple.com/documentation/uikit/uitableview/1614937-registernib
My method for register cell.
Syntax sugar
protocol BSCellProtocol {
// For `registerCell`
static var NibName: String! { get }
// For `registerCell`, `dequeueCellWithType`, and `dequeueHeaderFooterWithType`
static var Identifier: String! { get }
}
extension UITableView {
func registerCell(_ type: BSCellProtocol.Type) {
let nib = UINib(nibName: type.NibName, bundle: nil)
let identifier = type.Identifier!
self.register(nib, forCellReuseIdentifier: identifier)
}
func dequeueCellWithType<T: BSCellProtocol>(_ type: T.Type) -> T {
let cell = self.dequeueReusableCell(withIdentifier: type.Identifier) as! T
return cell
}
func dequeueCellWithType<T: BSCellProtocol>(_ type: T.Type, index: IndexPath) -> T {
let cell = self.dequeueReusableCell(withIdentifier: type.Identifier, for: index) as! T
return cell
}
}
Usage
class MyCustomCell: UITableViewCell, BSCellProtocol {
static var NibName: String! = "MyCustomCell"
static var Identifier: String! = "cellIdentifier_at_Xib"
#IBOutlet weak var lblTitle: UILabel!
// other IBOutlet components
}
// In ViewController, register cell
tableView.registerCell(MyCustomCell.self)
// dequeue cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// cell is `MyCustomCell` instance
let cell = tableView.dequeueCellWithType(MyCustomCell.self)
// configure cell ...
// ....
return cell
}
I had the same problem. I also was not using XIB for cell. My view was not connected to View in File's Owner Outlets. Maybe this info will help someone.
Table view cell in cellForRowAt alway has all properties set to nil
import UIKit
class TodoTableViewCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
}
class TodosViewController: UITableViewController {
#IBOutlet var TodosTableView: UITableView!
var projects = [Project]()
var todos = [Todo]()
override func viewDidLoad() {
super.viewDidLoad()
TodosTableView.delegate = self
self.tableView.register(TodoTableViewCell.self, forCellReuseIdentifier: "TodoTableViewCell1")
// data init
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "TodoTableViewCell1"
var todo = projects[indexPath.section].todos[indexPath.row]
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? TodoTableViewCell else {
fatalError("The dequeued cell is not an instance of MealTableViewCell.")
}
cell.label?.text = todo.text // cell.label is always nil
return cell
}
}
It seems like identical issue
Custom table view cell: IBOutlet label is nil
What I tried to do:
- restart Xcode
- recreate outlet
- clean project
- recreate view cell from scratch like here https://www.ralfebert.de/ios-examples/uikit/uitableviewcontroller/custom-cells/
Please help, iOS development drives me nuts already.
You don't need to register the class in the tableview if you're using prototype cells in Interface Builder. Try removing the registration function from viewDidLoad. Incidentally you can also set dataSource and delegate in IB - much neater code-wise.
You are using the UITableView instance method:
func register(AnyClass?, forCellReuseIdentifier: String)
This only works if your custom UITableViewCell subclass is not setup using Interface Builder
If you've created your subclass using an xib. You should use:
func register(UINib?, forCellReuseIdentifier: String)
like:
let nib = UINib(nibName: "\(TodoTableViewCell.self)", bundle: nil)
self.tableView.register(nib, forCellReuseIdentifier: "TodoTableViewCell1")
If you're using prototype cells in a storyboard you don't need to register your cells at all.
I think the identifier of the cell should be in the identifier from the attributes inspector column not the Identity inspector
and in module in Identity inspector add your project
Important note: One issue I haven't seen discussed is that if you use prototype cells in the storyboard, then explicitly registering the cell will make your outlets nil! If you explicitly register the cell then you are registering it without the storyboard which has your iboutlets. This will mean you defined your outlets in your cell but they aren't connected. Deleting the explicit registration will solve the issue.
Doesn't work:
tableVIew.register(MenuCell.self, forCellReuseIdentifier: "MenuCell")
Works:
// tableVIew.register(MenuCell.self, forCellReuseIdentifier: "MenuCell")
When we are creating customView, we set the view File's owner to custom class and we instantiate it with initWithFrame or initWithCode.
When we are creating customUITableViewCell, we set the view's class to custom class, instead File's owner's. And then register all the nibs so on.
İn this way, we always need to register the xibs to UIViewController and
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)so on.
What I find is that I don't want to register nibs all the time where I want to use customUITableViewCell. So I want to initialize xib inside my customUITableCell like the same process of creating customUIView. And I succeed. Here are the steps.
My question is what is the preferred way of creating customUITableCell?
With this method there is no need to register nibs and we can call customCell where we want to without loading/registering nib.
Set the view's File's Owner of xib to customUITableCell class. Not the view's class set to customClass, just File's Owner.
Image 1
My custom class called myView: UITableViewCell
import UIKit
class myView: UITableViewCell {
var subView: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initSubviews()
}
func initSubviews(){
subView = Bundle.main.loadNibNamed("TableViewCell", owner: self, options: nil)?.first as! UIView
subView.autoresizingMask = UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleHeight.rawValue)))
self.addSubview(subView)
}
}
İnside UIVivController, I did't register nibs and use
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
Instead, I did this.
let cell = myView(style: .default , reuseIdentifier: "TableViewCell")
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var tableStyle: UITableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableStyle.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
tableStyle.delegate = self
tableStyle.dataSource = self
view.addSubview(tableStyle)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100.00
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1 }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = myView(style: .default , reuseIdentifier: "TableViewCell")
return cell
}
}
Here is the result.
Image 4
THANKS FOR YOUR TIME!!!
Your approach means every single time the UITableView requests a new cell, you're creating a brand new cell from scratch. That it means it has to:
find the nib
load the nib
parse it to find the views
make the views
update the cell
This is no better than having a long scroll view with custom views for it's entire length.
The beauty of UITableView is it optimizes much of this process and re-uses cells, massively cutting down the performance cost of having more cells than fit on your screen. With the traditional (correct) approach, steps 1-4 only have to happen once.
To expand on the differences in the xib:
When creating a cell with UITableView, you only give it the nib, and the system looks in the nib to find a UITableViewCell. A simple UIView will not work.
You actually can subclass the UIView in your xib with your custom class. It just happens that the norm is to use fileOwner, largely because that's the norm when using nibs with UIViewControllers as was required in the pre-storyboard era
An addition to the accepted answer:
If your only problem with the "classic" approach is that you need to register the nib and call dequeueReusableCell, you can simplify the calls with a nice protocol extension as discussed in this article:
protocol ReuseIdentifying {
static var reuseIdentifier: String { get }
}
extension ReuseIdentifying {
static var reuseIdentifier: String {
return String(describing: Self.self)
}
}
extension UITableViewCell: ReuseIdentifying {}
To register you just call
self.tableView.register(UINib(nibName: MyTableViewCell.reuseIdentifier, bundle: nil), forCellReuseIdentifier: MyTableViewCell. reuseIdentifier)
And to create it you call
let cell = self.tableView.dequeueReusableCell(withIdentifier: MyTableViewCell. reuseIdentifier, for: indexPath) as! MyTableViewCell
(Of course this only works if class, xib and reuse identifier all have the same name)
Is it possible to init and load only custom cell and test outlets?
My ViewController has TableView with separated dataSource ( which is subclass of custom data source ). So it's kinda tricky to create cell using all of those.
Custom cell has only a couple of labels and config method for updating them from object, so if loaded, testing would be easy.
It is possible to write a unit test for custom UITableViewCell that will test its outlets and any other functionality included in it. The following sample demonstrates this:
class TestItemTableViewCell: XCTestCase {
var tableView: UITableView!
private var dataSource: TableViewDataSource!
private var delegate: TableViewDelegate!
override func setUp() {
tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 200, height: 400), style: .plain)
let itemXib = UINib.init(nibName: "ItemTableViewCell",
bundle: nil)
tableView.register(itemXib,
forCellReuseIdentifier: "itemCell")
dataSource = TableViewDataSource()
delegate = TableViewDelegate()
tableView.delegate = delegate
tableView.dataSource = dataSource
}
func testAwakeFromNib() {
let indexPath = IndexPath(row: 0, section: 0)
let itemCell = createCell(indexPath: indexPath)
// Write assertions for things you expect to happen in
// awakeFromNib() method.
}
}
extension TestItemTableViewCell {
func createCell(indexPath: IndexPath) -> ItemTableViewCell {
let cell = dataSource.tableView(tableView, cellForRowAt: indexPath) as! ItemTableViewCell
XCTAssertNotNil(cell)
let view = cell.contentView
XCTAssertNotNil(view)
return cell
}
}
private class TableViewDataSource: NSObject, UITableViewDataSource {
var items = [Item]()
override init() {
super.init()
// Initialize model, i.e. create&add object in items.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell",
for: indexPath)
return cell
}
}
private class TableViewDelegate: NSObject, UITableViewDelegate {
}
This approach mimics the way UITableViewCells are created/reused at runtime. The same methods get called, e.g. awakeFromNib, IBOutlets initialized, etc. I am sure you can even test the sizing of the cell (e.g. height) even though I haven't tried that yet. Note that having a view model where the "visualization" logic of your model object is contained is a good & modular approach and makes it easier to unit test parts of the code (as described in another answer above). However, with a unit test for a view model object, you cannot test the entire lifecycle of a UITableViewCell.
Performing unit tests against that is not really worth the hassle. However, there is an easier approach to this problem.
You can create a view model to support your cell, and then test that the view model is providing the correct values for each item.
A simple example of a view model that populates two labels and an image is here:
class MyCellModel {
var stringOne: String? {
return "Compute string 1"
}
var stringTwo: String? {
return "Compute string 2"
}
var image: UIImage? {
return UIImage(named: "myimage")
}
}
Using this model, you would place the logic for generating those values in the relevant computed properties. Then for testing purposes, you can initialize this model with the values you want to test against.
I have a table view and I am adding several cells to it based on my json.
So far the code for adding cells looks as follows:
override func viewWillAppear(animated: Bool) {
let frame:CGRect = CGRect(x: 0, y: 90, width: self.view.frame.width, height: self.view.frame.height-90)
self.tableView = UITableView(frame: frame)
self.tableView?.dataSource = self
self.tableView?.delegate = self
self.view.addSubview(self.tableView!)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("CELL")
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "CELL")
}
let user:JSON = JSON(self.items[indexPath.row])
cell!.textLabel?.text = user["description"].string
var photoURL = "/path/to/my/icon/google.png"
if let data = NSData(contentsOfFile: photoURL)
{
cell!.imageView?.image = UIImage(data: data)
}
return cell!
}
Besides the description in my json I have also username and price. So far - since I'm adding only imageView and description, 3 cells look like this:
Is there a way to style it so that each cell looks similar to this:
(price and username are grey here`)? How can I achieve this effect?
===EDIT:
this is how I populate my table:
I'm fetching data from rest webservice to json:
func getAllUsers() {
RestApiManager.sharedInstance.getUsers { json in
let results = json
for (index: String, subJson: JSON) in results {
let user: AnyObject = JSON.object
self.items.addObject(user)
dispatch_async(dispatch_get_main_queue(),{
self.tableView?.reloadData()
})
}
}
}
and I invoke this method in my viewWillAppear function
You can make your table use custom UITableViewCells and style them to your liking.
In a nutshell, you create a prototype cell in Storyboard that looks like the example you posted and connect it to a custom UITableViewCell class with the elements you created. At cellForRowInIndexPath you return your custom cell rather than regular UITableViewCells.
Check out this tutorial for details: http://shrikar.com/uitableview-and-uitableviewcell-customization-in-swift/
Create the layout of the cell using a custom style. Place labels and imageView like you would anywhere else in storyborad.
You will need to create a UITableViewCell file. The one I used is named ExampleTableViewCell. Make note of the subclass.
Now connect your cell to the ExampleTableViewCell you just created.
Now we can make outlets from the labels and imageView of the cell into the ExampleTableViewCell. Control drag from each element into the ExampleTableViewCell.
The final step is to configure the cell using the cellForRowAtIndexPath func. Make note of the var cell. We now cast this to the ExampleTableViewCell. Once we do this we can use the outlets in the ExampleTableViewCell to set our labels and image. Make sure you set the resuseIdentifier for the cell in the storyboard. If you are unfamiliar with this leave a comment and I can add instructions for this.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier") as! ExampleTableViewCell
cell.imageDisplay.image = yourImage
cell.descriptionLabel.text = yourDescription
cell.priceLabel.text = yourPrice
cell.usernameLabel.text = yourUsername
return cell
}
Subclass UITableViewCell. You can go to the TableView on your storyboard and go to one of the prototypes and set it's class to your custom class and it's style to Custom and then you can ctrl+click & drag outlets/actions to the UITableViewCell subclass the same way you would for a basic view controller.