So, I have a CollectionView inside a TableViewCell, and I want to pass the cell view of that embed Collection view to an ViewControllerAnimatedTransioning when someone click on the cell. My Problem is, when you click the cell I save the indexPath inside a var so when I execute the delegate in the transition I can retrieve that cell, the thing is that the delegate works fine but it's returning nil. I have several hours trying to figure out, and I don't really know whats happening. I don't know if it's a Problem with the instances but It isn't working. Here I left you my Code. The cell I'm trying to pass is in ForYouCell.
HomeCellTransition
import UIKit
protocol HomeCellTransitionDelegate {
func transition(for: HomeCellTransition) -> UIView!
}
class HomeCellTransition: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case presenting
case dismissing
case none
}
enum TransitionState {
case initial
case final
}
let duration: TimeInterval = 0.8
var type: TransitionType = .none
var topSnapshot: UIView!
var cellSnapshot: UIView!
var bottomSnapshot: UIView!
var secondViewTopSnapshot: UIView!
var secondViewBottomSnapshot: UIView!
var delegate: HomeCellTransitionDelegate = ForYouCell()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to), let fromVC = transitionContext.viewController(forKey: .from) else {
return
}
let containerView = transitionContext.containerView
let presentingViewController = type == .presenting ? fromVC : toVC
let dismissingViewController = type == .presenting ? toVC : fromVC
let cellView = delegate.transition(for: self)
let targetFrame = cellView?.convert((cellView?.frame)!, to: cellView?.superview)
let percentCutImage: CGFloat = 0.10
let secondImageView = (dismissingViewController as! DetailController).topImage
snapshotViews(presentingVC: presentingViewController, dismissingVC: dismissingViewController, cellView: cellView!, targetFrame: targetFrame!, percentCutImage: percentCutImage, secondImageView: secondImageView)
containerView.addSubview(toVC.view)
containerView.addSubview(topSnapshot)
containerView.addSubview(cellSnapshot)
containerView.addSubview(bottomSnapshot)
containerView.addSubview(secondViewTopSnapshot)
containerView.addSubview(secondViewBottomSnapshot)
toVC.view.isHidden = true
topSnapshot.frame = CGRect.init(x: 0, y: 0, width: (presentingViewController.view.frame.width), height: (targetFrame?.minY)!)
cellSnapshot.frame = CGRect.init(x: 0, y: 0, width: (cellView?.frame.width)!, height: (cellView?.frame.height)! - ((cellView?.frame.height)! * percentCutImage))
bottomSnapshot.frame = CGRect.init(x: 0, y: targetFrame!.maxY, width: (presentingViewController.view.frame.width), height: (presentingViewController.view.frame.height) - (targetFrame?.maxY)!)
UIView.animate(withDuration: duration, animations: {
self.topSnapshot.frame.size.height = 0
self.bottomSnapshot.frame.origin.y = 0
self.cellSnapshot.frame = secondImageView.frame
}) { (completed) in
print("completed")
}
}
func snapshotViews(presentingVC: UIViewController, dismissingVC: UIViewController, cellView: UIView, targetFrame: CGRect, percentCutImage: CGFloat, secondImageView: UIImageView) {
let presentingView = presentingVC.view
let dismissingView = dismissingVC.view
let percentCutImage: CGFloat = percentCutImage
let secondImageView = secondImageView
let targetFrame = targetFrame
topSnapshot = presentingView?.resizableSnapshotView(from: CGRect.init(x: 0, y: 0, width: (presentingView?.frame.width)!, height: targetFrame.minY), afterScreenUpdates: false, withCapInsets: .zero)
cellSnapshot = cellView.resizableSnapshotView(from: CGRect.init(x: 0, y: 0, width: cellView.frame.width, height: cellView.frame.height - (cellView.frame.height * percentCutImage)), afterScreenUpdates: false, withCapInsets: .zero)
bottomSnapshot = presentingView?.resizableSnapshotView(from: CGRect.init(x: 0, y: targetFrame.maxY, width: (presentingView?.frame.width)!, height: (presentingView?.frame.height)! - targetFrame.maxY), afterScreenUpdates: false, withCapInsets: .zero)
secondViewTopSnapshot = dismissingView?.resizableSnapshotView(from: CGRect.init(x: 0, y: 0, width: (dismissingView?.frame.width)!, height: secondImageView.frame.height - (secondImageView.frame.height * percentCutImage)), afterScreenUpdates: true, withCapInsets: .zero)
secondViewBottomSnapshot = dismissingView?.resizableSnapshotView(from: CGRect.init(x: 0, y: secondImageView.frame.maxY - (secondImageView.frame.height * percentCutImage), width: (dismissingView?.frame.width)!, height: (dismissingView?.frame.height)! - (secondImageView.frame.height - (secondImageView.frame.height * percentCutImage))), afterScreenUpdates: true, withCapInsets: .zero)
}
}
extension HomeCellTransition: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
type = .presenting
return self
}
}
ForYouCell
//
// ForYouCell.swift
// AirbnbNav
//
// Created by Leonardo Dominguez on 9/24/17.
// Copyright © 2017 Leonardo Dominguez. All rights reserved.
//
import UIKit
protocol HeaderControllerPresentDelegate {
func didSelectCell()
}
class ForYouCell: UITableViewCell {
let cellIdentifier = "forYouCell"
var delegate: HeaderControllerPresentDelegate?
let itemsInsets: CGFloat = 15
var selectedCell: IndexPath?
let sectionLabel: UILabel = {
let lbl = UILabel()
lbl.text = "Section"
lbl.font = UIFont.boldSystemFont(ofSize: 14)
return lbl
}()
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
return cv
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
collectionView.register(ItemCell.self, forCellWithReuseIdentifier: cellIdentifier)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = UIEdgeInsets(top: 0, left: itemsInsets, bottom: 0, right: itemsInsets)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
addSubview(sectionLabel)
addSubview(collectionView)
_ = sectionLabel.anchor(top: topAnchor, bottom: nil, right: rightAnchor, left: leftAnchor, topConstant: 0, bottomConstant: 0, rightConstant: 0, leftConstant: itemsInsets, widthConstant: 0, heightConstant: 30)
_ = collectionView.anchor(top: sectionLabel.bottomAnchor, bottom: bottomAnchor, right: rightAnchor, left: leftAnchor, topConstant: 0, bottomConstant: 0, rightConstant: 0, leftConstant: 0, widthConstant: 0, heightConstant: 0)
}
}
extension ForYouCell: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? ItemCell {
cell.backgroundColor = .red
return cell
}
return UICollectionViewCell()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = indexPath
delegate?.didSelectCell()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.height + 50, height: collectionView.frame.size.height - 20)
}
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 15
}
}
extension ForYouCell: HomeCellTransitionDelegate {
func transition(for: HomeCellTransition) -> UIView! {
let celll = collectionView.cellForItem(at: selectedCell!)
print(celll)
print("k")
return celll
}
}
HeaderController
//
// HeaderController.swift
// AirbnbNav
//
// Created by Leonardo Dominguez on 9/19/17.
// Copyright © 2017 Leonardo Dominguez. All rights reserved.
//
import UIKit
enum HeaderSizes {
case min
case med
case max
}
protocol HeaderControllerDelegate {
func didCollapse()
func didExpand()
}
class HeaderController: UIViewController {
// Status bar
var isHiddenStatusBar: Bool = false {
didSet {
UIView.animate(withDuration: 0.3) {
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return isHiddenStatusBar
}
override var preferredStatusBarStyle: UIStatusBarStyle {
if currentHeaderSize == .min {
return .default
}
return .lightContent
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .fade
}
// Properties
let tableView: UITableView = {
let tv = UITableView(frame: .zero)
tv.separatorStyle = .none
return tv
}()
let maxHeight: CGFloat = 300
let medHeight: CGFloat = 135 // 55 del height + 10 padding superior + 10 de padding inferior + 10 statusbar
let minHeight: CGFloat = 65 // 55 height del menu + 10 padding
var previousScroll: CGFloat = 0
var currentHeaderSize: HeaderSizes = .max
var currentHeaderHeight: NSLayoutConstraint?
fileprivate let cellIdentifier = "cellIdentifier"
let detailController = DetailController()
var selectedCell: UITableViewCell?
lazy var headerView: HeaderView = {
let hv = HeaderView(maxHeight: self.maxHeight, medHeight: self.medHeight, minHeight: self.minHeight, paddingBetween: 10)
return hv
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
tableView.delegate = self
tableView.dataSource = self
tableView.register(ForYouCell.self, forCellReuseIdentifier: cellIdentifier)
headerView.headerControllerDelegate = self
}
func setupViews() {
view.addSubview(headerView)
view.addSubview(tableView)
currentHeaderHeight = headerView.anchor(top: view.topAnchor, bottom: nil, right: view.rightAnchor, left: view.leftAnchor, topConstant: 0, bottomConstant: 0, rightConstant: 0, leftConstant: 0, widthConstant: 0, heightConstant: maxHeight)[3]
_ = tableView.anchor(top: headerView.bottomAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, left: view.leftAnchor)
}
}
extension HeaderController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? ForYouCell {
cell.delegate = self
return cell
}
return UITableViewCell()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 250
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let absoluteTop: CGFloat = 0
let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.height
let scrollRange: CGFloat = scrollView.contentOffset.y - previousScroll
let isScrollingDown = scrollView.contentOffset.y > previousScroll && scrollView.contentOffset.y > absoluteTop
let isScrollingUp = scrollView.contentOffset.y < previousScroll && scrollView.contentOffset.y < absoluteBottom
var newHeight: CGFloat = currentHeaderHeight!.constant
if isScrollingDown {
newHeight = max(minHeight, ((currentHeaderHeight?.constant)! - abs(scrollRange)))
} else if isScrollingUp {
newHeight = min(maxHeight, ((currentHeaderHeight?.constant)! + abs(scrollRange)))
}
if newHeight != currentHeaderHeight?.constant {
currentHeaderHeight?.constant = newHeight
tableView.contentOffset = CGPoint(x: tableView.contentOffset.x, y: previousScroll)
let minMedAverage: CGFloat = (minHeight + medHeight) / 2
let medMaxAverage: CGFloat = (medHeight + maxHeight) / 2
if currentHeaderHeight!.constant < minMedAverage {
currentHeaderSize = .min
} else if currentHeaderHeight!.constant >= minMedAverage && currentHeaderHeight!.constant < medMaxAverage {
currentHeaderSize = .med
} else if currentHeaderHeight!.constant >= medMaxAverage {
currentHeaderSize = .max
}
updateHeader()
}
previousScroll = scrollView.contentOffset.y
}
func snapHeader(toSize: HeaderSizes) {
switch toSize {
case .max:
currentHeaderHeight?.constant = maxHeight
case .med:
currentHeaderHeight?.constant = medHeight
case .min:
currentHeaderHeight?.constant = minHeight
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
snapHeader(toSize: currentHeaderSize)
updateHeader()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
snapHeader(toSize: currentHeaderSize)
updateHeader()
}
}
func updateHeader() {
let range = maxHeight - medHeight
let percent = (currentHeaderHeight!.constant - medHeight) / range
headerView.updateHeader(percentage: percent, currentHeaderHeight: currentHeaderHeight!.constant)
// Status bar
isHiddenStatusBar = currentHeaderHeight!.constant < (medHeight / 2) && currentHeaderHeight!.constant > minHeight ? true : false
}
}
extension HeaderController: HeaderControllerPresentDelegate {
func didSelectCell() {
present(detailController, animated: true, completion: nil)
}
}
extension HeaderController: HeaderControllerDelegate {
func didExpand() {
print("")
}
func didCollapse() {
snapHeader(toSize: .min)
}
}
Related
I have three custom views side by side in one line. If the content of the last view does not fit (as can be seen in the picture), move that view to a new line. How?
These is constraints:
firstCustomView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
secondCustomView.leadingAnchor.constraint(equalTo: firstCustomView.trailingAnchor, constant: 15),
thirdsCustomView.leadingAnchor.constraint(equalTo: secondCustomView.trailingAnchor, constant: 15),
thirdsCustomView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -15),
I did it in this way, first create custom collection view class:
class DynamicHeightCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return contentSize
}
}
Second, create custom layout:
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
}
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
collectionView != nil else {
return nil
}
if scrollDirection == .vertical {
if indexPath.item != 0,
let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) {
currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
} else {
currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
}
} else {
if indexPath.item != 0,
let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
currentItemAttributes.frame.intersects(CGRect(x: previousFrame.origin.x, y: -.infinity, width: previousFrame.size.width, height: .infinity)) {
currentItemAttributes.frame.origin.y = previousFrame.origin.y + previousFrame.size.height + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
} else {
currentItemAttributes.frame.origin.y = evaluatedSectionInsetForSection(at: indexPath.section).top
}
}
return currentItemAttributes
}
func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
}
func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
}
}
And at initialization:
#IBOutlet weak var collectionView: UICollectionView! {
didSet {
collectionView.dataSource = self
collectionView.delegate = self
collectionView.isScrollEnabled = false
collectionView.register(UINib(nibName: "BubbleCell", bundle: nil), forCellWithReuseIdentifier: "BubbleCell")
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = data[indexPath.row].name.size(withAttributes:[.font: UIFont(name: "Lato-Regular", size: 16)!])
let width = min(size.width + 52, collectionView.frame.width - collectionView.contentInset.left - collectionView.contentInset.right)
return CGSize(width: width, height: 34)
}
I want to create an app that shows an array of photos with captions (for now I have empty circles) that are arranged in horizontal uicollectionview. I want to make one larger circle to be displayed in the center of the screen, but the remaining circles in the line to be smaller and shaded. I did it by creating my own layout for uicollectionview. But I have one problem that I don't know how to fix it. I want the transition between circles to be smoothly animated. Here is an example of what I've done:
I want to achieve something like this:gif
I know that there are some library and frameworks which do that, but I'd like to do by my own.
Here is my code:
class FlowViewController: UIViewController {
let cellWidth : CGFloat = 275
let cellHeight : CGFloat = 300
let cellSpacing : CGFloat = 10
private let circleCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
private let layout = myCarouselFlowLayout()
private let collectionViewCellIdentifier = "PositionCollectionViewCell"
private var circles : [myCircle] = {
let pok1 = myCircle(name: "Bl4")
let pok2 = myCircle(name: "Bl3")
let pok3 = myCircle(name: "Bli")
let pok4 = myCircle(name: "Bl0")
let pok5 = myCircle(name: "Lal")
let pok6 = myCircle(name: "Te")
let pok7 = myCircle(name: "wTW")
let pok8 = myCircle(name: "H3RHG")
return [pok1, pok2, pok3, pok4, pok5, pok6, pok7, pok8]
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = #colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1)
view.addSubview(circleCollectionView)
configureCollectionView()
print(circles.count)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
configureCollectionViewLayoutItemSize()
}
func configureCollectionView(){
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: cellWidth, height: cellHeight)
layout.minimumLineSpacing = cellSpacing
//set layout
circleCollectionView.setCollectionViewLayout(layout, animated: true)
//set delegates
setTableViewDelegates()
//register cells
circleCollectionView.register(CircleCollectionViewCell.self, forCellWithReuseIdentifier: collectionViewCellIdentifier)
//set contraits
circleCollectionView.translatesAutoresizingMaskIntoConstraints = false
circleCollectionView.heightAnchor.constraint(equalToConstant: cellHeight).isActive = true
circleCollectionView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
circleCollectionView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
circleCollectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circleCollectionView.backgroundColor = .clear
circleCollectionView.decelerationRate = .fast
}
func calculateSectionInset() -> CGFloat { // should be overridden
let viewWith = UIScreen.main.bounds.size.width
return ( viewWith - cellWidth ) / 2
}
private func configureCollectionViewLayoutItemSize() {
let inset: CGFloat = calculateSectionInset()
layout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
layout.itemSize = CGSize(width: layout.collectionView!.frame.size.width - inset * 2,
height: layout.collectionView!.frame.size.height)
}
}
extension FlowViewController: UICollectionViewDelegate,UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
circles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionViewCellIdentifier, for: indexPath) as? CircleCollectionViewCell else {
fatalError("Bad instance of FavoritesCollectionViewCell")
}
cell.nameLabel.text = circles[indexPath.row].name
return cell
}
func setTableViewDelegates(){
circleCollectionView.delegate = self
circleCollectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(circles.count)
}
}
extension FlowViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = cellWidth
let height = cellHeight
return CGSize(width: width, height: height)
}
}
class myCarouselFlowLayout: UICollectionViewFlowLayout,UICollectionViewDelegateFlowLayout {
private let fadeFactor: CGFloat = 0.5
override var collectionViewContentSize: CGSize {
super.collectionViewContentSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
super.layoutAttributesForElements(in: rect)?.map {
self.layoutAttributesForItem(at: $0.indexPath)!
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let superValue = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else { return nil }
guard let cv = collectionView else { return nil }
let collectionMidPoint = CGPoint(x: cv.bounds.midX, y: cv.bounds.midY)
let itemMidPoint = superValue.center
let distance = abs(itemMidPoint.x - collectionMidPoint.x)
if distance > 100 {
UIView.animate(withDuration: 0.2) {
superValue.alpha = self.fadeFactor
superValue.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
cv.layoutIfNeeded()
}
}
return superValue
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
true
}
var velocityThresholdPerPage: CGFloat = 2
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return proposedContentOffset }
let pageLength: CGFloat
let approxPage: CGFloat
let currentPage: CGFloat
pageLength = (self.itemSize.width + self.minimumLineSpacing)
approxPage = collectionView.contentOffset.x / pageLength
currentPage = round(approxPage)
if velocity.x == 0 {
return CGPoint(x: currentPage * pageLength, y: 0)
}
var nextPage: CGFloat = currentPage + (velocity.x > 0 ? 1 : -1)
let increment = velocity.x / velocityThresholdPerPage
nextPage += round(increment)
return CGPoint(x: nextPage * pageLength, y: 0)
}
Please give me some advice on where and how should I use animations on it? Should I do it with UIView.animate or something else? I know that I need to use layoutIfNeeded() to animate constraints, but it doesn't work with it.
I am animating a UIView from another class however I'm getting different results from each and was hoping someone could explain why. When I trigger EditSongLauncher from handleEdit1, the 4 collectionview cells are not showing. However when I trigger EditSongLauncher from handleEdit2, I get all 4 cells showing.
I don't understand why handleEdit1 doesn't work as it is essentially doing the same thing when it calls setupViews()... I would prefer to use handleEdit1 as it allows for cleaner code so was hoping someone could explain why or tell me why the 4 cells arn't appearing for this function.
#objc func handleEdit1() {
let editLauncher = EditSongLauncher()
editLauncher.setupViews()
}
#objc func handleEdit2() {
if let window = UIApplication.shared.keyWindow {
let editLauncher = EditSongLauncher.init(frame: CGRect(x: 0, y: 0, width: collectionView.frame.width, height: collectionView.frame.height))
window.addSubview(editLauncher)
}
}
class EditSongLauncher: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
let cellId = "cellId"
let dimmedView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(white: 0, alpha: 0.5)
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSave(sender:))))
view.isUserInteractionEnabled = true
return view
}()
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
return cv
}()
func setupViews() {
if let window = UIApplication.shared.keyWindow {
let height: CGFloat = 400
let y = window.frame.height - height
collectionView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
dimmedView.frame = window.frame
dimmedView.alpha = 0
window.addSubview(dimmedView)
window.addSubview(collectionView)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.dimmedView.alpha = 1
self.collectionView.frame = CGRect(x: 0, y: y, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}, completion: nil)
}
}
#objc func handleSave(sender: UITapGestureRecognizer) {
print("Handling save")
UIView.animate(withDuration: 0.5) {
self.dimmedView.alpha = 0
}
}
override init(frame: CGRect) {
super.init(frame: frame)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(EditSongCell.self, forCellWithReuseIdentifier: cellId)
setupViews()
}
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)
cell.backgroundColor = .blue
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width, height: 50)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
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)
}
I am trying to do a custom Flow Layout similar to the Apple News app. The flowLayout.delegate = self is in the viewDidLoad() method while my networking code is in in an async method in the viewDidAppear().
The problem is that the methods for the custom flow Layout get called before I can retrieve all the data from the server, therefore the app crashes.
Any ideas on how I could make it work? Here's my ViewController implementation:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var newsArray = [News]()
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://url"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Filling the cells with the correct info...
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
return getFromDb.news[indexPath.row].cellType
}
}
Here below the struct for News:
struct News {
let image: String
let provider: String
let title: String
let cellType: NewsCellType
init(image: String, provider: String, title: String, cellType: NewsCellType) {
self.image = image
self.provider = provider
self.title = title
self.cellType = cellType
}}
The Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let screenWidth = UIScreen.main.bounds.width
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
return attributes
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if attributesArray == nil {
attributesArray = [UICollectionViewLayoutAttributes]()
print(collectionView!.numberOfItems(inSection: 0) - 1)
for i in 0 ... collectionView!.numberOfItems(inSection: 0) - 1
{
let attributes = self.layoutAttributesForItem(at: IndexPath(item: i, section: 0))
attributesArray!.append(attributes!)
}
}
return attributesArray
}
}
Two things which you need to do when we deal with custom collection view flow layouts.
changes in prepare() method in custom flow layout. you will start preparing the layout only if the number of items more than 0.
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{ }
}
use numberOfItems property whenever you wanted to do something with collectionView items in the customFlowLayout to avoid crashes.
Here below the functioning clean code if you are looking to implement a Custom Collection View Flow Layout similar to the Apple News app with networking:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://yourURL"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
print("RELOAD")
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let news = getFromDb.news[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: news.cellType.rawValue, for: indexPath) as! NewCell
cell.image.image = UIImage(named: "news")!
cell.provider.text = news.provider
cell.title.text = news.title
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
print(getFromDb.news[indexPath.row].cellType)
return getFromDb.news[indexPath.row].cellType
}
}
This is the Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var cache = [UICollectionViewLayoutAttributes]()
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let screenWidth = UIScreen.main.bounds.width
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
cache.append(attributes)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attribute in cache{
if attribute.frame.intersects(rect){
layoutAttributes.append(attribute)
}
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
}