In the App that I am currently working on, there is a nested collection view setup. The outer collection view contains large cells that are vertically scrollable, and each cell contains another collection view with smaller cells (the layout in the sub-collection-views differs, that's why there are multiple ones).
Visually, everything works fine, but I came across an issue with Voice Over: When I reach the last item in the first child-collection view (i.e., the last item of the first cell of the outer collection view), I can't select the next item by swiping. Instead, iOS makes the sound as if the last item had been reached.
I can three-finger swipe to scroll down, and then select the next element, but this should obviously not be necessary. Selecting the last element and going back up in reverse order works as intended.
The problem only seems to occur when only one cell (of the outer collection view) is visible initially. If multiple cells are visible, everything works. However, I cannot change the sizes of the outer cells, because that would completely change the layout.
I created a sample view controller below that demonstrates the issue. The two collection views in the project are the same, except for the sizes of the cells. The first collection view with the smaller cells works as expected. The second one does not (when swiping to select the next cell, iOS plays the "beep" sound when the last visible cell was selected, even though there are more cells further down).
So, my question is:
How can I make the second collection view behave like the first one?
How can I make the cells further down in the second collection view accessible through horizontal swiping?
Solutions I have attempted so far:
One SO post suggests creating a wrapper view for the nested collection views. This seems to make no difference.
I attempted to implement the UIAccessibilityContainer protocol myself in a custom subclass of the collection view. This seems to break scrolling in strange ways.
Placing "layout changed" notifications in random places to see if it helps (it does not)
Edit:
Full code of a ViewController that demonstrates the issue:
class ViewController: UIViewController, UICollectionViewDataSource {
let outerCollectionView = UICollectionView(frame: CGRect(x: 0, y: 40, width: 320, height: 250), collectionViewLayout: UICollectionViewFlowLayout())
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
outerCollectionView.backgroundColor = .blue
(outerCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).itemSize = CGSize(width: 250, height: 300)
outerCollectionView.register(OuterCell.self, forCellWithReuseIdentifier: "outerCell")
view.addSubview(outerCollectionView)
outerCollectionView.dataSource = self
}
func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 4 }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "outerCell", for: indexPath) as! OuterCell
cell.outerIndex = indexPath.row + 1
return cell
}
}
class OuterCell: UICollectionViewCell, UICollectionViewDataSource {
var innerCollectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 300, height: 500), collectionViewLayout: UICollectionViewFlowLayout())
var outerIndex: Int = 0
override init(frame: CGRect) {
super.init(frame: frame)
(innerCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).itemSize = CGSize(width: 140, height: 80)
innerCollectionView.backgroundColor = .yellow
innerCollectionView.register(InnerCell.self, forCellWithReuseIdentifier: "innerCell")
contentView.addSubview(innerCollectionView)
innerCollectionView.dataSource = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("unused")
}
func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "innerCell", for: indexPath) as! InnerCell
cell.label.text = "Cell \(outerIndex) / \(indexPath.item+1)"
return cell
}
}
class InnerCell: UICollectionViewCell {
let label = UILabel(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 30)))
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .white
contentView.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
fatalError("unused")
}
}
Edit: Here is a video that shows the issue: https://vimeo.com/229249955 (Note that I've added a little bit of description there).
From what i understood the problem is you are not able to scroll to the next cell in outer collection view once you have reached the last cell in inner collection view. But however a three finger swipe works.
This is because of the bounce property on collection view. Try turning off the bounce property on the inner collection view.
In the above code, add the following line of code in the OuterCell init method.
innerCollectionView.bounces = false
Related
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:
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
"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!
}
}
When paging is enabled a UICollectionView dequeues only 3 cells and the rest of the cells are dequeued as hidden for no reason. Perhaps, it is the way how UICollectionView works in general, but in my project I really need that every time a cell is dequeued cellForItemAt indexPath: IndexPath method is called it actually creates a non-hidden instance of a custom UICollectionViewCell.
Maybe, it only creates 3 instances because it needs to manage the memory correctly. However, in my project a custom UICollectionViewCell also contains a another collectionView which consists of 3 custom collectionView cells. These collectionView cells also have tableViews inside of them the data of which is encapsulated in these cells. My main questions are: why does a UICollectionView only creates 3 instances in my case and what can I do to avoid this behavior?
The hierarchy in my actual project looks like this: UICollectionView -> UICollectionView -> 3 custom UICollectionViewCell -> each UICollectionViewCell contains a tableView -> each tableView contains a specific custom TableViewCell.
Here is the whole code that I wrote in as an example of what’s going in my actual project (this is not the actual project, but the behavior is the same):
ViewController:
class ViewController: UIViewController {
let cellId = "uniqueCellId"
let sampleWords: [String] = ["one", "two", "three", "four", "five", "six"]
let colors: [UIColor] = [.green, .yellow, .blue, .purple, .gray, .red]
override func viewDidLoad() {
super.viewDidLoad()
registerCollectionViewCustomCell()
prepareUI()
setupViews()
setCollectionViewLayoutToHorizontal()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//collectionView is instantiated as a computed property. Initialized with a system flow layout. The frame is initially assigned to CGRect.zero because it is controlled by the constrains
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.layer.cornerRadius = 8
cv.isPagingEnabled = true
cv.delegate = self
cv.dataSource = self
return cv
}()
}
Extension 1
extension ViewController {
private func prepareUI() {
view.backgroundColor = UIColor.black
navigationController?.navigationBar.barTintColor = .white
navigationItem.title = "Collection View"
}
private func registerCollectionViewCustomCell() {
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
}
private func setCollectionViewLayoutToHorizontal() {
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .horizontal
}
}
private func setupViews() {
view.addSubview(collectionView)
view.addConstraintsWithFormat(format: "H:|-15-[v0]-15-|", views: collectionView)
view.addConstraintsWithFormat(format: "V:|-80-[v0]-140-|", views: collectionView)
}
}
Extension 2:
//configuring the dataSource and the delegate methods for the collectionView
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomCollectionViewCell
cell.backgroundColor = colors[indexPath.item]
cell.wordLabel.text = sampleWords[indexPath.item]
print("_____________________________________")
print(cell.isHidden)
if cell.isHidden {
print("CUSTOM CELL INSTANCE NOT CREATED")
}
print("_____________________________________")
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sampleWords.count
}
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 size = CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
return size
}
}
CustomCollectionViewCell class:
//custom cell class
class CustomCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
print("instance of CustomCollectionViewCell is created")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let wordLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private func setupViews() {
addSubview(wordLabel)
wordLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
addConstraint(NSLayoutConstraint(item: wordLabel, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 70))
}
}
Helper method addConstraintsWithFormat:
//helper method to add constraints to a view
extension UIView {
func addConstraintsWithFormat(format: String, views: UIView...){
var viewsDictionary = [String : UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
This is all the code that I have. Everything is done without a storyboard only programmatically.
To debug and to understand what’s going on for myself I’ve added a few print statements:
1) “Print” statement that outputs the boolean value if a cell is hidden, and if a cell.isHidden -> true -> an instance of the custom CollectionViewCell has not been created.
2) “Print” statement within the custom CollectionViewCell class in the init method to see whether a cell has been created or not.
The output is always the following:
instance of CustomCollectionViewCell is created
______________________________________
false
______________________________________
instance of CustomCollectionViewCell is created
______________________________________
false
______________________________________
instance of CustomCollectionViewCell is created
______________________________________
false
______________________________________
______________________________________
true
CUSTOM CELL INSTANCE NOT CREATED
and after that the cell.Hidden always returns a true values.
I found that some people were struggling with the same problem. But the solutions did not help me. Because I don't use the method collectionView.reloadData() anywhere and I cannot change the size of a single cell.
UICollectionViewCell gets hidden randomly
UICollectionView dequeues the cells that will be immediately visible on screen as isHidden = false and additional ones it is preparing that are offscreen as isHidden = true. This reflects the actual state that these cells will have when you view is displayed. You cannot override this behaviour nor the state of isHidden by setting hidden = false. This state automatically is updated by the UICollectionView as cells scroll into and out of view.
I have implemented a similar structure, where a UICollectionViewCell contains another UICollectionView. (One scrolling horizontally, the second vertically within that cell.) I can state categorically it is not necessary for a UICollectionView cell to have isHidden = false in order to correctly lay out subviews of the cell. It sounds like you're probably thinking that this hidden attribute is the cause of a problem you're having (I've been down that exact thought path) but you're actually wrong and the cause of the issue is something else.
In my case, for the record, the problem cells were reporting an auto layout constraint conflict, which I had been ignoring as I thought it was unrelated. The fix was that after I added the subview to my UICollectionViewCell, I needed to call layoutSubviews() on my cell view and reloadData() on the collection view it contained. If I did not do this, the embedded collections worked sometimes, but when it re-used previously created cells, rather than creating new ones, they would fail to display due to autolayout conflicts. This meant there were specific paths to get there that would or would not display the issue depending on what cells were created in previous views that were available for reuse.
I am using Swift to build an iOS application for the Hospital I work at.
Somehow, in a specific feature I have to put a UICollectionView inside the UICollectionViewCell. The one I want to achieve was for every content of the parent UICollectionView (vertical scrolling) would have several children (Which can be scrolled horizontal) depending on the parent row.
For illustration, I have to display list of doctors (name & photo) and then I have to display each of the practice schedule of them below their name and photo. The practice schedule would vary depending on each doctor. So, I have to put it inside the UICollectionView.
I have tried several solutions that I found on the web, but I still cannot approach it.
The most problem that I can't solve was: I don't know where is the place in the code to load the child data source (doctor schedule) and when I could load it, because I can't have two functions like below:
collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
this is the one I want to achieve
the UIImage and doctor name (UILabel) was in the parent UICollectionViewCell (scroll vertically), and then everything in the box (practice day n practice time) are the child UICollectionView (scroll horizontally)
PS: there are many doctors, and each of the doctor has several practice day.
please help me how to do this
If you really want to insert an collectionView inside a collectionViewCell then there is a pretty simple step. Create an instance of UICollectionView and add it the collectionViewCell. You can use this example if you like.
//
// ViewController.swift
// StackOverFlowAnswer
//
// Created by BIKRAM BHANDARI on 18/6/17.
// Copyright © 2017 BIKRAM BHANDARI. All rights reserved.
//
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
let cellId = "CellId"; //Unique cell id
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red; //just to test
collectionView.register(Cell.self, forCellWithReuseIdentifier: cellId) //register collection view cell class
setupViews(); //setup all views
}
func setupViews() {
view.addSubview(collectionView); // add collection view to view controller
collectionView.delegate = self; // set delegate
collectionView.dataSource = self; //set data source
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true; //set the location of collection view
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true; // top anchor of collection view
collectionView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true; // height
collectionView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true; // width
}
let collectionView: UICollectionView = { // collection view to be added to view controller
let cv = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()); //zero size with flow layout
cv.translatesAutoresizingMaskIntoConstraints = false; //set it to false so that we can suppy constraints
cv.backgroundColor = .yellow; // test
return cv;
}();
//deque cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath);
// cell.backgroundColor = .blue;
return cell;
}
// number of rows
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5;
}
//size of each CollecionViewCell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 200);
}
}
// first UICollectionViewCell
class Cell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
let cellId = "CellId"; // same as above unique id
override init(frame: CGRect) {
super.init(frame: frame);
setupViews();
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId); //register custom UICollectionViewCell class.
// Here I am not using any custom class
}
func setupViews(){
addSubview(collectionView);
collectionView.delegate = self;
collectionView.dataSource = self;
collectionView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true;
collectionView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true;
collectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true;
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true;
}
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout();
layout.scrollDirection = .horizontal; //set scroll direction to horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout);
cv.backgroundColor = .blue; //testing
cv.translatesAutoresizingMaskIntoConstraints = false;
return cv;
}();
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath);
cell.backgroundColor = .red;
return cell;
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5;
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.frame.width, height: self.frame.height - 10);
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This might be a little late, but for people out here still trying to find an answer.
After some research and digging, I stumbled upon several posts stating reasons why you should NOT have your cell be the delegate for you collectionView. So, I was lost because pretty much all solutions I had found were doing this, until I finally found what I believe is the best way to have nested collectionViews.
To give some background, my app included not only one but 2 collectionViews inside different cells of another collectionView, so setting the delegates with tags and all that, wasn't really the best approach nor the correct OO solution.
So the best way to do it is the following:
First you have to created a different class to serve as your delegate for the inner collectionView. I did it as such:
class InnerCollectionViewDelegate: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
// CollectionView and layout delegate methods here
// sizeForItemAt, cellForItemAt, etc...
}
Now, in your inner collectionView (or rather the cell where you have the inner collectionView) create a function that will allow you to set its delegates
class InnerCell: UICollectionViewCell {
var collectionView: UICollectionView
init() {
let layout = UICollectionViewFlowLayout()
collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height), collectionViewLayout: layout)
}
func setCollectionViewDataSourceDelegate(dataSourceDelegate: UICollectionViewDataSource & UICollectionViewDelegate) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.reloadData()
}
}
And lastly, in your ViewController where you have your outermost (main) collectionView do the following:
First instantiate the delegate for the inner collectionView
var innerDelegate = InnerCollectionViewDelegate()
and then
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if let cell = cell as? InnerCell {
cell.setCollectionViewDataSourceDelegate(dataSourceDelegate: innerDelegate)
}
}
This might not be perfect, but at least you have separation of concerns, as your cell should NOT be the delegate. Remember your cell should only be responsible for displaying info, not trying to figure out what the size of the collectionView should be, etc.
I did find similar answers that dealt with setting the collectionViews tag and whatnot, but I found that that made it way harder to deal with each collectionView individually, plus dealing with tags can't result in spaghetti code or unintended behaviours.
I left out registering and dequeuing the cell, but I'm sure you're all familiar with that. If not, just let me know and I'll try to walk you through it.
There are multiple ways to tackle the problem of a horizontal collection inside another a vertical list collection.
The simplest would be to make the ViewController you are presenting the main UICollectionView to the dataSouce and delegate for both collection views. You can set the collection view inside the cell also to be served from here.
This article about placing collection view inside a table view explains the problem in a much elaborate way and the code for the same can be found here.
Add collectionView in collection view cell , and add delagate methods in collectionviewclass.swift. Then pass list you want to show in cell in collectionview's cellforrowatindexpath. If you didn't success on implimenting it then let me know . i will provide you code as i have already implemented it in that way.
I am new to iOS development and am trying to create a PokeDex app. My main view controller has a collection view which returns two types of custom cells. My first cell called the chooser cell is immediately underneath the navigation bar or header bar whichever term, and I want my second custom cell(descriptioncell) to be immediately underneath the choosercell and take up the entire screen. I originally set the height of the description cell to be the height of the view - 50 to take the chooser cell into account but I saw that there was some space left over which shows the color of the collectionview. I then decided to make the descriptionview the height of the entire view.frame but there is still empty space for some reason. It is not a lot of space and by making the collectionview the same color as my descriptioncell everything would look fine, but why is this happening?
code in viewcontroller:
import UIKit
class PokeDexController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
private let cellID = "cellID"
private let cellID2 = "cellID2"
override func viewDidLoad() {
super.viewDidLoad()
self.title = "PokeDex 386"
//collectionView?.backgroundColor = UIColor(red: 52/255.0, green: 55/255.0, blue: 64/255.0, alpha: 1.0)
collectionView?.backgroundColor = UIColor.white
collectionView?.register(chooserCell.self, forCellWithReuseIdentifier: cellID)
collectionView?.register(DescriptionCell.self, forCellWithReuseIdentifier: cellID2)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if (indexPath.row == 0)
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! chooserCell
return cell
}
else
{
let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: cellID2, for: indexPath) as! DescriptionCell
return cell2
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if(indexPath.row == 0)
{
return CGSize(width: view.frame.width, height: 50)
}
else
{
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
}
how the app currently looks, empty space between two cells
Thanks
I would double check view.frame to make sure it is what you expect it to be, just to rule out something simple. More times that I'd like to admit i've used references to another view who's frame was not what I thought it was going to be.
Another thing to check would be the content insets/spacing of the UICollectionView especially if you're using storyboards, I don't know if storyboards set a default inset/cell spacing or not. If you always seem to have some spacing around the edges of your cells its most likely some padding/spacing configured on the UICollectionView itself.
Also a likely culprit can be AutoLayout constraints, by default storyboards attach to layout margins not the layout edges. The margins are inset slightly from the true edge creating a gap.
Hopefully that helps some or at least gets you looking in the right places
Here's a UICollectionView, and the cell in purple:
Quite simply, I want the cells to be 1/2 of the collection view width. (So TBC, it will be a two rows arrangement of cells in the collection view.)
(The collection view is simply fullscreen, so each cell is half the screen width.)
How do you do this in storyboard?
If I try to control-drag in the normal way, it basically doesn't work.
These are simple totally static cells (not dynamic).
For anyone googling here, to save your time: Here's exactly (2016) the simplest way to make a two-across UICollectionView layout; no gaps between the cells.
// Two - two-across UICollectionView
// use a completely standard UIViewController on the storyboard,
// likely change scroll direction to vertical.
// name the cell identifier "cellTwo" on the storyboard
import UIKit
class Two:UICollectionViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
let w = collectionView!.bounds.width / 2.0
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width:w,height:w)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView!.collectionViewLayout = layout
// Note!! DO NOT!!! register if using a storyboard cell!!
// do NOT do this:
// self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int
{ return 1 }
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{ return 5 }
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
return collectionView.dequeueReusableCellWithReuseIdentifier("cellTwo", forIndexPath: indexPath)
}
}
You can't do it in the storyboard. The collection view width is not known until runtime, and collection view cells are not under autolayout, so you cannot express the notion "1/2 the width" of anything else. (If you did know the collection view width in advance, you could use the flow layout in the storyboard to set the cell size absolutely, by dividing in your head; but you don't know it, because the width differs depending on the device.)