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.
Related
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.
I have a ViewController named MainViewController that acts as the central page for the app. This MainViewController has 6 properties in it that are instances of other viewControllers
lazy var shelvesView: ShelvesViewController = {
return ShelvesViewController()
}()
lazy var goalsView: GoalsViewController = {
return GoalsViewController()
}()
lazy var shoppingView: ShoppingListViewController = {
return ShoppingListViewController()
}()
lazy var tipsView: TipsViewController = {
return TipsViewController()
}()
lazy var myDenView: MyDenViewController = {
return MyDenViewController()
}()
lazy var settingsview: SettingsViewController = {
return SettingsViewController()
}()
when the mainViewController loads it starts out with the shelvesViewController laid over it just underneath the mainViewControllers custom NavBar like so
func setupShelvesView() {
shelvesView.willMove(toParentViewController: self)
addChildViewController(shelvesView)
self.view.addSubview(shelvesView.view)
shelvesView.view.frame = CGRect(x: 0, y: view.frame.height * 0.08, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
shelvesView.didMove(toParentViewController: self)
globalCurrentView = 1
}
I also have a menu that slides over this mainViewController with a list of different pages the user can navigate to. when the user switches to a new page, say the goalsViewController, the shelvesViewController will be animated off screen, removed from the parentViewController(MainViewController) and the goalsViewController will be initialized, moved to the MainViewController and animated onscreen in the same frame as the shelvesView. Anytime i switch a VC from the menu i use this method.
func changeVCfrom(OldVC oldVC: UIViewController, newVC: UIViewController) {
let newStartFrame = CGRect(x: 0 + self.view.frame.width, y: 0, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
let newEndframe = CGRect(x: 0, y: view.frame.height * 0.08, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
let oldfinishFrame = CGRect(x: 0 - self.view.frame.width, y: 0, width: self.view.frame.width, height: self.view.frame.height)
oldVC.willMove(toParentViewController: nil)
self.addChildViewController(newVC)
newVC.view.frame = newStartFrame
transition(from: oldVC, to: newVC, duration: 0.2, options: [.curveEaseOut], animations: {
oldVC.view.frame = oldfinishFrame
newVC.view.frame = newEndframe
}, completion: { (success) in
oldVC.willMove(toParentViewController: nil)
oldVC.view.removeFromSuperview()
oldVC.removeFromParentViewController()
newVC.willMove(toParentViewController: self)
self.view.addSubview(newVC.view)
})
}
the problem here is that i have done all my views programmatically and any of the above viewControllers(lazy properties) that have a tableView propery on them have a memory leak. any time the VC goes off the MainViewController the memory used for the tableView is not being dealocated and anytime it comes back on the MainViewController is is being allocated again for more memory.
as of right now this is how ive been setting up my tableViews
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupObjects()
tableView.register(ShelfTableViewCell.self, forCellReuseIdentifier: "shelfCell")
}
func setupTableView() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
setupTableViewConstraints()
}
func setupTableViewConstraints() {
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
}
I understand that I need to have my tableviews either weak, unowned or somehow get the tableview to completely deallocate when off screen. but whatever i try I get errors all over the place that i cant resolve or my ViewController is not able to load the tableview. I apologize for the lengthy question, but any help on this would be very much appreciated.
The table views are not getting deallocated when your child view controllers go off screen.
Your MainViewController owns those other view controllers. Yes, they're lazy, but once they're called for the first time they are initialized along with the tableviews they own.
The child view controllers are not deallocated by simply moving them offscreen. All you're doing is changing the frame and removing them as a child view controller of MainViewController. Since the they're still in memory, the table view is also still in memory.
If you really want to deallocate the table view every time the controller moves off screen, you can just set
self.tableView = nil
There shouldn't be a retain cycle here because the tableView's datasource and delegate properties are weak, meaning that they don't increase the reference count of the view controller.
while scrolling up and down on the tableView my memory would go up sharply. Turns out i had set up the dequeReusableCell wrong.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ShelfTableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "shelfCell")
tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath)
if let shelf = UserController.shared.user?.shelves?[indexPath.row] as? Shelf {
cell.shelf = shelf
return cell
} else {
return UITableViewCell()
}
}
essentially what was happening was every time the viewWillAppear was hit i reloaded the tableViewData which ran this code
let cell = ShelfTableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "shelfCell")
tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath)
which was causing the memory leak
replacing the above code with the following resolved the issue
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath) as! ShelfTableViewCell
if let shelf = UserController.shared.user?.shelves?[indexPath.row] as? Shelf {
cell.shelf = shelf
return cell
} else {
return UITableViewCell()
}
}
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
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)
}
I have two collection views within a ViewController. One Collection view represents the tab bar at the top, the other represents pages on the bottom half of the screen. I have the second UICollectionView (on the bottom half) within a custom UIView (loading from another class). My objective is to have when I click on the tab bar, to move the bottom collectionView accordingly, with regards to the scrollToIndex. My issue is that when I click on the tab bar at the top, and call the function that is in my custom UIView, nothing occurs. Do I need to share information through a delegate/protocol? What am I doing incorrectly?
In my MainVC I have:
/** Setting up the top header **/
let topBar = UIView()
/** Setting up the connection to the Bottom Collection View **/
let mainView = MainViewsHome()
override func viewWillAppear(_ animated: Bool) {
self.view.layoutIfNeeded()
/**Setting up the header that the buttons lay on **/
topBar.frame = CGRect(x:self.view.frame.width * 0, y:self.view.frame.height * 0, width:self.view.frame.width,height:self.view.frame.height / 6.2)
topBar.backgroundColor = UIColor.red
self.view.addSubview(topBar)
/** Setting up the buttons in the header**/
setUpHeaderBarButtons()
/** Setting up the bottom half **/
mainView.frame = CGRect(x:self.view.frame.width * 0, y:self.view.frame.height / 6.2, width:self.view.frame.width,height:self.view.frame.height / 1.1925)
mainView.backgroundColor = UIColor.clear
self.view.addSubview(mainView)
mainView.delegate = self
}
& Then setting up the collectionView for the MainVC (the bar buttons)
func setUpHeaderBarButtons() {
let flowLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: CGRect(x: topBar.frame.width * 0, y:topBar.frame.height / 1.75, width: topBar.frame.width, height: topBar.frame.height / 3.6), collectionViewLayout: flowLayout)
collectionView.register(SelectionCVC.self, forCellWithReuseIdentifier: cellId)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = UIColor.clear
topBar.addSubview(collectionView)
}
&& Then the didSelect for the MainVC (The bar buttons)
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selecting cell")
mainView.moveToPage()
}
&& Then my custom UIVIew with the bottom collection view that I wish to trigger a scrollToIndex
class MainViewsHome: UIView, UIGestureRecognizerDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var cellId = "Cell"
var collectionView2 : UICollectionView!
override init(frame: CGRect) {
super.init(frame: CGRect(x:0, y:0, width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 1.27))
/**Creation of the View **/
let flowLayout2 : UICollectionViewFlowLayout = UICollectionViewFlowLayout()
flowLayout2.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
flowLayout2.scrollDirection = .horizontal
let collectionView2 = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout2)
collectionView2.register(SelectionCVC.self, forCellWithReuseIdentifier: cellId)
collectionView2.isPagingEnabled = true
collectionView2.delegate = self
collectionView2.dataSource = self
collectionView2.backgroundColor = UIColor.purple
self.addSubview(collectionView2)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveToPage() {
// print("we are moving to page")
let indexPath = IndexPath(item: 2, section: 0) //Change section from 0 to other if you are having multiple sections
self.collectionView2?.scrollToItem(at: indexPath, at: [], animated: true)
}
}
where am i going wrong?
In the init of MainViewsHome instead of initializing instance property collectionView2 you are initializing completely new collectionView so you are not having reference in collectionView2.
It should be
self.collectionView2 = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout2)
Instead of
let collectionView2 = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout2)