AppsDev
AppsDev

Reputation: 12499

Collection view within another collection view: section header or cell?

I already have a UICollectionView which scrolls vertically and shows a collection of custom UICollectionViewCells which have a fixed size.

Now I've been asked for showing another UICollectionView in top of all other cells, which should scroll horizontally and whose cells size is dynamic (I'll only know the size after at an async network call completion). In addition, this inner collection view may not be always necessary to be shown (it depends on the data received from the network call), but if it is, it should be shown only once (on top of everything).

My question is: how the best way to deal with this second and inner collection view should be? Should I add it to the outer view controller as a different kind of cell of it, or maybe as a section header? Maybe another approach to layout this would be better?

EDIT: More considerations:

Upvotes: 4

Views: 5995

Answers (1)

RLoniello
RLoniello

Reputation: 2329

"how the best way to deal with this second and inner collection view should be?"

"Should I add it to the outer view controller as a different kind of cell of it, or maybe as a section header?"

"I'd need to animate the inner collection view when I'm going to show it"

"The whole thing should be vertically scrollable, this inner collection view should not stick to the top of the screen."

It sometimes helps to take a step back and write out your requirements, think of each one independently:

1) The first cell of the CollectionView Should Scroll Horizontally.

2) The first cell should scroll past the screen vertically.

First cell of the CollectionView needs to contain a CollectionView itself.

3a) The CollectionView's other cells are of static size.

3b) The CollectionViews's first cells are of dynamic size.

Two Cell Classes are needed, or one cell class with dynamic constrains and subviews.

4) The CollectionView's First cells should be animated.

The first cells' CollectionView needs to be the delegate of its dynamic cells. (Animation occurs in cellForItemAt indexPath)

Keep in mind that UICollectionView's are independent views. A UICollectionViewController is essentially a UIViewController, UICollectionViewDelegate and UICollectionViewDataSource that contains a UICollectionView. Just like any UIView you can subclass UICollectionView and add it to a subview of another view, say UICollectionViewCell. In this way you can add a collection view to a cell and add cells to that nested collection view. You can also allow that nested collection view handle all the delegate methods from UICollectionViewDelegate and UICollectionViewDataSource essentially making it modular and reusable. You can pass the data to be displayed in each cell of the nested UICollectionView within a convenience init method and allow that class to handle animation and setup. This is by far the best way of doing it, not only for reuse but also for performance, especially when you are creating the views programmatically.

In the example below I have one UICollectionViewController named ViewController that will be the view controller for all other views.

I also have two CollectionViews, ParentCollectionView and HorizontalCollectionView. ParentCollectionView is an empty implementation of UICollectionView. I could use the collectionView of my UICollectionViewController but because I want this to be thoroughly modular I will later assign my ParentCollectionView to the ViewController's collectionView. ParentCollectionView will handle all the cells static cells in the view, including the one containing our HorizontalCollectionView. HorizontalCollectionView will be the delegate and data source for all 'cells objects' (your data model) passed to it within its convenience initializer. That is to say that HorizontalCollectionView will manage it own cells so that our UICollectionViewController doesn't get fat.

In addition to two CollectionViews and a UICollectionViewController, I have two UICollectionViewCell classes one of static sizing and the other dynamic (randomly generated CGSize). For ease of use I also have a extension that returns the classname as the identifier, I don't like using hard coded strings for reusable cells. These cell classes are not all that different, one could use the same cell and change the cell size in sizeForItemAt indexPath or cellForItemAt indexPath but for the sake of demonstration I'm going to say that they are completely different cells that require entirely different data models.

Now, we don't want the first cell in our ParentCollectionView to be dequeued, this is because the cell will be removed from memory and thrown back into the queue for reuse and we certainly don't want our HorizontalCollectionView popping up randomly. To avoid this we need to register both our StaticCollectionViewCell and a generic cell that will only ever be used once, since I added an extension that gives me the classname for the cell earlier I will just use UICollectionViewCell as the identifier.

I'm sure you won't have much trouble figuring out the rest, Here is my full implementation:

ViewController.swift

import UIKit

class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    // Programmically add our empty / custom ParentCollectionView
    let parentCollectionView: ParentCollectionView = {
        let layout = UICollectionViewFlowLayout()
        let cv = ParentCollectionView(frame: .zero, collectionViewLayout: layout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        return cv
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setup()

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func setup() {
        // Assign this viewcontroller's collection view to our own custom one.
        self.collectionView = parentCollectionView

        // Set delegate and register Static and empty cells for later use.
        parentCollectionView.delegate = self
        parentCollectionView.register(StaticCollectionViewCell.self, forCellWithReuseIdentifier: StaticCollectionViewCell.identifier)
        parentCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: UICollectionViewCell.identifier)

        // Add simple Contraints
        let guide = self.view.safeAreaLayoutGuide

        parentCollectionView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
        parentCollectionView.leftAnchor.constraint(equalTo: guide.leftAnchor).isActive = true
        parentCollectionView.rightAnchor.constraint(equalTo: guide.rightAnchor).isActive = true
        parentCollectionView.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
    }

    // MARK: - CollectionView

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // Erroneous Data from your network call, data should be a class property.
        let data = Array.init(repeating: "0", count: 12)

        // Skip if we dont have any data to show for the first row.
        if (indexPath.row == 0 && data.count > 0) {

            // Create a new empty cell for reuse, this cell will only be used for the frist cell.
            let cell = parentCollectionView.dequeueReusableCell(withReuseIdentifier: UICollectionViewCell.identifier, for: IndexPath(row: 0, section: 0))

            // Programmically Create a Horizontal Collection View add to the Cell
            let horizontalView:HorizontalCollectionView = {
                // Only Flow Layout has scroll direction
                let layout = UICollectionViewFlowLayout()
                layout.scrollDirection = .horizontal
                // Init with Data.
                let hr = HorizontalCollectionView(frame: cell.frame, collectionViewLayout: layout, data: data)
                return hr
            }()
            // Adjust cell's frame and add it as a subview.
            cell.addSubview(horizontalView)
            return cell
        }

        // In all other cases, just create a regular cell.
        let cell = parentCollectionView.dequeueReusableCell(withReuseIdentifier: StaticCollectionViewCell.identifier, for: indexPath)
        // Update Cell.

        return cell
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // 30 sounds like enough.
        return 30
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        //If you need your first row to be bigger return a larger size.
        if (indexPath.row == 0) {
            return StaticCollectionViewCell.size()
        }


        return StaticCollectionViewCell.size()
    }

}

ParentCollectionView.swift

import UIKit

class ParentCollectionView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
    }

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

HorizontalCollectionView.swift

import Foundation
import UIKit

class HorizontalCollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    // Your Data Model Objects
    var data:[Any]?

    // Required
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
    }

    convenience init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout, data:[Any]) {
        self.init(frame: frame, collectionViewLayout: layout)

        // Set These
        self.delegate = self
        self.dataSource = self
        self.data = data
        // Setup Subviews.
        setup()
    }

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

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // return zero if we have no data to show.
        guard let count = self.data?.count else {
            return 0
        }
        return count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = self.dequeueReusableCell(withReuseIdentifier: DynamicCollectionViewCell.identifier, for: indexPath)
        // Do Some fancy Animation when scrolling.
        let endingFrame = cell.frame
        let transitionalTranslation = self.panGestureRecognizer.translation(in: self.superview)
        if (transitionalTranslation.x > 0)  {
            cell.frame = CGRect(x: endingFrame.origin.x - 200, y: endingFrame.origin.y - 100, width: 0, height: 0)
        } else {
            cell.frame = CGRect(x: endingFrame.origin.x + 200, y: endingFrame.origin.y - 100, width: 0, height: 0)
        }

        UIView.animate(withDuration: 1.2) {
            cell.frame = endingFrame
        }

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // See DynamicCollectionViewCell size method, generate a random size.
        return DynamicCollectionViewCell.size()
    }

    func setup(){
        self.backgroundColor = UIColor.white
        self.register(DynamicCollectionViewCell.self, forCellWithReuseIdentifier: DynamicCollectionViewCell.identifier)
        // Must call reload, Data is not loaded unless explicitly told to.
        // Must run on Main thread this class is still initalizing.
        DispatchQueue.main.async {
            self.reloadData()
        }
    }

}

DynamicCollectionViewCell.swift

import Foundation
import UIKit

class DynamicCollectionViewCell: UICollectionViewCell {

    /// Get the Size of the Cell
    /// Will generate a random width element no less than 100 and no greater than 350
    /// - Returns: CGFloat
    class func size() -> CGSize {
        let width = 100 + Double(arc4random_uniform(250))
        return CGSize(width: width, height: 100.0)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

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

    func setup() {
        self.backgroundColor = UIColor.green
    }
}

StaticCollectionViewCell.swift

import Foundation
import UIKit

class StaticCollectionViewCell: UICollectionViewCell {

    /// Get the Size of the Cell
    /// - Returns: CGFloat
    class func size() -> CGSize {
        return CGSize(width: UIScreen.main.bounds.width, height: 150.0)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

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

    func setup() {
        self.backgroundColor = UIColor.red
    }


}

CollectionViewCellExtentions.swift

import UIKit

extension UICollectionViewCell {

    /// Get the string identifier for this class.
    ///
    /// - Returns: String
    class var identifier: String {
        return NSStringFromClass(self).components(separatedBy: ".").last!
    }

}

Nested Collection Views

Upvotes: 5

Related Questions