How to animate when swiping through collection view - ios

I have a collectionView inside of a ViewController. When I swipe, it swipes to another Collection View Cell which is the height and width of the view.frame
I want to have an animation that starts whenever I start swiping, to then finish whenever the collection view locks into the centre of a cell just like the gif below. I don't have a label or textview in my code but could you point me in the right direction on how I would do this? Here's my relevent code:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.dataSource = self
cv.delegate = self
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
let cellId = "cellId"
let loginCellId = "loginCellId"
let pages: [Page] = {
let firstPage = Page(imageName: "introduction_1")
let secondPage = Page(imageName: "introduction_2")
let thirdPage = Page(imageName: "introduction_3")
let fourthPage = Page(imageName: "introduction_4")
return [firstPage, secondPage, thirdPage, fourthPage]
}()
lazy var pageControl: UIPageControl = {
let pc = UIPageControl()
pc.pageIndicatorTintColor = .lightGray
pc.currentPageIndicatorTintColor = .darkGray
pc.numberOfPages = self.pages.count + 1
return pc
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
view.addSubview(pageControl)
_ = pageControl.anchor(nil, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 40)
collectionView.anchorToTop(view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor)
registerCells()
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageNumber = Int(targetContentOffset.pointee.x / view.frame.width)
pageControl.currentPage = pageNumber
}
fileprivate func registerCells() {
collectionView.register(PageCell.self, forCellWithReuseIdentifier: cellId)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: loginCellId)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pages.count + 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == pages.count {
let loginCell = collectionView.dequeueReusableCell(withReuseIdentifier: loginCellId, for: indexPath)
return loginCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PageCell
let page = pages[indexPath.item]
cell.page = page
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
class PageCell: UICollectionViewCell {
var page: Page? {
didSet {
guard let page = page else {
return
}
imageView.image = UIImage(named: page.imageName)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.backgroundColor = .yellow
iv.clipsToBounds = true
return iv
}()
func setupViews() {
addSubview(imageView)
imageView.anchorToTop(topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

From my standpoint, in your case it is better to use UIPageViewController instead of UICollectionView. It has a whole bunch of features for you to implement the UI you've shown above. There is a good tutorial on this topic - https://www.veasoftware.com/posts/uipageviewcontroller-in-swift-xcode-62-ios-82-tutorial?rq=page

Related

SubViews are not adding in some UICollectionViewCells and flickering (programmatically)

I am trying to make custom Image Slider with collections view. I want to it to be reusable. So I made separate custom class where all collectionView stuff. and then call that class from UIViewController as shown in code below. And my UICollectonViewCell only contains imageView.
Problem is that in second cell. imageView is not being added and on third cell, it also flickers. I tried to debug these issues but could not.
ImageSlider class and UICollectionViewCell class at end end, with collection view stuff:
class ImageSlider: UIView {
var imgArr = [UIImage(named: "one.jpg"), UIImage(named: "3.jpg"), UIImage(named: "two.jpg"), UIImage(named: "4.jpg")]
var sliderCollection : UICollectionView = {
let widthRatio : Float = 16.0
let heightRatio : Float = 9.0
let collecionWidth = UIScreen.main.bounds.width - 30
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: CGRect(x: 15, y: 100, width: collecionWidth, height: CGFloat((Float(collecionWidth)*heightRatio)/widthRatio)), collectionViewLayout: layout)
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
collectionView.backgroundColor = .systemOrange
collectionView.isPagingEnabled = true
// collectionView.isScrollEnabled = true
collectionView.register(ImageSliderCell.self, forCellWithReuseIdentifier: "ImageSliderCell")
return collectionView
}()
}
extension ImageSlider: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imgArr.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageSliderCell", for: indexPath) as! ImageSliderCell
cell.imgView.image = imgArr[indexPath.item]
// cell.imgView.contentMode = .scaleAspectFit
print("cell frame : ", "(\(cell.frame.width), \(cell.frame.height)")
print("imgView frame : ", "(\(cell.imgView.frame.width), \(cell.imgView.frame.height)")
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
}
class ImageSliderCell: UICollectionViewCell {
var imgView = UIImageView()
// override func awakeFromNib() {
// self.addSubview(imgView)
// }
override init(frame: CGRect) {
super.init(frame: frame)
imgView.frame = frame
self.addSubview(imgView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This is ViewController, where I am calling ImageSlider() class.
class ImageSliderVC: UIViewController, UICollectionViewDelegate {
let imageSlider = ImageSlider()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(imageSlider.sliderCollection)
imageSlider.sliderCollection.delegate = imageSlider
imageSlider.sliderCollection.dataSource = imageSlider
imageSlider.sliderCollection.reloadData()
}
}
It looks like it does not work without constrains because UICollectionViewCell could be created with zero frame and it translated to imageView inside the cell. You need to add constrains to imageView to make it visible.
extension UIView {
func centerX(inView view: UIView, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: constant).isActive = true
}
func centerY(inView view: UIView, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant).isActive = true
}
func setDimensions(height: CGFloat, width: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
widthAnchor.constraint(equalToConstant: width).isActive = true
}
func setHeight(_ height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
class ImageSliderCell: UICollectionViewCell {
var imgView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(imgView)
// not sure about the right size of image ...
imgView.setDimensions(height: 100.0, width: 100.0)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Making animating horizontal indicator using collectionView

I'm working on App where I have to manage my horizontal indicator on CollectionviewCell, while scrolling CollectionView and by selecting any Cell.
This is what I’m looking for(Just focus on Horizontal CollectionView)
As I have implemented this But I’m not getting the exact functionality/behavior AND unable to stick horizontal indicator on collectionviewCell while scrolling. I can only stick if i make a horizontal indicator in CollectionViewCell But in this Case I’m unable to apply sliding animation.
This is what I have implemented
Here is my Code Snippet for MENUBAR
import UIKit
class MenuBar: UITableViewHeaderFooterView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIScrollViewDelegate {
//MARK:- Properties
let cellId = "cellId"
let menuNames = ["Recommeded", "Popular", "Free", "Trending", "Love Songs", " Free Songs"]
var horizontalBarLeftAnchorConstraint : NSLayoutConstraint?
lazy var collectionView : UICollectionView = {
let cv = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
cv.translatesAutoresizingMaskIntoConstraints = false
cv.dataSource = self
cv.delegate = self
cv.showsHorizontalScrollIndicator = false
cv.backgroundColor = UIColor.clear
return cv
}()
let horizontalView : UIView = {
let v = UIView()
v.backgroundColor = UIColor.red
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
//MARK:- default methods
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setupCollectionView()
setupHorizontalBar()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK:- Functions
private func setupCollectionView() {
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.scrollDirection = .horizontal
}
addSubview(collectionView)
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
collectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
private func setupHorizontalBar() {
addSubview(horizontalView)
let textSize = (menuNames[0] as NSString).size(withAttributes: nil)
let cellSize = textSize.width + 50
let indicatorLineWith = 25/2
let x = (cellSize/2) - CGFloat(indicatorLineWith)
//x,y,w,h
horizontalBarLeftAnchorConstraint =
horizontalView.leftAnchor.constraint(equalTo: leftAnchor, constant: x )
horizontalBarLeftAnchorConstraint?.isActive = true
horizontalView.heightAnchor.constraint(equalToConstant: 5).isActive = true
horizontalView.widthAnchor.constraint(equalToConstant: 25).isActive = true
horizontalView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
//MARK:- CollectionView methods
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return menuNames.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
cell.menuName = menuNames[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = (menuNames[indexPath.row] as NSString).size(withAttributes: nil)
return CGSize(width: (size.width + 50), height: frame.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//getting Cell size on screen
let attributes : UICollectionViewLayoutAttributes = collectionView.layoutAttributesForItem(at: indexPath)!
let indicatorSize = 25/2
let cellRect = attributes.frame
let cellFrameInSuperView = collectionView.convert(cellRect, to: collectionView)
let textSize = (menuNames[indexPath.row] as NSString).size(withAttributes: nil)
let cellSize = textSize.width + 50
let x = (CGFloat(cellFrameInSuperView.origin.x) + (cellSize/2)) - CGFloat(indicatorSize)
horizontalBarLeftAnchorConstraint?.constant = x
UIView.animate(withDuration: 0.75, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
self.layoutIfNeeded()
}, completion: nil)
}
}
Here is my code snippet for MENUCELL:-
import UIKit
//MARK:- CollectionViewBaseCell
class CollectionViewBaseCell : UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {}
}
//MARK:- MenuCell
class MenuCell : CollectionViewBaseCell {
//MARK:- Properties
var menuName : String? {
didSet {
label.text = menuName
}
}
let label : UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.text = "Label"
return lbl
}()
//MARK:- default methods
override func setupViews() {
addSubview(label)
//x,y,w,h Constraint
label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
TRY THIS:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.collectionView{
print( scrollView.contentOffset.x ) // use this contentOffset
}
}

How to fix images disappearing from UICollectionViewCell on scroll

enter code hereI'm creating an application that requires a UICollectionView inside a UICollectionViewCell. I've successfully added the UICollectionView and all of its delegate methods within the cell and, on initial load, everything loads correctly. However, as soon as the user attempts to scroll further down in the main UICollectionView, the cells which held the inner UICollectionView, essentially, loose their view. An interesting detail to note is that the issue only rears its head when the user scrolls at a non-snail rate. If the scroll slow enough, the cells will load correctly. Otherwise, the cells will become malformed.
Unfortunately, I've been dealing with this problem for quite a while, as in close to/more than a month at this point. I've tried just about every solution proposed on StackOverflow, Medium articles, YouTube tutorials, etc. but have yet to find the working solution.
This behavior is not exhibited without the conditional rendering if I always add the UICollectionView as a subview.
An important detail to note is that my cells are vertically self-sizing, and every cell is not guaranteed to contain an inner UICollectionView. I suspect part of my issue is born from my handling of this conditional rendering (i.e. if the cell's datasource has images, add the UICollectionView as a subview. Otherwise, don't add it at all).
Additionally, I'm using imported assets in my Xcode project as images. I'm not making any async calls that otherwise may attribute to the images not being fetched and the datasource having 0 images when it should have > 0. My logs in my parseDatasource(withDatasource datasource: (String, [UIImage]) function are showing that datasource.1 contains an array of images in the cells that should have images, as expected. I'm beyond stumped at this point and a bit more than slightly frustrated.
My main UIViewController controlling the cells:
class ExperimentalViewController: UIViewController {
private let cellReuseId = "cellReuseId"
fileprivate var data = [(String, [UIImage])]()
fileprivate let strings = [<a large array of strings>]
private let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
collection.backgroundColor = .clear
collection.alwaysBounceVertical = true
collection.contentInsetAdjustmentBehavior = .always
return collection
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupDummyData()
setupCollectionView()
}
private func setupDummyData() {
for (i, str) in strings.enumerated() {
var images = [UIImage]()
if i % 2 == 0 {
images = [UIImage(named: "boxed-water-is-better-1464052-unsplash")!, UIImage(named: "boxed-water-is-better-1464052-unsplash")!, UIImage(named: "boxed-water-is-better-1464052-unsplash")!]
}
data.append((str, images))
}
}
private func setupCollectionView() {
if #available(iOS 13.0, *) {
let size = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
heightDimension: NSCollectionLayoutDimension.estimated(440)
)
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
section.interGroupSpacing = 5
let layout = UICollectionViewCompositionalLayout(section: section)
collectionView.collectionViewLayout = layout
} else {
let layout = ExperimentalFlowLayout()
collectionView.collectionViewLayout = layout
}
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(ExperimentalCell.self, forCellWithReuseIdentifier: cellReuseId)
view.addSubview(collectionView)
collectionView.anchor(top: view.topAnchor, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: view.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
}
extension ExperimentalViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseId, for: indexPath) as! ExperimentalCell
cell.index = indexPath
cell.backgroundColor = .yellow
cell.setupViews(withDatasource: data[indexPath.item])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
}
My UICollectionViewCell subclass
class ExperimentalCell: UICollectionViewCell {
// https://stackoverflow.com/questions/37782659/swift-ios-uicollectionview-images-mixed-up-after-fast-scroll/37784212
// http://www.thomashanning.com/the-most-common-mistake-in-using-uitableview/
private let imageCellReuseId = "imageCellReuseId"
var index: IndexPath?
var images = [UIImage]()
private let titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
return label
}()
private let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
return label
}()
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
collection.backgroundColor = .clear
return collection
}()
override init(frame: CGRect) {
super.init(frame: frame)
print("RYANLOG \(index?.row) INIT")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
images = []
imageView.image = nil
collectionView.delegate = nil
collectionView.dataSource = nil
titleLabel.text = nil
label.text = nil
setupViews(withDatasource: nil)
}
func setupViews(withDatasource datasource: (String, [UIImage])?) {
if datasource != nil {
parseDatasource(datasource!)
}
contentView.addSubview(titleLabel)
titleLabel.anchor(top: contentView.topAnchor, leading: contentView.leadingAnchor, bottom: nil, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
contentView.addSubview(label)
if images.count > 0 {
titleLabel.text = "Images"
label.text = String(images.count)
label.anchor(top: titleLabel.bottomAnchor, leading: contentView.leadingAnchor, bottom: nil, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
setupCollectionView()
contentView.addSubview(collectionView)
collectionView.anchor(top: label.bottomAnchor, leading: contentView.leadingAnchor, bottom: contentView.bottomAnchor, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 200)
} else {
titleLabel.text = "No Images"
label.anchor(top: titleLabel.bottomAnchor, leading: contentView.leadingAnchor, bottom: contentView.bottomAnchor, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
}
private func parseDatasource(_ datasource: (String, [UIImage])) {
label.text = datasource.0
images = datasource.1
print("RYANLOG \(index?.row) Images:", images.count)
}
// MARK: - Used for self-sizing on <= iOS 12
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
layoutIfNeeded()
let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
layoutAttributes.bounds.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
return layoutAttributes
}
}
extension ExperimentalCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
fileprivate func setupCollectionView() {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(ScrollableImageView.self, forCellWithReuseIdentifier: imageCellReuseId)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellReuseId, for: indexPath) as! ScrollableImageView
cell.imageView.image = images[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 200, height: 200)
}
}
Please excuse the use of print("RYANLOG ...)s. I've been using them for debugging and figured they might be helpful in displaying the areas I believe might be part of the issue.
ScrollableImageView is a subclass of UIView. If you think seeing the code contained within this class would be helpful, please let me know!
Any help is greatly appreciated!
A gif demonstrating the flawed behavior:
https://imgur.com/a/btAXPca
Update: I finally solved this using two different reuse identifiers: "cellRuseId" and "imageCellReuseId". I'm still not sure whether this is the correct solution, but, now my collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) looks like so:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let reuseId = data[indexPath.item].1.count > 0 ? imageCellReuseId : cellReuseId
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) as! ExperimentalCell
cell.index = indexPath
cell.backgroundColor = .yellow
cell.setupViews(withDatasource: data[indexPath.item])
return cell
}
My collection view scrolls correctly and is properly reusing the cells. I won't accept this answer as it's not the most elegant (especially if you have multiple variations of the same cell) so if someone has a better answer, please give a response!

Cells won't show in UICollectionView

I'm trying to create a UICollectionView and display few cells there.
This is my code:
class MainVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var mForecast = [CustomCollectionViewCell]()
let CVCellIdentifier = "forecastCell"
lazy var mCollectionView: UICollectionView = {
var collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 300, height: 150), collectionViewLayout: UICollectionViewFlowLayout())
collectionView.clipsToBounds = true
collectionView.backgroundColor = .red
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 80/255, green: 135/255, blue: 179/255, alpha: 1.0)
setupNavBar()
self.navigationItem.searchController = mSearchBarController
setupMainWeatherIcon()
fillArrayWithData()
mCollectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CVCellIdentifier)
mCollectionView.dataSource = self
mCollectionView.delegate = self
}
private func fillArrayWithData(){
for _ in 1...6 {
let forecastCell: ForecastCell = ForecastCell()
forecastCell.mDayLabel = "DAY-TEST"
forecastCell.mWeatherIcon = UIImage(named: "partly-cloudy")
forecastCell.mTempLabel = "TEMP-TEST"
mForecast.append(forecastCell)
}
mCollectionView.reloadData()
}
//MARK: COLLECTION VIEW METHODS
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return mForecast.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = mCollectionView.dequeueReusableCell(withReuseIdentifier: CVCellIdentifier, for: indexPath) as! CustomCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (view.frame.width / 6) - 16 , height: 70)
}
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, insetForSectionAt
section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 8)
}
}
This is the CustomCollectionViewCell class:
import UIKit
class CustomCollectionViewCell: UICollectionViewCell {
var mDayLabel: String?
var mWeatherIcon: UIImage?
var mTempLabel: String?
let dayTV: UILabel = {
var label = UILabel()
label.textAlignment = .center
label.font = UIFont.boldSystemFont(ofSize: 12)
label.textColor = .blue
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let weatherImg: UIImageView = {
var img = UIImageView()
img.translatesAutoresizingMaskIntoConstraints = false
return img
}()
let tempLabel: UILabel = {
var label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 8)
label.textColor = .blue
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let label = mDayLabel{
dayTV.text = label
}
if let image = mWeatherIcon{
weatherImg.image = image
}
if let temp = mTempLabel{
tempLabel.text = temp
}
setupDayTextView()
setupWeatherImage()
setupTempLabel()
}
private func setupDayTextView(){
addSubview(dayTV)
dayTV.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
dayTV.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
}
private func setupWeatherImage(){
addSubview(weatherImg)
weatherImg.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
weatherImg.topAnchor.constraint(equalTo: dayTV.bottomAnchor, constant: 10).isActive = true
}
private func setupTempLabel(){
addSubview(tempLabel)
tempLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
tempLabel.topAnchor.constraint(equalTo: weatherImg.bottomAnchor, constant: 10).isActive = true
}
}
Of course I have a method to fill mForecast array with data, if I add a subview I do see the cell, but I know I shouldn't do that to view the cells inside the collection view.
I tried looking but couldn't find what's wrong here and why the cells won't be displayed.
This is what I get
ORIGINAL:
After setting the delegate and the datasource, you need to call collectionView.reloadData()
REVISED:
You are calling fillArrayWithData, which calls reloadData before you finish configuring the collectionView's datasource and delegate. Thus, when reloadData is called, there is no source that sets the data and loads the cells.
Try calling your fillArrayWithData after you finalize the configuration of your collection view.
I personally recommend configuring your collection view in viewDidLoad or in the didSet property observer of collectionView:
var collectionView: UICollectionView! {
didSet {
collectionView.delegate = self
collectionView.dataSource = self
}
}
And then I initiate the load of the data in my viewWillAppear method.
EXAMPLE:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 80/255, green: 135/255, blue: 179/255, alpha: 1.0)
setupNavBar()
self.navigationItem.searchController = mSearchBarController
setupMainWeatherIcon()
// This is where you are calling fillArrayWithData right now.
// fillArrayWithData()
mCollectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CVCellIdentifier)
mCollectionView.dataSource = self
mCollectionView.delegate = self
// This is where you should be calling fillArrayWithData
fillArrayWithData()
}

Issue With Passing Data Programmatically Swift

I am having trouble passing data from one UICollectionViewController to another UICollectionViewController programmatically.
Currently my setup is as follows:
The UICollectionViewController that is passing the data (RestaurantController)
1a. A UICollectionViewCell (RestaurantCell)
this UICollectionViewCell has a nested UICollectionViewController within it with another custom UICollectionViewCell (RestaurantCollectionViewCell)
The UICollectionViewController that is receiving the data (MenuController)
2a. A UICollectionViewCell (MenuCell)
Inside of my RestaurantCell I am loading data from JSON and appending it a new array as called restaurants: var restaurants = [RestaurantModel](). But when I try to load the restaurant name or any of the restaurant objects in my MenuController using var restaurant: RestaurantModel?, I get nil values. I have a feeling either my setup is incorrect or I'm making a stupid mistake somewhere. Perhaps both. I have pasted my code below for each class.
Where the values are returning nil inside of MenuController:
print("Restaurant Name:", restaurant?.name)
print("Restaurant Id:", restaurant?.id)
Is the custom delegation causing the issue?
You help and advice is greatly appreciated!
Inside of my RestaurantController:
import UIKit
import FBSDKLoginKit
class RestaurantController: UICollectionViewController, UICollectionViewDelegateFlowLayout, SWRevealViewControllerDelegate, UISearchBarDelegate, RestaurantDelegate {
var restaurantCell: RestaurantCell?
private let restaurantCellId = "restaurantCellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.qpizzaWhite()
collectionView?.register(RestaurantCell.self, forCellWithReuseIdentifier: restaurantCellId)
if self.revealViewController() != nil {
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "icon_menu_24dp").withRenderingMode(.alwaysOriginal), style: .plain, target: self.revealViewController(), action: #selector(SWRevealViewController.revealToggle(_:)))
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
}
// FIXME: issue with this...navigationcontroller is presenting, not pushing ontop of stack view
func didTapRestaurantCell(cell: RestaurantCell) {
print("Did Tap Restaurant Cell - Restaurant Controller")
let layout = UICollectionViewFlowLayout()
let controller = MenuController(collectionViewLayout: layout)
navigationController?.pushViewController(controller, animated: true)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: restaurantCellId, for: indexPath) as! RestaurantCell
cell.delegate = self
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
}
Inside of my RestaurantCell:
protocol RestaurantDelegate {
func didTapRestaurantCell(cell: RestaurantCell)
}
class RestaurantCell: BaseCell, UISearchBarDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var delegate: RestaurantDelegate?
var restaurants = [RestaurantModel]()
var filteredRestaurants = [RestaurantModel]()
private let restaurantCollectionViewCell = "restaurantCollectionViewCell"
private let activityIndicator = UIActivityIndicatorView()
lazy var searchBar: UISearchBar = {
let sb = UISearchBar()
sb.placeholder = "Search Restaurant"
sb.barTintColor = .white
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).backgroundColor = UIColor.qpizzaWhite()
sb.delegate = self
return sb
}()
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
return cv
}()
override func setupViews() {
super.setupViews()
collectionView.register(RestaurantCollectionViewCell.self, forCellWithReuseIdentifier: restaurantCollectionViewCell)
collectionView.delegate = self
collectionView.dataSource = self
backgroundColor = UIColor.qpizzaRed()
addSubview(searchBar)
addSubview(collectionView)
_ = searchBar.anchor(topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 4, leftConstant: 4, bottomConstant: 0, rightConstant: 4, widthConstant: 0, heightConstant: 50)
_ = collectionView.anchor(searchBar.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
loadRestaurants()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print(searchText)
filteredRestaurants = self.restaurants.filter({ (restaruant: RestaurantModel) -> Bool in
return restaruant.name?.lowercased().range(of: searchText.lowercased()) != nil
})
self.collectionView.reloadData()
}
// MARK - Helper Methods
func loadRestaurants() {
showActivityIndicator()
APIManager.shared.getRestaurants { (json) in
if json != .null {
// print("Restaurant JSON:", json)
self.restaurants = []
if let restaurantList = json["restaurants"].array {
for item in restaurantList {
let restaurant = RestaurantModel(json: item)
self.restaurants.append(restaurant)
}
self.collectionView.reloadData()
self.hideActivityIndicator()
}
} else {
print("Error loading JSON into Restaurant ViewController")
}
}
}
func loadImage(imageView: UIImageView, urlString: String) {
let imageUrl: URL = URL(string: urlString)!
URLSession.shared.dataTask(with: imageUrl) { (data, response, error) in
if let error = error {
print("Error loading image for Restaurant Controller:", error.localizedDescription)
}
guard let data = data, error == nil else { return }
DispatchQueue.main.async(execute: {
imageView.image = UIImage(data: data)
})
}.resume()
}
func showActivityIndicator() {
activityIndicator.frame = CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)
activityIndicator.center = center
activityIndicator.hidesWhenStopped = true
activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.whiteLarge
activityIndicator.color = UIColor.qpizzaGold()
addSubview(activityIndicator)
activityIndicator.startAnimating()
}
func hideActivityIndicator() {
activityIndicator.stopAnimating()
}
//MARK: CollectionView Delegate & DataSource Methods
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: restaurantCollectionViewCell, for: indexPath) as! RestaurantCollectionViewCell
let restaurant: RestaurantModel
if searchBar.text != "" {
restaurant = filteredRestaurants[indexPath.item]
} else {
restaurant = restaurants[indexPath.item]
}
cell.restaurantNameLabel.text = restaurant.name
cell.restaurantAddressLabel.text = restaurant.address
if let logoName = restaurant.logo {
let url = "\(logoName)"
loadImage(imageView: cell.restaurantLogoImageView, urlString: url)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if searchBar.text != "" {
return self.filteredRestaurants.count
}
return self.restaurants.count
}
//FIXME: Restaurant Name Navigation Title is still not be passed from RestaurantCell to MenuController
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Did Select Item - Restaurant Cell")
let layout = UICollectionViewFlowLayout()
let controller = MenuController(collectionViewLayout: layout)
controller.restaurant = self.restaurants[indexPath.item]
print("Controller", controller.restaurant) // Optional(QpizzaDelivery.RestaurantModel)
print("Restaurant:", self.restaurants) // [QpizzaDelivery.RestaurantModel, QpizzaDelivery.RestaurantModel, QpizzaDelivery.RestaurantModel]
print("IndexPath:", self.restaurants[indexPath.item]) // QpizzaDelivery.RestaurantModel
delegate?.didTapRestaurantCell(cell: self)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width, height: 200)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0.5
}
}
Inside of my MenuController:
import UIKit
class MenuController: UICollectionViewController, UICollectionViewDelegateFlowLayout, SWRevealViewControllerDelegate {
private let menuCellId = "menuCellId"
var restaurant: RestaurantModel?
var menuItems = [MenuItemsModel]()
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.qpizzaWhite()
collectionView?.register(MenuCell.self, forCellWithReuseIdentifier: menuCellId)
collectionView?.alwaysBounceVertical = true
if self.revealViewController() != nil {
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "menu2-black-32").withRenderingMode(.alwaysOriginal), style: .plain, target: self.revealViewController(), action: #selector(SWRevealViewController.revealToggle(_:)))
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
print("Restaurant Name:", restaurant?.name) // returns nil
if let restaurantName = restaurant?.name {
self.navigationItem.title = restaurantName
}
loadMenuItems()
}
func loadMenuItems() {
print("Restaurant Id:", restaurant?.id) // returns nil
if let restaurantId = restaurant?.id {
print("RestaurantId:", restaurantId)
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: menuCellId, for: indexPath) as! MenuCell
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let layout = UICollectionViewFlowLayout()
let controller = MenuDetailsController(collectionViewLayout: layout)
navigationController?.pushViewController(controller, animated: true)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 120)
}
}
Inside of my MenuCell:
import UIKit
class MenuCell: BaseCell {
let restaurantLabel: UILabel = {
let label = UILabel()
label.text = "Restaurant King"
label.font = UIFont.boldSystemFont(ofSize: 16)
label.textColor = .black
label.numberOfLines = 0
return label
}()
let mealImageView: UIImageView = {
let iv = UIImageView()
iv.image = #imageLiteral(resourceName: "button_chicken").withRenderingMode(.alwaysOriginal)
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
let mealDetailsLabel: UILabel = {
let label = UILabel()
label.text = "Grass fed grass, American cheese, and friez"
label.font = UIFont.boldSystemFont(ofSize: 12)
label.textColor = UIColor.qpizzaBlack()
label.numberOfLines = 0
return label
}()
let mealPriceLabel: UILabel = {
let label = UILabel()
label.text = "$12.00"
label.font = UIFont.boldSystemFont(ofSize: 12)
label.textColor = UIColor.qpizzaBlack()
return label
}()
let sepereatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.lightGray
return view
}()
override func setupViews() {
super.setupViews()
backgroundColor = UIColor.qpizzaWhite()
addSubview(restaurantLabel)
addSubview(mealImageView)
addSubview(mealDetailsLabel)
addSubview(mealPriceLabel)
addSubview(sepereatorView)
_ = mealImageView.anchor(topAnchor, left: nil, bottom: nil, right: rightAnchor, topConstant: 14, leftConstant: 0, bottomConstant: 0, rightConstant: 12, widthConstant: 60, heightConstant: 60)
_ = restaurantLabel.anchor(topAnchor, left: leftAnchor, bottom: nil, right: mealImageView.leftAnchor, topConstant: 14, leftConstant: 12, bottomConstant: 0, rightConstant: 10, widthConstant: 0, heightConstant: 20)
_ = mealDetailsLabel.anchor(restaurantLabel.bottomAnchor, left: leftAnchor, bottom: nil, right: mealImageView.leftAnchor, topConstant: 12, leftConstant: 12, bottomConstant: 0, rightConstant: 10, widthConstant: 0, heightConstant: 30)
_ = mealPriceLabel.anchor(mealDetailsLabel.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 10, leftConstant: 12, bottomConstant: 10, rightConstant: 10, widthConstant: 0, heightConstant: 20)
_ = sepereatorView.anchor(nil, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, topConstant: 0, leftConstant: 20, bottomConstant: 4, rightConstant: 20, widthConstant: 0, heightConstant: 1)
}
}
Just a quick look, declaring a variable of the correct type is the start. But you actually have to do an assignment (=) to move data or the class reference from once class to the next.
func didTapRestaurantCell(cell: RestaurantCell) {
print("Did Tap Restaurant Cell - Restaurant Controller")
let layout = UICollectionViewFlowLayout()
let controller = MenuController(collectionViewLayout: layout)
navigationController?.pushViewController(controller, animated: true)
// you need to set the restaurant attribute of your new
// controller
let indexPath = indexPath(for: cell)
controller.restaurant = self.restaurants[indexPath.item]
}
Thank you to everyone who commented and assisted me in figuring this out. I was able to solve my issue by changing my protocol and passing the controller as a parameter.
In RestaurantCell:
protocol RestaurantDelegate {
func didTapRestaurantCell(cell: RestaurantCell, withMenuController: MenuController)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Did Select Item - Restaurant Cell")
let layout = UICollectionViewFlowLayout()
let controller = MenuController(collectionViewLayout: layout)
controller.restaurant = self.restaurants[indexPath.item]
delegate?.didTapRestaurantCell(cell: self, withMenuController: controller)
}
In RestaurantController:
func didTapRestaurantCell(cell: RestaurantCell, withMenuController controller: MenuController) {
print("Did Tap Restaurant Cell - Restaurant Controller")
navigationController?.pushViewController(controller, animated: true)
}

Resources