Animate a collectionview swift - swift

Hello I am trying to animate a CollectionView. It need to change the position x and his width.
UIView.animate(withDuration: 1, delay: 0, options: .beginFromCurrentState, animations: {
collectionView.frame = CGRect(x: 500, y: 0, width: 1000, height: 700)
})
But the problem is the image in the cell doesn't resize, and i think the cell too.
Have you a solution for that ?

Create a simple custom flow layout.
Assign this flow layout class to your collection view in storyboard.
class CustomFlowLayout: UICollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
And it's better to animate constraints than frame. To do so:
Make outlet of width and leading constraints.
Change their constant inside UIView animation block.
Don't forget to call self.layoutIfNeeded() in the end of animations block.

UICollectionViewCell size is independent from the frame of the parrent UICollectionView.
maybe you can actually "zoom in" on the layer
var transformation = CGAffineTransform.identity
let translate = CGAffineTransform(translationX: 2, y: 2)
let scale = CGAffineTransform(scaleX: 2, y: 2)
transformation = scale.concatenating(translate)
UIView.animate(withDuration: 0.15, animations: {
self.collectionView.transform = transformation
})
Maybe invalidateLayout call can help you.
Also are you using customflowlayout

In the view controller, if you are using storyboard, connect UICollectionviewFlowLayout (collectionViewFlowLayout in my example) using IBOutlet, collection view width constraint (collectionViewWidth in my example) as IBOutlet and UICollectionView as IBOutlet (collectionView in my example). You can change the collection view frame and cells simultaneously with the following:
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5, execute: {
self.width = 400
self.height = 400
self.collectionView.performBatchUpdates({
self.collectionViewFlowLayout.invalidateLayout()
self.collectionView.setCollectionViewLayout(self.collectionViewFlowLayout, animated: true)
}, completion: { _ in })
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5, execute: {
self.collectionViewWidth.constant = 50
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut,
animations: self.view.layoutIfNeeded, completion: nil)
})
You need to declare width and height var as class properties with initial value of 100 for example. This is then changed in the above code. It is made possible because of the below implementation. Note this requires you adopt the protocol UICollectionViewDelegateFlowLayout.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: width, height: height)
}

Related

UICollectionView: Resizing cells with animations and custom layouts

I have a collection view with a custom layout and a diffable datasource. I would like to resize the cells with an animation.
With a custom layout: it looks like the content of the cells scaled to the new size during the animation. Only when the cells reaches its final size, it is finally redrawn (see animation below)
With a stock flow layout the subviews are laid out correctly during the animation: the label remains at the top, no resizing and same for the switch.
So I would assume that the issue is in the layout implementation, what could be added to the layout allow a correct layout during the animation?
Description
The custom layout used in this example is FSQCollectionViewAlignedLayout, I also got it with the custom I used in my code. I noticed that for some cases, it is important to always return the same layout attributes objects, which FSQCollectionViewAlignedLayout doesn't do. I modified it to not return copies, and the result is the same.
For the cells (defined in the xib), the contentMode of the cell and its contentView are set to .top. The cell's contentView contains 2 subviews: a label (laid out in layoutSubviews) and a switch (laid out with constraints, but its presence has no effect on the animation).
The code of the controller is:
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout {
enum Section {
case main
}
#IBOutlet weak var collectionView : UICollectionView!
var layout = FSQCollectionViewAlignedLayout()
var dataSource : UICollectionViewDiffableDataSource<Section, String>!
var cellHeight : CGFloat = 100
override func loadView() {
super.loadView()
layout.defaultCellSize = CGSize(width: 150, height: 100)
collectionView.setCollectionViewLayout(layout, animated: false) // uncommented to use the stock flow layout
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { cv, indexPath, item in
guard let cell = cv.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell else { return nil }
cell.label.text = item
return cell
})
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(["1", "2"], toSection: .main)
dataSource.apply(snapshot)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 150, height: cellHeight)
}
#IBAction func resizeAction(_ sender: Any) {
if let layout = collectionView.collectionViewLayout as? FSQCollectionViewAlignedLayout {
UIView.animate(withDuration: 0.25) {
layout.defaultCellSize.height += 50
let invalidation = FSQCollectionViewAlignedLayoutInvalidationContext()
invalidation.invalidateItems(at: [[0, 0], [0, 1]])
layout.invalidateLayout(with: invalidation)
self.view.layoutIfNeeded()
}
}
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
UIView.animate(withDuration: 0.25) {
self.cellHeight += 50
let invalidation = UICollectionViewFlowLayoutInvalidationContext()
invalidation.invalidateItems(at: [[0, 0], [0, 1]])
layout.invalidateLayout(with: invalidation)
self.view.layoutIfNeeded()
}
}
}
}
class Cell : UICollectionViewCell {
#IBOutlet weak var label: UILabel!
override func layoutSubviews() {
super.layoutSubviews()
label.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 30)
}
}
I spent a developer support ticket on this question. The answer was that custom animations when reloading cells are not supported.
So as a workaround, I ended up creating new cells instances that I add to the view hierarchy, then add autolayout constraints so that they overlap exactly the cells being resized. The animation is then run on those newly added cells. At the end of the animation, these cells are removed.

Adding a search bar to ui table view on overlay

I have managed to create, a dark overlay once clicked a search icon and have managed to add a table uiview on the dark overlay but now want to add a search bar within that table view. I cant seem to figure it out as my code seems different to everyone else's examples. I am new to swift so my code probably isn't the cleanest. can i ask if someone can show me how to do this as i am out of ideas. I have posted my code below can someone please show me where i'm going wrong.
Many thanks
class SearchLauncher: NSObject {
let blackView = UIView()
let tableView = UITableView()
#objc func showSearch() {
if let window = UIApplication.shared.keyWindow {
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
window.addSubview(blackView)
window.addSubview(tableView)
let height: CGFloat = 600
let y = window.frame.height - height
tableView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
blackView.frame = window.frame
blackView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackView.alpha = 1
self.tableView.frame = CGRect(x: 0, y: y, width: self.tableView.frame.width, height: self.tableView.frame.height)
}, completion: nil)
}
}
#objc func handleDismiss() {
UIView.animate(withDuration: 0.5) {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.tableView.frame = CGRect(x: 0, y: window.frame.height, width: self.tableView.frame.width, height: self.tableView.frame.height)
}
}
}
override init() {
super.init()
//start doing something here maybe
}
}```
How did you try to add a search controller?
I would do it by adding an UISearchController as a tableHeaderView of your table view. First make sure to add UISearchResultsUpdating protocol to your class.
class SearchLauncher: NSObject, UISearchResultsUpdating
Then add the search bar to your table view
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.sizeToFit()
searchController.searchResultsUpdater = self
//you probably don't need this
//searchController.dimsBackgroundDuringPresentation = false
tableView.tableHeaderView = searchController.searchBar
And then, implement the updateSearchResults(for:_) delegate method
func updateSearchResults(for searchController: UISearchController) {
filteredTableData.removeAll(keepingCapacity: false)
//filter the table data by using searchController.searchBar.text!
//filteredTableData = ...
self.tableView.reloadData()
}

Making a collection view scroll out side menu swift

I've looked all over the place for an answer and can't find one. I'm creating an app that has a menu that pops out from the left side using a button and animations. The menu itself is a collection view. Currently when the menu button is pressed, the entire collection view pops out from the left side. I want to do this same thing, but I want the collection view to respond to a swipe from left to right. Please keep in mind I don't want the individual things INSIDE the collection view to move, but the collection view itself as a whole menu. How would I go about doing this?
Good evening!
If i understand your question correctly. You simply have a button which you want to use to animate a slide in of a UICollectionView. Here is what I would do:
Create a new collection view class: (if you want a different frame size of the collectionView remember to change it in both animations as well as the initial setup in showSlideCollectionView
import UIKit
class CollectionViewLauncher : NSObject {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.white
return cv
}()
// reuseidentifier for the cells in the collectionview
let cellId = "cellId"
var viewController: UIViewController?
func showSlideCollectionView() {
if let window = UIApplication.shared.keyWindow {
window.addSubview(collectionView)
let width: CGFloat = window.frame.width * 0.55
//this is the original frame for the collectionview (so it is off screen by default)
collectionView.frame = CGRect(x: -window.frame.width, y: 0, width: width, height: window.frame.height)
//this animates the collectionview on screen
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
self.collectionView.frame = CGRect(x: 0, y: 0, width: width, height: window.frame.height)
}, completion: nil)
}
}
func hideSlideCollectionView() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
//sliding the collectionview off screen
if let window = UIApplication.shared.keyWindow {
self.collectionView.frame = CGRect(x: window.frame.width, y: 0, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}, completion: nil)
}
override init() {
super.init()
collectionView.dataSource = self
collectionView.delegate = self
//register your own cell class type here. The code wont work unless you create a cell class of type UICollectionViewCell and customise it there and then register the cell here. (you can also create a nib file and register that.
collectionView.register(YourCell.self, forCellWithReuseIdentifier: cellId)
}
}
extension CollectionViewLauncher: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! YourCell //your cell class
//setup what you need in the cells
return cell
}
//add additional UICollectionView Data source methods if you need further customisation of the collectionview.
}
Register your cell class in the overrode init() method inside the CollectionViewLauncher.
In the your ViewController do the following.
lazy var collectionViewLauncher : CollectionViewLauncher = {
let launcher = CollectionViewLauncher()
launcher.viewController = self
return launcher
}()
//in your button action you call the following to animate the collection view to appear.
collectionViewLauncher.showSlideCollectionView()
//when you want it to dissapear call the following to animate it away (perhaps in a UITapGestureRecognizer or something).
collectionViewLauncher.hideSlideCollectionView()
//example button action
#objc func yourButtonsAction(sender: UIButton) {
collectionViewLauncher.showSlideCollectionView()
}
Edit:
If you have data that you need to pass to this collection view from your view controller i would recommend implementing it directly in the same class as your view controller instead. (Just do exactly the same but put the stuff in the init() method in viewDidLoad instead and the rest within the ViewController class).
If you want it to be slidable using a drag gesture instead. You should use the approach in the edit here and then use a pan gesture to move it instead of the show/hideSlideCollectionView methods.
I hope this helps.

Swift 4 - Multiple inheritance from classes 'NSObject' and 'UICollectionViewFlowLayout'

I am trying to programatically size the collection cell to be the width of the frame, however, the cell size doesn't change when I run the app. Is this the right function to call for Swift 4 and Xcode 9?
import UIKit
class SettingsLauncher: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewFlowLayout {
let blackView = UIView()
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.white
return cv
}()
let cellID = "cell"
#objc func showMenu() {
// Show menu
if let window = UIApplication.shared.keyWindow { // Get the size of the entire window
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
let height: CGFloat = 200
let y = window.frame.height - height // The y value to appear at the bottom of the screen
collectionView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
// Add gesture recognizer on black view to dismiss menu
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
window.addSubview(blackView)
window.addSubview(collectionView)
blackView.frame = window.frame
blackView.alpha = 0
// Slow the animation down towards the end (curveEasOut)
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// Animate black view
self.blackView.alpha = 1
// Animate collection view
self.collectionView.frame = CGRect(x: 0, y: y, width: self.collectionView.frame.width, height: height)
}, completion: nil)
}
}
#objc func handleDismiss() {
// Dimisses menu view
UIView.animate(withDuration: 0.5) {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.collectionView.frame = CGRect(x: 0, y: window.frame.height, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 50)
}
override init() {
super.init()
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellID)
}
}
Edit:
This table view is being animated from the bottom of the page and presents a collection view, which is being called from the view controller that is using the pop up menu.
I now get the error:
Multiple inheritance from classes 'NSObject' and 'UICollectionViewFlowLayout'
UICollectionViewFlowLayout is not a protocol, it's a class. You need to use UICollectionViewDelegateFlowLayout instead of UICollectionViewFlowLayout.
Change
class SettingsLauncher: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewFlowLayout
to
class SettingsLauncher: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout

Swift CollectionView Vertical Paging

I enabled paging on my collectionview and encountered the first issue of each swipe not stopping on a different cell, but page.
I then tried to enter code and handle the paging manually in ScrollViewWillEndDecelerating.
However my problem is the changing the collectionviews ContentOffset or scrolling to a Point both do not work unless called in LayoutSubiews. that is the only place where they effect the CollectionView
Each cell in my collection view is full screen. I handle the cell sizing in layout subviews.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let layout = customCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let itemWidth = view.frame.width
let itemHeight = view.frame.height
layout.itemSize = CGSize(width: itemWidth, height: itemHeight)
layout.invalidateLayout()
}
let ip = IndexPath(row: 2, section: 0)
view.layoutIfNeeded()
customCollectionView.scrollToItem(at: ip, at: UICollectionViewScrollPosition.centeredVertically, animated: true)
let point = CGPoint(x: 50, y: 600)
customCollectionView.setContentOffset(point, animated: true)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageHeight = customCollectionView.bounds.size.height
let videoLength = CGFloat(videoArray.count)
let minSpace:CGFloat = 10
var cellToSwipe = (scrollView.contentOffset.y) / (pageHeight + minSpace) + 0.5
if cellToSwipe < 0 {
cellToSwipe = 0
}
else if (cellToSwipe >= videoLength){
cellToSwipe = videoLength - 1
}
let p = Int(cellToSwipe)
let roundedIP = round(Double(Int(cellToSwipe)))
let ip = IndexPath(row: Int(roundedIP), section: 0)
let ip = IndexPath(row: 2, section: 0)
view.layoutIfNeeded()
customCollectionView.scrollToItem(at: ip, at: UICollectionViewScrollPosition.centeredVertically, animated: true)
}
First off, I would recommend making your view controller class conform to UICollectionViewDelegateFlowLayout and use the following delegate method to size your UICollectionViewCells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = CGSize(width: view.frame.width, height: view.frame.height)
return size
}
Then in your viewDidLoad call
customCollectionView.isPagingEnabled = true