TableView issue with reuse system and design programmatically - ios

I have a problem with my cells where I remove/insert to my tableView, I delete and insert my cells like that :
self.tableView.beginUpdates()
user.lobbySurvey.remove(at: 0)
self.tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
self.tableView.endUpdates()
Insert:
self.tableView.beginUpdates()
user.lobbySurvey.insert(surveyEnded, at: rowToInsert)
self.tableView.insertRows(at: [IndexPath(row: rowToInsert, section: 0)], with: .fade)
self.tableView.endUpdates()
lobbySurvey is the data array of the TableView.
Problem is the cell I add keep the design of the first cell that I delete previously.
I think this issue is because I check if the card is draw or not. If not checked: the view of the card is added each time the cell is reuse.
this is how I check if the cell is draw or not :
func drawSurveyEnded(){
if(cardIsDraw == false){
surveyEnded.draw(cardView: self.cardView)
surveyEnded.delegate = self
self.addCardShadow()
cardIsDraw = true
}
}
The function is called many times cause to the reuse system of iOS. So I think my problem come from here.
Why my cell in my tableView keep the design of the previous cell that I deleted?
Other code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:"Celll") as! CardCell
for survey in user.lobbySurvey{
let index = user.lobbySurvey.index(where: {
//get the current index is nedeed else the cells reuse lazy
$0 === survey
})
if indexPath.row == index{
var surveyState : UserSurvey.state
surveyState = survey.state
switch surveyState{
case .selectSurvey:
cell.drawCard(statutOfCard: .selectSurvey)
case .goSurvey:
cell.drawCard(statutOfCard: .goSurvey(picture: survey.picture))
case .surveyEnded:
cell.drawCard(statutOfCard: .surveyEnded(picture: survey.picture))
case .surveyWork:
print("survey in progress to vote")
case .surveyWaiting:
cell.drawCard(statutOfCard: .surveyWaiting(selfSurveyId: survey.id, timeLeft: survey.timeLeft, picture: survey.picture))
case .buyStack:
cell.drawCard(statutOfCard: .buyStack(supView : self.view))
}
}
}
cell.delegate = self
cell.delegateCard = self
cell.layer.backgroundColor = UIColor.clear.cgColor
cell.backgroundColor = .clear
tableView.backgroundColor = .clear
tableView.layer.backgroundColor = UIColor.clear.cgColor
return cell
}
Draw card is a simple switch of the state of the survey where called the good function to create the card like this one I posted before : drawSurveyEnded

In your CardCell you have to implement prepareForReuse and clean the cell's view from there. So when cells are recycled, they are cleaned from the previous views that were drawn.

You should implement:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Update the cell here
}
This way you can make sure the cell gets updated whenever it is ready to be shown.
Remember the UIKit framework keeps a cache for the cells, so it can just reuse them.

Related

When calling prepareForReuse() in CustomTableViewCell.swift, UITableViewCells become unresponsive after several deletions from UITableView (Swift 3)

I have an app that pulls objects from Firebase, then displays them in a table. I've noticed that if I delete 5 entries (this is about when I get to the reused cells that were deleted), I can't delete any more (red delete button is unresponsive) & can't even select the cells. This behavior stops when I comment out override func prepareForReuse() in the TableViewCell.swift controller. Why???
The rest of the app functions normally while the cells are just unresponsive. Weirdly, if I hold one finger on a cell and tap the cell with another finger, I can select the cell. Then, if I hold a finger on the cell and tap the delete button, that cell starts acting normally again. What is happening here??? Here is my code for the table & cells:
In CustomTableViewCell.swift >>
override func prepareForReuse() {
// CELLS STILL FREEZE EVEN WHEN THE FOLLOWING LINE IS COMMENTED OUT?!?!
cellImage.image = nil
}
In ViewController.swift >>
override func viewDidLoad() {
super.viewDidLoad()
loadUserThings()
}
func loadUserThings() {
ref.child("xxx").child(user!.uid).child("yyy").queryOrdered(byChild: "aaa").observe(.value, with: { (snapshot) in
// A CHANGE WAS DETECTED. RELOAD DATA.
self.arr = []
for tempThing in snapshot.children {
let thing = Thing(snapshot: tempThing as! DataSnapshot)
self.arr.append(thing)
}
self.tableView.reloadData()
}) { (error) in
print(error)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomTableViewCell
let cellData = arr[indexPath.row]
...
// SET TEXT VALUES OF LABELS IN THE CELL
...
// Setting image to nil in CustomTableViewCell
let imgRef = storageRef.child(cellData.imgPath)
let activityIndicator = MDCActivityIndicator()
// Set up activity indicator
cell.cellImage.sd_setImage(with: imgRef, placeholderImage: nil, completion: { (image, error, cacheType, ref) in
activityIndicator.stopAnimating()
delay(time: 0.2, function: {
UIView.animate(withDuration: 0.3, animations: {
cell.cellImage.alpha = 1
})
})
})
if cell.cellImage.image == nil {
cell.cellImage.alpha = 0
}
// Seems like sd_setImage doesn't always call completion block if the image is loaded quickly, so we need to stop the loader before a bunch of activity indicators build up
delay(time: 0.2) {
if cell.cellImage.image != nil {
activityIndicator.stopAnimating()
cell.cellImage.alpha = 1
}
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// instantly deselect row to allow normal selection of other rows
tableView.deselectRow(at: tableView.indexPathForSelectedRow!, animated: false)
selectedObjectIndex = indexPath.row
self.performSegue(withIdentifier: "customSegue", sender: self)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
print("should delete")
let row = indexPath.row
let objectToDelete = userObjects[row]
userObjects.remove(at: row)
ref.child("users/\(user!.uid)/objects/\(objectToDelete.nickname!)").removeValue()
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
if (self.tableView.isEditing) {
return UITableViewCellEditingStyle.delete
}
return UITableViewCellEditingStyle.none
}
A few things. For performance reasons, you should only use prepareForReuse to reset attributes that are related to the appearance of the cell and not content (like images and text). Set content like text and images in cellForRowAtIndexPath delegate of your tableView and reset cell appearance attributes like alpha, editing, and selection state in prepareForReuse. I am not sure why it continues to behave badly when you comment out that line and leave prepareForReuse empty because so long as you are using a custom table view cell an empty prepareForReuse should not affect performance. I can only assume it has something to do with you not invoking the superclass implementation of prepareForReuse, which is required by Apple according to the docs:
override func prepareForReuse() {
// CELLS STILL FREEZE EVEN WHEN THE FOLLOWING LINE IS COMMENTED OUT?!?!
super.prepareForReuse()
}
The prepareForReuse method is only ever intended to do minor cleanup for your custom cell.

fatal error: Index out of range on second cell deletion

In my custom cell I have a timer. When the count down reach 0, I call my delegate method and the cell is automatically deleted.
The problem is that when the second cell reach 0, my app crashes with the error fatal error: Index out of range.
In my custom cell I setup my data:
protocol MyDelegateName {
func removeOfferExpired(offerId: String, indexPath: IndexPath)
}
class MyCustomCell: UITableViewCell {
var offer:Offers?
var cellIndexPath:IndexPath?
var delegate:MyDelegateName?
func setupData(offer:Offers, indexPath:IndexPath){
self.offer = offer
self.cellIndexPath = indexPath
//...other code not relevant
}
//When the time reach zero I call the following method
func updateTime() {
if timeLeft > 0 {
timeLeft = endTime.timeIntervalSinceNow
offerExpiresLabel.textColor = UIColor.white
offerExpiresLabel.text = timeLeft.hmmss
}else {
offerExpiresLabel.textColor = UIColor.red
offerExpiresLabel.text = "Offer Expired"
timer.invalidate()
self.delegate?.removeOfferExpired(offerId: (self.offer?.offer_id!)!, indexPath: self.cellIndexPath!)
}
}
In my ViewController I setup my cell data inside cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let offer = offers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! MyCustomCell
cell.setupData(offer: offer, indexPath: indexPath)
cell.delegate = self
return cell
}
Then inside func removeOfferExpired(offerId: String, indexPath: IndexPath) I have tried to use:
1. self.offers.remove(at: indexPath.row)
self.tableView.reloadData()
2. self.offers.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.tableView.reloadData()
3. //and even try to "wrap" it inside begin/end updates
tableView.beginUpdates()
self.offers.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
it always crashes the second times. I understand that the indexPath I assign to the cell in setupData is not the same after the first cell is deleted but I thought reloadData was the way to go to update the indexPath in the remaining cells.
Your primary issue is that fact that you tell a cell its index path and your cell then passes that index path to its delegate. But a cell's index path isn't stable. It changes as other rows are added, removed, or moved.
The method of your cell protocol should pass itself (the cell) as a parameter, not an index path. Then the delegate can query the table view to find the cell's up-to-date index path and perform the row deletion based on that up-to-date index path.
As rmaddy said, what I was doing it was completely wrong. This is what I did based on his answer:
func updateTime() {
if timeLeft > 0 {
timeLeft = endTime.timeIntervalSinceNow
offerExpiresLabel.textColor = UIColor.white
offerExpiresLabel.text = timeLeft.hmmss
}else {
offerExpiresLabel.textColor = UIColor.red
offerExpiresLabel.text = "Offer Expired"
timer.invalidate()
// when the time reach zero I passed self to the delegate instead of the indexPath
self.delegate?.removeOfferExpired(offerId: (self.offer?.offer_id!)!, cell: self as UITableViewCell)
}
}
protocol MyDelegateName {
func removeOfferExpired(offerId: String, cell: UITableViewCell) // delegate method now passes the cell instead of the index
}
func removeOfferExpired(offerId: String, cell: UITableViewCell) {
// and then I get the index path from the cell
let indexPath = tableView.indexPath(for: cell)
self.offers.remove(at: (indexPath?.row)!)
self.tableView.deleteRows(at: [indexPath!], with: .automatic)
}

Change properties of previous cells in tableView

I have a tableView, designed in storyboard, that mimics a chat UI. A cell consists of:
A TextView for the message text
A Profile Image of the sender
Right now, the profile image is displayed in every cell, next to the text bubble. This is fine, but if the same users send two or more messages directly after the other, the profile image should only appear on the last bubble and not on the previous one.
I tried calling cellForRowAtIndexPath to get the previous cell's properties and change the hidden property of the profile image, but this gave me two problems:
I'm calling cellForRowAtIndexPath inside cellForRowAtIndexPath, because that's where I make the cell UI and decide wether the profile image has to be hidden or not. I don't think it's a good idea to call this Method inside itself.
Sometimes (when scrolling up and down very fast) this does not work properly.
I also tried to store all the cells in an dictionary (indexPath.row: Cell), so I can access it faster later, but this gave me the same problem namely that it does not work when scrolling up and down really fast.
This is an illustration of how it should be: http://tinypic.com/view.php?pic=2qavj9w&s=8#.Vfcpi7yJfzI
You need to both look ahead inside of your cellForRowAtIndexPath method and, as Paulw11 recommended, call reloadRowsAtIndexPaths after inserting the cell:
import UIKit
struct MyMessage {
let sender: String
let text: String
}
class MyTableViewCell: UITableViewCell {
var message: MyMessage?
var showProfileImage: Bool = false
}
class MyTableViewController: UITableViewController {
private var _messages: [MyMessage] = []
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self._messages.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let message = self._messages[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! MyTableViewCell
cell.message = message
if self._messages.count > indexPath.row + 1 {
let nextMessage = self._messages[indexPath.row + 1]
cell.showProfileImage = message.sender != nextMessage.sender
} else {
cell.showProfileImage = true
}
return cell
}
func addMessage(message: MyMessage) {
let lastIndexPath = NSIndexPath(forRow: self._messages.count - 1, inSection: 0)
let indexPath = NSIndexPath(forRow: self._messages.count, inSection: 0)
self._messages.append(message)
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Bottom)
self.tableView.reloadRowsAtIndexPaths([lastIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.endUpdates()
}
}

Updating data source after animating cell removal from UITableView

I have a button in my custom cell that deletes the cell.
So i have a delegate that removes it.
code in view controller:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("swipeTableViewCell", forIndexPath: indexPath) as! swipeTableViewCell
cell.initCell(self, indexPath: indexPath, text: data[indexPath.row])
return cell
}
delegate method:
func removeCell(indexPath: NSIndexPath){
data.removeAtIndex(indexPath.row)
table.beginUpdates()
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
table.endUpdates()
}
code in cell:
func initCell(handler: handleCells, indexPath: NSIndexPath, text: String) {
self.handler = handler
self.indexPath = indexPath
}
button pressed:
#IBAction func OnDelButtonClickListener(sender: UIButton) {
self.handler.removeCell(indexPath)
}
This removes the cell with animation but the reloadData is not called and then the cells have the wrong indexPath.
So when I press a second cells delete the wrong cell gets removed.
If I call reloadData after table.endUpdates() there is no animation.
if I call
let indexSet = NSIndexSet(index: indexPath.section)
self.table.reloadSections(indexSet, withRowAnimation: UITableViewRowAnimation.Automatic)
instead of
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
I don't get a removal animation.
Any suggestions?
Thanks
Have a look at Apple's programming guide for UITableViews, at the row deleting section.
I may be missing something in your code, but it looks like you don't actually delete the object in the datasource that corresponds to your deleted cell. Try removing the object from your datasource in the removeCell function before you delete the row.
func removeCell(indexPath: NSIndexPath){
// here you delete the object form the datasource
// after that, you do this
table.beginUpdates()
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
table.endUpdates()
}
I think the key problem is in the Cell indexPath could not update when the table view delete the cell.
so we can try create a help array in the ViewController,help us update the really data to delate.
lazy var listHelper:Array<Int> = {
var array = [Int]()
for i in 0...self.data.count {
array.append(i)
}
return array
}()
update the removeCell function to this :
func removeCell(indexPath: NSIndexPath) {
// if first delete delete the date, and remove index in help list
if indexPath.row < listHelper.count - 1 && indexPath.row == listHelper[indexPath.row] {
data.removeAtIndex(indexPath.row)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
listHelper.removeAtIndex(indexPath.row)
}else {
// if indexPath.row != listHelper[indexPath.row],we find the really data we want to delete, used Array extension .indexOf
let locationData = listHelper.indexOf(indexPath.row)
data.removeAtIndex(locationData!)
// we create NSIndexPath and delete it.
let theindexPath = NSIndexPath(forRow: locationData!, inSection: 0)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([theindexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
listHelper.removeAtIndex(locationData!)
}
}
the Extension of Array :
extension Array {
func indexOf <U: Equatable> (item: U) -> Int? {
if item is Element {
return Swift.find(unsafeBitCast(self, [U].self), item)
}
return nil
}
}
My English is poor. and U can see the code . I had try it, and it can work. I hope can solve you problem.

Expand cell when tapped in Swift

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
}

Resources