My following code is part of a search controller. It works without a problem but after 8-10 searches, I encounter fatal error (Thread 1 : EXC_BAD_INSTRUCTION) on following line :
let movie = filteredMovies[indexPath.item]
Could you please advise the way of solving this kind of problems.
extension searchResults: UITableViewDataSource, UITableViewDelegate {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("searchCell", forIndexPath: indexPath) as! searchResultCell
let movie = filteredMovies[indexPath.item]
cell.searchLabel.text = movie.title
let fileUrl = NSURL(string: movie.thumb)!
if let data = NSData(contentsOfURL: fileUrl)
{
cell.searchImage.contentMode = UIViewContentMode.ScaleAspectFit
cell.searchImage.image = UIImage(data: data)
// to make images rounded
cell.searchImage.backgroundColor = UIColor.clearColor()
cell.searchImage.layer.cornerRadius = 15.0
cell.searchImage.clipsToBounds = true
cell.backgroundColor = UIColor.clearColor()
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return filteredMovies.count
}
}
You can check if indexPath.item is within the bounds of the array filteredMovies:
if (indexPath.item >= 0 || indexPath.item < movie.count) {
// now you can safely use filteredMovies[indexPath.item]
} else {
// print something so you can investigate
}
And if you wonder why indexPath.item might be out of the bounds of filteredMovies, it's other programming logic you have to investigate (maybe you remove some elements in filteredMovies after the tableView is loaded).
After all, always checking the bounds is a good thing when you have doubt or want to make sure it will never cause crash (fault tolerance concept).
Related
My tableView displays list of cards
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = cardsTable.dequeueReusableCellWithIdentifier("SelectableCardCell", forIndexPath: indexPath) as! CardSelectionTableViewCell
let cardCell: Card!
if self.delegate is CardsApprovalsViewController {
cardCell = approvalCards[indexPath.row] // approvalCard array of type card
} else {
cardCell = fetchedResultsController.objectAtIndexPath(indexPath) as! Card
}
cell.policyHolderName.text = cardCell.policy_holder_name
cell.alphaMark.text = cardCell.policy_holder_name.substringToIndex(advance(cardCell.policy_holder_name.startIndex, 1)).uppercaseString
var image: NSData = cardCell.photo as NSData
cell.picture.highlightedImage = UIImage(data: image)
cell.cardNumber.text = cardCell.member_id
cell.policyNumberLabel.text = cardCell.policy_no
cell.insuranceNameLabel.text = cardCell.insurance.company_name
return cell
}
Afetr selecting one cell, i want cell.alphaMark label to be hidden in order to display cell.picture.highlightedImage
and if the user select another cell, cell.alphaMark for the previous cell should appear again
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var selectedCell: CardSelectionTableViewCell = cardsTable.cellForRowAtIndexPath(indexPath) as! CardSelectionTableViewCell
selectedCell.alphaMark.hidden = true
let newSwiftColor = UIColor(red: 224, green: 227, blue: 224)
selectedCell.contentView.backgroundColor = newSwiftColor
if self.delegate is CardsApprovalsViewController {
self.card = approvalCards[indexPath.row]
} else {
self.card = fetchedResultsController.objectAtIndexPath(indexPath) as! Card
}
}
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
var selectedCell = cardsTable.cellForRowAtIndexPath(indexPath) as! CardSelectionTableViewCell // Some times crashes here
selectedCell.alphaMark.hidden = false
}
When i select cell and the select another cell ...etc (Selecting cells repeatedly) alphaMark and picture.highlightedImage works fine, but then it stops and gives me "unexpectedly found nil while unwrapping an Optional value" error
I checked the outlets of the cell and they are connected to the storyboard
Can someone help me with this error ?
Thanks
The easiest thing to do to prevent the crashing, is to change from forced unwrapping of the optional and use an if let instead:
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
if let selectedCell = cardsTable.cellForRowAtIndexPath(indexPath) as? CardSelectionTableViewCell {
selectedCell.alphaMark.hidden = false
}
}
This will take into account that cardsTable.cellForRowAtIndexPath(indexPath) may return nil, so trying to unwrap it into CardSelectionTableViewCell won't fail (because you can't unwrap nil into a CardSelectionTableViewCell)
That will prevent the crashing, but it won't entirely accomplish what you are trying to do. You'll also need to update cellForRowAtIndexPath to check if the current cell is selected and update alphaMark.hidden appropriately.
So one quick crack at how to do that would be to add this to the end of cellForRowAtIndexPath so that each time the cell is brought on screen, it's alphaMark is conditionally hidden based on the cell being selected:
cell.alphaMark.hidden = cell.selected
I'm creating a simple messenger app and using uitableview to display messages. I have created two XIB files for two types of cells i.e. incoming and outgoing. Also I have added constraints to each cell to make textViews to be not at full width of the screen (picture below)
The problem is, when i scroll table view up and down i see some jumps of content (i.e. bad performance). I made all views opaque, but it didn't help. As data storage i'm using Realm.io, to fetch messages from it. I get group_key, and list messages in this group.
Here's my code:
extension MessageChatViewController : UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count = 0
if !groupKey.isEmpty {
let messagesForGroupInRealm = realm.objects(Messages).filter("group_key = '\(groupKey)'")
if messagesForGroupInRealm.count != 0 {
count += messagesForGroupInRealm.count
}
}
return count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let messagesForGroupInRealm = messagesForCurrentChat.sorted("created_at", ascending: true)
if messagesForGroupInRealm.count != 0 {
if indexPath.row < messagesForGroupInRealm.count {
let message = messagesForGroupInRealm[indexPath.row]
if message.owner_key == gCredentials["account_key"] {
let cell = tableView.dequeueReusableCellWithIdentifier("OutgoingChatMessageCell", forIndexPath: indexPath) as! OutgoingChatMessageCell
cell.outgoingTextView.text = message.content
cell.outgoingTextView.font = UIFont(name: "NotoSans", size: 16)
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("IncomingChatMessageCell", forIndexPath: indexPath) as! IncomingChatMessageCell
cell.incomingTextView.text = message.content
cell.incomingTextView.font = UIFont(name: "NotoSans", size: 16)
return cell
}
}
}
return UITableViewCell()
}
}
I also use dynamic height of cells:
override func viewDidLoad() {
super.viewDidLoad()
....
tableView.registerNib(UINib(nibName: "OutgoingChatMessageCell", bundle: nil), forCellReuseIdentifier: "OutgoingChatMessageCell")
tableView.registerNib(UINib(nibName: "IncomingChatMessageCell", bundle: nil), forCellReuseIdentifier: "IncomingChatMessageCell")
....
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44.0
....
}
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
So is there any way to get best performance for tableView? Maybe i need somehow cache something ?:) Or maybe i made some mistake... Thanks for any advices (for Obj-C also)!
I seems like you are re-querying and sorting the entire dataset every time you draw a cell (or get a count). That would be a lot of unneeded overhead and could easily slow things down.
Why don't you make messagesForGroupInRealm a class member? Then you only have to do the query once (on init), and can then just keep accessing it directly from there.
EDITED CODE FROM BELOW ANSWER
WHAT I WANT
The images are correct size and show in the correct positions
WHAT I GET
A bunch of images showing up everywhere randomly when I scroll
I have looked through so many answers and I still haven't found a solution to my problem. It seems quite often it is a different problem for each person, and it doesn't help that most questions here are in Objective-C. I find it difficult converting from one to another.
I will just show my table view functions and explain my code and maybe somebody can identify the problem or help me to identify to solve it myself.
-----------------------------------------------------------------------------------
Explanation
Not all News Items (cells) contain pictures. So as you can see (in the first function below) I loop through the sections and the nested cells and if the news item contains a picture if newslists[i][j] != "None, I display the image. From this point it is familiar code, seen from the tutorial here
In the second function I once again loop through the sections and the cells till I land on one that should contain an image. I then change the height of this cell so the image will fit inside.
That is pretty much it...
any ideas?
-----------------------------------------------------------------------------------
CellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("NewsCell", forIndexPath: indexPath) as! UITableViewCell
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
let urlString = newslists[indexPath.section][indexPath.row].imageURL
let imgURL = NSURL(string: urlString)
cell.imageView?.image = UIImage(named: "first") // placeholder
if let img = imageCache[urlString] {
cell.imageView?.image = img
println("loaded from cache")
}
else {
let request: NSURLRequest = NSURLRequest(URL: imgURL!)
let mainQueue = NSOperationQueue.mainQueue()
NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
if error == nil {
let image = UIImage(data: data)
self.imageCache[urlString] = image
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
println("succesfully downloaded image")
println("size of cache is \(self.imageCache.count)")
}
})
}
else {
println("Error: \(error.localizedDescription)")
}
})
}
}
cell.backgroundColor = UIColor(red: 52/255, green: 138/255, blue: 169/255, alpha: 1.0)
return cell
}
I have one more function that may be causing problems but I doubt it:
HeightForRowAtIndexPath
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
println("Expanding cell for \(indexPath.section) \(indexPath.row)")
return 190.0
}
return 70.0
}
I'm not sure why you have a double for loop in both method. There are called for every indexPath possible so you can just get you appropriate data with the indexPath section and row.
You can try this code, i don't see any reason for it to not work.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("NewsCell", forIndexPath: indexPath) as! UITableViewCell
cell.imageView?.image = nil
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
let urlString = newslists[indexPath.section][indexPath.row].imageURL
let imgURL = NSURL(string: urlString)
cell.imageView?.image = UIImage(named: "first") // placeholder
if let img = imageCache[urlString] {
cell.imageView?.image = img
println("loaded from cache")
}
else {
...
}
}
return cell
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
println("Expanding cell for \(i) \(j)")
return 190.0
}
return 70.0
}
All we need to do is use the prepareForReuse() function. As discussed in this medium article, This function is called before cell reuse, letting you cancel current requests and perform a reset.
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
I'm working on my first Swift app, and stuck on a bug forever. When adding a new entry to the coredata, everything goes fine the first time. However, with additional items, the previously added item is duplicated in the table.
The data is not duplicated, only the cell. When the app is reloaded, the cells are displayed correctly.
Here's the code that populates the cells:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let fetchedSections: AnyObject = fetchedResultController.sections as AnyObject? {
return fetchedSections.count
} else {
return 0
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let fetchedSections: AnyObject = fetchedResultController.sections as AnyObject? {
return fetchedSections[section].numberOfObjects
} else {
return 0
}
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let lookup = sortedArray[indexPath.row] as! NSManagedObjectID
let spot = spotsDict[lookup] as! Spots
let cell = tableView.dequeueReusableCellWithIdentifier("SpotCell", forIndexPath:indexPath) as! SpotCell
println(sortedArray)
println(spot)
cell.spotLabel.text = spot.title
cell.spotPhoto.image = self.imagesDict[lookup]
cell.distanceLabel.text = self.distanceStringDict[lookup] as NSString! as String
cell.spotPhoto.layer.cornerRadius = 4
cell.spotPhoto.clipsToBounds = true
return cell
}
Please replace your code
let lookup = sortedArray[indexPath.row] as! NSManagedObjectID
with below code in cellForRowAtIndexPath method.
let lookup = sortedArray[indexPath.section] as! NSManagedObjectID
The root of the problem was in my sorting method.. simplified the code a lot, and sorted with sortDescriptors instead.
I have been trying to implement a feature in my app so that when a user taps a cell in my table view, the cell expands downwards to reveal notes. I have found plenty of examples of this in Objective-C but I am yet to find any for Swift.
This example seems perfect: Accordion table cell - How to dynamically expand/contract uitableviewcell?
I had an attempt at translating it to Swift:
var selectedRowIndex = NSIndexPath()
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRowIndex = indexPath
tableView.beginUpdates()
tableView.endUpdates()
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if selectedRowIndex == selectedRowIndex.row && indexPath.row == selectedRowIndex.row {
return 100
}
return 70
}
However this just seems to crash the app.
Any ideas?
Edit:
Here is my cellForRowAtIndexPath code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:CustomTransactionTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomTransactionTableViewCell
cell.selectionStyle = UITableViewCellSelectionStyle.None
if tableView == self.searchDisplayController?.searchResultsTableView {
cell.paymentNameLabel.text = (searchResults.objectAtIndex(indexPath.row)) as? String
//println(searchResults.objectAtIndex(indexPath.row))
var indexValue = names.indexOfObject(searchResults.objectAtIndex(indexPath.row))
cell.costLabel.text = (values.objectAtIndex(indexValue)) as? String
cell.dateLabel.text = (dates.objectAtIndex(indexValue)) as? String
if images.objectAtIndex(indexValue) as NSObject == 0 {
cell.paymentArrowImage.hidden = false
cell.creditArrowImage.hidden = true
} else if images.objectAtIndex(indexValue) as NSObject == 1 {
cell.creditArrowImage.hidden = false
cell.paymentArrowImage.hidden = true
}
} else {
cell.paymentNameLabel.text = (names.objectAtIndex(indexPath.row)) as? String
cell.costLabel.text = (values.objectAtIndex(indexPath.row)) as? String
cell.dateLabel.text = (dates.objectAtIndex(indexPath.row)) as? String
if images.objectAtIndex(indexPath.row) as NSObject == 0 {
cell.paymentArrowImage.hidden = false
cell.creditArrowImage.hidden = true
} else if images.objectAtIndex(indexPath.row) as NSObject == 1 {
cell.creditArrowImage.hidden = false
cell.paymentArrowImage.hidden = true
}
}
return cell
}
Here are the outlet settings:
It took me quite a lot of hours to get this to work. Below is how I solved it.
PS: the problem with #rdelmar's code is that he assumes you only have one section in your table, so he's only comparing the indexPath.row. If you have more than one section (or if you want to already account for expanding the code later) you should compare the whole index, like so:
1) You need a variable to tell which row is selected. I see you already did that, but you'll need to return the variable to a consistent "nothing selected" state (for when the user closes all cells). I believe the best way to do this is via an optional:
var selectedIndexPath: NSIndexPath? = nil
2) You need to identify when the user selects a cell. didSelectRowAtIndexPath is the obvious choice. You need to account for three possible outcomes:
the user is tapping on a cell and another cell is expanded
the user is tapping on a cell and no cell is expanded
the user is tapping on a cell that is already expanded
For each case we check if the selectedIndexPath is equal to nil (no cell expanded), equal to the indexPath of the tapped row (same cell already expanded) or different from the indexPath (another cell is expanded). We adjust the selectedIndexPath accordingly. This variable will be used to check the right rowHeight for each row. You mentioned in comments that didSelectRowAtIndexPath "didn't seem to be called". Are you using a println() and checking the console to see if it was called? I included one in the code below.
PS: this doesn't work using tableView.rowHeight because, apparently, rowHeight is checked only once by Swift before updating ALL rows in the tableView.
Last but not least, I use reloadRowsAtIndexPath to reload only the needed rows. But, also, because I know it will redraw the table, relayout when necessary and even animate the changes. Note the [indexPath] is between brackets because this method asks for an Array of NSIndexPath:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
println("didSelectRowAtIndexPath was called")
var cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCustomTableViewCell
switch selectedIndexPath {
case nil:
selectedIndexPath = indexPath
default:
if selectedIndexPath! == indexPath {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
}
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
3) Third and final step, Swift needs to know when to pass each value to the cell height. We do a similar check here, with if/else. I know you can made the code much shorter, but I'm typing everything out so other people can understand it easily, too:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let smallHeight: CGFloat = 70.0
let expandedHeight: CGFloat = 100.0
let ip = indexPath
if selectedIndexPath != nil {
if ip == selectedIndexPath! {
return expandedHeight
} else {
return smallHeight
}
} else {
return smallHeight
}
}
Now, some notes on your code which might be the cause of your problems, if the above doesn't solve it:
var cell:CustomTransactionTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomTransactionTableViewCell
I don't know if that's the problem, but self shouldn't be necessary, since you're probably putting this code in your (Custom)TableViewController. Also, instead of specifying your variable type, you can trust Swift's inference if you correctly force-cast the cell from the dequeue. That force casting is the as! in the code below:
var cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier" forIndexPath: indexPath) as! CustomTransactionTableViewCell
However, you ABSOLUTELY need to set that identifier. Go to your storyboard, select the tableView that has the cell you need, for the subclass of TableViewCell you need (probably CustomTransactionTableViewCell, in your case). Now select the cell in the TableView (check that you selected the right element. It's best to open the document outline via Editor > Show Document Outline). With the cell selected, go to the Attributes Inspector on the right and type in the Identifier name.
You can also try commenting out the cell.selectionStyle = UITableViewCellSelectionStyle.None to check if that's blocking the selection in any way (this way the cells will change color when tapped if they become selected).
Good Luck, mate.
The first comparison in your if statement can never be true because you're comparing an indexPath to an integer. You should also initialize the selectedRowIndex variable with a row value that can't be in the table, like -1, so nothing will be expanded when the table first loads.
var selectedRowIndex: NSIndexPath = NSIndexPath(forRow: -1, inSection: 0)
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == selectedRowIndex.row {
return 100
}
return 70
}
Swift 4.2 var selectedRowIndex: NSIndexPath = NSIndexPath(row: -1, section: 0)
I suggest solving this with modyfing height layout constraint
class ExpandableCell: UITableViewCell {
#IBOutlet weak var img: UIImageView!
#IBOutlet weak var imgHeightConstraint: NSLayoutConstraint!
var isExpanded:Bool = false
{
didSet
{
if !isExpanded {
self.imgHeightConstraint.constant = 0.0
} else {
self.imgHeightConstraint.constant = 128.0
}
}
}
}
Then, inside ViewController:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.estimatedRowHeight = 2.0
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.tableFooterView = UIView()
}
// TableView DataSource methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:ExpandableCell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell") as! ExpandableCell
cell.img.image = UIImage(named: indexPath.row.description)
cell.isExpanded = false
return cell
}
// TableView Delegate methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
else { return }
UIView.animate(withDuration: 0.3, animations: {
tableView.beginUpdates()
cell.isExpanded = !cell.isExpanded
tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true)
tableView.endUpdates()
})
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
else { return }
UIView.animate(withDuration: 0.3, animations: {
tableView.beginUpdates()
cell.isExpanded = false
tableView.endUpdates()
})
}
}
Full tutorial available here
A different approach would be to push a new view controller within the navigation stack and use the transition for the expanding effect. The benefits would be SoC (separation of concerns). Example Swift 2.0 projects for both patterns.
https://github.com/justinmfischer/SwiftyExpandingCells
https://github.com/justinmfischer/SwiftyAccordionCells
After getting the index path in didSelectRowAtIndexPath just reload the cell with following method
reloadCellsAtIndexpath
and in heightForRowAtIndexPathMethod check following condition
if selectedIndexPath != nil && selectedIndexPath == indexPath {
return yourExpandedCellHieght
}