I use this code to create animation with button:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
switch indexPath.section {
case 0:
self.button1.constant = self.button2.constant
case 6:
self.button3.constant = self.button4.constant
default:
self.button1.constant = 0
self.button3.constant = 0
}
UIView.animate(withDuration: 1.0, delay: 0.0,
options: [], animations: {
self.view.layoutIfNeeded()
})
}
I have collectionView. But with my buttons my collectionView animated too. How to do that I have animation only with button?
This code doesn't work:
UIView.animate(withDuration: 1.0, delay: 0.0,
options: [], animations: {
self.myButton.layoutIfNeeded()
self.myButton1.layoutIfNeeded()
})
Try to subclass UICollectionView and override layoutSubviews method as below:
override func layoutSubviews() {
UIView.performWithoutAnimation {
super.layoutSubviews()
}
}
Hope this helps.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
UIView.animate(withDuration: 1.0, delay: 0.0,
options: [], animations: {
switch indexPath.section {
case 0:
self.button1.constant = self.button2.constant
self.myButton1.layoutIfNeeded()
case 6:
self.button3.constant = self.button4.constant
self.myButton3.layoutIfNeeded()
default:
self.button1.constant = 0
self.button3.constant = 0
self.myButton1.layoutIfNeeded()
self.myButton6.layoutIfNeeded()
}
})
}
I confirmed this code working.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let cell = cell as? MyCollectionViewCell else { return }
cell.yourConstraint.constant = 10 //desired value
UIView.animate(withDuration: 0.3) {
cell.layoutIfNeeded()
}
}
I think your problem is that your constraints called 'button1', button2' and so on are hooked to your view controller's view or the collectionView.
You have to set up the constraint between the button(assumably inside the collection view cell) and the collection view cell's contentView, AND have that constraint property in the custom UICollectionViewCell class.
Related
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 have created a custom UICollectionViewCell and it contains a subView named animView which I intend to animate. The shape of animView is a regular vertical rectangle.
In the configureCell() method, I use the code below to configure the cells and create an animation, but I do not see the animation.
animView.transform = CGAffineTransform(scaleX: 1.0, y: 0.5)
self.layoutIfNeeded()
UIView.animate(withDuration: 0.7, delay: 0, options: [.curveEaseOut], animations: {
self.animView.transform = CGAffineTransform.identity
self.layoutIfNeeded()
}, completion: nil)
However, when I try to animate whole cell, I see the cell animate successfully.
self.transform = CGAffineTransform(scaleX: 1.0, y: 0.5)
self.layoutIfNeeded()
UIView.animate(withDuration: 0.7, delay: 0, options: [.curveEaseOut], animations: {
self.transform = CGAffineTransform.identity
self.layoutIfNeeded()
}, completion: nil)
I appreciate your time and answer.
EDIT:
As per the suggestions, I changed where I call the animation as shows below. Now, when I try to animate whole cell it is working. However, I want to animate the animView which is a subview of the custom UICollectionViewCell (BarCell). But it is not animating. Any ideas?
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cells = collectionView.visibleCells
for cell in cells {
let current = cell as! BarCell
let curAnimView = current.animView
curAnimView?.transform = CGAffineTransform(translationX: 0, y: current.animFillHeightCons.constant)
UIView.animate(withDuration: 0.7) {
curAnimView?.transform = CGAffineTransform.identity
}
}
}
You need to start animation for only visible cells.
Here is how you can do that.
add this
UIScrollViewDelegate
It will provide the method
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let cells = colAnim.visibleCells
for cell in cells
{
let current = cell as! testCollectionViewCell
UIView.animate(withDuration: 2.0, animations: {
current.animView.transform = CGAffineTransform(translationX: -400, y: 0)
//Change this animation
})
}
}
Also make sure do this also.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "testCollectionViewCell", for: indexPath) as! testCollectionViewCell
cell.animView.transform = CGAffineTransform(translationX: 0, y: 0)// This one
return cell
}
By doing this you can achieve animation only when cell is visible.
Thanks.
There are 2 conditions to make it happen:
You will need to reset the properties you are animating every cell binding
You will have to start the animation only when the cell is about to be visible
Also, there are exceptions (like what I had), sometimes the cell binding or the layout of your collection is complex or custom thus, in these cases we will have to delay the animation so it will actually work.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// your dequeue code...
// ...
// Reseting your properties as described in 1
cell.myImageView.alpha = 1
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let myCell = cell as! MyCell
// As described in 2
myCell.animate()
}
class MyCell: UICollectionViewCell {
func animate() {
// Optional: As described above, if your scene is custom/complex delaying the animation solves it though it depends on your code
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// Animation code
UIView.animate(withDuration: 1,
delay: 0,
options: [.autoreverse, .repeat]) {
self.seenNotSeenImageView.alpha = 0.5
}
}
}
}
There's also a third condition:
If your cell will be covered by another VC, you will want to initiate the same animated() method as follow, in your viewWillAppear of the VC that holds the collectionView:
collectionView.visibleCells.forEach{($0 as? MyCell)?.animate()}
How can i animate Horizontal Collectionview as it is scrolled i changed alpha to 0 in the cell and in cellForItemAt i animate the alpha back to 1 but that only happens when the Collectionview is scrolled through the first time here is the code i have tried
UIView.animate(withDuration: 0.8) {
cell.imageView.alpha = 1
cell.onboardLabel.alpha = 1
}
I also tried to do this in scrollViewDidEndDecelerating but still not working
let index = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width)
let indexPath = IndexPath(item: index, section: 0)
let cell = collectionView.cellForItem(at: indexPath) as? OnboardingCell
UIView.animate(withDuration: 0.8) {
cell?.imageView.alpha = 1
cell?.onboardLabel.alpha = 1
}
Swift 4:
Use this function from UICollectionViewDelegate:
override func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath) {
cell.alpha = 0
UIView.animate(withDuration: 0.8) {
cell.alpha = 1
}
}
Firstly you need to know which cells are visible so set this variable at the top of the file.
var visibleIndexPath: IndexPath? = nil
In scrollViewDidEndDecelerating use this code to set the visibleIndexPath:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var visibleRect = CGRect()
visibleRect.origin = collectionView.contentOffset
visibleRect.size = collectionView.bounds.size
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
if let visibleIndexPath = collectionView.indexPathForItem(at: visiblePoint) {
self.visibleIndexPath = visibleIndexPath
}
}
Now that you have a visibleIndexPath you can animate the cell in the willDisplay cell function.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if let visibleIndexPath = self.visibleIndexPath {
// This conditional makes sure you only animate cells from the bottom and not the top, your choice to remove.
if indexPath.row > visibleIndexPath.row {
cell.contentView.alpha = 0.3
cell.layer.transform = CATransform3DMakeScale(0.5, 0.5, 0.5)
// Simple Animation
UIView.animate(withDuration: 0.5) {
cell.contentView.alpha = 1
cell.layer.transform = CATransform3DScale(CATransform3DIdentity, 1, 1, 1)
}
}
}
}
I'm trying to program an expandable UICollectionViewCell.
If the user presses the topbutton, the cell should expand to a different height and reveal the underlying content.
My first implementation features a custom UICollectionViewCell, which can be expanded by clicking the topbutton, but the collectionview does not expand too. Atm. the cell is the last one in the collectionview and just by swiping down enough, you can see the expanded content. If you stop touching, the scrollview jumps up and nothing of the expanded cell could be seen.
Here's my code:
import Foundation
import UIKit
class ExpandableCell: UICollectionViewCell {
#IBOutlet weak var topButton: IconButton!
public var expanded : Bool = false
private var expandedHeight : CGFloat = 200
private var notExpandedHeight : CGFloat = 50
#IBAction func topButtonTouched(_ sender: AnyObject) {
if (expanded == true) {
self.deExpand()
} else {
self.expand()
}
}
public func expand() {
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: self.frame.size.width, height: self.expandedHeight)
}, completion: { success in
self.expanded = true
})
}
public func deExpand() {
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: self.frame.size.width, height: self.notExpandedHeight)
}, completion: { success in
self.expanded = false
})
}
}
I also have problems implementing the following method correctly, because I haven't got a direct reference to the cell:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
Basicly everything should be animated.
The screenshot shows an expanded and not expanded cell. Hope that helps!
Thanks!
If you have more than cell that needs to be expanded to different height when a button is touched inside the collection view cell, then, here is the code.
I have used the delegate pattern to let the controller know, button of which cell was touched using the indexPath.
You need to pass the indexpath of the cell to the cell when creating the cell.
when the button is touched the cell calls the delegate(ViewController), which updates the isExpandedArray accordingly and reloads the particular cell.
CollectionViewCell
protocol ExpandedCellDelegate:NSObjectProtocol{
func topButtonTouched(indexPath:IndexPath)
}
class ExpandableCell: UICollectionViewCell {
#IBOutlet weak var topButton: UIButton!
weak var delegate:ExpandedCellDelegate?
public var indexPath:IndexPath!
#IBAction func topButtonTouched(_ sender: UIButton) {
if let delegate = self.delegate{
delegate.topButtonTouched(indexPath: indexPath)
}
}
}
View Controller Class
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var expandedCellIdentifier = "ExpandableCell"
var cellWidth:CGFloat{
return collectionView.frame.size.width
}
var expandedHeight : CGFloat = 200
var notExpandedHeight : CGFloat = 50
var dataSource = ["data0","data1","data2","data3","data4"]
var isExpanded = [Bool]()
override func viewDidLoad() {
super.viewDidLoad()
isExpanded = Array(repeating: false, count: dataSource.count)
}
}
extension ViewController:UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: expandedCellIdentifier, for: indexPath) as! ExpandableCell
cell.indexPath = indexPath
cell.delegate = self
//configure Cell
return cell
}
}
extension ViewController:UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if isExpanded[indexPath.row] == true{
return CGSize(width: cellWidth, height: expandedHeight)
}else{
return CGSize(width: cellWidth, height: notExpandedHeight)
}
}
}
extension ViewController:ExpandedCellDelegate{
func topButtonTouched(indexPath: IndexPath) {
isExpanded[indexPath.row] = !isExpanded[indexPath.row]
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.collectionView.reloadItems(at: [indexPath])
}, completion: { success in
print("success")
})
}
}
Don't change the cell frame directly. Implement func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize and handle heights there.
Lets say you have one cell in your collectionView and a top button above it:
let expanded = false
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
if expanded{
// what is the size of the expanded cell?
return collectionView.frame.size
}else{
// what is the size of the collapsed cell?
return CGSizeZero
}
}
#IBAction func didTouchUpInsideTopButton(sender: AnyObject){
// top button tap handler
self.expanded = !self.expanded
// this call will reload the cell and make it redraw (animated) Since the expanded flag changed, the sizeForItemAt call above will return a different size and animate the size change
self.collectionView.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)])
}
You can use header to show non-expand cell. And when you two a button or something in it , then you can insert other part (expanding part ) as a row in that section which the header belongs to.
Benefits - you can use many provided animations when the expanding happen.
I have collection view with 30 items, and want to perform something on press. I do it in this way:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! ItemCell
var translucentView = ILTranslucentView(frame: CGRectMake(0, 0, cell.contentView.frame.size.width, cell.contentView.frame.size.height))
translucentView.translucentAlpha = 0.9
translucentView.translucentStyle = UIBarStyle.BlackTranslucent
translucentView.translucentTintColor = UIColor.clearColor()
translucentView.backgroundColor = UIColor.clearColor()
translucentView.alpha = 0.0
cell.contentView.addSubview(translucentView)
UIView.animateWithDuration(0.4, animations: {
translucentView.alpha = 1.0
})
}
The function works as expected, however the view appears not only on the tapped cell, but also on the cell in the same position that is not visible on the screen.
So if there are 3 visible cells on the screen and I tap on number 1, then when I scroll the view has been added to cell 4, 7, etc...
UICollectionView re-use cells like to UITableView. When a cell is scrolled offscreen it is added to queue, and will be re-used for the next cell to be scrolled onscreen(e.g. cell 4, 7 ...).
Simply removing the translucentView will solve this issue:
UIView.animateWithDuration(0.4, animations: { () -> Void in
translucentView.alpha = 1.0
}) { (finish) -> Void in
translucentView.removeFromSuperview()
}
You can create the translucentView in the ItemCell and update its status in the cellForItemAtIndexPath method:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(CellIdentifier, forIndexPath: indexPath) as! ItemCell
if find(collectionView.indexPathsForSelectedItems() as! [NSIndexPath], indexPath) != nil {
cell.translucentView.alpha = 1.0
} else {
cell.translucentView.alpha = 0.0
}
return cell
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! ItemCell
UIView.animateWithDuration(0.4, animations: {
cell.translucentView.alpha = 1.0
})
}
override func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! ItemCell
UIView.animateWithDuration(0.4, animations: {
cell.translucentView.alpha = 0.0
})
}
did you set your numberOfSectionsInTableView correctly like that?
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}