Why are my collectionview cells not animating? - swift

I have collection view cells and I'd like to animate all of them at the same time. Even though I'm using UIView.animate the animation does not happen and affine transform happens instantaneously.
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var collectionview: UICollectionView!
var moveText: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
collectionview = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionview.translatesAutoresizingMaskIntoConstraints = false
collectionview.dataSource = self
collectionview.delegate = self
collectionview.backgroundColor = .systemBackground
collectionview.register(MyCell.self, forCellWithReuseIdentifier: "cell")
view.addSubview(collectionview)
NSLayoutConstraint.activate([
collectionview.topAnchor.constraint(equalTo: view.topAnchor),
collectionview.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionview.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionview.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
cell.moveText = moveText
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return .init(width: collectionview.frame.width, height: 120)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { timer in
UIView.animate(withDuration: 1, delay: 0) {
print("Starting animatings")
self.moveText = !self.moveText
self.collectionview.reloadData()
}
}
}
}
class MyCell: UICollectionViewCell {
let label = UILabel()
var moveText: Bool = false {
didSet {
if moveText == true {
label.transform = CGAffineTransform(translationX: 50, y: 50)
} else {
label.transform = CGAffineTransform(translationX: 0, y: 0)
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
label.text = "mytext"
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.topAnchor.constraint(equalTo: topAnchor)
])
backgroundColor = .orange
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

This is because you are animating the wrong thing.
// adding weak self so the timer will not retain the view controller
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
self?.moveText.toggle() // you can use toggle() on a `Bool`
self?.collectionview.reloadData()
}
Instead, you should try to animate setting the value of individual cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
UIView.animate(withDuration: 1, delay: 0) {
cell.moveText = self.moveText
}
return cell
}
Also, instead of using CGAffineTransform(translationX: 0, y: 0) you could use .identity
label.transform = .identity
Worth noting is the fact that reloading collection view every 2 seconds and animating directly in cellForItemAt is very inefficient. You should find another way to pass information to the cell's they should animate the content

Related

How to scroll to a given indexPath.item?

I have 2 collection views in parallel. One of them is a menu and the other is a big one containing vertical table views that are populated with API queried data.
When the user scrolls right, then Order History is highlighted and the next cell in the big collection view at the bottom takes over the rest of the screen and it shows its own tableview data. It's just like the implementation here.
Everything works EXCEPT when I tap on Order History in the state indicated in the attached image, the collection view at the bottom does not scroll to the 2nd cell in the big collection view. If I tap on the Current Orders cell then it does scroll to the 1st cell in the big collection view. I have no idea what's causing this problem after spending the last 48 hours on this.
This is the menu bar:
class MenuBar: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
lazy var menuCollection: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionview = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionview.translatesAutoresizingMaskIntoConstraints = false
collectionview.backgroundColor = .white
collectionview.delegate = self
collectionview.dataSource = self
collectionview.register(InvoiceCell.self, forCellWithReuseIdentifier: InvoiceCell.identifier)
return collectionview
}()
var leftIndicatorConstraint: NSLayoutConstraint?
private let menuOptions = ["Current Orders", "Order History"]
var containerViewController: ContainerViewController? // I use an instance of the parent VC to pass on the value
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(menuCollection)
menuCollection.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
menuCollection.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
menuCollection.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
menuCollection.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
menuCollection.selectItem(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .left)
setupIndicator()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupIndicator() {
let indicatorBar = UIView()
indicatorBar.backgroundColor = UIColor(hex: Constants.Colors.primary)
addSubview(indicatorBar)
indicatorBar.translatesAutoresizingMaskIntoConstraints = false
leftIndicatorConstraint = indicatorBar.leadingAnchor.constraint(equalTo: self.leadingAnchor)
leftIndicatorConstraint?.isActive = true
indicatorBar.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
indicatorBar.heightAnchor.constraint(equalToConstant: 5).isActive = true
indicatorBar.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.5).isActive = true
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
containerViewController?.scrollToMenu(menuIndex: indexPath.item) // Here I pass on the value of the menu item that's tapped
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return menuOptions.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvoiceCell.identifier, for: indexPath) as? InvoiceCell else { return UICollectionViewCell() }
cell.thisOption = menuOptions[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: menuCollection.frame.size.width / CGFloat(menuOptions.count), height: menuCollection.frame.size.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 0
}
}
And this is the parent UIView Controller that contains the menu collection view as well as the big base collection view
class ContainerViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
lazy var menuBar: MenuBar = {
let menuBar = MenuBar()
menuBar.containerViewController = self
menuBar.translatesAutoresizingMaskIntoConstraints = false
return menuBar
}()
lazy var baseCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(PastCell.self, forCellWithReuseIdentifier: PastCell.pastCellId)
collectionView.register(CurrentCell.self, forCellWithReuseIdentifier: CurrentCell.currentCellId)
collectionView.isPagingEnabled = true
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = .white
return collectionView
}()
var orderViewModel = OrderViewModel(order: Order(addressOne: "", addressTwo: "", city: "", postalCode: "", mpName: "", planType: 0, restaurantAddress: "", restaurantId: "", restaurantName: "", timeOfCreation: nil, currentTotal: 0.00, orderMenu: [], status: 0, itemsHTML: ""))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupView()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
orderViewModel.removeListener()
}
private func setupView() {
view.addSubview(menuBar)
menuBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
menuBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
menuBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
menuBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
view.addSubview(baseCollectionView)
baseCollectionView.topAnchor.constraint(equalTo: menuBar.bottomAnchor, constant: 0).isActive = true
baseCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
baseCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
baseCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
}
func scrollToMenu(menuIndex: Int) {
// Here I am using the passed on menu index to scroll to the appropriate cell in the big collection view.
let indexPath = IndexPath(item: menuIndex, section: 0)
baseCollectionView.scrollToItem(at: indexPath, at: .left, animated: true) // For some reason it ALWAYS SCROLL TO THE FIRST CELL }
func scrollViewDidScroll(_ scrollView: UIScrollView) {
menuBar.leftIndicatorConstraint?.constant = scrollView.contentOffset.x / 2
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let index = Int(targetContentOffset.pointee.x / view.frame.width)
menuBar.menuCollection.selectItem(at: IndexPath(item: index, section: 0), animated: true, scrollPosition: .left)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch indexPath.row {
case 0:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CurrentCell.currentCellId, for: indexPath) as? CurrentCell {
if let tabBarHeight = self.tabBarController?.tabBar.frame.size.height {
cell.adjustSize(top: menuBar.frame.height * 1.2, bottom: tabBarHeight * 0.5)
return cell
}
}
case 1:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PastCell.pastCellId, for: indexPath) as? PastCell {
cell.backgroundColor = .systemOrange
return cell
}
default:
return UICollectionViewCell()
}
return UICollectionViewCell()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.size.width, height: view.frame.size.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
Turns out there's a big bug with scrollToItem that Apple hasn't fixed. In my case it's caused by isPagingEnabled. So the best solution I found was here where you disable paging before scrolling to item and then enabling it right again.

how to prevent delay when trying to animate invisible cells in collectionview

I would like to animate all of my collectionview cells and this simplified example illustrates the problem. The problem is that if I scroll down while animating cells the ones that are not visible gets animated with a delay. I would like invisible cells to just be in the final state of animation instead of having animation delay.
In the video you see that when I scroll to the bottom, the final cells starts to animate with delay.
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var collectionview: UICollectionView!
var moveText: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
collectionview = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionview.translatesAutoresizingMaskIntoConstraints = false
collectionview.dataSource = self
collectionview.delegate = self
collectionview.backgroundColor = .systemBackground
collectionview.register(MyCell.self, forCellWithReuseIdentifier: "cell")
view.addSubview(collectionview)
NSLayoutConstraint.activate([
collectionview.topAnchor.constraint(equalTo: view.topAnchor),
collectionview.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionview.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionview.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
UIView.animate(withDuration: 1, delay: 0, options: [
UIView.AnimationOptions.allowAnimatedContent,
UIView.AnimationOptions.allowUserInteraction,
]) {
cell.moveText = self.moveText
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return .init(width: collectionview.frame.width, height: 120)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { timer in
print("Starting animatings")
self.moveText.toggle()
self.collectionview.reloadData()
}
}
}
class MyCell: UICollectionViewCell {
let label = UILabel()
var moveText: Bool = false {
didSet {
if moveText == true {
label.transform = CGAffineTransform(translationX: 50, y: 50)
} else {
label.transform = .identity
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
label.text = "mytext"
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.topAnchor.constraint(equalTo: topAnchor)
])
backgroundColor = .orange
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
instead of animating inside the cellForItemAt add this helper function.
private func animateVisibleCells() {
collectionview.visibleCells.compactMap { $0 as? MyCell }.forEach { cell in
UIView.animate(withDuration: 1, delay: 0, options: [
UIView.AnimationOptions.allowAnimatedContent,
UIView.AnimationOptions.allowUserInteraction,
]) {
cell.moveText = self.moveText
}
}
}
Now call it instead of reloading the collection view
Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { timer in
print("Starting animatings")
self.moveText.toggle()
self.animateVisibleCells()
}
and convert the cellForItemAt implementation to:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
cell.moveText = moveText
return cell
}

How to achieve a dynamic CollectionView Cell Height (Size for item at)?

I have decided start a project with no storyboard for the first time and at the moment I am stuck trying to figuring out how to achieve a proper dynamic cell in my CollectionViewController. Reading some of the solutions here in Stackoverflow I got the point in using a layout.estimatedItemSize but it somehow stops the bouncing effect from the collection view and also in my second cell which is a horizontal scroll view will not work after this implementation.
Here is my code(UICollectionViewController):
class InfoEmpaVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {
fileprivate let cell1 = "cell1"
fileprivate let cell2 = "cell2"
fileprivate let cellID = "cellID"
fileprivate let headerID = "headerID"
fileprivate let padding: CGFloat = 10
//
//
//GET THE DATA FROM:
var empanada: Empanadas!
struct Cells {
static let empanadaStepsCell = "EmpanadaStepsCell"
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionViewLayout()
setupCollectionView()
}
//CHANGE COLOR OF STATUS BAR
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
fileprivate func setupCollectionView() {
collectionView.backgroundColor = UIColor(named: "ColorBackground")
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(InfoHeaderVC.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerID)
//FirstCELL
collectionView.register(EmpaIngredientsListCell.self, forCellWithReuseIdentifier: cell1)
//SecondCELL
collectionView.register(EmpaStepsCell.self, forCellWithReuseIdentifier: cellID)
}
fileprivate func setupCollectionViewLayout() {
if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
layout.sectionInset = .init(top: padding, left: padding, bottom: padding, right: padding)
layout.estimatedItemSize = CGSize(width: view.frame.width, height: 50)
}
}
var headerView: InfoHeaderVC!
//HEADER COLLECTION VIEW
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerID, for: indexPath) as? InfoHeaderVC
headerView.empaImageView.image = UIImage(named: empanada.image)
headerView.empaTitleLabel.text = empanada.name
headerView.empaDescriptionLabel.text = empanada.info
headerView.buttonX.addTarget(self, action: #selector(dismissVC), for: .touchUpInside)
headerView.buttonAddFavorite.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
return headerView!
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return .init(width: view.frame.width, height: 350)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cell1, for: indexPath)
guard let cellOne = cell as? EmpaIngredientsListCell else {
fatalError("Wrong cell type for section 0. Expected CellTypeOne")
}
//INGREDIENT LIST
cellOne.empaIngredientList.ingredientList.append(contentsOf: empanada.ingredients)
cellOne.empaIngredientList.configure()
return cellOne
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! EmpaStepsCell
cell.pasos.append(contentsOf: empanada.pasos)
cell.titleHeaderLabel.text = "Step by Step"
cell.configure()
print (cell.pasos.count)
return cell
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 {
return .init(width: view.frame.width - 2 * padding, height: 300)
} else {
return .init(width: view.frame.width - 2 * padding, height: 300)
}
}
//OBJC FUNC
#objc func dismissVC() {
dismiss(animated: true)
}
//SAVE DATA
#objc func addButtonTapped() {
configureSaveToFavorites(empanada: empanada!)
}
}
Cell 1:
import UIKit
import SnapKit
class EmpaIngredientsListCell: UICollectionViewCell {
let empaIngredientList = EmpaIngredientsContainerView()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
print(intrinsicContentSize)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var frame = layoutAttributes.frame
frame.size.height = ceil(size.height)
layoutAttributes.frame = frame
return layoutAttributes
}
func setupUI() {
//contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(empaIngredientList)
empaIngredientList.snp.makeConstraints { (make) in
make.top.bottom.left.right.equalTo(self.contentView)
make.edges.equalTo(self.safeAreaLayoutGuide)
}
}
}

Cannot assign value of type '(JournalListViewController) -> () -> JournalListViewController' to type 'JournalListViewController?'

I am trying to show another ViewController from a custom tab bar but, I cannot pass a view controller to the custom class.
I've tried var jlvc = HomeController() but that would crash the app.
class JournalListViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
print(self)
navigationController?.navigationBar.isTranslucent = false
let titleLabel = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.width-32, height: view.frame.height))
titleLabel.textColor = UIColor.white
titleLabel.text = "Journal List"
titleLabel.font = UIFont.systemFont(ofSize: 20)
navigationItem.titleView = titleLabel
collectionView?.backgroundColor = UIColor.white
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Register cell class
collectionView?.register(JournalCell.self, forCellWithReuseIdentifier: "cellId")
// To unblock view from menubar
collectionView?.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 60, right: 0)
collectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 60, right: 0)
// Do any additional setup after loading the view.
setupMenuBar()
}
let menuBar: MenuBar = {
let mb = MenuBar()
mb.jlvc = self
return mb
}()
private func setupMenuBar(){
view.addSubview(menuBar)
view.addConstraintsWithFormat(format: "H:|[v0]|", views: menuBar)
view.addConstraintsWithFormat(format: "V:[v0(60)]|", views: menuBar)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
override 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 {
let height = (view.frame.width - 16 - 16) * 9 / 16 //-16 for top, - 16 for bot, 16/9 aspect ratio so * by 9/16
return CGSize(width:view.frame.width, height:height + 16 + 41) //height is height if image + additional space we added
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func showHomeController(){
let hc = HomeController()
navigationController?.pushViewController(hc, animated: true)
}
import Foundation
import UIKit
class MenuBar: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var jlvc: JournalListViewController?
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor(red: 152/255, green: 216/255, blue: 142/255, alpha: 1)
cv.dataSource = self
cv.delegate = self
return cv
}()
let cellId = "cellId"
let imageNames = ["home","camera","journal"]
override init(frame: CGRect) {
super.init(frame: frame)
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)
addSubview(collectionView)
addConstraintsWithFormat(format: "H:|[v0]|", views: collectionView)
addConstraintsWithFormat(format: "V:|[v0]|", views: collectionView)
let selectIndexPath = IndexPath(item: 0, section: 0)
collectionView.selectItem(at: selectIndexPath, animated: false, scrollPosition: [])
self.collectionView.isScrollEnabled = false
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
// Returning Cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MenuCell
cell.imageView.image = UIImage(named: imageNames[indexPath.item])?.withRenderingMode(.alwaysTemplate)
cell.tintColor = UIColor.black
cell.jlvc = self.jlvc
return cell
}
// Size of Cell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width / 3, height: frame.height)
}
// Spaceing in between cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
// Selecting cell
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! MenuCell
if cell.imageView.image == UIImage(named: "home")?.withRenderingMode(.alwaysTemplate) {
jlvc?.showHomeController()
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MenuCell: BaseCell {
var jlvc: JournalListViewController?
let imageView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "home")?.withRenderingMode(.alwaysTemplate)
iv.tintColor = UIColor.black
return iv
}()
override var isHighlighted: Bool {
didSet{
imageView.tintColor = isHighlighted ? UIColor.white : UIColor.black
}
}
override var isSelected: Bool {
didSet{
imageView.tintColor = isSelected ? UIColor.white : UIColor.black
}
}
override func setupViews() {
super.setupViews()
addSubview(imageView)
addConstraintsWithFormat(format: "H:[v0(28)]", views: imageView)
addConstraintsWithFormat(format: "V:[v0(28)]", views: imageView)
addConstraint(NSLayoutConstraint(item: imageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0))
addConstraint(NSLayoutConstraint(item: imageView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0))
}
}
Cannot assign value of type '(JournalListViewController) -> () -> JournalListViewController' to type 'JournalListViewController?'
let menuBar: MenuBar = {
let mb = MenuBar()
mb.jlvc = self
return mb
}()
This doesn't do what you're thinking. self in this context is the class, not the instance. You either meant to make this a lazy var (as you did with collectionView), or to assign it explicitly.

scrollToItem horizontal scrolling collectionView crash

I am having an issue with implementing a horizontal scrollToItem collectionView. I have a collectionView (menuBar) that on tap of a cell the page collectionView will horizontally scroll to the appropriate cell. On tap of the menuBar collectionViewCell the app crashes. Not sure where the error is. Thanks in advance!
// controller class
class CurrentUserPlaceDetailsVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var titleText: String?
let cellId = "cellId"
// UI elements
lazy var contentCollectionView: UICollectionView = {
let cv = UICollectionView()
cv.backgroundColor = UIColor.red
cv.layer.zPosition = 0
cv.translatesAutoresizingMaskIntoConstraints = false
cv.layer.masksToBounds = true
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
setupMenuBar()
setupCollectionView()
}
func scrollToMenuIndex(_ menuIndex: Int) {
let indexPath = IndexPath(item: menuIndex, section: 0)
contentCollectionView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition(), animated: true)
}
lazy var menuBar: MenuBar = {
let mb = MenuBar()
mb.currentUserPlaceDetailsVC = self
return mb
}()
private func setupMenuBar() {
view.addSubview(menuBar)
view.addConstraintsWithFormat("H:|[v0]|", views: menuBar)
view.addConstraintsWithFormat("V:|[v0(114)]", views: menuBar)
}
func setupCollectionView() {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
// layout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 10, right: 10)
layout.itemSize = CGSize(width: self.view.frame.width, height: self.view.frame.height)
let contentCollectionView:UICollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
contentCollectionView.dataSource = self
contentCollectionView.delegate = self
// register cells
contentCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellId")
contentCollectionView.backgroundColor = .white
// contentCollectionView.scrollIndicatorInsets = UIEdgeInsets(top:114, left: 10, bottom: 10, right: 10)
self.view.addSubview(contentCollectionView)
_ = contentCollectionView.anchor(view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, topConstant: 114, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
view.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
contentCollectionView.isPagingEnabled = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset.x)
menuBar.horizontalBarLeftAnchorConstraint?.constant = scrollView.contentOffset.x / 4
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath)
let colors: [UIColor] = [.blue, .red, .yellow, .green]
myCell.backgroundColor = colors[indexPath.item]
return myCell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
// menu bar class
class MenuBar: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.headerReferenceSize = .zero
layout.sectionInset = .zero
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .green
cv.dataSource = self
cv.delegate = self
return cv
}()
let cellId = "cellId"
var currentUserPlaceDetailsVC: CurrentUserPlaceDetailsVC?
override init(frame: CGRect) {
super.init(frame: frame)
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)
addSubview(collectionView)
addConstraintsWithFormat("H:|[v0]|", views: collectionView)
addConstraintsWithFormat("V:|[v0]|", views: collectionView)
let selectedIndexPath = IndexPath(item: 0, section: 0)
collectionView.selectItem(at: selectedIndexPath, animated: false, scrollPosition: UICollectionViewScrollPosition())
setupHorizontalBar()
}
var horizontalBarLeftAnchorConstraint: NSLayoutConstraint?
func setupHorizontalBar() {
let horizontalBarView = UIView()
horizontalBarView.backgroundColor = .white
horizontalBarView.translatesAutoresizingMaskIntoConstraints = false
addSubview(horizontalBarView)
horizontalBarLeftAnchorConstraint = horizontalBarView.leftAnchor.constraint(equalTo: self.leftAnchor)
horizontalBarLeftAnchorConstraint?.isActive = true
horizontalBarView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
horizontalBarView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1/4).isActive = true
horizontalBarView.heightAnchor.constraint(equalToConstant: 4).isActive = true
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath.item)
let x = CGFloat(indexPath.item) * frame.width / 4
horizontalBarLeftAnchorConstraint?.constant = x
UIView.animate(withDuration: 0.75, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
currentUserPlaceDetailsVC?.scrollToMenuIndex(indexPath.item)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
cell.imageView.image = UIImage(named: imageNames[indexPath.item])?.withRenderingMode(.alwaysTemplate)
cell.tintColor = UIColor.rgb(91, green: 14, blue: 13)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width / 4, height: frame.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In your comment you said:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'UICollectionView must be initialized with a non-nil layout parameter'. I believe there is something wrong in my func scrollToMenuIndex()... when I remove that func the app does not crash.
That happens, because you inside your View Controller your collectionView declared as lazy, that means it will be calculated on-demand.
The problem is in initializing your collectionView (reason: 'UICollectionView must be initialized with a non-nil layout parameter')
In your view controller replace UICollectionView initialization with this code:
lazy var contentCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.red
cv.layer.zPosition = 0
cv.translatesAutoresizingMaskIntoConstraints = false
cv.layer.masksToBounds = true
return cv
}()
P.S. As I wrote in my comment to your question, you have reference cycle. class MenuBar should hold weak reference to your view controller to prevent this.