CollectionViewCell press effect - ios

I would like to give press effect for collectionViewCell on didSelectItem. On click I would like to add some color and after some delay I would like to revert the color. While trying the code below, press effect is added only after some delay. Can anyone suggest how to achieve this?
DispatchQueue.global(qos: .utility).async {
let cell = collectionView.cellForItem(at: indexPath) as ..cell
// Change bg color for foreground layer
DispatchQueue.main.async {
Timer.scheduledTimer(timeInterval: 0.10, target: self, selector: #selector(self.updateCell(timer:)) , userInfo: ["cell":cell,"collectionview":collectionView], repeats: false)
}
// Some calculations
}

.cellForItem(at: indexPath) as ..cell should usually be called in the main thread, you might see weird behavior if you are doing it from your utility queue
I suggest also to not use DispatchQueues and handle animation timing by yourself, there's already a nice function that does exactly what you want. This would be my suggestion:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as ..cell
UIView.animate(withDuration: 0.2, delay: 0.4, options: .curveEaseInOut, animations: {
cell.backgroundColor = UIColor.red
}, completion: { _ in
// here the animation is done
})
}
Update with a nicer solution
Anyway, i think a nicer approach than handling the animation in the didSelectItemAt function of your UICollectionViewDelegate would be overriding the isSelected property of the cell.
In this example, I animate the backgroundColor whenever the selection state of the UICollectionViewCell changes - red when selected, green otherwise. Just replace that with whatever style you desire.
class YourCell: UICollectionViewCell {
override var isSelected: Bool {
willSet(newValue) {
let newColor = isSelected ? UIColor.red : UIColor.green
UIView.animate(withDuration: 1, delay: 0.4, options: .curveEaseInOut, animations: {
self.backgroundColor = newColor
}, completion: { _ in
// here the animation is done - nothing to do here probably
})
}
}
}

Related

UITableViewCell animation is hit or miss. Sometimes table data/cells just pop in without the animation

So I'm using a custom animation for how the table cells appear. They sort of fade in and slide up a little. I'm populating the table with data from an API call.
What I'm noticing is that it doesn't always happen. Sometimes it works as expected, other times the cells/data just appear abruptly, no animation/movement. They just sort of pop in at what would would be the end of the animation. I don't really see any consistent pattern in when it works and when it doesn't. Seems pretty random.
Any idea what is causing this behavior?
Here's the code for the animation and where it is in the tableview function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: RecentActivityCell = recentActivityView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! RecentActivityCell
var lastInitialDisplayableCell = false
//change flag as soon as last displayable cell is being loaded (which will mean table has initially loaded)
if userActivityArray.count > 0 && !finishedLoadingInitialTableCells {
if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows,
let lastIndexPath = indexPathsForVisibleRows.last, lastIndexPath.row == indexPath.row {
lastInitialDisplayableCell = true
}
}
if !finishedLoadingInitialTableCells {
if lastInitialDisplayableCell {
finishedLoadingInitialTableCells = true
}
//animates the cell as it is being displayed for the first time
cell.transform = CGAffineTransform(translationX: 0, y: 40/2)
cell.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0.05*Double(indexPath.row), options: [.curveEaseInOut], animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0)
cell.alpha = 1
}, completion: nil)
}
cell.backgroundColor = UIColor.clear
if userActivityArray[indexPath.row].status == "Success" {
cell.statusImage.image = UIImage(named: "passImage")
} else {
cell.statusImage.image = UIImage(named: "failImage")
}
cell.activity = userActivityArray[indexPath.row]
return cell
}
Try to use your animation in "yourTableViewCell" by overriding:
override func prepareForReuse() {
super.prepareForReuse()
\\ Animation code over here
}
dont use in "CellForRowAt"

Scale tableView cell with CGAffineTransform to cover entire screen

I have a tableView where when a user taps on that cell, I want the cell to expand and fill the entire screen with that color. Currently, it does scale to fill the entire screen but the cells above it will also show. The colors come from an API and each cell changes color when the user taps the "Generate" button. Due to how the API's JSON file is structured, I only want 5 colors on the screen at a time.
I can't figure out why the cells above it don't move upwards or how I can move the clicked cell to the top of the screen. I've tried offsetting the coordinates of the tapped cell and changing its frame size to be the height of the tableView tappedCell.bounds.size = CGSize(width: UIScreen.main.bounds.width, height: self.paletteTableView.bounds.height) but when I click the 1st or 2nd cells from the top, the screen doesn't fill all the way to the bottom.
I've also tried animating from a UIView (called colorDetailsView here) but wasn't able to successfully get it to animate from the center of each cell that's tapped on, only from the center of the screen.
The animation happens here:
#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: 0.5, animations: {
switch self.currentAnimation {
case 0:
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
//...
case 1:
tappedCell.transform = .identity
default: break
}
} )
currentAnimation += 1
if currentAnimation > 1 {
currentAnimation = 0
} } } } }
Full code:
class PaletteController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
var colorPalette = [Color]()
var currentAnimation = 0
let colorDetailsView: UIView = {
let view = UIView()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.isScrollEnabled = false
\\...
view.addSubview(colorDetailsView)
}
//Color cell expands to fill the screen of that color when user taps on the cell
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.tableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 0.5, animations: {
//When user taps on a cell, it is scaled to fill the screen with that color
switch self.currentAnimation {
case 0:
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
for i in 0..<self.colorPalette.count {
if tapIndexPath.row == i {
self.colorDetailsView.backgroundColor = UIColor(hexString: self.colorPalette[i])
}
}
case 1:
tappedCell.transform = .identity
default: break
}
} )
currentAnimation += 1
if currentAnimation > 1 {
currentAnimation = 0
}
}
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:"ColorCell", for: indexPath)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(userTap(sender:)))
cell.addGestureRecognizer(tapGesture)
cell.isUserInteractionEnabled = true
//Assigns a new color to each row. Colors are converted from HEX to RGB
for i in 0..<colorPalette.count {
if tapIndexPath.row == i {
cell.backgroundColor = UIColor(hexString: colorPalette[i])
}
}
return cell
}
}
I know the cell is scaling when I set tableView.isScrollEnabled to true, as shown below. Each cell should end up filling the whole screen and not get covered by any other cells, like the top-most cell. Ideally, I'd like the tableView to not scroll, but I'm not sure if the top cells will "move up" if scroll is disable.
Any help is appreciated!
The cells are after all sub-views of the table view. You need to bring the tapped cell at the front of the sub-view list.
I would suggest adding
tableView.bringSubviewToFront(tappedCell)
just after the line
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {.
See if this works!

How to synchronize cells animation

I'm trying to simulate a shimmering animation in each cell of a UICollectionView.
When I start the app it works fine:
But when I scroll to the left each cell gets itself animation:
What I want is when I to scroll it to left, it works like the first image. For a while, I've just put this code below to do the animation:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let cell = cell as? ShimmeringCell else { return }
UIView.animate(withDuration: 1.0, delay: 0, options: [.autoreverse, .repeat], animations: {
cell.alpha = 0.4
}, completion: nil)
}
This is the full code: Github
Can anyone help me?
I found the answer.
To solve this issue, I had to create a new UIView and add this in a subview of UICollectionView:
let newCollectionView: UICollectionView = {
// Here was creating a collectionView
return collection
}()
let viewExample: UIView = {
// Here was creating a new view
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
newCollectionView.delegate = self
newCollectionView.dataSource = self
newCollectionView.register(ShimmeringCell.self, forCellWithReuseIdentifier: cellId)
self.view.addSubview(self.newCollectionView)
setupCollection()
}
private func setupCollection() {
// Constraints...
self.newCollectionView.addSubview(self.viewExample)
setupViewExample()
}
private func setupViewExample() {
// Constraints...
}
After that, I just put the animation in this view:
override func viewDidLoad() {
super.viewDidLoad()
newCollectionView.delegate = self
newCollectionView.dataSource = self
newCollectionView.register(ShimmeringCell.self, forCellWithReuseIdentifier: cellId)
self.view.addSubview(self.newCollectionView)
setupCollection()
self.viewExample.alpha = 0.6
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
self.viewExample.alpha = 0
}, completion: nil)
}
It's resulted:
Thanks for all and see you somewhere on the world.
Github with this solution: Github

Question about making UIView disappear from screen in Swift

I want to show the View for a few seconds and make it disappear from the screen.
If I wait 10 seconds on this screen
I want to make this view go off screen.
How can I animate the view slowly descending down?
code...
fileprivate func setupViewToAnimate(view: UIView) {
UIView.animate(withDuration: 2, delay: 10, options: .curveEaseInOut , animations: {
}) { _ in
}
}
You can use Timer to schedule the dismiss of your snackBar/notification view. it will be triggered after the completion of animation for showing your view. Please check the following code for the same.
private let keyWindow = UIApplication.shared.keyWindow!
let snackbarView = UIView()
fileprivate func setupViewToAnimate() {
let targetYPost = snackbarView.frame.origin.y
let dismissTimerLength = 3.0
snackbarView.frame.origin.y = keyWindow.frame.height
UIView.animate(withDuration: 0.4, animations: {
self.snackbarView.frame.origin.y = targetYPost
}, completion: {
_ in
Timer.scheduledTimer(timeInterval: TimeInterval(dismissTimerLength), target: self, selector: #selector(self.hideSnackbarView), userInfo: nil, repeats: false)
})
}
//After 3.4 sec snack bar will be hidden
#objc private func hideSnackbarView() {
UIView.animate(withDuration: 0.4, animations: {
self.snackbarView.frame.origin.y = self.keyWindow.frame.height
}, completion: {
_ in
self.snackbarView.isHidden = true
})
}
Output:-
I would suggest something like this:
private func setupViewToAnimate(view: UIView, bottomConstraint: NSLayoutConstraint) {
UIView.animate(withDuration: 2, delay: 10, options: .curveEaseInOut , animations: {
// Set the bottomConstraint to minus the height of the view so it gets entierly hidden
bottomConstraint.constant = -view.frame.height
// Tell the view to re-layout its subviews again so we have something to animate
self.view.layoutIfNeeded()
}) { _ in
// If the animation is completed, we can remove the view from the superview since we don't need it anymore
view.removeFromSuperview()
}
}

wait until scrollToRowAtIndexPath is done in swift

I have a UITableView with more cells than fit on screen. When I get a notification from my data model I want to jump to a specific row and show a very basic animation.
My code is:
func animateBackgroundColor(indexPath: NSIndexPath) {
dispatch_async(dispatch_get_main_queue()) {
NSLog("table should be at the right position")
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) as? BasicCardCell {
var actColor = cell.backgroundColor
self.manager.vibrate()
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = UIColor.redColor() }, completion: {
_ in
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = actColor }, completion: { _ in
self.readNotificationCount--
if self.readNotificationCount >= 0 {
var legicCard = self.legicCards[indexPath.section]
legicCard.wasRead = false
self.reloadTableViewData()
} else {
self.animateBackgroundColor(indexPath)
}
})
})
}
}
}
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int {
dispatch_sync(dispatch_get_main_queue()){
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: index), atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
self.animateBackgroundColor(NSIndexPath(forRow: 0, inSection: index))
}
}
I hoped that the dispatch_sync part would delay the execution of my animateBackgroundColor method until the scrolling is done. Unfortunately that is not the case so that animateBackgroundColor gets called when the row is not visible yet -> cellForRowAtIndexPath returns nil and my animation won't happen. If no scrolling is needed the animation works without problem.
Can anyone tell my how to delay the execution of my animateBackgroundColor function until the scrolling is done?
Thank you very much and kind regards
Delaying animation does not seem to be a good solution for this since scrollToRowAtIndexPath animation duration is set based on distance from current list item to specified item. To solve this you need to execute animateBackgroudColor after scrollToRowAtIndexPath animation is completed by implementing scrollViewDidEndScrollingAnimation UITableViewDelegate method. The tricky part here is to get indexPath at which tableview did scroll. A possible workaround:
var indexPath:NSIndexpath?
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int{
dispatch_sync(dispatch_get_main_queue()){
self.indexPath = NSIndexPath(forRow: 0, inSection: index)
self.tableView.scrollToRowAtIndexPath(self.indexPath, atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
}
}
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
self.animateBackgroundColor(self.indexPath)
indexPath = nil
}
Here are my solution
1) Create a .swift file, and copy code below into it:
typealias SwagScrollCallback = (_ finish: Bool) -> Void
class UICollectionViewBase: NSObject, UICollectionViewDelegate {
static var shared = UICollectionViewBase()
var tempDelegate: UIScrollViewDelegate?
var callback: SwagScrollCallback?
func startCheckScrollAnimation(scroll: UIScrollView, callback: SwagScrollCallback?){
if let dele = scroll.delegate {
self.tempDelegate = dele
}
self.callback = callback
scroll.delegate = self
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
callback?(true)
if let dele = self.tempDelegate {
scrollView.delegate = dele
tempDelegate = nil
}
}
}
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, _ callback: SwagScrollCallback?){
UICollectionViewBase.shared.startCheckScrollAnimation(scroll: self, callback: callback)
self.scrollToItem(at: indexPath, at: scrollPosition, animated: true)
}
}
2) Example:
#IBAction func onBtnShow(){
let index = IndexPath.init(item: 58, section: 0)
self.clv.scrollToItem(at: index, at: .centeredVertically) { [weak self] (finish) in
guard let `self` = self else { return }
// Change color temporarily
if let cell = self.clv.cellForItem(at: index) as? MyCell {
cell.backgroundColor = .yellow
cell.lbl.textColor = .red
}
// Reset
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.clv.reloadData()
}
}
}
3) My github code example: github here
I has similar problem. I just do this.
UIView.animateWithDuration(0.2,delay:0.0,options: nil,animations:{
self.tableView.scrollToRowAtIndexPath(self.indexPath!, atScrollPosition: .Middle, animated: true)},
completion: { finished in UIView.animateWithDuration(0.5, animations:{
self.animateBackgroundColor(self.indexPath)})})}

Resources