Custom Cell for 2 TableViews/Segmented Control Swift - ios

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
}

Related

Iterating through TableViewCells skips some

I have a tableview and in each cell there is a checkbox. I also have a "select all" button.
My problem is that when I click select all I want to update all the checkboxes to checked state. So from a list of 100 cells, all get checked but every 13th cell does not. To make it clearer, on my simulators screen are 12 cells visible that all get checked. When I start scrolling, the first cell that comes up is unchecked, and is then followed by 12 checked ones :S
When I scroll a little and click "select all" again, the skipped ones become also checked..
Anyone have a clue what am I missing?
This is the cell code:
class ListTableViewCell: UITableViewCell {
#IBOutlet weak var checkbox: UIButton!
var buttonState = false{
didSet{
if buttonState{
checkbox.setImage(#imageLiteral(resourceName: "checked"), for: .normal)
}else{
checkbox.setImage(#imageLiteral(resourceName: "unchecked"), for: .normal)
}
}
}
#IBAction func checkboxAction(_ sender: UIButton) {
if buttonState {
buttonState = false
}else{
buttonState = true
}
}
func simulateCheck(){
buttonState = true
}
And here are some snipets from my controller:
private var articleValues: [ArticleValue] = []{
didSet{
tableView.reloadData()
}
}
func selectAll(){
for i in 0..<articleValues.count{
let cell = tableView.cellForRow(at: IndexPath(item: i, section: 0)) as? ListTableViewCell
cell?.simulateCheck()
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
}
return cell
}
Your UITableView is backed by a data source. This means that you shouldn't change cells directly like you do here:
cell?.simulateCheck()
tableView.reloadData()
Instead you should keep a list of all the checked positions, maybe another array that has bools for each corresponding articleValue (this is not the best design).
var checkedValues = Bool
In your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method you would then set the state of the cell:
articleValueCell.buttonState = checkedValues[indexPath.row]
In your selectAll method fill this array with true values and then call tableView.reloadData()
private var checkedValues = [Bool]()
private var articleValues: [ArticleValue] = []{
didSet{
checkedValues = Array(repeating: false, count: articleValues.count)
tableView.reloadData()
}
}
func selectAll(){
checkedValues = Array(repeating: true, count: articleValues.count)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
articleValueCell.buttonState = checkedValues[indexPath.row]
}
return cell
}
Another mistake is that you should never iterate on all the cells in the table because they are reused, no point in going through your data source and getting a cell for each. It only makes sense to iterate through tableView.visibleCells. But like in your case, most of the time you don't need that either, you should just update your data source accordingly and reload the table or just the modified cell.
It's not recommended that you refer to cells directly within a table view. The reason is that UITableViews have an efficient method of only loading the cells as they are needed (and deallocating them when they are no longer needed, e.g. the cell scrolls off screen). Because of this the cell you are try to refer to may not be loaded.
Instead you should interact with it via the cellForRowAt method. If you want to "select all" cells, you should create a property that stores the value of checked or not checked via a Bool and then set all of the ArticleValue elements to true for that property and reload the data inside selectAll().
It could work something like this:
func selectAll() {
articleValues.forEach {
$0.checked = true
}
tableView.reloadData()
}
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
if articleValue.checked {
articleValueCell.simulateCheck()
}
}
return cell
}

Variable use of multiple custom cells

I'm using a unclickable tableView to display different information of one object.
For this informations I have different custom cell types one where I placed a map, if my object have locations, one have a list with links, and another a multiple line label for a little description...for example.
I manage this cells with:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! MapCell
return cell
} else if indexPath.row == 1 {
let cell: textCell = tableView.dequeueReusableCellWithIdentifier("textCell") as! TextCell
return cell
} else if indexPath.row == 2 {
let cell: listCell = tableView.dequeueReusableCellWithIdentifier("listCell") as! ListCell
return cell
}
}
So far so good, everything working fine. My problem is, not every object needs a map, some of them just need some text and a list, other objects need a map and a list, other all of them. I want my tableView to skip some cells if there is a condition.
I know, I can make an symbolic array for changing the number of cells of my tableView, but that deleting just from the end of my tableView, not specific cells.
One of my ideas is to generate a empty cell, maybe with a height of 0 or 1 so that I can do something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
if mapCellNeeded {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
} else {
let cell: emptyCell = tableView.dequeueReusableCellWithIdentifier("emptyCell") as! EmptyCell
}
return cell
} else if indexPath.row == 1 {
...
}...
}
put I don't know if there isn't an efficient way. Hope you guys can help me.
Your solution would work. Another approach (very nice and swifty) would be not to hardcode row numbers, but rather use enum instead:
enum InfoCellType {
case Map
case Text
case Links
}
...
var rows = [InfoCellType]()
...
// when you know what should be there or not
func constructRows() {
if (mapCellNeeded) {
rows.append(InfoCellType.Map)
}
rows.append(InfoCellType.Text)
... etc
}
Then in the table view methods just see what's the type for current indexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellType: InfoCellType = self.rows[indexPath.row]
switch cellType {
case .Map:
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
return cell
case .Text:
...
case.Links:
...
}
}
This solution also allows to easily change order of rows - just change the order of items in rows array.

Error when creating tableview with multiple custom cells

I have encountered an error in swift when attempting to create a tableview made up of custom cells dependent upon a set of conditions.
Here is my code:
var tableData: [String] = []
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let phonenocell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier("phonecell", forIndexPath: indexPath) as! MyCustomCell
let pincell:SocialCell = self.tableView.dequeueReusableCellWithIdentifier("socialcell", forIndexPath: indexPath) as! SocialCell
let fbcell:FacebookCell = self.tableView.dequeueReusableCellWithIdentifier("facebookcell", forIndexPath: indexPath) as! FacebookCell
let snapcell:SnapchatCell = self.tableView.dequeueReusableCellWithIdentifier("snapchatcell", forIndexPath: indexPath) as! SnapchatCell
let twitcell:TwitterCell = self.tableView.dequeueReusableCellWithIdentifier("twittercell", forIndexPath: indexPath) as! TwitterCell
let instacell:InstagramCell = self.tableView.dequeueReusableCellWithIdentifier("instagramcell", forIndexPath: indexPath) as! InstagramCell
if tableData.contains("Number") {
return phonenocell
}
if tableData.contains("Social") {
return pincell
}
if tableData.contains("Facebook") {
return fbcell
}
if tableData.contains("Snapchat") {
return snapcell
}
if tableData.contains("Twitter") {
return twitcell
}
if tableData.contains("Instagram") {
return instacell
}
}
When attempting to build and run I get a build failed with the following fault:
"Missing Return in a function expected to return 'UITableViewCell'
I have been over and over my code but I honestly cannot see where I am going wrong...
Any help would be greatly appreciated!
You need to return cell for sure.
You already do in conditions, but in case none of your condition statements would success, your return call wouldn't be fired.
Appending, for example:
return phonenocell
to the end of the function, should be quick fix for your code. It ensures, that the function will return a cell (that is mandatory).
My data source is the array tableData. This is constructed on the previous view as: #IBAction func switch1Toggled(sender: UISwitch) { if mySwitch1.on { fbTextBox.text = "Selected" dataArray.append("Facebook")
And this may be the main issue:
Assuming, that you choose 'facebook' and that you reload your tableView, every row will pass the first condition as it IS contained.
You should put this in your method:
//assuming your data source contains multiple members, and your numberOfRowsInSections... method return tableData.count, you need to get each item for each row:
let currentTag = tableData[indexPath.row]
if (currentTag == "something") { //e.g. Facebook
let somethingcell:MySomethingCell = ...
self.tableView.dequeueReusableCellWithIdentifier("somethingcell", forIndexPath: indexPath) as! MySomethingCell
return somethingcell
} else if {
...
}
return emptycell //this line is just for the case, when no of your conditions will pass and you don't catch all the situations...
maybe your array elements doesn't match the condition, it's better to return default value instead of ur conditions failed

UITableView Cells shifting positions or disappearing entirely on scroll

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?

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