Swift: Set custom UITableViewCell class inside if statement - ios

I'm a Swift newbie and struggling to do something pretty simple.
I want to change the class of a tableViewCell when its tapped. After a lot of Googling I'm now trying to do this by setting a boolean flag (in a dict) and checking the value to determine which class to use.
I've come unstuck with Swift basics of trying to set a variable inside an if statement:
// I think I need to instantiate the cell variable here to be used inside
// and after the if statement but don't know what class type to use.
// I've tried lots of "var cell: xxx = yyy" variations but no luck
if selectedRows[indexPath.row] == true {
let cell = tableView.dequeueReusableCellWithIdentifier("Tier3CellExpanded", forIndexPath: indexPath) as! Tier3CellExpanded
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("Tier3Cell", forIndexPath: indexPath) as! Tier3TableViewCell
}
let image = UIImage(named: entry.thumbnail)
cell.thumbImageView.image = image
cell.busNameLabel.text = entry.busName
cell.busAddressLabel.text = entry.address
return cell
If anyone could point me in the right direction that would be great.

I'm just expanding on Charles A. 's answer to show you how to declare cell outside the if statement but still use 2 different cell types.
//All shared properities would belong to this class
var cell: MySuperclassCellsInheritFrom
if selectedRows[indexPath.row] {
cell = tableView.dequeueReusableCellWithIdentifier("Tier3CellExpanded", forIndexPath: indexPath) as! Tier3CellExpanded
if let expandedCell = cell as? Tier3CellExpanded {
//Set properties specific to Tier3CellExpanded
}
}
else {
cell = tableView.dequeueReusableCellWithIdentifier("Tier3Cell", forIndexPath: indexPath) as! Tier3TableViewCell
if let regularCell = cell as? Tier3TableViewCell {
//Set properties specific to Tier3TableViewCell
}
}
// Configure cell
// Properties that both subclasses share can be set here
return cell
This is possible since we declared cell as UITableViewCell and then cast it after dequeing with identifiers. The cast is possible because the cells you are dequeuing are subclasses of UITableViewCell. So after casting you can now set all of that subclasses individual properties.
This method is useful in case there is other code that you want to apply to both cells that you won't need to duplicate in each if statement such as backgroundColor changes or other base UITableViewCell properties.

You can try like this
if selectedRows[indexPath.row] == true {
let cell = tableView.dequeueReusableCellWithIdentifier("Tier3CellExpanded", forIndexPath: indexPath) as! Tier3CellExpanded
let image = UIImage(named: entry.thumbnail)
cell.thumbImageView.image = image
cell.busNameLabel.text = entry.busName
cell.busAddressLabel.text = entry.address
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("Tier3Cell", forIndexPath: indexPath) as! Tier3TableViewCell
let image = UIImage(named: entry.thumbnail)
cell.thumbImageView.image = image
cell.busNameLabel.text = entry.busName
cell.busAddressLabel.text = entry.address
return cell
}

In your code you are declaring a constant in the if block and another in the else block (that's what the let keyword does), so those will go out of scope immediately after you've set them. Do you have another variable outside your if statement called cell?
I would expect the code to look something like:
let cell: SomeCellType
if selectedRows[indexPath.row] {
cell = ...
}
else {
cell = ...
}
// Configure cell
return cell

Related

How toaccess properties / values from a UITableViewCell?

I want to access my view objects from UITableViewCell, but I cannot. I can't cast my cell object because I only have the string UITableViewCell. For example "ClientTableViewCell".
How can I access the views and objects in the cell without casting?
I tried to do as follows:
let cell = (val as AnyObject).dequeueReusableCell(withIdentifier: "ClientTableViewCell") as! UITableViewCell
I can get a cell like this but I can't reach the contents.Itried mirror reflection like this:
let mirror = Mirror(reflecting: cell)
but child label and value comes some optional
guard let cell: ClientTableViewCell = tableView.dequeueReusableCell(withIdentifier: String(describing: ClientTableViewCell.self), for: indexPath) as? ClientTableViewCell else { return UITableViewCell() }
try this

convert String content to UITableViewCell type in swift

Can you convert the content of a Swift 3 String into a type through a specific function? I'll include an example:
I've declared multiple UITableViewCell classes as follows:
class ScrollFeedCell : UITableViewCell {...}
class AdCell : UITableViewCell {...}
class MovieCell : UITableViewCell {...}
I want to declare the conversion function, in my view controller, as follows:
func convert(String) -> Any {}
Then I want to use the following:
class TableView : UITableViewController {
let typeArray = [String]
override func viewDidLoad() {
//add a huge ton of strings into the typeArray
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
let c = typeArray[indexPath.section]
if c == "ScrollFeddCell" || c == "AdCell" || c == "MovieCell" {
cell = tableView.dequeueReusableCell(withIdentifier: content[indexPath.section], for: indexPath) as! convert(c)
} else {
cell = tableView.dequeueReusableCell(withIdentifier: "CategoryScrollFeed_Cell", for: indexPath)
}
return cell
}
}
I do not think this is possible. Even if it is somehow possible, I think it is going to involve lots of dirty tricks which is not really worth it in this situation.
In fact, the only place you used your imaginary convert method is here:
cell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath) as! convert(c)
^^^^^^^^^^
Why do you want to cast it to the right type? Since this is very dynamic, the compiler can't know what members will the type returned by convert have. Basically, too dynamic. It is not useful to cast it to the right type here.
The enclosing method returns a UITableViewCell anyway, so you can just return the return value of dequeueResuableCell without the compiler complaining.
"But I want to configure the cell after dequeuing it though..." you might say.
Well, you are going to configure a ScrollFeedCell in a different way from a MovieCell, right? So you can't just write all the configuration code after this line:
cell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath) as! convert(c)
You still have to write an if statement and check whether the cell is a MovieCell, ScrollFeedCell or AdCell. So why not delete the above line and do this instead:
if c == "ScrollFeedCell" {
let scrollFeedCell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath) as! ScrollFeedCell
// configure cell here
cell = scrollFeedCell
} else if c == "AdCell" {
let adCell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath) as! AdCell
// configure cell here
cell = adCell
} else if c == "MovieCell" {
let movieCell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath) as! MovieCell
// configure cell here
cell = movieCell
} else {
cell = tableView.dequeueReusableCell(withIdentifier: "CategoryScrollFeed_Cell", for: indexPath)
}
Edit:
Try this:
if c == "ScrollFeedCell" {
let scrollFeedCell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath) as! ScrollFeedCell
scrollFeedCell.delegate = self
cell = scrollFeedCell
} else if c == "AdCell" || c == "MovieCell" { // add your other cell types here.
cell = tableView.dequeueReusableCell(withIdentifier:
content[indexPath.section], for: indexPath)
} else {
cell = tableView.dequeueReusableCell(withIdentifier: "CategoryScrollFeed_Cell", for: indexPath)
}
Please consider what you want to do is necessary or not. Why you want to convert them to specific cell type? It will work just keep the cell as UITableViewCell and return it. If you have specific actions for different cells, you should separate the if cases:
if c == "ScrollFeddCell" {
cell = tableView.dequeueReusableCell(withIdentifier: c, for: indexPath) as! ScrollFeddCell
//cell.doSomethingForScorll()
}
else {
cell = tableView.dequeueReusableCell(withIdentifier: c, for: indexPath)
//do nothing for common cells.
}
//....
A little late, but for those looking for an answer:
I know what you want, and I agree with your need.
In my case, I need to do this because in my app, I not only receive the data from the server, but ALSO the layout of such data inside the cell. So far, I haven't been able to find a solution. In your case, it seems a little easier:
// 1. Create a protocol with the configuring method
protocol configureCellProtocol
{
func configure(cell:MyData)
}
// 2. Add this protocol to the 8 classes derived from UITableViewCell that define your cells
// 3. Cast the reusable cell to this protocol: (note that you have to do a double cast,
// both to configureCellProtocol and UITableViewCell, (that's what the & is for) otherwise,
// you won't be able to return the configured cell
let thisCell=tableView.dequeReusableCell(
withReuseIdentifier: cellClass, for: indexPath) as! UITableViewCell & configureCellProtocol
// 4. then you can call the method like this:
thisCell.configure(cell:configurationData)
// which in turn will call the specific method in each class. Then you have to define the configure
// method in each class, otherwise you'll get a crash at runtime. Do what you need to configure
// your cells in each class in this method
//
in step 3, cellClass is a String, which in turn is the class name that you register. In your case, you would have to select it from an array according to the criteria that makes every cell different

How to get table all current row information

I am new to swift . i am doing my project programatically and I load data from api to the tableView and tableView like ios setting page ..
now i need all rows information when click "Add to cart" button. How can i do it?
here is my code sample :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: cartHeaderCell, for: indexPath) as! CartHeaderCell
cell.configureCell(indexPath.item)
return cell
case 1:
let obj = data?[indexPath.row]
var cell = UITableViewCell()
switch obj {
case is Addon:
cell = tableView.dequeueReusableCell(withIdentifier: addonCell, for: indexPath) as! AddonCell
let switchView = UISwitch(frame: .zero)
switchView.setOn(false, animated: true)
cell.accessoryView = switchView
guard let addon = obj as? Addon else {
return cell
}
cell.textLabel?.text = "\(addon.name) + €\(addon.price)"
case is AddonGroup:
cell = tableView.dequeueReusableCell(withIdentifier: addonGroupCell, for: indexPath) as! AddonGroupCell
cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator
guard let addonGroup = obj as? AddonGroup else {
return cell
}
if let addons = addonGroup.addonList {
cell.detailTextLabel?.text = ""
var selectedAddons = ""
for _addon in addons
{
if _addon.isSelect == true {
selectedAddons = selectedAddons + "\(_addon.name)"
}
}
cell.detailTextLabel?.text = selectedAddons
}
cell.textLabel?.text = addonGroup.name
...........................................
As Fahim was mentioning, you need to set up a data model that records that status of each cell before / during / after the user interaction with each cell. So when the cell goes off screen and then comes back on, it will be presented with the correct state of the model.
Secondly, for the UISwitchViews, you should be instantiating and adding those to the contentView within each cell in order to keep the cellForRow function clean and problem free. The reason leads me into my next point: how to record the status of each UISwitchView after the user has interacted with a UISwitchView. You are going to want to create a protocol and add a delegate within the UICollectionViewCell(that inherits class and the delegate should be a weak var), in order to update the model whenever the UISwitch is tapped.
If you have any more questions i can do my best to help!

Table view cell doesn't show updated data

I reached a correct value and printed it during the debug sessions. However, when i run the application, the calculated value (newcalory) doesn't show up the specific table cell text field. (aka. cell.itemTotalCalory.text) Do you have any ideas for the solution?
*I attached the related code blocks below.
Thanks a lot,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! IngredientTableViewCell
cell.ingredientNameTextField.text = ingredients [indexPath.row].ingredientName
cell.numberofItem.text = "1"
let cellcalory = ingredients [indexPath.row].ingredientCalory
cell.itemTotalCalory.text = cellcalory
cell.plusButton.tag = Int(cell.itemTotalCalory.text!)! //indexPath.row
cell.plusButton.addTarget(self, action:#selector(plusAction), for: .touchUpInside)
cell.minusButton.tag = Int(cell.itemTotalCalory.text!)!
cell.minusButton.addTarget(self, action:#selector(minusAction), for: .touchUpInside)
return cell
}
#IBAction func plusAction(sender: UIButton)
{
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell") as! IngredientTableViewCell
let buttonRow = sender.tag
if cell.numberofItem.text == "1" || cell.numberofItem.text != "1"
{
cell.numberofItem.text = "1"
let textValue1 = cell.numberofItem.text
var textValue = Int(textValue1!)
textValue = textValue! + 1
cell.numberofItem.text = String(describing: textValue)
let oldcalory = buttonRow
cell.itemTotalCalory.text = String (((textValue! * Int(oldcalory)) + Int(oldcalory)))
let newcalory = cell.itemTotalCalory.text
refresh(newcalory: newcalory!);
}
}
func refresh(newcalory :String)
{
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell") as! IngredientTableViewCell
cell.itemTotalCalory.text = newcalory
DispatchQueue.main.async {
self.ingredientTableView.reloadData()
}
}
What you should do is to update the value in ingredients array and then call ingredientTableView.reloadData() to reflect this to the UI.
Calling dequeueReusableCell(withIdentifier:) in refresh method will not work as expected for what are you trying to do:
For performance reasons, a table view’s data source should generally
reuse UITableViewCell objects when it assigns cells to rows in its
tableView(_:cellForRowAt:) 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 using the class
or nib file you previously registered. If no cell is available for
reuse and you did not register a class or nib file, this method
returns nil.
So, refresh method should be similar to:
func refresh() {
// updating ingredients array upon reqs satisfaction...
// and then:
ingredientTableView.reloadData()
// nameOfYourRefreshControl.endRefreshing()
}
Also, if you are pretty sure that you want to get a specific cell from the tableView, you might want to use cellForRow(at:) instance method:
Returns the table cell at the specified index path.
func refresh() {
let cell = ingredientTableView?.cellForRow(at: YOUR_INDEX_PATH)
//...
}
Hope this helped.
I found the solution, the lines that are listed below are useless.
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell") as! IngredientTableViewCell
cell.itemTotalCalory.text = newcalory
I updated the ingredient array with the new value inside the plusAction function and my problem solved. Thanks for all postings.

Swift - Type Casting

I am building a custom UITableView with custom cells.
Each of the custom cells are a subclass of FormItemTableViewCell
I am attempting to populate the cell data in cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = FormItemTableViewCell();
if(indexPath.row == 1){
cell = tableView.dequeueReusableCellWithIdentifier(twoOptionCellIdentifier, forIndexPath: indexPath) as! TwoOptionTableViewCell
} else {
cell = tableView.dequeueReusableCellWithIdentifier(oneTextFieldCellIdentifier, forIndexPath: indexPath) as! OneTextFieldTableViewCell
}
cell.questionLabel.text = "What is the meaning of life?";
return cell
}
How do I access the elements in the subclass?
For example: TwoOptionTableViewCell has a segControl
while the OneTextFieldTableViewCell has a answerTextField
There are some decent answers in this question but most of them have one bad thing in common, they force unwrapped optionals, which you should avoid as much as you can (pretty much the only acceptable place to use them is in IBOutlets)
This is what I think is the best way to handle this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("Identifier", forIndexPath: indexPath) as? FormItemTableViewCell else {
fatalError("Cell is not of kind FormItemTableViewCell")
}
switch cell {
case let cell as TwoOptionTableViewCell where indexPath.row == 1:
// Configure cell, which is an object of class TwoOptionTableViewCell, but only when we are in row 1
break
case let cell as TwoOptionTableViewCell:
// Configure cell, which is an object of class TwoOptionTableViewCell, when the row is anything but 1
break
case let cell as OneTextFieldTableViewCell:
// Configure cell, which is an object of class OneTextFieldTableViewCell
break
case _: print("The cell \(cell) didn't match any patterns: \(indexPath)")
}
cell.questionLabel.text = "What is the meaning of life?";
return cell
}
Now let me walk you through the reasons I think it's the best way.
First of all, it doesn't force unwraps any optionals, everything is unwrapped nicely in the switch case.
It dequeues your cell from the table (something you should always do) and makes sure it's a subclass of FormItemTableViewCell, otherwise it throws a fatal error.
By using a switch case, it casts cell into the class you need, and at the same time it checks if it's the index path you want. So if you want to share some logic in different rows that share a class, you can compare indexPath.row to multiple values. If you don't use the where clause, it will use the same logic in all places where it finds a cell with that class.
Do note that you will need to add some logic to get the desired identifier depending on the row.
You can use one of the two approaches:
1) The best way:
if(indexPath.row == 1) {
let cell = tableView.dequeueReusableCellWithIdentifier(twoOptionCellIdentifier, forIndexPath: indexPath) as! TwoOptionTableViewCell
// the type of cell is TwoOptionTableViewCell. Configure it here.
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier(oneTextFieldCellIdentifier, forIndexPath: indexPath) as! OneTextFieldTableViewCell
// the type of cell is TwoOptionTableViewCell. Configure it here.
return cell
}
2) If you declare cell just once, as a superclass, then you have to downcast it like this.
var cell: FormItemTableViewCell
cell = ... // dequeue and assign the cell like you do in your code.
if let twoOptionCell = cell as? TwoOptionTableViewCell
{
// configure twoOptionCell
}
else if let oneTextFieldCell = cell as? OneTextFieldTableViewCell
{
// configure oneTextFieldCell
}
return cell
This is more verbose, once you add the code to dequeue the cell. So I personally prefer and recommend the first approach.
If I understand correctly, you want to keep main declaration of cell as FormItemTableViewCell to access common properties.
You can create a new variable and assign it the casted version.
Do your stuff with this instance as this is a class object it will point to same reference.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = FormItemTableViewCell();
// this can be replaced with below line as I don't see the purpose of creating an instance here while you use dequeue below.
// var cell: FormItemTableViewCell!
if(indexPath.row == 1){
cell = tableView.dequeueReusableCellWithIdentifier(twoOptionCellIdentifier, forIndexPath: indexPath);
let tempCell = cell as! TwoOptionTableViewCell;
// access members of TwoOptionTableViewCell on tempCell
tempCell.segControl.someProperty = 0;
} else {
cell = tableView.dequeueReusableCellWithIdentifier(oneTextFieldCellIdentifier, forIndexPath: indexPath);
let tempCell = cell as! OneTextFieldTableViewCell;
// access members of OneTextFieldTableViewCell on tempCell
tempCell.answerTextField.text = "42";
}
cell.questionLabel.text = "What is the meaning of life?";
return cell
}
You're going to have to conditionally cast them in that case. I like using Enums for Rows/Sections instead of == 1 (depending on how your TableView is setup), but basically you'll want to do the following:
if indexPath.row == 1 {
let cell = tableView.dequeueReusableCellWithIdentifier(twoOptionCellIdentifier, forIndexPath: indexPath) as! TwoOptionTableViewCell
// Note that we cast the cell to TwoOptionTableViewCell
// access `segControl` here
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier(oneTextFieldCellIdentifier, forIndexPath: indexPath) as! OneTextFieldTableViewCell
// This cell we cast to OneTextFieldTableViewCell.
// access `answerTextField` here
return cell
}
What you were doing was defining the cell as FormItemTableViewCell, so subsequent accesses would only know it in that form even though you explicitly cast it to a subclass during assignment.
As a side-note, you don't have to assign to the var as you did there, what you could do is let cell: FormItemTableViewCell. Then in the if-statements you could define new cells of the subclasses, operate on them, and then assign back to your original cell and then return that. This is useful if you're going to be performing the same operations on both cell types after the if statements (such as setting a background colour or something, regardless of which subclass you have).
Here is my favourite way of handling this situation:
enum CellTypes {
case TwoOption, OneTextField
init(row: Int) {
if row == 1 {
self = .TwoOption
} else {
self = .OneTextField
}
}
var reuseIdentifier: String {
switch self {
case .TwoOption: return "twoOptionReuseIdentifier"
case .OneTextField: return "oneTextFieldReuseIdentifier"
}
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: FormItemTableViewCell
let cellType = CellTypes(row: indexPath.row)
switch cellType {
case .TwoOption:
let twoOptionCell = tableView.dequeueReusableCellWithIdentifier(cellType.reuseIdentifier, forIndexPath: indexPath) as! TwoOptionTableViewCell
// do stuff with the `segControl`
cell = twoOptionCell
case .OneTextField:
let textFieldCell = tableView.dequeueReusableCellWithIdentifier(cellType.reuseIdentifier, forIndexPath: indexPath) as! OneTextFieldTableViewCell
// do stuff with the `answerTextField`
cell = textFieldCell
}
// Here do something regardless of which CellType it is:
cell.questionLabel.text = "What is the meaning of life?"
return cell
}

Resources