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)
}
}
Related
Why do animations not work when deselecting a UITableViewCell in a UITableView asynchronously?
Below is a simple example. If you tap and release the first cell, it deselects with the animation as you would expect.
If you tap and release the second cell, you may not notice anything happen at all. You may even need to hold the cell to see the selection, and when you release it reverts back to the unselected state immediately.
This is obviously a contrived example but it shows the essence of the issue. If I wanted to delay the deselection until something had happened I would face the same issue.
Why does this happen and is there any way around it?
import UIKit
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = indexPath.row == 0 ? "Regular deselection" : "Async deselection"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
tableView.deselectRow(at: indexPath, animated: true)
} else {
DispatchQueue.main.async {
tableView.deselectRow(at: indexPath, animated: true)
}
}
}
}
This is a little curious...
There are a number of UI controls that have "internal processes" - that is, stuff goes on that we're not immediately aware of.
One example is a standard UIButton. During the touch-down / touch-up sequence, the button's Title Label does a cross-fade from .normal to .highlighted and back again.
So, my assumption is:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
// 1
tableView.deselectRow(at: indexPath, animated: true)
} else {
DispatchQueue.main.async {
// 2
tableView.deselectRow(at: indexPath, animated: true)
}
}
}
For the first case, UIKit probably "queues up" the highlight/unhighlight sequence.
Whereas in the second case, we are telling the table view to deselect the row on the next run-loop ... which will happen pretty much immediately. At that point, we're (in effect) interrupting the default sequence, and the table view un-highlights the row before it finishes (or, practically, before it starts) highlighting the row.
What's even more curious... if we leave a row selected, and then at a later point (such as a tap elsewhere) and then call tableView.deselectRow(at: indexPath, animated: true), the animation appears to be much shorter, to the point where it doesn't really even look animated.
Here's something to play around with -- maybe you'll find one approach suitable for your goal.
class DeselViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
let noAnimBtn = UIBarButtonItem(title: "No Anim", style: .plain, target: self, action: #selector(noAnim(_:)))
let animBtn = UIBarButtonItem(title: "With Anim", style: .plain, target: self, action: #selector(withAnim(_:)))
navigationItem.rightBarButtonItems = [animBtn, noAnimBtn]
}
#objc func noAnim(_ b: Any?) -> Void {
if let p = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: p, animated: false)
}
}
#objc func withAnim(_ b: Any?) -> Void {
if let p = tableView.indexPathForSelectedRow {
UIView.animate(withDuration: 0.3, animations: {
self.tableView.deselectRow(at: p, animated: false)
})
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 8
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
switch indexPath.row {
case 0:
cell.textLabel?.text = "Regular deselection"
case 1:
cell.textLabel?.text = "Async deselection"
case 2:
cell.textLabel?.text = "Async deselection with Anim Duration"
case 3:
cell.textLabel?.text = "Asnyc Delay deselection"
case 4:
cell.textLabel?.text = "Asnyc Delay Plus Anim Duration deselection"
case 5:
cell.textLabel?.text = "Asnyc Long Delay deselection"
case 6:
cell.textLabel?.text = "Asnyc Long Delay Plus Anim Duration deselection"
default:
cell.textLabel?.text = "Manual deselection"
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.row {
case 0:
tableView.deselectRow(at: indexPath, animated: true)
case 1:
DispatchQueue.main.async {
tableView.deselectRow(at: indexPath, animated: true)
}
case 2:
DispatchQueue.main.async {
UIView.animate(withDuration: 0.3, animations: {
tableView.deselectRow(at: indexPath, animated: true)
})
}
case 3:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
tableView.deselectRow(at: indexPath, animated: true)
})
case 4:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
UIView.animate(withDuration: 0.3, animations: {
tableView.deselectRow(at: indexPath, animated: true)
})
})
case 5:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75, execute: {
tableView.deselectRow(at: indexPath, animated: true)
})
case 6:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75, execute: {
UIView.animate(withDuration: 0.3, animations: {
tableView.deselectRow(at: indexPath, animated: true)
})
})
default:
()
// leave selected
}
}
}
Set that as the root view of a navigation controller, so it can put right-bar buttons for "Manual" deselection:
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 want to detect a tap on imageview in uicollectionviewcell inside uitableviewcell
I'm using an api response to build a data in my tableview
I have this API response:
{"status":1,"data":{"blocks":[{"name":"CustomBlock","description":"CustomDescription","itemsType":"game","items":[510234,78188,15719,37630]}], "items":[{"id:"1", name: "testgame"}]}
BlocksViewController.swift
class BlocksViewController: UIViewController, UITableViewDataSource, UICollectionViewDataSource, UICollectionViewDelegate, UITableViewDelegate {
var blocks = [Block]() // I'm getting blocks in this controller
var items : BlockItem! // and items
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return blocks[collectionView.tag].items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GameCollectionCell", for: indexPath) as? GameCollectionCell else { return
UICollectionViewCell() }
if let found = items.game.first(where: {$0.id == String(blocks[collectionView.tag].items[indexPath.row])}) {
cell.gameName.text = found.name
cell.gameImage.kf.indicatorType = .activity
let processor = DownsamplingImageProcessor(size: CGSize(width: 225, height: 300))
cell.gameImage.kf.setImage(
with: URL(string: found.background_image ?? ""),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(0.2)),
.cacheOriginalImage
])
}
else {
cell.gameName.text = ""
cell.gameImage.image = nil
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return blocks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "BlockCell") as? BlockCell else { return UITableViewCell() }
cell.blockName.text = blocks[indexPath.row].name
cell.blockDescription.text = blocks[indexPath.row].description
cell.setScrollPosition(x: offsets[indexPath] ?? 0)
cell.gameCollectionCell.delegate = self
cell.gameCollectionCell.dataSource = self
cell.gameCollectionCell.tag = indexPath.row
cell.gameCollectionCell.reloadData()
return cell
}
I'm getting blocks and items in this controller. Now i want to detect a tap using LongTapGestureRecognizer on image in gamecollectionCell(UIcollectionViewCell inside BlockCell(TableviewCell). How can i do this? Or maybe any advice how to improve logic here?
Okay, i've added gesture recognizer like this in cellForItemAt :
cell.addGestureRecognizer(UILongPressGestureRecognizer.init(target: self, action: #selector(addGamePopUp)))
Then i need to animate uiimageview on long tap.
var selectedGameCell : GameCollectionCell?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedGameCell = collectionView.dequeueReusableCell(withReuseIdentifier: "GameCollectionCell", for: indexPath) as? GameCollectionCell
}
And
#IBAction func addGamePopUp(_ sender: UILongPressGestureRecognizer) {
if (sender.state == UIGestureRecognizer.State.began){
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage.transform = CGAffineTransform(scaleX: 0.95,y: 0.95);
}) { (Bool) in
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage.transform = CGAffineTransform(scaleX: 1,y: 1);
});
}
}
}
But it still doesn't work. Did i miss something?
If you want to use longTapGestureRecognizer, just add one to the cell in your cellForItemAtIndexPath method of your collectionView, like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SubjectCellId", for: indexPath) as? SubjectCell {
cell.addGestureRecognizer(UILongPressGestureRecognizer.init(target: self, action: #selector(someMethod)))
return cell
}
return UICollectionViewCell()
}
You can use following delegate method of uicollectionview to detect tap on collection view cell.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath){
print("cell tapped")
}
For Adding Long Press Gesture Add Following Code in Cell For item at indexpath method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : GameCollectionCell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! GameCollectionCell
cell.backgroundColor = model[collectionView.tag][indexPath.item]
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(addGamePopUp(_:)))
cell.addGestureRecognizer(lpgr)
return cell
}
#IBAction func addGamePopUp(_ sender: UILongPressGestureRecognizer){
print("add game popup")
if (sender.state == UIGestureRecognizer.State.began){
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage?.transform = CGAffineTransform(scaleX: 0.95,y: 0.95);
}) { (Bool) in
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage?.transform = CGAffineTransform(scaleX: 1,y: 1);
});
}
}
}
You can use touchesBegan method inside tableview cell and from the touch location get the collection view cell object inside it.
NOTE: When you implement this method the didSelectRow method would not be called for the TableViewCell.
extension TableViewCell {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let point = touch.location(in: self)
if let path = collectionView.indexPathForItem(at: point) {
//Get cell of collection view from this index path
}
}
}
}
In order to get things working, I'd recommend changing your function from
#IBAction func addGamePopUp(_ sender: UILongPressGestureRecognizer) {
to
#objc func addGamePopUp() {
And when you're adding the longTapGestureRecognizer to your collectionViewCell, you'll have it trigger that method by changing the line to:
cell.addGestureRecognizer(UILongPressGestureRecognizer.init(target: self, action: #selector(addGamePopUp)))
Let me know if that works!
(psst: also take out this if check in your addGamePopupMethod if you're going this route)
if (sender.state == UIGestureRecognizer.State.began){
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.
When a user returns to the UITableView from the detailViewController, I want the tableView to return to the previous position. Found a solution from this post, but it does not work as I have not saved the selected row. This is where I am stuck.
func scrollToSelectedRow() {
let selectedRows = self.tableView.indexPathsForSelectedRows
if let selectedRow = selectedRows?[0] as NSIndexPath? {
self.tableView.scrollToRow(at: selectedRow as IndexPath, at: .middle, animated: true)
}
}
with
scrollToSelectedRow()
in the viewDidAppear.
I do save the row as below to send the detail to the DetailViewController
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
{
self.selected = (indexPath as NSIndexPath).row
return indexPath
}
However, not sure how to associate the two together.
Any help appreciated
Problem with your code is this part
let selectedRows = self.tableView.indexPathsForSelectedRows
if let selectedRow = selectedRows?[0] as NSIndexPath? {
self.tableView.scrollToRow(at: selectedRow as IndexPath, at: .middle, animated: true)
}
If statement is never called
Trying doing this
Declare your index path like this
var selected = IndexPath(row: NSNotFound, section: NSNotFound)
Now in didselect class
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "ChatViewController")
self.present(nextViewController, animated:true, completion:nil)
self.selected = (indexPath as NSIndexPath) as IndexPath
}
Call this function
func scrollToSelectedRow() {
if self.selected.row != NSNotFound
{
self.tableView.scrollToRow(at: self.selected as IndexPath, at: .middle, animated: true)
}
}
In
override func viewWillAppear(_ animated: Bool) {
scrollToSelectedRow()
}