iOS UITableView move Cells in Code - ios

I recently started working on a small iOS Project where I have a Game Score Leaderboard represented by a UITableView, whenever the Player gets a new Highscore the Leaderboard gets visible and his Score Entry (Including Picture, Name and Score) represented by a UITableViewCell is supposed to move to the new right Spot in the TableView Leaderboard it now belongs to. The calculation of the new index is working fine but the cell is not moving at all.
Some info:
The Leaderboard is populated succesful,
I set the func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool to true
Also implemented the func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
correctly.
I am thinking that maybe I am missing something but the problem could also lie somewhere else, I don't know and would really appreciate some help.
Delegate Mehods for Editing
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
// Handles reordering of Cells
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let player = players[sourceIndexPath.row]
players.remove(at: sourceIndexPath.row)
players.insert(player, at: destinationIndexPath.row)
}
Code where I try to move Row
func newHighscore(highscore: Int) {
friendsTableView.reloadData()
let myNewIndex = Player.recalculateMyIndex(players: players, score: highscore)
friendsTableView.moveRow(at: [0,Player.myIndex], to: [0,myNewIndex])
}

This code works, but it only example. adapt it for you. create new file and put this code in it, build it and check.
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var scores = ["1", "2", "3", "4", "5"]
override func viewDidLoad() {
super.viewDidLoad()
}
func move(from: IndexPath, to: IndexPath) {
UIView.animate(withDuration: 1, animations: {
self.tableView.moveRow(at: from, to: to)
}) { (true) in
// write here code to remove score from array at position "at" and insert at position "to" and after reloadData()
}
}
#IBAction func buttonPressed(_ sender: UIButton) {
let fromIndexPath = IndexPath(row: 4, section: 0)
let toIndexPath = IndexPath(row: 1, section: 0)
move(from: fromIndexPath, to: toIndexPath)
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return scores.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? TableViewCell
if let cell = cell {
cell.setText(text: scores[indexPath.row])
}
return cell ?? UITableViewCell()
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
}
For correctly use, you need to insert new element in array at last, reload data and after you can call method move() and put in it current indexPath and indexPath to insert you need.

Try this code for move row Up/Down
var longPress = UILongPressGestureRecognizer (target: self, action:#selector(self.longPressGestureRecognized(_:)))
ToDoTableView.addGestureRecognizer(longPress)
//MARK: -longPressGestureRecognized
func longPressGestureRecognized(_ gestureRecognizer: UIGestureRecognizer) {
let longPress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longPress.state
let locationInView = longPress.location(in: ToDoTableView)
let indexPath = ToDoTableView.indexPathForRow(at: locationInView)
struct Path {
static var initialIndexPath : IndexPath? = nil
}
switch state {
case UIGestureRecognizerState.began:
if indexPath != nil {
let cell = ToDoTableView.cellForRow(at: indexPath!) as UITableViewCell!
UIView.animate(withDuration: 0.25, animations: { () -> Void in
cell?.alpha = 0.0
}, completion: { (finished) -> Void in
if finished {
UIView.animate(withDuration: 0.25, animations: { () -> Void in
cell?.alpha = 1
})
} else {
cell?.isHidden = true
}
})
}
case UIGestureRecognizerState.changed:
if ((indexPath != nil) && (indexPath != Path.initialIndexPath)) {
itemsArray.insert(itemsArray.remove(at: Path.initialIndexPath!.row), at: indexPath!.row)
ToDoTableView.moveRow(at: Path.initialIndexPath!, to: indexPath!)
Path.initialIndexPath = indexPath
}
default:
print("default:")
}
}

Related

How to change tableView numberOfRows and Cells based on which button is clicked in Swift iOS

I have two buttons in my user's profile page, one for the saved shop items and one for his reviews.
I want when the user clicks the saved button it would load his saved shop's items in the table view and when he clicks the reviews button it would load his reviews.
I'm struggling on how to figure out how to do this
Any help, please?
here is my code:
#IBOutlet weak var reviewsBtn: UIButton!
#IBOutlet weak var saveBtntab: UIButton!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if(reviewsBtn.isSelected == true){
print("review selected")
return reviews.count
}
if(saveBtntab.isSelected == true){
print("saved selected")
return shops.count
}
return shops.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellFave", for: indexPath) as! FaveTableViewCell
let shops = self.shops[indexPath.row]
let reviews = self.reviews[indexPath.row]
// i want to do the same idea for the number of rows here.
}
#IBAction func reviewsTapped(_ sender: Any) {
reviewsBtn.isSelected = true
reviewsBtn.isEnabled = true
faveBtntab.isEnabled = false
faveBtntab.isSelected = false
}
#IBAction func savedTapped(_ sender: Any) {
faveBtntab.isSelected = true
faveBtntab.isEnabled = true
reviewsBtn.isEnabled = false
reviewsBtn.isSelected = false
}
First of all if there are only two states you can simplify numberOfRows
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return reviewsBtn.isSelected ? reviews.count : shops.count
}
In cellForRow do the same thing, display the items depending on reviewsBtn.isSelected
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellFave", for: indexPath) as! FaveTableViewCell
if reviewsBtn.isSelected {
let reviews = self.reviews[indexPath.row]
// assign review values to the UI
} else {
let shops = self.shops[indexPath.row]
// assign shop values to the UI
}
}
And don't forget to call reloadData when the state has changed.
You can create two different dataSource instances for clarity and separation like following -
class ShopsDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
var shops: [Shop] = []
var onShopSelected: ((_ shop: Shop) -> Void)?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shops.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ShopTableViewCell", for: indexPath) as! ShopTableViewCell
let shop = self.shops[indexPath.row]
cell.populateDetails(shop: shop)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.onShopSelected?(shops[indexPath.row])
}
}
class ReviewsDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
var reviews: [Review] = []
var onReviewSelected: ((_ review: Review) -> Void)?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return reviews.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReviewTableViewCell", for: indexPath) as! ReviewTableViewCell
let review = self.reviews[indexPath.row]
cell.populateDetails(review: review)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.onReviewSelected?(reviews[indexPath.row])
}
}
class ViewController: UIViewController {
let shopsDataSource = ShopsDataSource()
let reviewsDataSource = ReviewsDataSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ShopTableViewCell.self, forCellReuseIdentifier: "ShopTableViewCell")
tableView.register(ReviewTableViewCell.self, forCellReuseIdentifier: "ReviewTableViewCell")
shopsDataSource.onShopSelected = { [weak self] (shop) in
self?.showDetailsScreen(shop: shop)
}
reviewsDataSource.onReviewSelected = { [weak self] (review) in
self?.showDetailsScreen(review: review)
}
}
#IBAction func shopsTapped(_ sender: Any) {
tableView.dataSource = shopsDataSource
tableView.delegate = shopsDataSource
tableView.reloadData()
}
#IBAction func addNewShop(_ sender: Any) {
/// ask user about shop details and add them here
shopsDataSource.shops.append(Shop())
tableView.reloadData()
}
func showDetailsScreen(shop: Shop) {
/// Go to shop details screen
}
#IBAction func reviewsTapped(_ sender: Any) {
tableView.dataSource = reviewsDataSource
tableView.delegate = reviewsDataSource
tableView.reloadData()
}
#IBAction func addNewReview(_ sender: Any) {
/// ask user about review details and add them here
reviewsDataSource.reviews.append(Review())
tableView.reloadData()
}
func showDetailsScreen(review: Review) {
/// Go to review details screen
}
}

How to toggle open close uitableViewCell [duplicate]

I'm able to expand and collapse cells but i wanna call functions (expand and collapse) inside UITableViewCell to change button title.
import UIKit
class MyTicketsTableViewController: UITableViewController {
var selectedIndexPath: NSIndexPath?
var extraHeight: CGFloat = 100
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MyTicketsTableViewCell
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if(selectedIndexPath != nil && indexPath.compare(selectedIndexPath!) == NSComparisonResult.OrderedSame) {
return 230 + extraHeight
}
return 230.0
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if(selectedIndexPath == indexPath) {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
}
import UIKit
class MyTicketsTableViewCell: UITableViewCell {
#IBOutlet weak var expandButton: ExpandButton!
#IBOutlet weak var detailsHeightConstraint: NSLayoutConstraint!
var defaultHeight: CGFloat!
override func awakeFromNib() {
super.awakeFromNib()
defaultHeight = detailsHeightConstraint.constant
expandButton.button.setTitle("TAP FOR DETAILS", forState: .Normal)
detailsHeightConstraint.constant = 30
}
func expand() {
UIView.animateWithDuration(0.3, delay: 0.0, options: .CurveLinear, animations: {
self.expandButton.arrowImage.transform = CGAffineTransformMakeRotation(CGFloat(M_PI * 0.99))
self.detailsHeightConstraint.constant = self.defaultHeight
self.layoutIfNeeded()
}, completion: { finished in
self.expandButton.button.setTitle("CLOSE", forState: .Normal)
})
}
func collapse() {
UIView.animateWithDuration(0.3, delay: 0.0, options: .CurveLinear, animations: {
self.expandButton.arrowImage.transform = CGAffineTransformMakeRotation(CGFloat(M_PI * 0.0))
self.detailsHeightConstraint.constant = CGFloat(30.0)
self.layoutIfNeeded()
}, completion: { finished in
self.expandButton.button.setTitle("TAP FOR DETAILS", forState: .Normal)
})
}
}
If you want the cell to get physically bigger, then where you have your store IndexPath, in heightForRow: use:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if selectedIndexPath == indexPath {
return 230 + extraHeight
}
return 230.0
}
Then when you want to expand one in the didSelectRow:
selectedIndexPath = indexPath
tableView.beginUpdates
tableView.endUpdates
Edit
This will make the cells animate themselves getting bigger, you dont need the extra animation blocks in the cell.
Edit 2
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if(selectedIndexPath == indexPath) {
selectedIndexPath = nil
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? MyTicketsTableViewCell {
cell.collapse()
}
if let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow:indexPath.row+1, section: indexPath.section) as? MyTicketsTableViewCell {
cell.collapse()
}
} else {
selectedIndexPath = indexPath
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? MyTicketsTableViewCell {
cell.expand()
}
if let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow:indexPath.row+1, section: indexPath.section) as? MyTicketsTableViewCell {
cell.expand()
}
}
tableView.beginUpdates()
tableView.endUpdates()
}
All you need is implement UITableView Delegate this way:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
By default, estimatedHeight is CGRectZero, when you set some value for it, it enables autoresizing (the same situation with UICollectionView), you can do even also so:
tableView?.estimatedRowHeight = CGSizeMake(50.f, 50.f);
Then you need to setup you constraints inside your cell.
Check this post: https://www.hackingwithswift.com/read/32/2/automatically-resizing-uitableviewcells-with-dynamic-type-and-ns
Hope it helps)
In MyTicketsTableViewController class, inside cellForRowAtIndexPath datasource method add target for the button.
cell.expandButton.addTarget(self, action: "expandorcollapsed:", forControlEvents: UIControlEvents.TouchUpInside)
I tried to implement lots of the given examples on this and other pages with similar questions, but none worked for me since I had to perform some logic in my custom cell (eg. hide unneeded UILables in CustomCell.swift when the cell is collapsed).
Here is the implementation that worked for me:
Create a dictionary:
private var _expandedCells: [IndexPath:Bool] = [:]
Implement the heightForRowAt method:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return _expandedCells[indexPath] == nil ? 70 : _expandedCells[indexPath]! ? 150 : 70
}
Implement the didSelectRowAt method:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
_expandedCells[indexPath] = _expandedCells[indexPath] == nil ? true : !_expandedCells[indexPath]!
tableView.reloadRows(at: [indexPath], with: .fade)
}
Adjust your customCell:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? YourCustomTableViewCell, let isExpanded = _expandedCells[indexPath] else { return }
cell.set(expanded: isExpanded)
}

Dequeue reusable cell with delay

I have a tableView where I insert 20 rows with delay using DispatchQueue method. First 10 rows appear fine. Problem starts with 11th one when Xcode begins to dequeue reusable rows. In simulator it looks like it starts to insert by 2 rows nearly at the same time (11th+12th, then 13th+14th).
I wonder why is that. Do DispatchQueue and tableView.dequeueReusableCell methods conflict? And if yes how to organize things properly?
var numberOfCells = 0
//My TableView
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextCell")! as UITableViewCell
return cell
}
//Function that inserts rows
func updateTableView(nextPassageID: Int) {
for i in 0...numberOfCells - 1 {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(i)) {
self.numberOfCells += 1
let indexPath = IndexPath(row: i, section: 0)
self.tableView.insertRows(at: [indexPath], with: .fade)
}
}
}
I think using a Timer is the better solution in your use case:
private var cellCount = 0
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Cell \(indexPath.row)"
return cell
}
func addCells(count: Int) {
guard count > 0 else { return }
var alreadyAdded = 0
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] t in
guard let self = self else {
t.invalidate()
return
}
self.cellCount += 1
let indexPath = IndexPath(row: self.cellCount - 1, section: 0)
self.tableView.insertRows(at: [indexPath], with: .fade)
alreadyAdded += 1
if alreadyAdded == count {
t.invalidate()
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addCells(count: 20)
}
Your code didn't work for me. May be you missed something to mention in your question. But with the information that I understood, I did some modification and now it runs (tested in iPhone X) as expected. Following is the working full source code.
import UIKit
class InsertCellViewController: UIViewController, UITableViewDataSource {
var dataArray:Array<String> = []
let reusableCellId = "AnimationCellId"
var timer = Timer()
var index = -1
#IBOutlet weak var tableView: UITableView!
// UIViewController lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: reusableCellId)
tableView.separatorStyle = .none
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateTableView()
}
// MARK : UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reusableCellId)!
cell.textLabel?.text = dataArray[indexPath.row]
return cell
}
// Supportive methods
func updateTableView() {
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(updateCounting), userInfo: nil, repeats: true)
}
#objc func updateCounting(){
if index == 19 {
timer.invalidate()
}
index += 1
let indexPath = IndexPath(row: index, section: 0)
self.tableView.beginUpdates()
self.dataArray.append(String(index))
self.tableView.insertRows(at: [indexPath], with: .fade)
self.tableView.endUpdates()
}
}
Try to place code inside DispatchQueue.main.asyncAfter in
self.tableView.beginUpdates()
self.tableView.endUpdates()

Swift 3 - Expandable Table View Cells with first cell already expanded

I am using Swift 3.
I've followed this tutorial to get it so that I can tap on a table view cell which will expand revealing more information.
https://www.youtube.com/watch?v=VWgr_wNtGPM&t=294s
My question is: how do I do it so that the first cell is expanded when the view loads already (i.e. the user doesn't have to click to see that cell expand) but all other behavior remains the same (e.g. if it's clicked again, it de-collapses)?
UITableViewCell:
import UIKit
class ResultsCell: UITableViewCell {
#IBOutlet weak var introPara : UITextView!
#IBOutlet weak var section_heading : UILabel!
class var expandedHeight : CGFloat { get { return 200.0 } }
class var defaultHeight : CGFloat { get { return 44.0 } }
var frameAdded = false
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
section_heading.translatesAutoresizingMaskIntoConstraints = false
}
func checkHeight() {
introPara.isHidden = (frame.size.height < ResultsCell.expandedHeight)
}
func watchFrameChanges() {
if(!frameAdded) {
addObserver(self, forKeyPath: "frame", options: .new, context: nil)
checkHeight()
}
}
func ignoreFrameChanges() {
if(frameAdded){
removeObserver(self, forKeyPath: "frame")
}
}
deinit {
print("deinit called");
ignoreFrameChanges()
}
// when our frame changes, check if the frame height is appropriate and make it smaller or bigger depending
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "frame" {
checkHeight()
}
}
}
UITableViewController
// class declaration and other methods above here...
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// number of rows in the table view
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section_heading.count
}
// return the actual view for the cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let resultcell = tableView.dequeueReusableCell(withIdentifier: "resultCellTemplate", for: indexPath) as! ResultsCell
resultcell.section_heading.text = section_heading[indexPath.row]
resultcell.introPara.attributedText = contentParagraphs[indexPath.row]
return resultcell
}
// when a cell is clicked
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let previousIndexPath = selectedIndexPath
// the row is already selected, then we want to collapse the cell
if indexPath == selectedIndexPath {
selectedIndexPath = nil
} else { // otherwise, we expand that cell
selectedIndexPath = indexPath
}
var indexPaths : Array<IndexPath> = []
// only add a previous one if it exists
if let previous = previousIndexPath {
indexPaths.append(previous)
}
if let current = selectedIndexPath {
indexPaths.append(current)
}
// reload the specific rows
if indexPaths.count > 0 {
tableView.reloadRows(at: indexPaths, with: .automatic)
}
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
(cell as! ResultsCell).watchFrameChanges()
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
(cell as! ResultsCell).ignoreFrameChanges()
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath == selectedIndexPath {
return ResultsCell.expandedHeight
} else {
return ResultsCell.defaultHeight
}
}
So this works as intended.
But how do I make it so that the first cell is already expanded?
Thanks for your help.
I feel like you do not fully understand your own code but since you did put a lot of effort into your question I will give you a hint.
In your UITableViewController somewhere at the top you initialise selectedIndexPath which should look something like
var selectedIndexPath: IndexPath?
You can set that to a default value like this
var selectedIndexPath: IndexPath? = IndexPath(row: 0, section: 0)
So cell at (row: 0, section: 0) will expand on default.
Yesterday I have completed similar feature with reference to this sample: https://github.com/justinmfischer/SwiftyAccordionCells
As per your implementation, you are tracking the current expanded cell using "selectedIndexPath". So when your view is loaded you have to set "selectedIndexPath" row and section value to 0 as you are using only one section.
Hope this is helpful!
In viewDidLoad set selectedIndexPath = IndexPath(row: 0, section: 0)
That should "auto-expand" the first row.
Take a look at This, I followed this long time ago. So basically you are setting a flag isExpanded:, so that you can then set each cell to be expended or not.
With a quick google search, here is another tutorial.

Expand and Collapse tableview cells

I'm able to expand and collapse cells but i wanna call functions (expand and collapse) inside UITableViewCell to change button title.
import UIKit
class MyTicketsTableViewController: UITableViewController {
var selectedIndexPath: NSIndexPath?
var extraHeight: CGFloat = 100
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MyTicketsTableViewCell
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if(selectedIndexPath != nil && indexPath.compare(selectedIndexPath!) == NSComparisonResult.OrderedSame) {
return 230 + extraHeight
}
return 230.0
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if(selectedIndexPath == indexPath) {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
}
import UIKit
class MyTicketsTableViewCell: UITableViewCell {
#IBOutlet weak var expandButton: ExpandButton!
#IBOutlet weak var detailsHeightConstraint: NSLayoutConstraint!
var defaultHeight: CGFloat!
override func awakeFromNib() {
super.awakeFromNib()
defaultHeight = detailsHeightConstraint.constant
expandButton.button.setTitle("TAP FOR DETAILS", forState: .Normal)
detailsHeightConstraint.constant = 30
}
func expand() {
UIView.animateWithDuration(0.3, delay: 0.0, options: .CurveLinear, animations: {
self.expandButton.arrowImage.transform = CGAffineTransformMakeRotation(CGFloat(M_PI * 0.99))
self.detailsHeightConstraint.constant = self.defaultHeight
self.layoutIfNeeded()
}, completion: { finished in
self.expandButton.button.setTitle("CLOSE", forState: .Normal)
})
}
func collapse() {
UIView.animateWithDuration(0.3, delay: 0.0, options: .CurveLinear, animations: {
self.expandButton.arrowImage.transform = CGAffineTransformMakeRotation(CGFloat(M_PI * 0.0))
self.detailsHeightConstraint.constant = CGFloat(30.0)
self.layoutIfNeeded()
}, completion: { finished in
self.expandButton.button.setTitle("TAP FOR DETAILS", forState: .Normal)
})
}
}
If you want the cell to get physically bigger, then where you have your store IndexPath, in heightForRow: use:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if selectedIndexPath == indexPath {
return 230 + extraHeight
}
return 230.0
}
Then when you want to expand one in the didSelectRow:
selectedIndexPath = indexPath
tableView.beginUpdates
tableView.endUpdates
Edit
This will make the cells animate themselves getting bigger, you dont need the extra animation blocks in the cell.
Edit 2
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if(selectedIndexPath == indexPath) {
selectedIndexPath = nil
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? MyTicketsTableViewCell {
cell.collapse()
}
if let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow:indexPath.row+1, section: indexPath.section) as? MyTicketsTableViewCell {
cell.collapse()
}
} else {
selectedIndexPath = indexPath
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? MyTicketsTableViewCell {
cell.expand()
}
if let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow:indexPath.row+1, section: indexPath.section) as? MyTicketsTableViewCell {
cell.expand()
}
}
tableView.beginUpdates()
tableView.endUpdates()
}
All you need is implement UITableView Delegate this way:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
By default, estimatedHeight is CGRectZero, when you set some value for it, it enables autoresizing (the same situation with UICollectionView), you can do even also so:
tableView?.estimatedRowHeight = CGSizeMake(50.f, 50.f);
Then you need to setup you constraints inside your cell.
Check this post: https://www.hackingwithswift.com/read/32/2/automatically-resizing-uitableviewcells-with-dynamic-type-and-ns
Hope it helps)
In MyTicketsTableViewController class, inside cellForRowAtIndexPath datasource method add target for the button.
cell.expandButton.addTarget(self, action: "expandorcollapsed:", forControlEvents: UIControlEvents.TouchUpInside)
I tried to implement lots of the given examples on this and other pages with similar questions, but none worked for me since I had to perform some logic in my custom cell (eg. hide unneeded UILables in CustomCell.swift when the cell is collapsed).
Here is the implementation that worked for me:
Create a dictionary:
private var _expandedCells: [IndexPath:Bool] = [:]
Implement the heightForRowAt method:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return _expandedCells[indexPath] == nil ? 70 : _expandedCells[indexPath]! ? 150 : 70
}
Implement the didSelectRowAt method:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
_expandedCells[indexPath] = _expandedCells[indexPath] == nil ? true : !_expandedCells[indexPath]!
tableView.reloadRows(at: [indexPath], with: .fade)
}
Adjust your customCell:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? YourCustomTableViewCell, let isExpanded = _expandedCells[indexPath] else { return }
cell.set(expanded: isExpanded)
}

Resources