It was my understanding that in Swift you don't need to import your classes (only for some frameworks) but intatiation was required, but to my surprise this may not be true, I created a custom class (CustomCell.swift) for a custom cell I'm using in my UITableView and apparently there is no need to make an instance of the custom class before using it. Here is how I'm using it.
CustomCell Class
// CustomCell.swift
import UIKit
class CustomCell: UITableViewCell {
#IBOutlet weak var labelDisplayWattage: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
View Controller
// ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
// some code ...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reusableCell", forIndexPath: indexPath) as! CustomCell
cell.labelDisplayWattage.text = String(totalWattageList[indexPath.row])
return cell
}
}
As you can see there are no instances of the CustomCell class exept for the call on method cellForRowAtIndexPath (as! CustomCell).
Can someone tell me why there was no need to create an instance of class CustomCell before using it in method cellForRowAtIndexPath? I was expecting to see something like...
var myCustomCell = CustomCell()
Thanks
As per apple docs for Datasource method
func dequeueReusableCellWithIdentifier(_ identifier: String,forIndexPath indexPath: NSIndexPath) -> UITableViewCell
For performance reasons, a table view’s data source should generally
reuse UITableViewCell objects when it assigns cells to rows in its
tableView:cellForRowAtIndexPath: method. A table view maintains a
queue or list of UITableViewCell objects that the data source has
marked for reuse. Call this method from your data source object when
asked to provide a new cell for the table view. This method dequeues
an existing cell if one is available, or creates a new one based on
the class or nib file you previously registered, and adds it to the
table.
so basically it creates a new object for you if the queue is empty.
Source: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/#//apple_ref/occ/instm/UITableView/dequeueReusableCellWithIdentifier:forIndexPath:
There is a really important concept underneath the code you're seeing right now. If you're using storyboards, table views and collection views have the option of using a prototype cell. A prototype cell is a template of the cell that can/will be used with the table view or collection view. If your table/collection view is set to use a prototype cell, there is no need to create a new cell,UIKit takes care of creating one for you when you ask for a queued cell by calling tableView.dequeueReusableCellWithIdentifier.
Consider the following when NOT using prototype cells:
// ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
// some code ...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("reusableCell", forIndexPath: indexPath) as? CustomCell
if nil == cell {
cell = CustomCell()
// more cell options can be set here
}
cell.labelDisplayWattage.text = String(totalWattageList[indexPath.row])
return cell
}
}
let cell = tableView.dequeueReusableCellWithIdentifier("reusableCell", forIndexPath: indexPath) as! CustomCell
Instantiation of the CustomCell is responsibility of tableview, we just fetch the cell, if the cell is not instantiated when we fetch it tableview will instantiate it before returning it to us.
This behavior is of architectural design, it is not specific to swift. It works in same way for objective-c as well.
Related
I have a Social Network Feed in form UItableView which has a cell. Now each cell has an image that animates when an even is triggered. Now, This event is in form of a string, will be triggered at every cell. the options for the event are defined in another class(of type NSObject).
My issue:
I constructed a protocol delegate method in table view, which will be called whenever the event is triggered for each cell. Then, I define this function in UITableViewCell Class, since my the image will be animating on that.
All is working well but I am unable to figure out how to assign the delegate of TableView class to cell class. What I mean is, how can I use UITableView.delegate = self in cellView class. I have tried using a static variable, but it doesn't work.
I have been playing around the protocols for a while now but really unable to figure out a solution to this.
I hope I am clear. If not, I will provide with an example in the comments. I am sorry, This is a confidential project and I cant reveal all details.
If I understand you correctly, you are trying to make each of your cells conform to a protocol that belongs to their UITableView? If this is the case then this cannot be done. The Delegation design pattern is a one to one relationship, i.e only one of your UITableViewCells would be able to conform to the UITableView's delegate.
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object—the delegate—and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object.
Quote from the Apple Docs
I would suggest that your UITableViewCell should call a block (Objective-C) or a closure (Swift) whenever your specified event is triggered to achieve what you are looking for. Set up this closure in your tableView:cellForRowAtIndexPath function.
EXAMPLE
TableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCellID", for: indexPath) as! MyTableViewCell
cell.eventClosure = {
//Do something once the event has been triggered.
}
return cell
}
TableViewCell
func eventTriggered()
{
//Call the closure now we have a triggered event.
eventClosure()
}
If I correctly understood your question, maybe this could help:
class ViewController: UIViewController, YourCustomTableDelegate {
#IBOutlet weak var tableView: YourCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.customTableDelegate = self
}
// table delegate method
func shouldAnimateCell(at indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.animate(...)
}
}
}
Try something like this:
Define your delegate protocol:
protocol CustomCellDelegate: class {
func animationStarted()
func animationFinished()
}
Define your CustomCell. Extremely important to define a weak delegate reference, so your classes won't retain each other.
class CustomCell: UITableViewCell {
// Don't unwrap in case the cell is enqueued!
weak var delegate: CustomCellDelegate?
/* Some initialization of the cell */
func performAnimation() {
delegate?.animationStarted()
UIView.animate(withDuration: 0.5, animations: {
/* Do some cool animation */
}) { finished in
self.delegate?.animationFinished()
}
}
}
Define your view controller. assign delegate inside tableView:cellForRowAt.
class ViewController: UITableViewDelegate, UITableViewDataSource {
/* Some view controller customization */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CustomCell.self)) as? CustomCell
cell.delegate = self
cell.performAnimation()
return cell
}
}
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
My table view allows multiple cell selection, where each cell sets itself as selected when a button inside the cell has been clicked (similar to what the gmail app does, see picture below). I am looking for a way to let the UITableViewController know that cells have been selected or deselected, in order to manually change the UINavigationItem. I was hoping there is a way to do this by using the delegate methods, but I cannot seem to find one. didSelectRowAtIndexPath is handling clicks on the cell itself, and should not affect the cell's selected state.
The most straight forward way to do this would be to create our own delegate protocol for your cell, that your UITableViewController would adopt. When you dequeue your cell, you would also set a delegate property on the cell to the UITableViewController instance. Then the cell can invoke the methods in your protocol to inform the UITableViewController of actions that are occurring and it can update other state as necessary. Here's some example code to give the idea (note that I did not run this by the compiler, so there may be typos):
protocol ArticleCellDelegate {
func articleCellDidBecomeSelected(articleCell: ArticleCell)
func articleCellDidBecomeUnselected(articleCell: ArticleCell)
}
class ArticleCell: UICollectionViewCell {
#IBAction private func select(sender: AnyObject) {
articleSelected = !articleSelected
// Other work
if articleSelected {
delegate?.articleCellDidBecomeSelected(self)
}
else {
delegate?.articleCellDidBecomeUnselected(self)
}
}
var articleSelected = false
weak var delegate: ArticleCellDelegate?
}
class ArticleTableViewController: UITableViewController, ArticleCellDelegate {
func articleCellDidBecomeSelected(articleCell: ArticleCell) {
// Update state as appropriate
}
func articleCellDidBecomeUnselected(articleCell: ArticleCell) {
// Update state as appropriate
}
// Other methods ...
override tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! ArticleCell
cell.delegate = self
// Other configuration
return cell
}
}
I would have a function like 'cellButtomDidSelect' in the view controller and in 'cellForRowAtIndexPath', set target-action to the above mentioned function
When I use the UITableViewController to create a tableView, you get a lot of override functions, but when you use a regular UIViewController you get an error when using these same override functions and you are forced to change them to regular functions. I believe this is why my core data won't load into my cells, and tried to use the viewDidLoad function to get my data to load.
I know my code should work since all I'm trying to do is transfer all my code from a UITableViewController to a UIViewController, and my code worked in my UITableViewController.
My effort so far:
override func viewDidLoad() {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
let CellID:NSString = "CELL"
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier(CellID) as UITableViewCell
if let ip = indexPath as Optional {
var data:NSManagedObject = myList[ip.row] as NSManagedObject
cell.textLabel!.text = data.valueForKeyPath("username") as? String
}
return cell
}
}
Are the override functions the reason my cells are empty, or are there other aspects when using a regular UIViewController to show a tableView?
Any suggestions would be appreciated.
(1) You have to add UITableViewDelegate to the class in order to access the delegate methods, ex:
class ViewController: UIViewController, UITableViewDelegate {
After adding the UITableViewDelegate to the class, your UITableView delegate functions should auto-complete.
Also, make sure to set the UITableView's delegate to self in order to have the delegate methods populate the table.
(2) Right now, your cellForRowAtIndexPath method is within your viewDidLoad. Move it so it's not contained within any other method.
I have a basic method for populating a tableView with a custom cell in Swift
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let object = array[UInt(indexPath!.row)] as Word
var cell:DictionaryTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("dictionaryCell") as DictionaryTableViewCell
cell.originalText.text = object.original
return cell
}
Works fine. Now, I got a custom cell DictionaryTableViewCell with custom properties and some IBActions (for different buttons inside that cell).
My question is: How do I expose data that was passed to the cell from tableView and use it from inside my custom cell controller? For instance I would like to be able to manipulate it for each cell using IBActions.
For now my custom cell controller is nothing fancy
class DictionaryTableViewCell: UITableViewCell {
#IBOutlet weak var originalText: UILabel!
#IBAction func peak(sender: AnyObject) {
}
}
Is there a custom property I can use to recover data passed onto it? Or do I have to implement some sort of a protocol? If so - how?