Access to array's indexPath in a function Swift - ios

I am trying to access an array's indexPath inside a function to update this array's data but I don't know how to pass the indexPath as a parameter (espacially what to pass when calling) to the function or if this is even the solution.
I included cellForRowAt to illustrate how this function access indexPath.
var cryptosArray: [Cryptos] = []
extension WalletTableViewController: UITableViewDelegate, UITableViewDataSource, CryptoCellDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let crypto = cryptosArray[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.setCrypto(crypto: crypto)
cell.delegate = self
cell.amountTextField.delegate = self
return cell
}
func cellAmountEntered(_ walletTableViewCell: WalletTableViewCell) {
if walletTableViewCell.amountTextField.text == "" {
return
}
let str = walletTableViewCell.amountTextField.text
let crypto = cryptosArray[indexPath.row] //<---- How to do that?
crypto.amount = walletTableViewCell.amountTextField.text
//Then update array's amount value at correct index
walletTableViewCell.amountTextField.text = ""
}
}

Instead of hacking something, simply ask the tableView to tell you the indexPath of the given cell:
// use indexPath(for:) on tableView
let indexPath = tableView.indexPath(for: walletTableViewCell)
// then you can simply use it
let crypto = cryptosArray[indexPath.row]
UITableView.indexPath(for:) documentation says:
Returns an index path representing the row and section of a given table-view cell.
And this is exactly what you want, you don't want to hack the indexPath to the cell. indexPath should be taken care of by the tableView, not the cell. Ideally, cell should be completely oblivious of its indexPath.
Always try to use the standard way to solve your problems. In general, when you are trying to solve something, I would recommend you to first look at the documentation of the UITableView, there are many useful methods there.

if you want to get index path.row when user clicked on cell , you should get index path.row when user clicked and then use this to your func
for ex :
var indexrow : int = 0
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// table cell clicked
indexrow = indexPath.row
}
func cellAmountEntered(_ walletTableViewCell: WalletTableViewCell) {
if walletTableViewCell.amountTextField.text == "" {
return
}
let str = walletTableViewCell.amountTextField.text
let crypto = cryptosArray[indexrow]
crypto.amount = walletTableViewCell.amountTextField.text
//Then update array's amount value at correct index
walletTableViewCell.amountTextField.text = ""
}

Related

How to set enabled checkimage in tableview based on the selected cells

I am fetching previously selected categorylist from the server. say for an example.cateogrylist i fetched from the server was in following formate
categoryid : 2,6,12,17
now what i need to do is want to enable checkmark in my tableview based on this categorylist,for that purpose i converted this list into an [Int] array like this :
func get_numbers(stringtext:String) -> [Int] {
let StringRecordedArr = stringtext.components(separatedBy: ",")
return StringRecordedArr.map { Int($0)!}
}
in viewDidLoad() :
selectedCells = self.get_numbers(stringtext: UpdateMedicalReportDetailsViewController.catId)
print(myselection)
while printing it's giving me results like this : [12,17,6,8,10]
i want to enable checkimage based on this array.I tried some code while printing its giving me the right result like whatever the categories i selected at the time of posting ,i am able to fetch it but failed to place back this selection in tableview.Requirement : while i open this page it should show me the selection based on the categorylist i fetched from the server.
var selectedCells : [Int] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell1 = table.dequeueReusableCell(withIdentifier: "mycell") as! catcell
cell1.mytext.text = categoriesName[indexPath.row]
if UpdateMedicalReportDetailsViewController.flag == 1
{
selectedCells = self.get_numbers(stringtext: UpdateMedicalReportDetailsViewController.catId)
cell1.checkimage.image = another
print(selectedCells)
}
else
{
selectedCells = []
cell1.checkimage.image = myimage
}
return cell1
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = table.cellForRow(at: indexPath) as! catcell
cell.checkimage.image = myimage
if cell.isSelected == true
{
self.selectedCells.append(indexPath.row)
cell.checkimage.image = another
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = table.cellForRow(at: indexPath) as! catcell
if cell.isSelected == false
{
self.selectedCells.remove(at: self.selectedCells.index(of: indexPath.row)!)
cell.checkimage.image = myimage
}
}
output :
This is a very common use case in most apps. I'm assuming you have an array of all categories, and then an array of selected categories. What you need to do is in cellForRowAtIndexPath, check to see if the current index path row's corresponding category in the "all categories" array is also present in the "selected categories" array. You can do this by comparing id's etc.
If you have a match, then you know that the cell needs to be selected/checked. A clean way to do this is give your cell subclass a custom load method and you can pass a flag for selected/checked.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "mycell") as! catcell
let category = self.categories[indexPath.row] // Let's say category is a string "hello"
Bool selected = self.selectedCategories.contains(category)
cell.load(category, selected)
return cell
}
So with the code above, let's say that categories is just an array of category strings like hello, world, and stackoverflow. We check to see if the selectedCategories array contains the current cell/row's category word.
Let's say that the cell we're setting up has a category of hello, and selectedCategories does contain it. That means the selected bool gets set to true.
We then pass the category and selected values into the cell subclass' load method, and inside that load method you can set the cell's title text to the category and you can check if selected is true or false and if it's true you can display the checked box UI.

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

Error: Index out of Range UITableView

I am creating an app where it uses custom cells. I also have these UITextView's where if you input a word, that word should then go to one of the four labels I created in the custom cell. I am still coding it however I got an error saying "Error: Index Out of Range".
Here is the code, and I also commented where it is giving that error
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) ->
UITableViewCell{
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableView
cell.lbl.text = todolist[indexPath.row]
cell.lbl2.text = todolist2[indexPath.row] // This is the error code
cell.lbl3.text = todolist3[indexPath.row]
cell.lbl4.text = todolist4[indexPath.row]
return cell
}
Here is where I append my texts
#IBAction func ClickedforSelection(sender: AnyObject) {
todolist.append(txt.text!)
todolist2.append(txt1.text!)
todolist3.append(txt2.text!)
todolist4.append(txt3.text!)
self.view.endEditing(true)
txt.text = ""
txt1.text = ""
txt2.text = ""
txt3.text = ""
NSUserDefaults.standardUserDefaults().setObject(todolist, forKey: "list")
NSUserDefaults.standardUserDefaults().setObject(todolist2, forKey: "list2")
NSUserDefaults.standardUserDefaults().setObject(todolist3, forKey: "list3")
NSUserDefaults.standardUserDefaults().setObject(todolist4, forKey: "list4")
Here is my NumberofRowsInSection
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return todolist.count
}
I have a conjecture that it may be the reuse of indexPath.row. Any solution?
If numberOfRowsInSection is returning todolist.count, you are accessing todolist2 in your cell. If todolist has 2 items and todolist2 has 1 item, it will do this because you are trying to access an item in a list that doesn't exist. Put a breakpoint at the first call of cell.lbl.text and check each array (todolist, todolist1, etc...). You should see that todolist2 does not have have a record at whatever "row" it's calling. If that is the case, you should just test it prior to calling it. (verify todolist2.count has enough items in it - or better yet, change the code to not have 4 arrays tracking 1 row (convert to a struct of some type with all 4 values, or something similar).
First, change the following code by commenting out lines:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) ->
UITableViewCell{
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableView
cell.lbl.text = todolist[indexPath.row]
// cell.lbl2.text = todolist2[indexPath.row] // This is the error code
// cell.lbl3.text = todolist3[indexPath.row]
// cell.lbl4.text = todolist4[indexPath.row]
return cell
}
And test to verify existing code (should work but of course it will not update the labels.)
Then add code to print the number of items in each array:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) ->
UITableViewCell{
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableView
cell.lbl.text = todolist[indexPath.row]
// cell.lbl2.text = todolist2[indexPath.row] // This is the error code
// cell.lbl3.text = todolist3[indexPath.row]
// cell.lbl4.text = todolist4[indexPath.row]
print("Row: \(indexPath.row)")
print("List 1: \(todolist.count)") //this will print to the console
print("List 2: \(todolist2.count)")
print("List 3: \(todolist3.count)")
print("List 4: \(todolist4.count)")
return cell
}
What you will likely see is that they don't have the same number of items, and as soon as it his a "row" that is equal to or greater than the number of items, it will break. Remember that Row's start at Zero, while count starts at 1.
If this is what you find, then there is problem something wrong with the code where you are adding the values to the todolist arrays. If you want to see how to convert that to a struct, I can post that for you.
Converting to struct
The code that is executing when something is clicked:
#IBAction func ClickedforSelection(sender: AnyObject) {
shows that a value is written to each of the 4 todolists every time. While I don't have the full requirements, if this is what you want to do, then you could implement a struct. Put this code in it's own ToDoList.swift file (ideally):
struct ToDoListItem {
var listItem: String?
var list1Item: String?
var list2Item: String?
var list3Item: String?
}
Then replace where you define your todolislt arrays (all 4 of them) with a single:
var listItems = [ToDoListItem]() //creates an array of ToDoListItems and initializes it with no values
Then in the ClickedForSelection function, change it to:
let listItem = ToDoListItem(listItem: txt.text, list1Item: txt1.text, list2Item: txt2.text, list3Item: txt3.text)
listItems.append(listItem) //add it to your array
//todolist.append(txt.text!)
//todolist2.append(txt1.text!)
//todolist3.append(txt2.text!)
//todolist4.append(txt3.text!)
self.view.endEditing(true)
txt.text = ""
txt1.text = ""
txt2.text = ""
txt3.text = ""
// This routine will need to be updated. Leaving that for you to figure out :)
// NSUserDefaults.standardUserDefaults().setObject(todolist, forKey: "list")
// NSUserDefaults.standardUserDefaults().setObject(todolist2, forKey: "list2")
// NSUserDefaults.standardUserDefaults().setObject(todolist3, forKey: "list3")
// NSUserDefaults.standardUserDefaults().setObject(todolist4, forKey: "list4")
...then numberOfRowsInSection changes to:
return listItems.count
...then cellForRowAtIndexPath changes to:
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableView
let listItem = listItems[indexPath.row]
cell.lbl.text = listItem.listItem ?? "" // Since listItems.listItem is an optional value, ?? unwraps it safely. If it is nill, it uses "" instead
cell.lbl2.text = listItem.list1Item ?? ""
cell.lbl3.text = listItem.list2Item ?? ""
cell.lbl4.text = listItem.list3Item ?? ""
return cell
Again...I would strongly consider how you are storing a value for a todolist for all 4 lists every time (if it is a todo list app, it seems like this may not be ideal?)

Programmatically altering NSIndexPath in Swift

I'm implementing AdMob in a UITableView by putting banner ad in the first row of a section. I'm most of the way there implementing it, however I'm having a tough time getting cellForRowAtIndexPath to work as desired.
This is what my numberOfRowsInSection looks like:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count = Int()
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
count = currentSection.numberOfObjects
count = count + 1 // add another row for an ad
}
return count
}
My cellForRowAtIndexPath looks like this:
override func tableView(tableView: UITableView, var cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let adCell: BannerAdTableViewCell = tableView.dequeueReusableCellWithIdentifier(BannerAdTableViewCell.reuseIdentifier(), forIndexPath: indexPath) as! BannerAdTableViewCell
// customization
return adCell
} else {
// Cell for vanilla item to display
// TODO: fix indexpath here. need to add 1
let newIndexPath = indexPath.indexPathByAddingIndex(indexPath.row+1)
indexPath = newIndexPath
// Cell for a Routine
let customCell = tableView.dequeueReusableCellWithIdentifier(RoutineSelectionTableViewCell.reuseIdentifier(), forIndexPath: indexPath) as! RoutineSelectionTableViewCell
let routine = fetchedResultsController.objectAtIndexPath(indexPath) as! SavedRoutines
customCell.routineNameLabel.text = routine.routineTitle
return customCell
}
}
I know I need to adjust the value of the indexPath to account for an extra row in the indexPathSection, but everything I've tried triggers out of bounds exceptions of some sort. Any feedback would be greatly appreciated.
indexPathByAddingIndex adds a new index, it does not increment a value of an index but adds one. If you previously had two indices / dimensions (section and row) you now have 3 indices / dimension: section, row and "the newly added one".
Provides an index path containing the indexes in the receiving index path and another index.
What you should do instead is either create a new NSIndexPath by hand. And I do not think you need to add one, but subtract one, since the item at index 1 should actually be the element in your result at index 0:
let customIndexPath = NSIndexPath(forRow: indexPath.row - 1, inSection: indexPath.section)
which you then use to access the "correct" routine at the right index:
let routine = fetchedResultsController.objectAtIndexPath(customIndexPath) as! SavedRoutines
Your call to tableView.dequeueReusableCellWithIdentifier should stay the same and still pass in the default indexPath.

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