UICollectionView with Compositional Layout & Diff Datasources disappears on scroll - ios

I currently have a UICollectionView which I'm using compositional layouts and Diffable Data sources. I'm not doing anything crazy just loading 150 cells into it and defining a 4 column layout. I seem to be encountering some weird behaviour whenever I tap the collectionview to scroll the items it seems to be disappearing... Below is my file with the entire code that I'm using so that you can copy & paste this and see this weird behaviour. Does anyone know why this might happening?
import UIKit
class ViewController: UIViewController {
private lazy var myCollectionViewLayout = MyCollectionViewLayout()
override func viewDidLoad() {
super.viewDidLoad()
setup()
// Do any additional setup after loading the view.
}
}
private extension ViewController {
func setup() {
let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: myCollectionViewLayout.createLayout())
collectionVw.translatesAutoresizingMaskIntoConstraints = false
collectionVw.register(MyCustomCell.self, forCellWithReuseIdentifier: MyCustomCell.cellId)
self.view.addSubview(collectionVw)
NSLayoutConstraint.activate([
collectionVw.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
collectionVw.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
collectionVw.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionVw.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
let dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
cell?.configure(at: indexPath.row)
return cell
}
collectionVw.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()
snapshot.appendSections([0])
Range(0...150).forEach { item in
snapshot.appendItems([UUID()], toSection: 0)
}
dataSource.apply(snapshot, animatingDifferences: true)
}
}
class MyCollectionViewLayout {
func createLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnv: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item,
count: 4)
group.interItemSpacing = .fixed(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 16,
bottom: 0,
trailing: 16)
return section
}
return layout
}
}
class MyCustomCell: UICollectionViewCell {
static let cellId = "MyCustomCell"
private var lbl: UILabel?
func configure(at index: Int) {
self.contentView.layer.cornerRadius = 8
self.contentView.backgroundColor = .blue
lbl = UILabel()
lbl?.translatesAutoresizingMaskIntoConstraints = false
lbl?.text = index.description
lbl?.textAlignment = .center
lbl?.font = .preferredFont(forTextStyle: .headline)
self.contentView.addSubview(lbl!)
NSLayoutConstraint.activate([
lbl!.topAnchor.constraint(equalTo: self.contentView.topAnchor),
lbl!.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
lbl!.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
lbl!.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
lbl?.removeFromSuperview()
}
}

The reason is that your data source is a local variable inside your setup method (let dataSource...). Therefore it works just the one time to populate the collection view initially, and then vanishes in a puff of smoke, leaving the collection view with no data.
The data source needs to be a property of the view controller so that it persists! Once you make that change, all will be well.
It's actually a little tricky to do that, because the data source depends on the collection view. So you have to use lazy instantiation. You create the collection view with an empty layout and hook everything together in the first step of the setup.
Here's a minimal rewrite of your code; look for my comments explaining the changes.
import UIKit
class ViewController: UIViewController {
// delete; this is pointless
// private lazy var myCollectionViewLayout = MyCollectionViewLayout()
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
// need persistent pointer to collection view
let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
// need persistent pointer to data source
lazy var dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
cell?.configure(at: indexPath.row)
return cell
}
}
private extension ViewController {
func setup() {
// let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: myCollectionViewLayout.createLayout())
// everything is persistent, now just hook them together
collectionVw.collectionViewLayout = MyCollectionViewLayoutMaker.createLayout()
collectionVw.translatesAutoresizingMaskIntoConstraints = false
collectionVw.register(MyCustomCell.self, forCellWithReuseIdentifier: MyCustomCell.cellId)
self.view.addSubview(collectionVw)
NSLayoutConstraint.activate([
collectionVw.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
collectionVw.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
collectionVw.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionVw.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
// let dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
//
// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
// cell?.configure(at: indexPath.row)
// return cell
// }
collectionVw.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()
snapshot.appendSections([0])
Range(0...150).forEach { item in
snapshot.appendItems([UUID()], toSection: 0)
}
dataSource.apply(snapshot, animatingDifferences: true)
}
}
class MyCollectionViewLayoutMaker {
// make this static; the class is not the layout, it serves no purpose except as a factory
static func createLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnv: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item,
count: 4)
group.interItemSpacing = .fixed(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 16,
bottom: 0,
trailing: 16)
return section
}
return layout
}
}
class MyCustomCell: UICollectionViewCell {
static let cellId = "MyCustomCell"
private var lbl: UILabel?
func configure(at index: Int) {
self.contentView.layer.cornerRadius = 8
self.contentView.backgroundColor = .blue
lbl = UILabel()
lbl?.translatesAutoresizingMaskIntoConstraints = false
lbl?.text = index.description
lbl?.textAlignment = .center
lbl?.font = .preferredFont(forTextStyle: .headline)
self.contentView.addSubview(lbl!)
NSLayoutConstraint.activate([
lbl!.topAnchor.constraint(equalTo: self.contentView.topAnchor),
lbl!.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
lbl!.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
lbl!.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
lbl?.removeFromSuperview()
}
}

Related

Simple orthogonal collection layout, but has odd behaviour on tvOS. Not sure if it's me?

My ViewController loads and lays out correctly. This is a pretty simple layout! Works as expected on iOS, but I have weird issues on tvOS.
So - at first all looks good. But when the user scrolls down, then scrolls back to the top - the collectionview stays partially under the titlebar. Focus then moves to the barItem, which is correct - but the top section header is under the bar. If the user then scrolls the top section right (to load new cells) - the layout gets fixed. Once this has happened the whole view works as expected.
Some code:
class ViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Int, String>!
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [collectionView]
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
makeDataSource()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.title = "Some Title"
navigationItem.leftBarButtonItem = .init(systemItem: .refresh)
applySnapshot()
}
// Hack..
override func viewSafeAreaInsetsDidChange() {
collectionView.contentInset.top = view.safeAreaInsets.top
collectionView.layoutMargins = .init(top: 0, left: view.safeAreaInsets.left, bottom: 0, right: view.safeAreaInsets.right)
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: TestLayout.makeSampleLayout() )
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.insetsLayoutMarginsFromSafeArea = false
// Some margins to move headers from sides
collectionView.layoutMargins = .init(top: 0, left: view.safeAreaInsets.left, bottom: 0, right: view.safeAreaInsets.right)
// Some (any) space on top contentInset
//collectionView.contentInset.top = 150
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
self.collectionView = collectionView
}
func applySnapshot() {
var snap = NSDiffableDataSourceSnapshot<Int, String>()
snap.appendSections([1])
snap.appendItems(["item1-1", "item1-2", "item1-3", "item1-4", "item1-5", "item1-6", "item1-7", "item1-8", "item1-9"])
snap.appendSections([2])
snap.appendItems(["item2-1", "item2-2", "item2-3", "item2-4"])
snap.appendSections([3])
snap.appendItems(["item3-1", "item3-2", "item3-3", "item3-4", "item3-5"])
snap.appendSections([4])
snap.appendItems(["item4-1", "item4-2", "item4-3", "item4-4", "item4-5"])
snap.appendSections([5])
snap.appendItems(["item5-1", "item5-2", "item5-3", "item5-4", "item5-5"])
snap.appendSections([6])
snap.appendItems(["item6-1", "item6-2", "item6-3"])
dataSource.apply(snap, animatingDifferences: false)
}
func makeDataSource() {
let textHeader = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader,
handler: { (cell: FixedHeaderView, string: String, indexPath: IndexPath) in
})
let testCell = UICollectionView.CellRegistration(handler: { (cell: TestCell, indexPath: IndexPath, itemIdentifier: String) in
})
// Make dataSource and supplementaryViewProvider
dataSource = UICollectionViewDiffableDataSource<Int, String> (collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: testCell, for: indexPath, item: itemIdentifier)
})
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
switch kind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueConfiguredReusableSupplementary(using: textHeader, for: indexPath)
default:
assertionFailure("Unknown SupplementaryView")
return nil
}
}
}
}
class TestLayout {
// Return a standard section header
static func standardSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(78) )
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top)
header.extendsBoundary = true
return header
}
static func orthogonalSectionInsets() -> UIEdgeInsets {
return .init(top: 38, left: 0, bottom: 38, right: 0)
}
static func orthogonalSectionSpacing() -> CGFloat {
return 52
}
static func orthogonalSection() -> NSCollectionLayoutSection? {
let insets = orthogonalSectionInsets()
let itemSpacing = orthogonalSectionSpacing()
let viewItems = 6 + 0.5
let heightDimension = NSCollectionLayoutDimension.estimated(1)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1 / viewItems),
heightDimension: heightDimension)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// iOS15
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitem: item, count: 1)
// iOS16 - weird output?
//let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, repeatingSubitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
// Add space to section sides, and between headers
section.contentInsets = NSDirectionalEdgeInsets(top: insets.top, leading: insets.left, bottom: insets.bottom, trailing: insets.right)
section.contentInsetsReference = .layoutMargins
// Add space between horizontal items
section.interGroupSpacing = itemSpacing
section.orthogonalScrollingBehavior = .continuous
// Header
section.boundarySupplementaryItems = [standardSectionHeader() ]
section.supplementaryContentInsetsReference = .layoutMargins
return section
}
static func makeSampleLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { (sectionNumber, env)
-> NSCollectionLayoutSection? in
return orthogonalSection()
}
// Top level layout settings
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
layoutConfig.scrollDirection = .vertical
layout.configuration = layoutConfig
return layout
}
}
class TestCell: UICollectionViewCell {
let someView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
insetsLayoutMarginsFromSafeArea = false
someView.translatesAutoresizingMaskIntoConstraints = false
someView.backgroundColor = .red
// We shouldn't be able to see this if margins are correct
contentView.backgroundColor = .yellow
someView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
someView.setContentHuggingPriority(.defaultHigh, for: .vertical)
contentView.addSubview(someView)
let bottom: NSLayoutConstraint = someView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
bottom.priority = .defaultHigh // .init(998)
NSLayoutConstraint.activate([
someView.topAnchor.constraint(equalTo: contentView.topAnchor),
someView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
someView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
someView.heightAnchor.constraint(equalTo: someView.widthAnchor),
bottom
])
}
// Some visible focus so we can see what is going on
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if (context.nextFocusedView == self) {
someView.backgroundColor = .green
}
else if (context.previouslyFocusedView == self) {
someView.backgroundColor = .red
}
}
}
class FixedHeaderView: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
backgroundColor = .none
label.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
label.setContentHuggingPriority(.defaultHigh, for: .vertical)
label.textColor = .white
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 40, weight: .heavy)
label.text = "Some Header Title"
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor)
])
backgroundColor = .red.withAlphaComponent(0.25)
}
}
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Create the window
let newWindow = UIWindow(frame: UIScreen.main.bounds)
let nav = UINavigationController(rootViewController: ViewController() )
newWindow.rootViewController = nav
newWindow.makeKeyAndVisible()
self.window = newWindow
return true
}
I've tried using all sorts of different margin and safeArea layouts, but this issue appears to affect any layout that requires the content to sit off the top of the screen.
Sometimes it loads and works perfectly first time, making me think it's a race condition or something.
Cells are dynamic height (because I need to support a lot of cell types). If I set the height to 'absolute' values, it seems to work - but this would create a lot of work and I'd lose accessibility such as dynamic text sizing.
But I've been at this for 2 days, nudging code and reading docs. I don't do much tvOS and I'm a sole developer (so I don't have any other devs to bounce off) - would really appreciate some input!! Thanks

collectionview global header does not push cells down and stays on top instead

I have a collection view header (blue) and cells (red). I want to be able to show/hide header programmatically, however when I show the header programatically it appears on top of the cell (or makes scrollview go down a bit). I would like the header to push the whole scrollview down, so I wouldn't have to scroll up after clicking "Toggle header".
I tried very hard to reproduce minimal code for the issue which is below. Please share any insights.
struct Section: Hashable {
var items: [Int]
}
class ViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Int>?
var showHeader = true
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: view.frame, collectionViewLayout: createCompositionalLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
collectionView.register(CellView.self, forCellWithReuseIdentifier: "cellId")
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "headerId")
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
createDataSource()
addData()
}
func createCompositionalLayout() -> UICollectionViewLayout {
// layout for cell
let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(200))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
return layoutSection
}
let config = UICollectionViewCompositionalLayoutConfiguration()
if showHeader {
let layoutSectionHeader = createGlobalHeader()
config.boundarySupplementaryItems = [layoutSectionHeader]
}
layout.configuration = config
return layout
}
func createGlobalHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let layoutSectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
layoutSectionHeader.pinToVisibleBounds = true
return layoutSectionHeader
}
func createDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) { collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as? CellView else { fatalError("Unable to dequeue ") }
cell.button.addTarget(self, action: #selector(self.onButtonClick), for: .touchUpInside)
return cell
}
dataSource?.supplementaryViewProvider = { (collectionView, kind, indexPath) in
guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "headerId", for: indexPath) as? HeaderView else { return nil }
return header
}
}
#objc func onButtonClick() {
print("toggle")
showHeader.toggle()
collectionView.collectionViewLayout = createCompositionalLayout()
collectionView.layoutIfNeeded()
}
func addData() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
var sections: [Section] = []
sections.append(Section(items: [0,1,2,3,4,5,6,7,8,9,10,11,12]))
snapshot.appendSections(sections)
for section in sections {
snapshot.appendItems(section.items, toSection: section)
}
dataSource?.apply(snapshot)
}
}
class CellView: UICollectionViewCell {
let button = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
addSubview(button)
button.setTitle("Toggle header", for: .normal)
button.setTitleColor(.black, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.topAnchor.constraint(equalTo: topAnchor).isActive = true
button.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class HeaderView: UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .blue
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If I got your question right, you can scroll the collection view to top with:
collectionView.setContentOffset(.zero, animated: false)
whenever you want to display header.
It's a work around but I think it should do the trick.
I think you need to embed the collection view and a regular view(that you will use as a header) in a Stack View, and programmatically hide and show the header view. This will make the collectionView expand in the stack view when the header is hidden and vise-versa. I would completely abandon the UICollectionReusableView header view. So the view hierarchy would look something like this:
StackView
HeaderView
CollectionView
I hope I explained myself clear enough.

UICollectionCellView delegate not being fired

A UICollectionView I have created doesn't want to fire a custom cell delegate.
The cells are displaying, and the test button accepts presses.
I have debugged the code and the cell.delegate is being assigned self.
I have researched this on google and I have come up with nothing. I think my code is ok but I must be missing something?
I would really appreciate any help.
//HomeControllerDelegateTesting.swift
import UIKit
class HomeControllerDelegateTesting: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, PostCellOptionsDelegate {
var collectionView: UICollectionView! = nil
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.createLayout())
self.collectionView.register(PostCell.self, forCellWithReuseIdentifier: PostCell.reuseIdentifier)
self.collectionView.backgroundColor = .systemBackground
self.view.addSubview(collectionView)
self.collectionView.dataSource = self
self.collectionView.delegate = self
}
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(300))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostCell.reuseIdentifier, for: indexPath) as? PostCell else { fatalError("Cannot create cell") }
cell.postTextLabel.text = "Test"
cell.delegate = self
//Test to eliminiate the button accepts events
//cell.optionsButtons.addTarget(self, action: #selector(test), for: .touchUpInside)
return cell
}
#objc func handlePostOptions(cell: PostCell) {
print("123")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
}
// PostCell.swift
import UIKit
protocol PostCellOptionsDelegate: class {
func handlePostOptions(cell: PostCell)
}
class PostCell: UICollectionViewCell {
static let reuseIdentifier = "list-cell-reuse-identifier"
var delegate: PostCellOptionsDelegate?
let usernameLabel: UILabel = {
let label = UILabel()
label.text = "Username"
label.font = .boldSystemFont(ofSize: 15)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let postImageView: UIImageView = {
let control = UIImageView()
control.translatesAutoresizingMaskIntoConstraints = false
control.contentMode = .scaleAspectFill
control.clipsToBounds = true
return control
}()
let postTextLabel: UILabel = {
let label = UILabel()
label.text = "Post text spanning multiple lines"
label.font = .systemFont(ofSize: 15)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// private let optionsButtons: UIButton = {
// let control = UIButton.systemButton(with: #imageLiteral(resourceName: "post_options"), target: self, action: #selector(handleOptions))
// control.translatesAutoresizingMaskIntoConstraints = false
// return control
// }()
let optionsButtons: UIButton = {
let control = UIButton(type: .system)
control.setTitle("Test", for: .normal)
control.addTarget(self, action: #selector(handleOptions), for: .touchUpInside)
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
#objc fileprivate func handleOptions() {
print("Handle options button")
delegate?.handlePostOptions(cell: self)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupComponents()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupComponents() {
self.optionsButtons.heightAnchor.constraint(equalToConstant: 50).isActive = true
let labelStack = UIStackView(arrangedSubviews: [optionsButtons])
labelStack.translatesAutoresizingMaskIntoConstraints = false
labelStack.axis = .horizontal
labelStack.alignment = .fill
labelStack.distribution = .fill
labelStack.isLayoutMarginsRelativeArrangement = true
labelStack.layoutMargins.left = 16
labelStack.layoutMargins.right = 16
labelStack.layoutMargins.top = 16
labelStack.layoutMargins.bottom = 16
let postTextLabelStack = UIStackView(arrangedSubviews: [postTextLabel])
postTextLabelStack.translatesAutoresizingMaskIntoConstraints = false
postTextLabelStack.axis = .vertical
postTextLabelStack.alignment = .fill
postTextLabelStack.distribution = .fill
postTextLabelStack.isLayoutMarginsRelativeArrangement = true
postTextLabelStack.layoutMargins.left = 16
postTextLabelStack.layoutMargins.right = 16
postTextLabelStack.layoutMargins.top = 16
postTextLabelStack.layoutMargins.bottom = 16
let stack = UIStackView(arrangedSubviews: [labelStack, postImageView, postTextLabelStack])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.alignment = .fill
stack.distribution = .fill
stack.backgroundColor = .blue
self.addSubview(stack)
//postImageView.heightAnchor.constraint(equalTo: postImageView.widthAnchor).isActive = true
stack.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
}
Change your button declaration to this:
// make this a lazy var
lazy var optionsButtons: UIButton = {
let control = UIButton(type: .system)
control.setTitle("Test", for: .normal)
// add self. to the selector
control.addTarget(self, action: #selector(self.handleOptions), for: .touchUpInside)
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
The issue is that when the optionsButtons is created the self is still not initialized. Hence you need to make the button lazy var so that it could be loaded lazily when the optionsButtons is called and the UICollectionViewCell is initialized. Adding the target before self is initialized doesn't work.
To fix your issue, modify your optionsButtons declaration to lazy var, like this:
lazy var optionsButtons: UIButton = {
OR
Add the target after the UICollectionViewCell is initialized, like this:
override init(frame: CGRect) {
super.init(frame: frame)
optionsButtons.addTarget(self, action: #selector(handleOptions), for: .touchUpInside)
setupComponents()
}

Collection View Diffable Data Source cells disappearing and not resizing properly?

I'm having a really weird issue with my collection view. I'm using the Compositional Layout and Diffable Data Source APIs for iOS 13+, but I'm getting some really weird behavior. As seen in the video below, when I update the data source, the first cell that is added to the top section doesn't resize properly, then when I add the second cell both cells disappear, and then when I add a third cell, all load in with the proper sizes and appear. When I unadd all the cells and add them back in a similar fashion a second time, that initial issue doesn't happen again.
Video of Error
I have tried using the following solutions in some fashion:
collectionView.collectionViewLayout.invalidateLayout()
cell.contentView.setNeedsLayout() followed by cell.contentView.layoutIfNeeded()
collectionView.reloadData()
I can't seem to figure out what might be causing this issue. Perhaps it could be that I have two different cells registered with the collection view and dequeueing them improperly or my data types aren't correctly conforming to hashable. I believe I've fixed both of those issues, but I will also provide my code to help. Also the data controller mentioned is a simple class that stores an array of view models for the cells to use for configuration (there shouldn't be any issue there). Thanks!
Collection View Controller
import UIKit
class PartyInvitesViewController: UIViewController {
private var collectionView: UICollectionView!
private lazy var layout = createLayout()
private lazy var dataSource = createDataSource()
private let searchController = UISearchController(searchResultsController: nil)
private let dataController = InvitesDataController()
override func loadView() {
super.loadView()
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
backButton.tintColor = UIColor.Fiesta.primary
navigationItem.backBarButtonItem = backButton
let titleView = UILabel()
titleView.text = "invite"
titleView.textColor = .white
titleView.font = UIFont.Fiesta.Black.header
navigationItem.titleView = titleView
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
// definesPresentationContext = true
navigationItem.largeTitleDisplayMode = .never
navigationController?.navigationBar.isTranslucent = true
extendedLayoutIncludesOpaqueBars = true
collectionView.register(InvitesCell.self, forCellWithReuseIdentifier: InvitesCell.reuseIdentifier)
collectionView.register(InvitedCell.self, forCellWithReuseIdentifier: InvitedCell.reuseIdentifier)
collectionView.register(InvitesSectionHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: InvitesSectionHeaderReusableView.reuseIdentifier)
collectionView.delegate = self
collectionView.dataSource = dataSource
dataController.cellPressed = { [weak self] in
self?.update()
}
dataController.start()
update(animate: false)
view.backgroundColor = .secondarySystemBackground
collectionView.backgroundColor = .secondarySystemBackground
}
}
extension PartyInvitesViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// cell.contentView.setNeedsLayout()
// cell.contentView.layoutIfNeeded()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.section == InvitesSection.unselected.rawValue {
let viewModel = dataController.getAll()[indexPath.item]
dataController.didSelect(viewModel, completion: nil)
}
}
}
extension PartyInvitesViewController {
func update(animate: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<InvitesSection, InvitesCellViewModel>()
snapshot.appendSections(InvitesSection.allCases)
snapshot.appendItems(dataController.getTopSelected(), toSection: .selected)
snapshot.appendItems(dataController.getSelected(), toSection: .unselected)
snapshot.appendItems(dataController.getUnselected(), toSection: .unselected)
dataSource.apply(snapshot, animatingDifferences: animate) {
// self.collectionView.reloadData()
// self.collectionView.collectionViewLayout.invalidateLayout()
}
}
}
extension PartyInvitesViewController {
private func createDataSource() -> InvitesCollectionViewDataSource {
let dataSource = InvitesCollectionViewDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, viewModel -> UICollectionViewCell? in
switch indexPath.section {
case InvitesSection.selected.rawValue:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvitedCell.reuseIdentifier, for: indexPath) as? InvitedCell else { return nil }
cell.configure(with: viewModel)
cell.onDidCancel = { self.dataController.didSelect(viewModel, completion: nil) }
return cell
case InvitesSection.unselected.rawValue:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvitesCell.reuseIdentifier, for: indexPath) as? InvitesCell else { return nil }
cell.configure(with: viewModel)
return cell
default:
return nil
}
})
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath -> UICollectionReusableView? in
guard kind == UICollectionView.elementKindSectionHeader else { return nil }
guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: InvitesSectionHeaderReusableView.reuseIdentifier, for: indexPath) as? InvitesSectionHeaderReusableView else { return nil }
switch indexPath.section {
case InvitesSection.selected.rawValue:
view.titleLabel.text = "Inviting"
case InvitesSection.unselected.rawValue:
view.titleLabel.text = "Suggested"
default: return nil
}
return view
}
return dataSource
}
}
extension PartyInvitesViewController {
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { section, _ -> NSCollectionLayoutSection? in
switch section {
case InvitesSection.selected.rawValue:
return self.createSelectedSection()
case InvitesSection.unselected.rawValue:
return self.createUnselectedSection()
default: return nil
}
}
return layout
}
private func createSelectedSection() -> NSCollectionLayoutSection {
let width: CGFloat = 120
let height: CGFloat = 60
let layoutSize = NSCollectionLayoutSize(widthDimension: .estimated(width), heightDimension: .absolute(height))
let item = NSCollectionLayoutItem(layoutSize: layoutSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: layoutSize, subitems: [item])
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]
section.orthogonalScrollingBehavior = .continuous
// for some reason content insets breaks the estimation process idk why
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
section.interGroupSpacing = 20
return section
}
private func createUnselectedSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(60))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
section.interGroupSpacing = 20
return section
}
}
Invites Cell (First Cell Type)
class InvitesCell: FiestaGenericCell {
static let reuseIdentifier = "InvitesCell"
var stackView = UIStackView()
var userStackView = UIStackView()
var userImageView = UIImageView()
var nameStackView = UIStackView()
var usernameLabel = UILabel()
var nameLabel = UILabel()
var inviteButton = UIButton()
override func layoutSubviews() {
super.layoutSubviews()
userImageView.layer.cornerRadius = 28
}
override func arrangeSubviews() {
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.addArrangedSubview(userStackView)
stackView.addArrangedSubview(inviteButton)
userStackView.addArrangedSubview(userImageView)
userStackView.addArrangedSubview(nameStackView)
nameStackView.addArrangedSubview(usernameLabel)
nameStackView.addArrangedSubview(nameLabel)
setNeedsUpdateConstraints()
}
override func loadConstraints() {
// Stack view constraints
NSLayoutConstraint.activate([
stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
stackView.heightAnchor.constraint(equalTo: contentView.heightAnchor)
])
// User image view constraints
NSLayoutConstraint.activate([
userImageView.heightAnchor.constraint(equalToConstant: 56),
userImageView.widthAnchor.constraint(equalToConstant: 56)
])
}
override func configureSubviews() {
// Stack view configuration
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .equalSpacing
// User stack view configuration
userStackView.axis = .horizontal
userStackView.alignment = .center
userStackView.spacing = Constants.inset
// User image view configuration
userImageView.image = UIImage(named: "Image-4")
userImageView.contentMode = .scaleAspectFill
userImageView.clipsToBounds = true
// Name stack view configuration
nameStackView.axis = .vertical
nameStackView.alignment = .leading
nameStackView.spacing = 4
nameStackView.distribution = .fillProportionally
// Username label configuration
usernameLabel.textColor = .white
usernameLabel.font = UIFont.Fiesta.Black.text
// Name label configuration
nameLabel.textColor = .white
nameLabel.font = UIFont.Fiesta.Light.footnote
// Invite button configuration
let configuration = UIImage.SymbolConfiguration(weight: .heavy)
inviteButton.setImage(UIImage(systemName: "circle", withConfiguration: configuration), for: .normal)
inviteButton.tintColor = .white
}
}
extension InvitesCell {
func configure(with viewModel: InvitesCellViewModel) {
usernameLabel.text = viewModel.username
nameLabel.text = viewModel.name
let configuration = UIImage.SymbolConfiguration(weight: .heavy)
if viewModel.isSelected {
inviteButton.setImage(UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration), for: .normal)
inviteButton.tintColor = .green
} else {
inviteButton.setImage(UIImage(systemName: "circle", withConfiguration: configuration), for: .normal)
inviteButton.tintColor = .white
}
}
}
Invited Cell (Second Cell Type)
import UIKit
class InvitedCell: FiestaGenericCell {
static let reuseIdentifier = "InvitedCell"
var mainView = UIView()
var usernameLabel = UILabel()
// var cancelButton = UIButton()
var onDidCancel: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
mainView.layer.cornerRadius = 8
}
override func arrangeSubviews() {
mainView.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(mainView)
mainView.addSubview(usernameLabel)
}
override func loadConstraints() {
// Main view constraints
NSLayoutConstraint.activate([
mainView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
mainView.heightAnchor.constraint(equalTo: contentView.heightAnchor)
])
// Username label constraints
NSLayoutConstraint.activate([
usernameLabel.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 20),
usernameLabel.leftAnchor.constraint(equalTo: mainView.leftAnchor, constant: 20),
usernameLabel.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: -20),
usernameLabel.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -20)
])
}
override func configureSubviews() {
// Main view configuration
mainView.backgroundColor = .tertiarySystemBackground
// Username label configuration
usernameLabel.textColor = .white
usernameLabel.font = UIFont.Fiesta.Black.text
}
}
extension InvitedCell {
func configure(with viewModel: InvitesCellViewModel) {
usernameLabel.text = viewModel.username
}
#objc func cancel() {
onDidCancel?()
}
}
Invites Cell View Model (model for the cells)
import Foundation
struct InvitesCellViewModel {
var id = UUID()
private var model: User
init(_ model: User, selected: Bool) {
self.model = model
self.isSelected = selected
}
var username: String?
var name: String?
var isSelected: Bool
mutating func toggleIsSelected() {
isSelected = !isSelected
}
}
extension InvitesCellViewModel: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(isSelected)
}
static func == (lhs: InvitesCellViewModel, rhs: InvitesCellViewModel) -> Bool {
lhs.id == rhs.id && lhs.isSelected == rhs.isSelected
}
}
If I need to provide anything else to better assist in answering this question, please let me know in the comments!
This may not be a solution for everyone, but I ended up fully switching over to RxSwift. For those who are debating the switch, I now use RxDataSources and the UICollectionViewCompositionalLayout with virtually no problems (outside of the occasional bug or two). I know this may not be the answer most are looking for, but looking back, this issue seems to be on Apple's end, so I figured it was best to find another path. If anybody has found a solution that is simpler than completely jumping over to Rx, please feel free to add your answer as well.

Delegate Function Not Being Called

So I am trying to use protocols and delegates to connect two functions so I can perform some operation on a variable a collectionView in this case in a different file.
import Foundation
import UIKit
protocol EventCollectionCellDelegate: NSObjectProtocol {
func setupCollectionView(for eventCollectionView: UICollectionView?)
}
class EventCollectionCell:UICollectionViewCell {
weak var delegate: EventCollectionCellDelegate?
var eventArray = [EventDetails](){
didSet{
self.eventCollectionView.reloadData()
}
}
var enentDetails:Friend?{
didSet{
var name = "N/A"
var total = 0
seperator.isHidden = true
if let value = enentDetails?.friendName{
name = value
}
if let value = enentDetails?.events{
total = value.count
self.eventArray = value
seperator.isHidden = false
}
if let value = enentDetails?.imageUrl{
profileImageView.loadImage(urlString: value)
}else{
profileImageView.image = #imageLiteral(resourceName: "Tokyo")
}
self.eventCollectionView.reloadData()
setLabel(name: name, totalEvents: total)
}
}
let container:UIView={
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 16
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.3
return view
}()
//profile image view for the user
var profileImageView:CustomImageView={
let iv = CustomImageView()
iv.layer.masksToBounds = true
iv.layer.borderColor = UIColor.lightGray.cgColor
iv.layer.borderWidth = 0.3
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
//will show the name of the user as well as the total number of events he is attending
let labelNameAndTotalEvents:UILabel={
let label = UILabel()
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
let seperator:UIView={
let view = UIView()
view.backgroundColor = .lightGray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//collectionview that contains all of the events a specific user will be attensing
let flow = UICollectionViewFlowLayout()
lazy var eventCollectionView = UICollectionView(frame: .zero, collectionViewLayout: flow)
// var eventCollectionView:UICollectionView?
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpCell()
self.setupCollectionView(for: eventCollectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupCollectionView(for eventCollectionView: UICollectionView?){
delegate?.setupCollectionView(for: eventCollectionView)
}
}
This is the file that creates a collectionViewCell with a collectionView in it. I am trying to perform some operation on that collectionView using the delegate pattern. My problem is that the delegate function is never called in the accompanying viewController. I feel like I have done everything right but nothing happens in the accompanying vc. Anyone notice what could possibly be wrong.
I have shown the code for the VC below
class FriendsEventsView: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout,EventCollectionCellDelegate {
var friends = [Friend]()
var followingUsers = [String]()
var height:CGFloat = 0
var notExpandedHeight : CGFloat = 50
var isExpanded = [Bool]()
//so this is the main collectonview that encompasses the entire view
lazy var mainCollectionView:UICollectionView={
// the flow layout which is needed when you create any collection view
let flow = UICollectionViewFlowLayout()
//setting the scroll direction
flow.scrollDirection = .vertical
//setting space between elements
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = spacingbw
flow.minimumInteritemSpacing = 0
//actually creating collectionview
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//register a cell for that collectionview
cv.register(EventCollectionCell.self, forCellWithReuseIdentifier: "events")
cv.translatesAutoresizingMaskIntoConstraints = false
//changing background color
cv.backgroundColor = .white
//sets the delegate of the collectionView to self. By doing this all messages in regards to the collectionView will be sent to the collectionView or you.
//"Delegates send messages"
cv.delegate = self
//sets the datsource of the collectionView to you so you can control where the data gets pulled from
cv.dataSource = self
//sets positon of collectionview in regards to the regular view
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
return cv
}()
lazy var eventCollectionView:UICollectionView={
let flow = UICollectionViewFlowLayout()
flow.scrollDirection = .vertical
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = 0
flow.minimumInteritemSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//will register the eventdetailcell
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .white
cv.register(EventDetailsCell.self, forCellWithReuseIdentifier: "eventDetails")
cv.delegate = self
cv.dataSource = self
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
cv.showsVerticalScrollIndicator = false
cv.bounces = false
return cv
}()
func setupCollectionView(for eventCollectionView: UICollectionView?) {
print("Attempting to create collectonView")
eventCollectionView?.backgroundColor = .blue
}
//label that will be displayed if there are no events
let labelNotEvents:UILabel={
let label = UILabel()
label.textColor = .lightGray
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.italicSystemFont(ofSize: 14)
label.text = "No events found"
label.isHidden = true
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
//will set up all the views in the screen
self.setUpViews()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
}
func setUpViews(){
//well set the navbar title to Friends Events
self.title = "Friends Events"
view.backgroundColor = .white
//adds the main collection view to the view and adds proper constraints for positioning
view.addSubview(mainCollectionView)
mainCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
mainCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
mainCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
mainCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
//adds the label to alert someone that there are no events to the collectionview and adds proper constrains for positioning
mainCollectionView.addSubview(labelNotEvents)
labelNotEvents.centerYAnchor.constraint(equalTo: mainCollectionView.centerYAnchor, constant: 0).isActive = true
labelNotEvents.centerXAnchor.constraint(equalTo: mainCollectionView.centerXAnchor, constant: 0).isActive = true
//will fetch events from server
self.fetchEventsFromServer()
}
// MARK: CollectionView Datasource for maincollection view
//woll let us know how many cells are being displayed
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print(friends.count)
isExpanded = Array(repeating: false, count: friends.count)
return friends.count
}
//will control the size of the cell that is displayed in the containerview
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
height = 100
let event = friends[indexPath.item]
if let count = event.events?.count,count != 0{
height += (CGFloat(count*40)+10)
}
return CGSize(width: collectionView.frame.width, height: height)
}
//will do the job of effieicently creating cells for the eventcollectioncell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "events", for: indexPath) as! EventCollectionCell
cell.delegate = self
cell.enentDetails = friends[indexPath.item]
cell.eventCollectionView = eventCollectionView
return cell
}
}
I have cut the code down to what I believe is needed to answer the question for simplicity. Any help is appreciated
You set delegate after setupCollectionView. In your case, you can't call setupCollectionView before you set delegate, because setupCollectionView called in init

Resources