3

I want to build an iOS app with a UICollectionViewController that always have the same number of cells per row. Because I don't want my UICollectionViewController to deal with too much things, I've refactored my code and implemented funny things like protocol associatedtype and generic types. Now, my app is composed of 4 different .swift files.


1. CustomFlowLayout.swift

CustomFlowLayout is a simple subclass of UICollectionViewFlowLayout that allows us to set its minimumInteritemSpacing, minimumLineSpacing and sectionInset properties with dependency injection thanks to a convenience initializer.

import UIKit

class CustomFlowLayout: UICollectionViewFlowLayout {

    convenience init(minimumInteritemSpacing: CGFloat = 0,
                     minimumLineSpacing: CGFloat = 0,
                     sectionInset: UIEdgeInsets = .zero) {
        self.init()
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
    }

    override init() {
        super.init()
    }

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

}

2. ColumnDataSource.swift

ColumnDataSource is a subclass of NSObject that conforms to UICollectionViewDataSource, UICollectionViewDelegate and UICollectionViewDelegateFlowLayout. Its implements collectionView(_:layout:sizeForItemAt:) in order to display the correct number of UICollectionViewCells per row. Also note that ColumnDataSource is a generic class that requires us to pass it a type parameter at initialization.

import UIKit

class ColumnDataSource<FlowLayoutType: UICollectionViewFlowLayout>: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {

    let cellsPerRow: Int

    init(cellsPerRow: Int) {
        self.cellsPerRow = cellsPerRow
        super.init()
    }

    // MARK: - UICollectionViewDataSource

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    }

    // MARK: - UICollectionViewDelegateFlowLayout

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let flowLayout = collectionView.collectionViewLayout as! FlowLayoutType
        let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + flowLayout.minimumInteritemSpacing * (CGFloat(cellsPerRow) - 1)
        let itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)
        return CGSize(width: itemWidth, height: itemWidth)
    }

}

3. ColumnFlowLayoutable.swift

The purpose of ColumnFlowLayoutable protocol is to make sure that any class that conforms to it has columnDataSource and customFlowLayout properties where columnDataSource type's type parameter matches customFlowLayout type.

import UIKit

protocol ColumnFlowLayoutable {

    associatedtype FlowLayoutType: UICollectionViewFlowLayout
    var columnDataSource: ColumnDataSource<FlowLayoutType> { get }
    var customFlowLayout: FlowLayoutType { get }

}

4. CollectionViewController.swift

CollectionViewController is a subclass of UICollectionViewController that conforms to ColumnFlowLayoutable protocol. It also implement viewWillTransition(to:with:) in order to deal with container size changes.

import UIKit

class CollectionViewController: UICollectionViewController, ColumnFlowLayoutable {

    let columnDataSource = ColumnDataSource<CustomFlowLayout>(cellsPerRow: 2)
    let customFlowLayout = {
        CustomFlowLayout(minimumInteritemSpacing: $0, minimumLineSpacing: $0, sectionInset: UIEdgeInsets(top: $0, left: $0, bottom: $0, right: $0))
    }(10)

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = customFlowLayout
        collectionView?.dataSource = columnDataSource
        collectionView?.delegate = columnDataSource
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        collectionView?.collectionViewLayout.invalidateLayout()
    }

}

The complete project can be found at this Github repo: CollectionViewColumnsProtocol.


This code works fine. I can use it with subclasses of CustomFlowLayout and it still works. However, I cannot use it with subclasses of ColumnDataSource.

If I try to build the project by using a subclass of ColumnDataSource (e.g class SubColumnDataSource: ColumnDataSource<CustomFlowLayout>) in CollectionViewController, Xcode throws the following build time error message:

Type 'CollectionViewController' does not conform to protocol 'ColumnFlowLayoutable'

What would I have to change in ColumnFlowLayoutable protocol in order to allow CollectionViewController to work with subclasses of ColumnDataSource?

Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • This topic is related to [Swift Protocol - Property type subclass](https://stackoverflow.com/questions/32231420/swift-protocol-property-type-subclass). – Imanou Petit Jan 08 '18 at 23:22

1 Answers1

4

Create a DataSource protocol with an associated type for your layout type:

protocol ColumnDataSourceProtocol: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    associatedtype Layout: UICollectionViewFlowLayout
}

Make your DataSource base class conform to this protocol. You may need to add a typealias to specify the associatedtype if the compiler cannot infer it:

class ColumnDataSource<FlowLayoutType: UICollectionViewFlowLayout>: NSObject, ColumnDataSourceProtocol {

    typealias Layout = FlowLayoutType

    // the rest stays the same
}

Adapt ColumnFlowLayoutable to associate the data source type instead of the layout type. Constraining it to the ColumnDataSourceProtocol lets you access its associated Layout type:

protocol ColumnFlowLayoutable {

    associatedtype DataSource: ColumnDataSourceProtocol

    var columnDataSource: DataSource { get }
    var customFlowLayout: DataSource.Layout { get }
}

Now you can subclass ColumnDataSource:

class DataSource: ColumnDataSource<CustomFlowLayout> { /* ... */ }
nils
  • 1,786
  • 13
  • 18