1

I want to place a collectionView programmatically between a navigationBar, a label and the tabBar on the bottom.

I do not want to have any spacing between the different parts, how do I correctly ad the views to the view? Whats the best way to do it?

enter image description here

import UIKit

private let reUseIdentifierCollectionView = "CollectionViewCell1"

class CVControllerViewController: UIViewController {


    var heightNavigationBarTop: CGFloat{
        get{
            let barHeight = self.navigationController?.navigationBar.frame.height ?? 0
            let statusBarHeight = UIApplication.shared.isStatusBarHidden ? CGFloat(0) : UIApplication.shared.statusBarFrame.height
            return barHeight + statusBarHeight
        }
    }
    //Here I am getting the SafeArea above the NavigationBar
    var topSafeArea: CGFloat{
        get{
            var topSafeArea: CGFloat
            if #available(iOS 11.0, *) {
                topSafeArea = view.safeAreaInsets.top
            } else {
                topSafeArea = topLayoutGuide.length

            }
            return topSafeArea
        }
    }

    var tabBarHeight: CGFloat{ get{ return (self.tabBarController?.tabBar.frame.height)! } }

    lazy var label: UILabel = {
        let lb = UILabel()
        lb.frame = CGRect(x: 0, y: heightNavigationBarTop + topSafeArea, width: view.frame.width, height: 50)
        lb.backgroundColor = .red
        return lb
    }()

    var collectionView: UICollectionView!
    var content = [Int: Any]()

    override func viewDidLoad() {
        super.viewDidLoad()
        loadContent()
        loadCollectionView()
        view.addSubview(label)
        view.addSubview(collectionView)
    }


    func loadCollectionView() {
        let layout = UICollectionViewFlowLayout()
        let frame = CGRect(x: 0, y: heightNavigationBarTop + label.frame.height + topSafeArea, width: self.view.frame.height, height: self.view.frame.height - (heightNavigationBarTop + label.frame.height + tabBarHeight + topSafeArea))
        collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.register(CoinCVCCell.self, forCellWithReuseIdentifier: reUseIdentifierCollectionView)
        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.scrollDirection = .horizontal
            flowLayout.minimumLineSpacing = 0
        }
        collectionView.delegate   = self
        collectionView.dataSource = self
    }

    func loadContent() {
        content[0] = Content(name: "Blau", color: .blue)
        content[1] = Content(name: "Grün", color: .green)
        content[2] = Content(name: "Gelb", color: .yellow)
        content[3] = Content(name: "brown", color: .brown)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

}

extension CVControllerViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 4
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let outputCell: UICollectionViewCell
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseIdentifierCollectionView, for: indexPath) as! CoinCVCCell
        cell.content = (content[indexPath.item] as! Content)
        outputCell = cell
        return outputCell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = view.frame.width
        let height = view.frame.height - (heightNavigationBarTop + label.frame.height + topSafeArea + tabBarHeight)

        let output = Utility.shared.CGSizeMake(width, height)
        return output
    }
}


class BaseCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super .init(frame: frame)
        setUpViews()
    }
    func setUpViews() {

    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been impemented")
    }
}

class CoinCVCCell: BaseCell {
    var content: Content? {
        didSet{
            backgroundColor = content?.color
        }
    }
}

class Content {
    var name: String?
    var color: UIColor?
    init(name: String, color: UIColor) {
        self.name = name
        self.color = color
    }
}

final class Utility: NSObject {
    private override init() { }
    static let shared = Utility()

    func CGRectMake(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat) -> CGRect {
        return CGRect(x: x, y: y, width: width, height: height)
    }

    func CGSizeMake( _ width:CGFloat, _ height:CGFloat) -> CGSize{
        return CGSize(width: width, height: height)
    }
}

I do not understand why the collectionView does not fit in to the space, and why the margins occur, because I subtract the height of the different elements on the view from the total height of the view.

EDIT:

private let reUseIdentifierCollectionView = "CollectionViewCell1"

class CVControllerViewController: UIViewController {


    var heightNavigationBarTop: CGFloat{
        get{
            let barHeight = self.navigationController?.navigationBar.frame.height ?? 0
            let statusBarHeight = UIApplication.shared.isStatusBarHidden ? CGFloat(0) : UIApplication.shared.statusBarFrame.height
            return barHeight + statusBarHeight
        }
    }
    //Here I am getting the SafeArea above the NavigationBar
    var topSafeArea: CGFloat{
        get{
            var topSafeArea: CGFloat
            if #available(iOS 11.0, *) {
                topSafeArea = view.safeAreaInsets.top
            } else {
                topSafeArea = topLayoutGuide.length
            }
            return topSafeArea
        }
    }

    var tabBarHeight: CGFloat{ get{ return (self.tabBarController?.tabBar.frame.height)! } }

    lazy var label: UILabel = {
        let lb = UILabel()
        //lb.frame = CGRect(x: 0, y: heightNavigationBarTop + topSafeArea, width: view.frame.width, height: 50)
        lb.backgroundColor = .red
        return lb
    }()

    var collectionView: UICollectionView!
    var content = [Int: Any]()

    override func viewDidLoad() {
        super.viewDidLoad()
        loadContent()
        loadCollectionView()
        setLayout()
//        view.addSubview(label)
//        view.addSubview(collectionView)
    }

    func setLayout(){
        view.sv(label, collectionView)
        view.layout(
            heightNavigationBarTop,
            |-label-| ~ 80,
            0,
            |collectionView|,
            tabBarHeight
        )
    }

    func loadCollectionView() {
        let layout = UICollectionViewFlowLayout()
        let frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
        collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.register(CoinCVCCell.self, forCellWithReuseIdentifier: reUseIdentifierCollectionView)
        collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.scrollDirection = .horizontal
            flowLayout.minimumLineSpacing = 0
        }
        collectionView.delegate   = self
        collectionView.dataSource = self
    }


    func loadContent() {
        content[0] = Content(name: "Blau", color: .blue)
        content[1] = Content(name: "Grün", color: .green)
        content[2] = Content(name: "Gelb", color: .yellow)
        content[3] = Content(name: "brown", color: .brown)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

}

extension CVControllerViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 4
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let outputCell: UICollectionViewCell
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseIdentifierCollectionView, for: indexPath) as! CoinCVCCell
        cell.content = (content[indexPath.item] as! Content)
        outputCell = cell
        return outputCell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = view.frame.width
        let height = view.frame.height - (tabBarHeight + label.frame.height + heightNavigationBarTop)

        let output = Utility.shared.CGSizeMake(width, height)
        return output
    }
}

I am using the framework Stevia, basically what sv does is setting an subview and setting "translatesAutoresizingMaskIntoConstraints" to false. With layout I am setting the constraints, the solution now does what I want but is that the way you recommended?

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) ->

Here I still use the frame, does that make sense?

PaFi
  • 888
  • 1
  • 9
  • 24

2 Answers2

3

because I subtract the height of the different elements on the view from the total height of the view

But you don’t know the total height of the view. That’s the problem.

You are calling loadCollectionView in viewDidLoad. That is way too early, because nothing has its correct frame yet. Therefore all your calculations to calculate the frame of the collection view, based on the frame of other things, are incorrect.

You could solve this by waiting until viewDidLayoutSubviews to call loadCollectionView, but don't. You shouldn't be calculating frames at all! Instead, do everything with autolayout and let the autolayout engine do the layout for you. That way, you can do the construction in viewDidLoad without any frame values coming into play.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • great thanks, but as I understood for initialising the collectionview I have to set a frame? Do I set the frame to a random size and then do the layout with autolayout afterwards? – PaFi Apr 10 '18 at 06:36
  • Sure, if “afterwards” means “as soon as I add the collection view to the view hierarchy”. – matt Apr 10 '18 at 12:41
  • So I updated the code, is that what you meant by your solution? I think I now understand more about how to set up the view, thanks! – PaFi Apr 12 '18 at 14:27
  • Yes the collectionView now perfectly fits between the label and the tabBar. So I guess its solved :) – PaFi Apr 12 '18 at 14:50
  • 1
    As long as the collection view is going where you want it, _and_ when you run the app and pause in the View Debugger you are not getting any warnings about autolayout issues, then you've done it right. – matt Apr 12 '18 at 15:04
  • Generally I agree with this (sane) approach, but how can I use autolayout **within** the collection view? E.g. so that in a two-row flowlayout all items' height is scaled to fill vertical available space within the collection view? Because inner items are not available at design time in I.B. – superjos Feb 12 '20 at 16:40
  • @superjos With the iOS 13 compositional layout, that's pretty much trivial. Otherwise you will just have to know the "available space" (after layout) and set the cell sizes accordingly. – matt Feb 12 '20 at 16:43
  • thanks for quick reply. We're not yet on compositional layout. So within `sizeForItemAt` we should use whatever `collectionView.frame.height` we get (the one from IB, possibly wrong), and then when layout is known we should change cell sizes? How? Forcing a re-layout? – superjos Feb 12 '20 at 16:48
  • @superjos Well what I would do is return 0 for the number of items until I have the needed layout info, then I would call `reloadItems` to get the visible collection view to rebuild itself. – matt Feb 12 '20 at 16:55
1

Programmatically : set sectionInset and invalidate layout

https://stackoverflow.com/a/35975792/6948184

OR

set the section insets to 0 for Bottom and Top in the size inspector of your colection view

ColectionView Section Inset

Quentin N
  • 641
  • 7
  • 5
  • Thanks for the answer. I added this code: collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0) but it does not change anything. – PaFi Apr 09 '18 at 22:14
  • have you add the collectionViewLayout?.invalidateLayout() – Quentin N Apr 09 '18 at 22:20