profile picture
18 Aug 2015
UICollectionView with Swift

A simple strategy to create efficient, synchronized and highly reusable data source delegate for all UICollectionView in your application

Swift Programming Language Logo
Swift Programming Language Logo

In mobile development the Model-View-Controller architecture can quickly evolve to a Massive-View-Controller nightmare where a cumbersome view controller manages almost everything in the application.

Massive view controllers are an antipattern because they are difficult to scale, test and mantain. A trick to avoid them is to remember these principles:

Specialization: one objet, one task.
Delegation: share responsability for targeted or iterative tasks
Communication: when complexity increase notifications can ease
Abstraction: make code as reusable as possible

Apple Cocoa API makes an extensive use of delegation for one-to-one communication between instances. Delegation is a powerful tool that should be employed at its best.

A common practice is to delegate anything to the view controller. This delegation strategy ends with the implementation of some methods (the ones required by the protocol we want to conform to) directly inside the body of the view controller.

class CustomViewController: UIViewController, UICollectionViewDataSource
{
  var data = [CustomModel]()

  override func viewDidLoad()
  {
    super.viewDidLoad()
  }

  // MARK: - DATASOURCE DELEGATE

  func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
  {
    return self.data.count
  }

  func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
  {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(self.cellIdentifier, forIndexPath: indexPath) as! CustomCollectionViewCell
    cell.data = self.data[indexPath.item];
    return cell
  }
}

This code is sneaky because it seems to be clean an compact... until we need to scale or add new features to the application

A better strategy is to delegate some responsibilities (like data sourcing, for exemple) to external objects instanciated in the view controller, making the code more reusable, expressive and semantically coherent.

Let's consider a mobile application with several collection views distributed on indipendent view controllers. For each of them we should implement at least two methods and instanciate an array with the data. Morover we should sychronize this array with the data contained in the collection view cells for the whole view controller lifecycle.

This was the old way! The smarter one is to create a new class conformed to UICollectionViewDataSource protocol that manages everything autonomously.

This class will be reusable for any collection view with any data type. Two callbacks are used to instanciate the model and to configure the collection view cells

import Foundation
import UIKit

typealise createCallback = ([String : AnyObject]) -> AnyObject
typealise configCallback = (UICollectionViewCell, AnyObject) -> Void

class CollectionDataSourceManager : NSObject, UICollectionViewDataSource
{
  var items = [AnyObject]()
  let cellIdentifier : String
  let cellConfigurator : (UICollectionViewCell, AnyObject) -> Void
  let newCellCreator : ([String : AnyObject]) -> AnyObject

  init(cellIdentifier: String, newCellCreator: createCallback, cellConfigurator: configCallback)
  {
    self.cellIdentifier = cellIdentifier
    self.cellConfigurator = cellConfigurator
    self.newCellCreator = newCellCreator
    super.init()
  }

  func itemAtIndexPath(indexPath: NSIndexPath) -> AnyObject
  {
    return self.items[indexPath.item];
  }

  func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
  {
    return self.items.count
  }

  func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
  {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(self.cellIdentifier, forIndexPath: indexPath) as! UICollectionViewCell
    self.cellConfigurator(cell, self.itemAtIndexPath(indexPath));
    return cell
  }

  func retrieveData(didRetrieveDataCallback: () -> Void)
  {
    // here we retrieve the data (for ex. with Alamofire)
    Alamofire.request(.GET, "https://www.app.com/api", parameters: ["foo": "bar"])
    .response { request, response, data, error in

      // do some parsing here before update the collection view
      // data array and calliing the callback closure

      self.items.append(self.newCellCreator(retrieved data)
      didRetrieveDataCallback()
    }
  }
}

After this, we just need to create a CollectionDataSourceManager instance in the view controller

class CustomViewController: UIViewController
{
  var data : CollectionDataSourceManager!

  override func viewDidLoad()
  {
    super.viewDidLoad()
    self.setCollectionView()
  }

  private func setCollectionView()
  {
    self.data = CollectionDataSourceManager(cellIdentifier: "CustomCell", newCellCreator: {CustomCellData(data: $0)}){
      let cell = $0 as! CustomCell
      cell.data = $1 as! CustomCellData
    }
    self.collectionView.dataSource = self.data
    self.data.retrieveData(){
      //here the callback to perform once data has been retrieved
      self.loader.stopAnimating()
      self.collectionView.realoadData
    }
  }
}

For both callbacks we use some handy Swift features: type inferring and implicit closure parameters ($0, $1). See documentation for details

The first callback is a oneliner that creates an instance of the model:

CustomCellData(data: $0)

The second one assigns the model to the custom UICollectionViewCell class:

let cell = $0 as! CustomCell
cell.data = $1 as! CustomCellData

That's all. Collection view delegation is done once forever. It is fully reusable and indipendent.

SHARE BACK