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.
Related
I have problem with lottie animations, I have some kind of onboarding on my app and what I would like to achive is to everytime view in collectionview is changed, to start my animation, I have 4 pages and 4 different lottie animations. Currently if I call animation.play() function, once app is started, all of my animations are played at the same time, so once I get to my last page, animation is over. And I want my lottie to be played only once, when view is shown.
This is my cell
class IntroductionCollectionViewCell: UICollectionViewCell {
#IBOutlet var title: UILabel!
#IBOutlet var subtitleDescription: UILabel!
#IBOutlet var animationView: AnimationView!
override func awakeFromNib() {
super.awakeFromNib()
}
public func configure(with data: IntroInformations) {
let animation = Animation.named(data.animationName)
title.text = data.title
subtitleDescription.text = data.description
animationView.animation = animation
}
static func nib() -> UINib {
return UINib(nibName: "IntroductionCollectionViewCell", bundle: nil)
}
}
This is how my collection view is set up
extension IntroductionViewController {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
pages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "IntroductionCollectionViewCell", for: indexPath) as! IntroductionCollectionViewCell
cell.configure(with: pages[indexPath.row])
cell.animationView.loopMode = .loop
cell.animationView.play()
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollPos = scrollView.contentOffset.x / view.frame.width
self.pageControl.currentPage = Int(floorf(Float(scrollPos)))
let n = pageControl.numberOfPages
if self.pageControl.currentPage == n - 1 {
continueButton.isHidden = false
} else {
continueButton.isHidden = true
}
}
}
Thanks in advance!!
You can use the collectionViewDelegate willDisplay and didEndDisplaying methods to start/stop the animations. And not when configuring the cell. - https://developer.apple.com/documentation/uikit/uicollectionviewdelegate
If you want the animation to run only once dont use the loop option.
if let cell = cell as? IntroductionCollectionViewCell {
cell.animationView.loopMode = .playOnce
cell.animationView.play()
}
this is the answer, I need to check is cell once is loaded my cell where lottie animation is
I am trying to code the button in the top right so that when tapped, it'll only display the name in the white bar and not display the photo, or the position of the person on the card. Here is a photo illustrating the expanded section.
I tried manipulating the size by using the sizeForItemAt in collectionView, but that did not work. Here is what I did:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
let cell = collectionView.cellForItem(at: indexPath) as? CardCollectionViewCell
if expand {
cell?.profileimage.alpha = 0
cell?.poscomp.alpha = 0
return CGSize(width: 360, height: 208)
} else {
cell?.profileimage.alpha = 1
cell?.poscomp.alpha = 1
return CGSize(width: 360, height: 40)
}
}
And for the button, I did...
#IBAction func collapse(_ sender: Any) {
expand = !expand
for index in 0...associates.count {
cardCollectionView.reloadItems(at: [IndexPath(row: 0, section: index)])
}
}
Is there a correct/simpler way to manipulate heights and elements in a CollectionViewCell?
Thanks in advance.
You are doing right code, but I din't recognise your issue.
Add following code which are working fine, that may be expecting you.
var expandedHeight : CGFloat = 200
var notExpandedHeight : CGFloat = 50
var isExpanded = false
In button's action method add code to reload collectionView with/without animation
#IBAction func btnExpandTapped(_ sender: UIButton) {
self.isExpanded = !self.isExpanded
// Simple reload
// self.cvList.reloadData()
// Reload with animation
for i in 0..<5 {
let indexPath = IndexPath(item: i, section: 0)
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: .curveEaseInOut, animations: {
self.cvList.reloadItems(at: [indexPath])
}, completion: { success in
print("success")
})
}
}
Add UICollectionViewDelegateFlowLayout's sizeForItemAt method and add following code to change height of collection view cell.
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if isExpanded {
return CGSize(width: collectionView.frame.width, height: 200)
} else {
return CGSize(width: collectionView.frame.width, height: 40)
}
}
}
Check following link for demo App.
https://freeupload.co/files/02c31165b3c6876678301ae075f5722b.zip
Hey i want to create stack of UIImageView like the photo, how to do this
It must be dynamic. How can I move the cells so that they are one below the other?
Use UICollectionViewDelegateFlowLayout methods to make this kind of UI.
Example:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.layer.borderWidth = 2.0
cell.layer.borderColor = UIColor.white.cgColor
cell.layer.cornerRadius = 50.0
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.height, height: collectionView.bounds.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return -50
}
}
In the above code
Configure minimumLineSpacingForSectionAt and minimumInteritemSpacingForSectionAt methods of UICollectionViewDelegateFlowLayout.
Since the cell in collectionView are aligned left to right default, so to get the desired result we need to align them right to left.
As shown in above screenshot, use semantic property of collectionView for right to left alignment of cells.
Screenshot:
Edit-1:
One way to centre the cells in collectionView is to play with collectionView's width and centre it horizontally.
CollectionView constraints - top, width, centeredorizontally
In viewDidAppear, manually calculate the width of collectionView according to the numberOfItems. In your case numberOfItems = 3
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let width = min((collectionView.bounds.height) * CGFloat((numberOfItems-1)/2 + 1), UIScreen.main.bounds.width)
self.collectionViewWidthConstraint.constant = width
}
You can use UICollectionView Custom Layout .
Collection view layouts are subclasses of the abstract UICollectionViewLayout class. They define the visual attributes of every item in your collection view. The individual attributes are instances of UICollectionViewLayoutAttributes and contain the properties of each item in your collection view, such as the item’s frame or transform.
The similar Tutorial
This is a more complex layout , Change some parameters , you will get what you want.
Copyed from github , mpospese/IntroducingCollectionViews
translate some to Swift
class SpiralLayout: UICollectionViewLayout{
let itemSize: CGFloat = 170
var pageSize: CGSize!
var contentSize: CGSize!
var radius: CGFloat!
var cellCounts: [Int]!
var pageRects: [CGRect]!
var pageCount: Int!
override func prepare() {
super.prepare()
pageSize = collectionView?.bounds.size
let iPad = UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad
let scaleFactor: CGFloat = iPad ? 1 : 0.5
let side = itemSize * scaleFactor
radius = min(pageSize.width - side, pageSize.height - side * 1.2 )/2 - 5
pageCount = collectionView?.numberOfSections
var counts = [Int]()
var rects = [CGRect]()
for section in 0..<pageCount{
counts.append((collectionView?.numberOfItems(inSection: section))!)
rects.append(CGRect(origin: CGPoint(x: CGFloat(section) * pageSize.width, y: 0), size: pageSize))
}
cellCounts = counts
pageRects = rects
contentSize = CGSize(width: pageSize.width * CGFloat(pageCount), height: pageSize.height)
}
override var collectionViewContentSize: CGSize{
return contentSize
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return !pageSize.equalTo(newBounds.size)
}
func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
var attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.size = CGSize(width: itemSize, height: itemSize)
// ...
return attributes
}
}
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()}
I have a collection view that is aligned horizontally and vertically in the container. When a user selects the cell I want that collection view to move to the bottom of the screen, and then I want a UIView to appear right above the collection view.
Below is how my collection view is constrained.
So the way I went about this was making the bottom space constraint an outlet in my code file.
#IBOutlet var collectionViewBottomConstraint: NSLayoutConstraint!
Then when a user tapped on the cell, i ran this function here
func showWatchView(selectedPath: Int) {
UIView.animate(withDuration: 0.3, animations: {
self.collectionViewBottomConstraint.constant = 0
})
clipsCollectionView.layoutIfNeeded()
}
However, when I tried this it moved the collection view to the top of the screen instead of the bottom of the screen, and it didn't even animate it, it just went up there.
The constant needs to be set to 0
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var collectionViewOutlet: UICollectionView!
#IBOutlet weak var bottomContraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
collectionViewOutlet.delegate = self
collectionViewOutlet.dataSource = self
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
showWatchView(selectedPath: indexPath.item)
}
func showWatchView(selectedPath: Int) {
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
self.bottomContraint.constant = 0
self.view.layoutIfNeeded()
}, completion: { (completed) in
// do something
})
}
}
Since you want the collectionView to be at the bottom and self.view.layoutIfNeeded() needs to be called inside the animation with a duration of your choosing, 0.3 is ok for starters.