I am trying to create expandable UITableViewCells using the following code:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cell = tableView.cellForRow(at: indexPath) as! feedBaseCell
if cell.expandButton.isExpanded == true {
return 128
} else {
return 64
}
return 64
}
.isExpanded is a custom property of feedBaseCell. When I run this code, the line let cell = tableView.cellForRow(at: indexPath) as! feedBaseCell gets a EXC_BAD_ACCESS error. How can I check the .isExpanded property and return the height based on whether or not it is true or false? Why is my code not working?
EDIT: Code for both cellForRowAt and numberOfRowsInSection:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "feedBaseCell") as! feedBaseCell
cell.backgroundColor = .clear
cell.selectionStyle = UITableViewCellSelectionStyle.none
cell.contentView.isUserInteractionEnabled = false
cell.expandButton.tag = indexPath.row
cell.expandButton.addTarget(self, action: #selector(self.expandTheCell(_:)), for: .touchUpInside)
cell.contentView.bringSubview(toFront: cell.expandButton)
return cell
}
Here is the expandTheCell method:
func expandTheCell(_ sender: UIButton) {
if sender.isExpanded == false {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
sender.transform = sender.transform.rotated(by: .pi/2)
sender.isExpanded = true
}, completion: nil)
} else {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
sender.transform = sender.transform.rotated(by: -.pi/2)
sender.isExpanded = false
}, completion: nil)
}
tableView.beginUpdates()
tableView.endUpdates()
}
Never call cellForRow(at:) from within heightForRowAt. It causes infinite recursion because the table view tries to get the cell's height when you ask for the cell.
If you really, really need to get a cell to calculate its height, directly call your view controller's dataSource method tableView(_:cellForRowAt:) method instead of the table view's cellForRow(at:) method.
But in this case you don't need the cell. You should be storing the expanded state of each row in your data source data, not in the cell itself. Look at that data, not the cell, to determine the height in your heightForRowAt method. And of course you will need this state information in your cellForRowAt data source method so you can set the cell's state there since cells get reused as you scroll the table view.
Related
I have tableView cells that are populated with a color in each cell. What I want is when the user taps on the cell, it "opens"/expands so that that color fills the entire screen. Currently, it only scales downwards from the cell that I click on. I also need it to scale upwards along the y-axis, each cell expanding to the top of the screen, but I'm not sure what's prohibiting it to.
let expandedColorView: UIView = {
let view = UIView()
return view
}()
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.paletteTableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 1.0, animations: {
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
} )
}
}
}
}
UITapGestureRecognizer is declared in tableView(cellForRowAt:) with cell.isUserInteractionEnabled = true enabled.
I've tried changing the expandedColorView bounds self.expandedColorView.bounds.size.height = UIScreen.main.bounds.height in UIView.animate but that doesn't change anything. I was thinking the cell's frame would need to change so that it matches the parent view frame (which I think would be tableView) but I couldn't figure out how to do that.
Any help would be appreciated!
I've attached a gif of the issue:
If that's what you want
This is what I have done in data source extension
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = self.tableView.dequeueReusableCell(withIdentifier: "colorCell") as? ColorFulTableViewCell {
let color = colors[Int(indexPath.row % 7)]
cell.backgroundColor = color
return cell
}
return UITableViewCell()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let row = expandCell?.row, row == indexPath.row {
return self.tableView.bounds.size.height
}
else {
return 100
}
}
}
And tableView delegate extension looks like
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadRows(at: [indexPath], with: .automatic)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Whats happening her?
In heightForRowAt of TableView data source method, I check if cell need to cover the whole tableView size by using if let row = expandCell?.row, row == indexPath.row { and set its height to match the tableView height by returning self.tableView.bounds.size.height else I return 100
In didSelectRowAt I update the indexPath of cell to expand by saving it in expandCell and I reload the row (so that this time when height for row is called it can return self.tableView.bounds.size.height and I also call scrollToRow(at with position as .top to ensure my cell scrolls to top and makes itself visible completely
Because you are reloading only a specific cell, though from cost perspective its efficient, but animation might look rusty as other cells in visible indexPath array are adjusting them selves abruptly, you can always call reload Data to get much better smoother experience.
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadData()
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Hope this helps
I have already read some issues and not properly uses about spring animations in Swift but I am a little bit confused about this case. I have a ViewController which has a UITableView. I would like to add some little spring bouncing animation to its cells. When a cell tapped it should be expanding and running the bouncing animation and it works perfectly for the first time. But after a cell is expanded and tapped again, the animation is ignored, but the code inside animations is perfectly running (e.g. a print command). Do you have any idea to achieve that goal to make the animation work twice or more? I think I theoretically missed something.
My ViewController:
class TestTableViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var selectedIndex: IndexPath = IndexPath(row: 0, section: 0)
var isExpanded = [Bool]()
var currentExpandedIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
isExpanded = Array(repeating: false, count: 15)
tableView.delegate = self
tableView.dataSource = self
}
}
extension TestTableViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 15
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestTableViewCell", for: indexPath) as! TestTableViewCell
cell.selectionStyle = .none
cell.animate(duration: 0.5, delay: 0.2, damping: 0.5, options: .curveEaseOut)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if isExpanded[indexPath.row] == true { return 300 }
return 150
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// This is just for handling that to be only one cell that is expanded at one time
isExpanded[indexPath.row] = !isExpanded[indexPath.row]
if (currentExpandedIndexPath != nil) {
if (indexPath == currentExpandedIndexPath) {
currentExpandedIndexPath = nil
} else {
isExpanded[currentExpandedIndexPath!.row] = false
currentExpandedIndexPath = indexPath
}
} else {
currentExpandedIndexPath = indexPath
}
tableView.beginUpdates()
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}
And this is my TableViewCell class:
class TestTableViewCell: UITableViewCell {
func animate(duration: TimeInterval, delay: TimeInterval, damping: CGFloat, options: UIView.AnimationOptions = []) {
UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: 1, options: options, animations: {
self.contentView.layoutIfNeeded()
print("this command runs everytime")
})
}
}
These links are GIFs that show how it's working now. If I tap another cell after one expanded it has correct animation (first link). But if I tap the expanded one, it is not animated (second link).
Tapping one after one expanded
Tapping the same cell after it is expanded
There are two possibilities here:
1 - Following code possibly overrides your animation:
tableView.reloadRows(at: [indexPath], with: .automatic)
Try passing .none or some other flag.
2 - Following code must be called from main thread a.k.a DispatchQueue.main.
self.contentView.layoutIfNeeded()
There is also a 3rd possibility that is reloadRows does not necessarily call cellForRowAt where you are currently handling animations.
I currently have a tableView which I've created custom selection and deselection actions for (fade in and out a view). I am facing a problem where on unwind back to the tableView the deselect action isn't being called. I have added the necessary deselect code to my viewWillAppear so can't seem to work out what could be going wrong. Is there a different method for this use-case?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Deselect row on unwind
if let path = folderTableView.indexPathForSelectedRow {
folderTableView.deselectRow(at: path, animated: true)
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Select")
switch indexPath.section {
case 2:
let cell = tableView.cellForRow(at: indexPath) as! FolderTagTableViewCell
cell.folderTagSelectionBKG.alpha = 1.0
default:
let cell = tableView.cellForRow(at: indexPath) as! FolderTableViewCell
cell.folderSelectionBKG.alpha = 1.0
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
print("Should deselect")
switch indexPath.section {
case 2:
let cell = tableView.cellForRow(at: indexPath) as! FolderTagTableViewCell
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut, animations: {
cell.folderTagSelectionBKG.alpha = 0
})
default:
let cell = tableView.cellForRow(at: indexPath) as! FolderTableViewCell
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut, animations: {
cell.folderSelectionBKG.alpha = 0
})
}
}
From the documentation of deselectRow(at:animated:)
Calling this method does not cause the delegate to receive a tableView(_:willDeselectRowAt:) or tableView(_:didDeselectRowAt:) message, nor does it send selectionDidChangeNotification notifications to observers.
Calling this method does not cause any scrolling to the deselected row.
A solution is to move the code in didDeselectRowAt into an extra method
func deselectRowAnimated(at indexPath : IndexPath)
{
switch indexPath.section {
case 2:
let cell = tableView.cellForRow(at: indexPath) as! FolderTagTableViewCell
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut, animations: {
cell.folderTagSelectionBKG.alpha = 0
})
default:
let cell = tableView.cellForRow(at: indexPath) as! FolderTableViewCell
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut, animations: {
cell.folderSelectionBKG.alpha = 0
})
}
}
and call it in viewWillAppear and didDeleselect
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Deselect row on unwind
if let indexPath = folderTableView.indexPathForSelectedRow {
deselectRowAnimated(at: indexPath)
}
}
...
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
deselectRowAnimated(at: indexPath)
}
selectrow
If you using UITableViewController subclass then just set property
self.clearsSelectionOnViewWillAppear = YES;
else on viewDidAppear just call
NSIndexPath *indexPath = self.tableView.indexPathForSelectedRow;
if (indexPath) {
[self.tableView deselectRowAtIndexPath:indexPath animated:animated];
}
// MARK: - Swift 3
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
How to unselect a uitableview cell when the user returns to the view controller
From your tableView unwind action, try to put your deselect code. Like this:
#IBAction func <UnwindName>(segue : UIStoryboardSegue) {
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
}
I created a table view and each cell contains an image (covers the entire cell) and a title in the center. However, when I highlight one of the cells, nothing happens. It doesn't get darker which I thought was default. I tried using the code below to fix this, but it doesn't work.
func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
let cell = (tableView.cellForRow(at: indexPath) as! MainPackCell)
UIView.animate(withDuration: 0.4) {
cell.packImageView.highlightedImage = cell.packImageView.image?.withRenderingMode(.alwaysTemplate)
if #available(iOS 10.0, *) {
cell.packImageView.tintColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 0.3)
} else {
// Fallback on earlier versions
}
UIView.animate(withDuration: 0.4, animations: {
tableView.deselectRow(at: indexPath, animated: true)
})
}
}
Populate the TableVIew and refer to the Screenshot below:---
Use this and it is Done:-- Man
implement it after cellForRowAt
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
I have this code that is setup so that it calls and animates the tableview cells up upon displaying. How could I set this up so it only animates the cells in that appear on the initial loading of the view?
Thanks in advance...
var preventAnimation = Set<NSIndexPath>()
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
//
if !preventAnimation.contains(indexPath) {
preventAnimation.insert(indexPath)
let rotationTransform = CATransform3DTranslate(CATransform3DIdentity, 0, +600, 0)
cell.layer.transform = rotationTransform
UIView.animateWithDuration(0.6, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .CurveEaseOut, animations: {
cell.layer.transform = CATransform3DIdentity
}, completion: nil)
}
}
You can just use a variable maxCellIndexAppeared to recode the maximum indexPath.row for the cell that have appeared,it better than using a set to contain all the indexPath.
In the initialize method set maxCellIndexAppeared to zero,and then compare it with the indexPath.row when the cell will display,if current cell indexPath greater than the variable then do your animation otherwise return.
Just like this:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if maxCellIndexAppeared > indexPath.row {
return
}
//here reset the current maximum index
maxCellIndexAppeared = indexPath.row
//And then do your animation
....do your animation.....
}
//Declare Bool array which will keep track of the rows which are animated
var animationShown : [Bool]?
// Initalize animationShown array in your viewDidLoad
self.animationShown = [Bool] (repeating: false, count: YourTableViewRowsCount)
// MARK: - Animate Tableview Cell
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
if self.animationShown?[indexPath.row] == false
{
//Transform Animation
let transform = CATransform3DTranslate(CATransform3DIdentity, -200, 0, 0)
cell.layer.transform = transform
UIView.animate(withDuration: 0.5, animations:
{
cell.layer.transform = CATransform3DIdentity
})
self.animationShown?[indexPath.row] = true
}
}
Hope this helps!! Happy Coding...