Avoiding excessive ! use in Swift function (cellForRowAtIndexPath) - swift

Simply put: How can I avoid writing ! for every line in the Swift code below? I considered guard, but the UITableViewCell initialiser can return nil, but on the other hand cellForRowAtIndexPath must return non-nil, which seems like a contradiction in itself. Hope there is a short and sweet way.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier)
if cell == nil {
cell = UITableViewCell(style: .Default, reuseIdentifier: reuseIdentifier)
}
cell!.textLabel?.text = ...
cell!.textLabel?.textColor = ...
cell!.detailTextLabel?.textColor = ...
cell!.detailTextLabel?.textColor = ...
return cell!
}

The ?? operator understands that if the rhs is not optional, then the result is not optional:
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier)
?? UITableViewCell(style: .Default, reuseIdentifier: reuseIdentifier)
cell.textLabel?.text = ... // No ! needed
Better yet, if you register your cell identifiers (either in a storyboard or with the registerNib / registerClass methods), then you can use the newer form of dequeueReusableCellWithIdentifier which does not return an optional:
let cell = tableView.dequeueReusableCellWithIdentifier("repo", forIndexPath: indexPath)
cell.textLabel?.text = ... // No ! needed

Let me paste someting copied directly from the UITableView public API:
public func dequeueReusableCellWithIdentifier(identifier: String, forIndexPath indexPath: NSIndexPath) -> UITableViewCell
// newer dequeue method guarantees a cell is returned and resized properly, assuming identifier is registered
Looking at the method above, it guarantees returning a correct cell, thus the return type is not optional and you can avoid force unwrapping.

You can also code like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier)!
cell.textLabel?.text = ...
cell.textLabel?.textColor = ...
cell.detailTextLabel?.textColor = ...
cell.detailTextLabel?.textColor = ...
return cell
}

Related

Swift tableview cell doesn't save the state with different identifier

I am very confused on the reuse of the cells.
I have a table, each cell is a cell with a switch on it. If I toggle a switch I set the background color of that cell to a different color. However every time I scroll these changes don't persist.
I am subclassing UITalbeViewCell to create my own custom cell. Each cell has a different identifier. However when I scroll through the table, whatever changes I made to the cell still doesn't save. I've read similar questions but none of them worked.. Some suggested subclass which I did, some suggested use different identifier which I also did...
Here is the code of my tableview.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let key = Array(dataSource[indexPath.section].keys)[indexPath.row]
let cell = CellWithSwitch.init(style: .subtitle, reuseIdentifier: key)
cell.awakeFromNib()
let val = Array(dataSource[indexPath.section].values)[indexPath.row]
cell.switchView?.addTarget(self, action: #selector(self.switchChanged(_:)), for: .valueChanged)
if let index = key.firstIndex(of: "."){
cell.textLabel?.text = String(key.suffix(from: key.index(index, offsetBy: 1)))
}else{
cell.textLabel?.text = key;
}
cell.switchView?.setOn(val, animated: true)
return cell
}
You can change array value in switchChange action
lets i take array for switch as below:
var arrSwitch = [false,false,false,false,false,false,false,false,false,false]
Below is my cellForRowAt Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! customCell
cell. switchView.setOn(self.arrSwitch[indexPath.row], animated: false)
cell. switchView.tag = indexPath.row
cell. switchView.addTarget(self, action: #selector(self.onSwitchTap(_:)), for: .valueChanged)
return cell
}
Here is my onSwitchTap Action
#IBAction func onSwitchTap(_ sender: UISwitch) {
self.arrSwitch[sender.tag] = !self.arrSwitch[sender.tag]
}
Now on scroll it will persist last changes you have done.

How to dequeue subtitle UITableViewCell in code properly?

I'm trying to create a UITableView with regular, non-custom .subtitle cells in pure code. However, the following code never gives me a cell with a proper detailTextLabel, instead opting for a .default cell.
public var cellIdentifier: String { return "wordsCell" }
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
if let newCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
// Always succeeds, so it never goes to the alternative.
cell = newCell
}
else {
// This is never reached.
cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
}
let word = wordAtIndexPath(indexPath: indexPath)
cell.textLabel?.text = word.text
cell.detailTextLabel?.text = word.subText
return cell
}
This is apparantly because dequeueReusableCell(withIdentifier:) doesn't actually return nil, even if no cell is currently available. Instead, it always returns a .default if no cells have been created.
The other option, dequeueReusableCell(withIdentifier:for:) also always succeeds, so that wouldn't work either.
So far, it looks impossible to create a non-.default style cell in pure code, without Interface Builder to define the style of a prototype cell. The closest I can come up with is this answer, which notices the same problem. All the other questions I found also address either IB issues or custom cells.
Does anyone know how to dequeue a .subtitle cell for a table view without using Interface Builder?
I tested it and it works. I thought you wanted to subclass UItableViewCell but you don't have to register the cell in this case.
class TableViewController: UITableViewController {
let wordAtIndexPath = ["one", "two", "three"]
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return wordAtIndexPath.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let word = wordAtIndexPath[indexPath.row]
let cell: UITableViewCell = {
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId) else {
return UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: cellId)
}
return cell
}()
cell.textLabel?.text = "My cell number"
cell.detailTextLabel?.text = word
return cell
}
}
I think your problem may be that you have registered a cell in interface builder (or in viewDidLoad) with the same name as 'cellIdentifier'.
You shouldn't register any cell if you want to use the subtitle type cell. By registering a cell, it will try to create that cell first (which will not be a subtitle type of cell).
I had been facing similar issue and found the most elegant way would be to subclass UITableViewCell if you're not using storyboards. And dequeue cell with that custom class.
Two Steps:
Create a subtitle Cell :
class DetailCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: "reuseIdentifier")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
Dequeue cell optionally casting it to that class as such:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) as? DetailCell else {
return UITableViewCell.init(style: .subtitle, reuseIdentifier: "reuseIdentifier")
}
// Configure the cell...
cell.textLabel?.text = "Title"
cell.detailTextLabel?.text = "Subtitle"
return cell
}

Swift 3 Breaks Cell For Row At Index Path

This code was from a now inactive tutorial that helped me load in data to a table view. Since the tutorial was written in Swift 2.0, I believe that this was changed in Swift 3. I know that the override function itself was changed, which I handled. But now, it brings me a Thread 1: EXC_BAD_INSTRUCTION(code=EXC_1386_INVOP, subcode=0x0) error.
Update: I have tried multiple things including creating a custom class for the cell. I still either get the same error I listed above, or a Thread 1: Signal SIGABRT error on the first line of my App Delegate file. Creating a breakpoint hasn't helped me because I know where the error is coming from.
import UIKit
import Firebase
import FirebaseDatabase
struct postStruct {
let title : String!
let message : String!
}
class LoggedInController: UITableViewController {
var posts = [postStruct]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Posts").queryOrderedByKey().observe(.childAdded, with: {
snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let title = snapshotValue!["title"] as? String
let message = snapshotValue!["message"] as? String
self.posts.insert(postStruct(title: title, message: message), at: 0)
self.tableView.reloadData()
})
post()
}
func post(){
let title = "Title"
let message = "Message"
let post : [String : AnyObject] = ["title" : title as AnyObject,
"message": message as AnyObject]
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Posts").childByAutoId().setValue(post)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell")
let label1 = cell?.viewWithTag(1) as! UILabel
label1.text = posts[indexPath.row].message
let label2 = cell?.viewWithTag(2) as! UILabel
label2.text = posts[indexPath.row].message
return cell!
}
}
Update 2: Here is the new code I used. It's not pretty and only gets the title.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell?.textLabel?.text = posts[indexPath.row].title
cell?.detailTextLabel?.text = posts[indexPath.row].message
return cell!
} else {
let label1 = cell?.viewWithTag(1) as? UILabel
label1?.text = posts[indexPath.row].title
let label2 = cell?.viewWithTag(2) as? UILabel
label2?.text = posts[indexPath.row].message
return cell!
}
}
Using dequeueReusableCell, you are accessing cell which doesn't exists. To make your code work change the below line:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
To
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
Ok this code let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") produces an optional called cell that may or may not contain a a valid instance of UITableViewCell. Optionals in Swift are a way to safeguard against nil values, you can read more about optionals here: Optionals
On the first run when your table view wants to load its data it calls all the required methods of your UITableViewDataSource. The first run is a critical one because there aren't any instances of the UITableViewCell the table view can dequeue yet. To solve your problem you have to do something similar to the code below:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
cell?.textLabel?.text = "New value"
cell?.detailTextLabel?.text = "New value"
return cell!
} else {
cell?.textLabel?.text = "" //reset value
cell?.detailTextLabel?.text = "" // resetValue
cell?.textLabel?.text = "New value"
cell?.detailTextLabel?.text = "New value"
return cell!
}
}
Code similar to the one above are usually used to programmatically add an instance of UITableViewCell. However, if you used interface builder to add a prototype cell use the let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) method to dequeue your cells in which case it does not return an optional and you do not need to do all the if / else blocks.
Something else I wanted to mention about your code is finding sub views buy their ID will not produce a very object oriented code and that maybe the source of your errors where the compiler can not find the sub views. The better way would be to use one of the built in instances of UITableViewCell such as .default or alliteratively you could subclass the said class and make your very own custom cells.
Hope this helped!
Try this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell:UITableViewCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
let label1 = cell.viewWithTag(1) as! UILabel
label1.text = posts[indexPath.row].message
let label2 = cell.viewWithTag(2) as! UILabel
label2.text = posts[indexPath.row].message
return cell
}
Edited
Make Sure you did these things
UITableViewController
In viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier:"catCell" )
let catNib : UINib = UINib.init(nibName: "TableViewCategoryCell", bundle: nil)
self.tableView.register(catNib, forCellReuseIdentifier: "TableViewCategoryCell")
In cellForRowAt indexPath
let cell : OPM_AlarmTableViewCategoryCell = tableView.dequeueReusableCell(withIdentifier:"OPM_AlarmTableViewCategoryCell" ) as! OPM_AlarmTableViewCategoryCell!
cell.categoryLabel?.text = "Some Text"
return cell
UITableviewCell.XIB
Hope u did these things
UITableviewCell
Make sure the dot appears so that the #IBOutlet is connected with the
xib label

ERROR unwrapping an Optional value AND dequeueReusableCellWithIdentifier(:cell identifier)

this is app for search
Not error before app running !
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell()
cell = tableView.dequeueReusableCellWithIdentifier("SpotListCell")!
if(cell.isEqual(NSNull))
{
cell = (NSBundle.mainBundle().loadNibNamed("SpotListCell", owner: self, options: nil)[0] as? UITableViewCell)!;
}
if tableView == self.tableView {
cell.textLabel?.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
} else {
cell.textLabel?.text = self.filteredPosts[indexPath.row]
}
return cell
}
Moment to run apps, searching the error.
The following error.
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
Where should be modified?
Thank you for reading it.
Note I am a Korean high school student.
This line is what is causing your troubles I guess:
cell = tableView.dequeueReusableCellWithIdentifier("SpotListCell")!
It seems your table view is unable to create a SpotListCell for you and since you add the ! you force the compiler to give you the value regardless of it being nil or not.
In the next line you then say:
if(cell.isEqual(NSNull))
but cell is nil so you can not ask it for anything (and besides...NSNull probably isn't what you're looking for).
Edit: updated my answer
First you should register you Nib so the UITableView can use it.
make an outlet to your UITableView and connect that:
#IBOutlet weak var tableView: UITableView!
Then in your viewDidLoad() you can do something like:
contentTableView.registerNib(UINib(nibName: "SpotListCell", bundle: nil), forCellReuseIdentifier: "SpotListCell")
And finally you can use your cell like this, notice the guard letto safely unwrap your cell:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("SpotListCell") else {
return UITableViewCell()
}
//Populate as you did before
if tableView == self.tableView {
cell.textLabel?.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
} else {
cell.textLabel?.text = self.filteredPosts[indexPath.row]
}
return cell
}
See if that is any better (and I haven't checked it with a compiler so there might be errors...I'm sure the compiler will let you know :))
Hope that helps you.

Swift didDeselectRowAtIndexPath not working on preset cell

I have a table I use that shows timezone options. I use a checkmark to show which one is currently selected. When the table is created I checkmark the cell that is saved as the users timezone.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cellData")
switch indexPath.row {
case 0: cell.textLabel?.text = "Eastern"
case 1: cell.textLabel?.text = "Central"
case 2: cell.textLabel?.text = "Mountain"
case 3: cell.textLabel?.text = "Mountain (No DST)"
case 4: cell.textLabel?.text = "Pacific"
default: cell.textLabel?.text = ""
}
cell.selectionStyle = UITableViewCellSelectionStyle.None
if(cell.textLabel?.text == keychain.get("timezone")) {
cell.accessoryType = UITableViewCellAccessoryType.Checkmark
}
return cell
}
Then I use these functions to change the checkmark when a user chooses a new timezone.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(indexPath)!.accessoryType = UITableViewCellAccessoryType.Checkmark
}
override func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(indexPath)!.accessoryType = UITableViewCellAccessoryType.None
}
However, when I preset the checkmark it is not removed when I choose a new timezone. It will only work if I first select it and then choose a new one. Is there a reason the original cell is not affected on deselect?
The reason your original cell isn't being deselected is that it's not selected in the first place. Enabling the checkmark accessory doesn't select the cell.
It's a bit of a pain to set up, but one way you could get this working is by storing a reference to the cell that needs to be selected, and when the view is going to appear, select it manually. Then, deselections will work.
Add a class variable to remember the cell that should be selected
var initiallySelectedPath: NSIndexPath?
Set the variable in your cellForRowAtIndexPath (personally, I'd do this setting elsewhere in the class due to how the actual cell selection is going to be performed, but this is good enough to demonstrate a solution.
...
if (cell.textLabel?.text == keychain.get("timezone")) {
cell.accessoryType = UITableViewCellAccessoryType.Checkmark
initiallySelectedPath = indexPath
}
...
Then, in your viewWillAppear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let indexPath = initiallySelectedPath {
// I force unwrap (sorry!) my tableView, you'll need to change this to however you reference yours
tableView!.selectRowAtIndexPath(indexPath, animated: false, scrollPosition: UITableViewScrollPosition.None)
}
}
Now your original cell should deselect with the first tap on a different cell.