I am currently developing and application for the iPhone in Swift, and I have run into a very peculiar error: in one of my UITableViewControllers,enter code here cells disappear or change sections when I scroll up and down on the Table View.
I've been dealing with this issue for a few days now, and it has even prompted me to recode the entire class, but to no avail. I have researched extensively on this error, and I believe it has something to do with my data source and how the tableView handles it, and I have also noticed that other users have had the same problem before, but I cannot find a solution that applies to my problems.
For example, here seems to deal with the cell height, but I have continued to check and double check my code, and the cell height returns the correct values.
In addition, this post talks about different errors with the tableView's data source, but I have a strong pointer to the datasource's alert array and my content and heights are correct in cellForRowAtIndexPath.
This post also deals with my question, but everything I am currently doing with the tableView is on the main thread.
Currently the tableView has 4 sections: the first, second, and fourth contain only one cell and the third has a dynamic amount of cells based on the amount of alerts the user has added (for example, it has 3 alert cells plus one "Add Alert" cell always at the bottom). The only cells that are affected are those in the 2, 3, and 4 sections.
This is what my tableView should look like always:
But, however, here is what happens when I scroll:
I first create the variables here:
var currentPrayer: Prayer! // This is the prayer that the user is currently editing
var prayerAlerts: NSMutableOrderedSet! // This is the mutable set of the prayer alerts that are included in the prayer
Then I initialize them in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
if currentPrayer == nil {
NSException(name: "PrayerException", reason: "Current Prayer is nil! Unable to show prayer details!", userInfo: nil).raise()
}
navItem.title = currentPrayer.name // Sets the Nav Bar title to the current prayer name
prayerAlerts = currentPrayer.alerts.mutableCopy() as! NSMutableOrderedSet // This passes the currentPrayer alerts to a copy called prayerAlerts
prayerAlertsCount = prayerAlerts.count + 1
}
Below are my TableView methods:
Here is my cellForRowAtIndexPath:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
println("cellForRowAtIndexPath called for the \(cellForRowRefreshCount) time")
cellForRowRefreshCount += 1
switch indexPath.section {
case 0:
var cell = tableView.dequeueReusableCellWithIdentifier(DetailsExtendedCellID, forIndexPath: indexPath) as! PrayerDetailsExtendedCell
cell.currentPrayer = currentPrayer
cell.refreshCell()
return cell
case 1:
var cell = tableView.dequeueReusableCellWithIdentifier(SetPrayerDateCellID, forIndexPath: indexPath) as! AddPrayerDateCell
cell.currentPrayer = currentPrayer
cell.refreshCell(false, selectedPrayer: cell.currentPrayer)
return cell
case 2:
if indexPath.row == prayerAlerts.count {
var cell = tableView.dequeueReusableCellWithIdentifier(AddNewAlertCellID, forIndexPath: indexPath) as! AddPrayerAlertCell
cell.currentPrayer = currentPrayer
cell.refreshCell(false, selectedPrayer: currentPrayer)
cell.saveButton.addTarget(self, action: "didSaveNewAlert", forControlEvents: .TouchDown)
return cell
} else {
var cell = tableView.dequeueReusableCellWithIdentifier(PrayerAlertCellID, forIndexPath: indexPath) as! PrayerAlertCell
let currentAlert = prayerAlerts[indexPath.row] as! Alert
cell.alertLabel.text = AlertStore.sharedInstance.convertDateToString(currentAlert.alertDate)
return cell
}
case 3:
var cell = tableView.dequeueReusableCellWithIdentifier(AnsweredPrayerCellID, forIndexPath: indexPath) as! PrayerAnsweredCell
cell.accessoryType = currentPrayer.answered == true ? .Checkmark : .None
return cell
default:
return UITableViewCell()
}
}
And my numberOfRowsInSection:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: println("Returning 1 Row for section 0"); return 1
case 1: println("Returning 1 Row for section 1"); return 1
case 2: println("Returning \(prayerAlertsCount) Rows for section 2"); return prayerAlertsCount
case 3: println("Returning 1 Row for section 3"); return 1
default: println("Returning 0 Rows for section Default"); return 0
}
}
And my heightForRowAtIndexPath:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.section {
case 0: return UITableViewAutomaticDimension
case 1:
let cell = tableView.cellForRowAtIndexPath(indexPath) as? AddPrayerDateCell
if let thisCell = cell {
let isAdding = thisCell.isAddingDate
if isAdding {
if thisCell.selectedType == PrayerType.None || thisCell.selectedType == PrayerType.Daily {
println("Selected Type is None or Daily")
println("Returning a height of 89 for AddPrayerDateCell")
return 89
} else {
println("Returning a height of 309 for AddPrayerDateCell")
return 309
}
} else {
println("Returning a height of 44 for AddPrayerDateCell")
return 44
}
} else {
println("Returning a default height of 44 for AddPrayerDateCell")
return 44
}
case 2:
if indexPath.row == prayerAlerts.count {
let cell = tableView.cellForRowAtIndexPath(indexPath) as? AddPrayerAlertCell
if let thisCell = cell {
let isAdding = thisCell.isAddingAlert
if isAdding { return 309 }; return 44
} else {
return 44
}
} else {
return 44
}
case 3: return 44
default: return 44
}
}
And estimatedHeightForRowAtIndexPath:
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.section {
case 0: return 130
case 1: return 44
case 2: return 44
case 3: return 44
default: return 44
}
}
I have tried editing these methods extensively, as well as checking the code in each individual cell. Nothing seems to work.
Does anyone have any solutions to this error? I can always update with more code if necessary, but I believe that either my data source could be the problem, or that the cell's resuse could be creating this error, but I cannot seem to pinpoint anything. Thanks in advance for any help!
UPDATE
Here is my AddAlertCell "refreshCell()" method as well as my UITableViewCell extension:
func refreshCell(didSelect: Bool, selectedPrayer: Prayer!) {
tableView?.beginUpdates()
selectionStyle = didSelect == true ? .None : .Default
saveButton.hidden = !didSelect
cancelButton.hidden = !didSelect
addNewAlertLabel.hidden = didSelect
isAddingAlert = didSelect
dateLabel.text = AlertStore.sharedInstance.convertDateToString(datePicker.date)
println("AddPrayerAlertCell: Cell Refreshed")
tableView?.scrollEnabled = !didSelect
tableView?.endUpdates()
}
UITableViewCell Extension:
extension UITableViewCell {
var tableView: UITableView? {
get {
var table: UIView? = superview
while !(table is UITableView) && table != nil {
table = table?.superview
}
return table as? UITableView
}
}
}
You shouldn't need to call beginUpdates / endUpdates when you refresh the cell - these methods are used if you are adding / deleting rows or sections from the tableview.
What happens if you remove the beginUpdates() and endUpdates() calls?
Related
This question already has answers here:
reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling
(22 answers)
Closed 4 years ago.
I have three Cells in TableView.
It has a button on the first cell and when it is pressed, the vertical size of the first cell is expanded.
In ViewController :
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 1 {
return 0
}
else if indexPath.section == 0 {
return CGFloat(expandedHeight) //700 //500
}
else if indexPath.section == 2 {
return 240
}
return 133
}
//I've modified some of my code, but it works this way.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "one", for: indexPath) as! one
return cell
}
else if indexPath.section == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "two", for: indexPath) as! two
return cell
}
else if indexPath.section == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "thr", for: indexPath) as! thr
return cell
}
return UITableViewCell()
}
func ExpandedButtonClick(_ height: Int)
{
tableView.beginUpdates()
expandedHeight = height // 500 700 switch
tableView.endUpdates()
}
In TableViewCell (one) :
#IBAction func btnClickExpanded(_ sender: Any)
{
if let myViewController = parentViewController as? ViewController
{
if constraintExpend.constant == 500
{
constraintExpend.constant = 700
myViewController.ExpandedButtonClick(700)
}
else
{
constraintExpend.constant = 500
myViewController.ExpandedButtonClick(500)
}
}
}
Everything worked perfectly, but there is a problem.
Only the parts of the screen that I see should be animated, but the whole screen is moving. It is disturbing to see this.
What if I want to animate only the expanded part?
I want to expand while holding the screen fixed.
I had difficulty finding it because the question was a little different.
In conclusion, I found the perfect answer.
#Sujan Simha
reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling
In my question, the ExpandedButtonClick function
func ExpandedButtonClick (_ height: Int)
{
expandedHeight = height
tableView.beginUpdates ()
tableView.endUpdates ()
tableView.layer.removeAllAnimations ()
tableView.setContentOffset (tableView.contentOffset, animated: false)
}
This works perfectly!
This is my codes;
// MARK: - Table View Delegate && Data Source Methods
// **************************************************
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let index = indexPath.row
print(index)
if index == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("HeaderCell", forIndexPath: indexPath)
cell.backgroundColor = ColorHelper.getCellBackgroundColor()
return cell
}
else {
if let cell = tableView.dequeueReusableCellWithIdentifier("GradeCell", forIndexPath: indexPath) as? GradeCell {
cell.activeViewController = self;
cell.gradeButton.tag = index
cell.creditButton.tag = index
cell.lessonNameTextField.tag = index
cell.lessonNameTextField.delegate = self
cell.backgroundColor = ColorHelper.getCellBackgroundColor()
return cell
}
return UITableViewCell()
}
}
I have 11 cells and someone are missing, When i scrolled table view index returns like this;
0-1-2-3-4-5-6-7-8-0-1-2..
After reload process, my values are confused. Wrong value in wrong cell, how can i fix this ?
Your problem could possible be how many cells you are returning. Especially, if you are having problems with the last one or two. From what you said, it sounds like you have 11 cells, make sure you return 12. The cells in a UITableView always start counting with 0 being the first cell and 10 being that last cell, in your case.
func tableView(tableView: UITableView, numberOfRowsInSection section:Int) -> Int {
return 12
}
So I have a Segmented Control that switches between 2 TableViews & 1 MapView inside a MainVC.
I'm able to switch the views in the simulator by adding an IBAction func changedInSegmentedControl to switch which views are hidden while one of them is active.
I created 2 custom TableViewCells with XIBs. I also added tags with each TableView.
My question is how do I add them in cellForRowAtIndexPath?
Currently, my code is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// var cell: UITableViewCell
if (tableView.tag == 1) {
cell: CardTableViewCell = tableView.dequeueReusableCellWithIdentifier("CardCell") as! CardTableViewCell
return cell
}
else if (tableView.tag == 2) {
cell: ListTableViewCell = tableView.dequeueReusableCellWithIdentifier("ListCell") as! ListTableViewCell
return cell
}
}
Of course Swift requires a "return cell" for the function outside the If statements. I tried with a var cell: UITableViewCell outside, but run into trouble finishing the dequeuReusableCellWithIdentifier.
Anyone have some idea how to do this? Thanks.
This is how I approached it (Swift 3.0 on iOS 10). I made one tableView with two custom cells (each is their own subclass). The segmented control is on my navigationBar and has two segments: People and Places.
There are two arrays within my class, (people and places) which are the data sources for the two table views. An action attached to the segmentedControl triggers the reload of the table, and the switch statement in cellForRowAtIndex controls which cell from which array is loaded.
I load data into the two data arrays from an API call, the asynchronous completion of which triggers dataLoaded(), which reloads the tableView. Again I don't have to worry about which segment is selected when the table is reloaded: cellForRowAtIndex takes care of loading the correct data.
I initialize a basic cell just as UITableViewCell and then in the case statement I created and configure the custom cell. Then I return my custom type cell at the end, and as long as the reuseIdentifiers and classes are correct in cellForRowAtIndex, the correct cell is initialized and displayed in the tableView.
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var peoplePlacesControl: UISegmentedControl!
fileprivate var placesArray: [PlaceModel]?
fileprivate var usersArray: [UserModel]?
#IBAction func segmentedControlActionChanged(_ sender: AnyObject) {
tableView.reloadData()
switch segmentedControl.selectedSegmentIndex {
case 0:
loadUsersfromAPI()
case 1:
loadPlacesFromAPI()
default:
// shouldnt get here
return
}
}
func dataLoaded() {
switch peoplePlacesControl.selectedSegmentIndex {
case 0: // users
if favoriteUsersArray == nil {
self.tableView.reloadData()
} else {
hideTableViewWhileEmpty()
}
case 1: // places
if placesArray != nil {
self.tableView.reloadData()
} else {
hideTableViewWhileEmpty()
}
default:
return
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch segmentedControl.selectedSegmentIndex {
case 0:
if usersArray != nil {
return usersArray!.count
} else {
return 0
}
case 1: // places
if placesArray != nil {
return placesArray!.count
} else {
return 0
}
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
switch peoplePlacesControl.selectedSegmentIndex {
case 0: // people
let userCell = tableView.dequeueReusableCell(withIdentifier: "MyUserCell", for: indexPath) as! MyUserTableViewCell
if usersArray != nil && indexPath.row < usersArray!.count {
let user = usersArray![indexPath.row]
userCell.configure(user)
userCell.myDelegate = self
}
cell = userCell as MyUserTableViewCell
case 1: // places
let placeCell = tableView.dequeueReusableCell(withIdentifier: "MyPlaceCell", for: indexPath) as! MyPlaceTableViewCell
if favoritePlacesArray != nil && indexPath.row < favoritePlacesArray!.count {
let place = placesArray![indexPath.row]
placeCell.configure(place)
placeCell.myDelegate = self
}
cell = placeCell as MyPlaceTableViewCell
default:
break
}
return cell
}
I have made change in your code.
Use following code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
if (tableView.tag == 1) {
cell: CardTableViewCell = tableView.dequeueReusableCellWithIdentifier("CardCell") as! CardTableViewCell
return cell
}
cell: ListTableViewCell = tableView.dequeueReusableCellWithIdentifier("ListCell") as! ListTableViewCell
return cell
}
I am trying to build a table view for events, like so:
I have two cell prototypes:
An event cell with identifier "event"
A separator cell with identifier "seperator"
Also, I have this class to represent a date:
class Event{
var name:String = ""
var date:NSDate? = nil
}
And this is the table controller:
class EventsController: UITableViewController {
//...
var eventsToday = [Event]()
var eventsTomorrow = [Event]()
var eventsNextWeek = [Event]()
override func viewDidLoad() {
super.viewDidLoad()
//...
self.fetchEvents()//Fetch events from server and put each event in the right property (today, tomorrow, next week)
//...
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let event = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
let seperator = tableView.dequeueReusableCellWithIdentifier("seperator", forIndexPath: indexPath) as SeperatorTableViewCell
//...
return cell
}
}
I have all the information I need at hand, but I can't figure out the right way to put it all together. The mechanics behind the dequeue func are unclear to me regrading multiple cell types.
I know the question's scope might seem a little too broad, but some lines of code to point out the right direction will be much appreciated. Also I think it will benefit a lot of users since I didn't found any Swift examples of this.
Thanks in advance!
The basic approach is that you must implement numberOfRowsInSection and cellForRowAtIndexPath (and if your table has multiple sections, numberOfSectionsInTableView, too). But each call to the cellForRowAtIndexPath will create only one cell, so you have to do this programmatically, looking at the indexPath to determine what type of cell it is. For example, to implement it like you suggested, it might look like:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventsToday.count + eventsTomorrow.count + eventsNextWeek.count + 3 // sum of the three array counts, plus 3 (one for each header)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var index = indexPath.row
// see if we're the "today" header
if index == 0 {
let separator = tableView.dequeueReusableCellWithIdentifier("separator", forIndexPath: indexPath) as SeparatorTableViewCell
// configure "today" header cell
return separator
}
// if not, adjust index and now see if we're one of the `eventsToday` items
index--
if index < eventsToday.count {
let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
let event = eventsToday[index]
// configure "today" `eventCell` cell using `event`
return eventCell
}
// if not, adjust index and see if we're the "tomorrow" header
index -= eventsToday.count
if index == 0 {
let separator = tableView.dequeueReusableCellWithIdentifier("separator", forIndexPath: indexPath) as SeparatorTableViewCell
// configure "tomorrow" header cell
return separator
}
// if not, adjust index and now see if we're one of the `eventsTomorrow` items
index--
if index < eventsTomorrow.count {
let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
let event = eventsTomorrow[index]
// configure "tomorrow" `eventCell` cell using `event`
return eventCell
}
// if not, adjust index and see if we're the "next week" header
index -= eventsTomorrow.count
if index == 0 {
let separator = tableView.dequeueReusableCellWithIdentifier("separator", forIndexPath: indexPath) as SeparatorTableViewCell
// configure "next week" header cell
return separator
}
// if not, adjust index and now see if we're one of the `eventsToday` items
index--
assert (index < eventsNextWeek.count, "Whoops; something wrong; `indexPath.row` is too large")
let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
let event = eventsNextWeek[index]
// configure "next week" `eventCell` cell using `event`
return eventCell
}
Having said that, I really don't like that logic. I'd rather represent the "today", "tomorrow" and "next week" separator cells as headers, and use the section logic that table views have.
For example, rather than representing your table as a single table with 8 rows in it, you could implement that as a table with three sections, with 2, 1, and 2 items in each, respectively. That would look like:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 3
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
return "Today"
case 1:
return "Tomorrow"
case 2:
return "Next week"
default:
return nil
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return eventsToday.count
case 1:
return eventsTomorrow.count
case 2:
return eventsNextWeek.count
default:
return 0
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
var event: Event!
switch indexPath.section {
case 0:
event = eventsToday[indexPath.row]
case 1:
event = eventsTomorrow[indexPath.row]
case 2:
event = eventsNextWeek[indexPath.row]
default:
event = nil
}
// populate eventCell on the basis of `event` here
return eventCell
}
The multiple section approach maps more logically from the table view to your underlying model, so I'd to adopt that pattern, but you have both approaches and you can decide.
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
}