Reputation: 12499
I already have a UICollectionView
which scrolls vertically and shows a collection of custom UICollectionViewCell
s 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
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!
}
}
Upvotes: 5