UICollectionView Animate when touched - ios

Could someone point me in the right direction on how to animate a UICollesctionView's Cell when it is touched? I've read there are several methods between didSelectItemAt with UIView.Animate or willDisplayCell with CAAnimations. Could someone please point me in the right direction in Swift? The goal is to tap the cell and have it scale/ change x position

I will choose "didSelectItemAt", with "UIView.animate"
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let cell = collectionView.cellForItem(at: indexPath)
animatedImage = UIImageView(frame: cell.frame)
animatedImage.image = ...
view.addSubview(animatedImage)
UIView.animate(withDuration: 0.5, animations: {
self.animatedImage.frame = self.view.bounds
self.view.layoutIfNeeded()
}, completion: {(finished) in
...
})
}

Related

Cant get correct indexPath of collectionView cell

EDIT:
I try to get a correct indexPath of a current cell in a collectionView.
The project is simple: an album of photos and a label with a text. Text in label should be the current indexPath.
Concerning photos - everything is ok. Problem is with indexPath parameter in a label.
First right swipe changes indexPath for + 2 instead of +1: from [0,0] to [0,2] - instead of [0,1]. Right swipes that have been made after this work correctly.
But first left swipe changes the value of indexPath for -3. For example, if indexPath was [0,6] - the first left swipe will change it to [0, 3].
I have made a 10-sec video, representing a problem: https://www.youtube.com/shorts/Qxqr_Q9SDJ8
The buttons are made in order it would be easier to notice changes. They work like right/left swipe. Native swipes give the same result.
The code is this one:
var currentIndexPath: IndexPath = [0,0]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
var testIndexPath = indexPath
cell.imageView.image = allPhotos[testIndexPath.item].faceCard
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
currentIndexPath = indexPath
}
label.text = "\(currentIndexPath)"
You can override scrollViewDidScroll method and get the visible cell's indexPath when swiped:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
if let testIndexPath = collectionView.indexPathForItem(at: visiblePoint) {
label.text = "testIndexPath: \(testIndexPath)"
}
}
i think you should use collectionview.indexPathForVisibleItems to get the indexPath when collectionView end scroll.
you can check collectionView end scroll by this:
Swift 4 UICollectionView detect end of scrolling

UICollectionView: inconsistent expanding cell animations

After a frustrating amount of time in trying to "fix" my inconsistent expanding collection cell animations, I have determined that UICollectionView / FlowLayout uses different animations depending on the ratio of cell expansion, with the threshold of 2:1.
If the ratio is under 2:1, you get this:
If the ratio is equal to or greater than 2:1, you get this:
Does anyone know if this can be configured?
The offending code (with some syntactic sugar) is:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = SimpleCalculatedSizeCell.prototype
let contents = data[indexPath.item]
cell.label.text = contents
let width = collectionView.bounds
.inset(collectionView.contentInset)
.inset(layout.sectionInset)
.width
let finalSize = cell.systemLayoutSizeFitting(
.init(width: width, height: 0),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
.withWidth(width)
if let selected = selected, selected == indexPath {
print("\(#function): \(finalSize)")
return finalSize.withHeight(150)
}
print("\(#function): \(finalSize)")
// return finalSize.withHeight(76) // expands smoothly
return finalSize.withHeight(75) // fades and snaps
}
with an the selection handling of:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selected = selected == indexPath ? nil : indexPath
UIView.animate(withDuration: 0.25, animations: {
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutIfNeeded()
})
Thanks!

UICollectionViewCell - contents do not animate alongside cell's contentView

Problem looks like this: http://i.imgur.com/5iaAiGQ.mp4
(red is a color of cell.contentView)
Here is the code: https://github.com/nezhyborets/UICollectionViewContentsAnimationProblem
Current status:
The content of UICollectionViewCell's contentView does not animate alongside contentView frame change. It gets the size immediately without animation.
Other issues faced when doing the task:
The contentView was not animating alongside cell's frame change either, until i did this in UICollectionViewCell subclass:
override func awakeFromNib() {
super.awakeFromNib()
//Because contentView won't animate along with cell
contentView.frame = bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
Other notes:
Here is the code involved in cell size animation
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.performBatchUpdates({
collectionView.reloadData()
}, completion: nil)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let isSelected = self.selectedIndex == indexPath.row
let someSize : CGFloat = 90 //doesn't matter
let sizeK : CGFloat = isSelected ? 0.9 : 0.65
let size = CGSize(width: someSize * sizeK, height: someSize * sizeK)
return size
}
I get the same results when using collectionView.setCollectionViewLayout(newLayout, animated: true), and there is no animation at all when using collectionView.collectionViewLayout.invalidateLayout() instead of reloadData() inside batchUpdates.
UPDATE
When I print imageView.constraints inside UICollectionView's willDisplayCell method, it prints empty array.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
for view in cell.contentView.subviews {
print(view.constraints)
}
//Outputs
//View: <UIImageView: 0x7fe26460e810; frame = (0 0; 50 50); autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x608000037280>>
//View constraints: []
}
This is a finicky problem, and you're very close to the solution. The issue is that the approach to animating layout changes varies depending on whether you're using auto layout or resizing masks or another approach, and you're currently using a mix in your ProblematicCollectionViewCell class. (The other available approaches would be better addressed in answer to a separate question, but note that Apple generally seems to avoid using auto layout for cells in their own apps.)
Here's what you need to do to animate your particular cells:
When cells are selected or deselected, tell the collection view layout object that cell sizes have changed, and to animate those changes to the extent it can do so. The simplest way to do that is using performBatchUpdates, which will cause new sizes to be fetched from sizeForItemAt, and will then apply the new layout attributes to the relevant cells within its own animation block:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.performBatchUpdates(nil)
}
Tell your cells to layout their subviews every time the collection view layout object changes their layout attributes (which will occur within the performBatchUpdates animation block):
// ProblematicCollectionViewCell.swift
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
If you want greater control over your animations, you can nest the call to performBatchUpdates inside a call to one of the UIView.animate block-based animation methods. The default animation duration for collection view cells in iOS 10 is 0.25.
The solution is very easy. First, in ViewController.collectionView(_,didSelectItemAt:), write only this:
collectionView.performBatchUpdates({
self.selectedIndex = indexPath.row
}, completion: nil)
And then, in the class ProblematicCollectionViewCell add this func:
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
self.layoutIfNeeded()
}
Enjoy!
You can apply a transform to a cell, although it has some drawbacks, such as handling orientation changes.
For extra impact, I have added a color change and a spring effect in the mix, neither of which could be achieved using the reloading route:
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
UIView.animate(
withDuration: 0.4,
delay: 0,
usingSpringWithDamping: 0.4,
initialSpringVelocity: 0,
options: UIViewAnimationOptions.beginFromCurrentState,
animations: {
if( self.selectedIndexPath.row != NSNotFound) {
if let c0 =
collectionView.cellForItem(at: self.selectedIndexPath)
{
c0.contentView.layer.transform = CATransform3DIdentity
c0.contentView.backgroundColor = UIColor.lightGray
}
}
self.selectedIndexPath = indexPath
if let c1 = collectionView.cellForItem(at: indexPath)
{
c1.contentView.layer.transform =
CATransform3DMakeScale(1.25, 1.25, 1)
c1.contentView.backgroundColor = UIColor.red
}
},
completion: nil)
}

Expanding UICollectionView and its cell when tapped

I am trying to make a transition animation like the demonstration in the link here. So when I clicked the cell, it expands and covers the whole screen.
Here are my codes(I have to admit that I am not familiar with CollectionView)`
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var mainDesLabel: UILabel!
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var secDesLabel: UILabel!
let searchBar = UISearchBar()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.backgroundColor = UIColor.clearColor()
////////////////////////////////////////////////////////////////////////
self.searchBar.frame = CGRect(x: 175, y: 0, width: 200, height: 50)
self.searchBar.searchBarStyle = UISearchBarStyle.Minimal
self.searchBar.backgroundColor = UIColor.whiteColor()
self.view.addSubview(searchBar)
////////////////////////////////////////////////////////////////////////
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as UICollectionViewCell
cell.layer.cornerRadius = 5
return cell
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
//Use for size
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
self.collectionView.frame = self.view.bounds
let cell = collectionView.cellForItemAtIndexPath(indexPath)
cell!.frame = CGRectMake(0, 0, self.view.bounds.width, self.view.bounds.height)
}
}
So I thought use 'didSelectItemAtIndexPath' would help, however it turns out like this
thoughts? Any help would be highly appreciated!
Or what you can do is expand the item and change its frame with UIAnimation.
And when he cell is tapped, you get the views inside the cell to be expanded also using auto layout and I'm hinting towards (clips to bounds).
something like this:
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let item = collectionView.cellForItemAtIndexPath(indexPath) as! cellCollectionViewExpress // or whatever you collection view cell class name is.
UIView.animateWithDuration(1.0, animations: {
self.view.bringSubviewToFront(collectionView)
collectionView.bringSubviewToFront(item)
item.frame.origin = self.view.frame.origin /// this view origin will be at the top of the scroll content, you'll have to figure this out
item.frame.size.width = self.view.frame.width
item.frame.size.height = self.view.frame.height
})
}
I would suggest you that you use UICollectionView Controller, so things are at ease in general with using that.
You cannot just use didSelectItemAtIndexPath or any similar methods to update the size of a UICollectionViewCell once the UICollectionView is done performing the view layout.
To update cell height,
You can first capture which cell had been tapped in didSelectItemAtIndexPath.
Then, you can either reload the entire collection view with the new cell frame being passed in the sizeForItemAtIndexpath.
Or, you can just reload the specific cell with reloadItemsAtIndexPaths, but you still need to pass the updated size of the cell via sizeForItemAtIndexpath.
UPDATE
I now see the question details have been updated by an animation which you desire to have.
I had performed a similar animation by:-
Capturing the cell which had been tapped in didSelectItemAtIndexPath.
Adding a replica view to the UIViewContrller, but with its frame totally coinciding with the cell which had been tapped.
Then, animating this view which had been added. Thus giving an impression that the cell was animated.
Any additional functionality which has to be given can also be written in this view. Thus the code of the cell and the animated view is separated too.
When a cell tapped or a button or any tappable thing got tapped inside the cell, then you get the call from
didSelectItemAtIndexPath or through delegate, then to give the cell the required size, you have to invalidate the layout of the current collectionview. After this, size for item will get called and give the new size for the,
This will update the size of the collectioncell without reloading it.
You can give animation also.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.invalidateLayout()
}
}
extension UICollectionView {
func transactionAnimation(with duration: Float, animateChanges: #escaping () -> Void) {
UIView.animate(withDuration: TimeInterval(duration)) {
CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(duration))
CATransaction.setAnimationTimingFunction(.init(name: .default))
animateChanges()
CATransaction.commit()
}
}
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
collectionView.transactionAnimation(with: 0.3) {
self.carInfoCollectionView?.performBatchUpdates({}, completion: { _ in })
}
}

Swift 2 - performBatchUpdates - Animate cell one by one when visibile inside the UICollectionView frame

I am trying to animate each cell of my UICollectionView visibile in the frame.
Every time I scroll a new cell appears with the animation.
I am doing this using performBatchUpdates inside cellForItemAtIndexPath however, the animation is applied to all cells at the same time and its very fast. It seems that the animation of 1 second its not recognised.
Also I am trying to find the way to apply an animation to a cell when a button is pressed with no success.
The code I use is:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let Cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CellClass
self.collectionView?.performBatchUpdates({
Cell.layer.cornerRadius = 200
return
}){
completed in
UIView.animateWithDuration(1, animations: {
Cell.layer.cornerRadius = 0
})
}
Cell.playAnimationBtn.layer.setValue(indexPath.row, forKey: "indexPlayBtn")
}
#IBAction func actionGetAnimation(sender: UIButton)
{
let indexUser = (sender.layer.valueForKey("indexPlayBtn")) as! Int
//Cell selected do animation corners = 200
}
You can make this work when you move the animation to willDisplayCell(_:cell:indexPath:). That method is called every time a new cell is about to be displayed.
You cannot use UIView.animateWithDuration for the layer properties. You have to use CABasicAnimation for that.
If you want to animate the cell when the user presses a button you can call animateCellAtIndexPath from the code example below. You have to know the cell's indexPath to do so. In this example I call this method when the user selects the cell.
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
animateCell(cell)
}
func animateCell(cell: UICollectionViewCell) {
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.fromValue = 200
cell.layer.cornerRadius = 0
animation.toValue = 0
animation.duration = 1
cell.layer.addAnimation(animation, forKey: animation.keyPath)
}
func animateCellAtIndexPath(indexPath: NSIndexPath) {
guard let cell = collectionView.cellForItemAtIndexPath(indexPath) else { return }
animateCell(cell)
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
animateCellAtIndexPath(indexPath)
}

Resources