Generics over Legacy


At Shaadi.com our top priority is providing the best and suitable prospects  to our customers, and hence we diligently work on providing an array of such profiles via various sections such as Preferred Matches, Near me Matches, New Matches, etc. In our effort to do that, there is a constant pursuit to improvise on –  profile listing cards!

Listings in apps has been a conventional feature for almost any well known app as of today. At Shaadi, the front end developers spend most of their development time dealing with lists i.e UITableView and UICollectionView.

It’s quite straightforward when you need to display a list with the same data type, e.g., list of Profiles. But what if you need to present a bunch of different cells in one table view (banner, astro, education, profile….)? That can lead to a real mess!

And then.. there’s that moment..

With iOS development we have introduced MVVM architecture that allows us to segregate business logic into view models, that could have test cases written on. However, we have been dealing with an issue with Table Lists with different cells . The display logic had to be written under the view controller relenting the developer to actually test or separate the table display logic as MVVM pattern doesn’t have any layer that supports it.

The Legacy Code –

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | 

   let item = viewModel.items[indexPath.row]
    if let user = item as? User {

   let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) as! UserCell
  /// Configure Cell 1
  return cell

   } else if let message = item as? String {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
 /// Configure Cell 2
       return cell

   } else if let imageUrl = item as? URL {
      let cell = tableView.dequeueReusableCell(withIdentifier: "ImageCell", for: indexPath) as! ImageCell
     /// Configure Cell 3
       return cell
    }
..... and this can go on....

  return UITableViewCell()

 }

This legacy code has several issues:

  • If statements – It can become huge and unreadable as the number of cells grow
  • Type casting ( as! ) Casts are a code smell
  • For each data type we can have only one cell
  • Boilerplate code

                Gosh!!  Should I add my code in here. I am at my wits end?

Generics.. that’s our modus operandi

How about making this code more lucid and elegant no matter how many cells we have?

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | 

  let item = viewModel.items[indexPath.row]
  let cell = tableView.dequeueReusableCell(withIdentifier: type(of: item).reuseId)!
  item.configure(cell: cell)
  return cell
}

Generic programming is an excellent way to avoid boilerplate code and helps to define errors during compilation time.

The Generic prototype is as below:- 

protocol CellConfigurator {
    static var reuseId: String { get }
    func configure(cell: UIView)
}

class TableCellConfigurator<CellType: ConfigurableCell, DataType>: CellConfigurator where CellType.DataType == DataType, CellType: UITableViewCell {
    static var reuseId: String { return String(describing: CellType.self) }
    
    let item: DataType

    init(item: DataType) {
        self.item = item
    }

    func configure(cell: UIView) {
        (cell as! CellType).configure(data: item)
    }
}


CellConfigurator
 – we need this protocol as we can’s hold generic classes in array without defining data types.
TableCellConfigurator 
– generic class with type constraints for CellType and DataType. Cell can be configured only if DataType of ConfigurableCell equals to DataType used in TableCellConfigurator.

That’s it! And it’s great!


You can add new cells easily without a need to edit your ViewController’s code. Furthermore, it also eliminated the boiler plate code for dequeuing each cells for reuse purpose. Also, the overhead of the registrations of cell types for a tableview can be minimised with this technique.

Case in Shaadi:

New Payment Page Controller Display Logic:

   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {       
       let currentCellConfigurator :CellConfigurator = self.paymentViewModel.getConfigurator(data: self.productList as AnyObject, indexPath: indexPath ) 
       let cell = tableView.dequeueReusableCell(withIdentifier: type(of: currentCellConfigurator).reuseId)!
       currentCellConfigurator.configure(cell: cell, delegate: self)    
       return cell  
  }

 Further the logic of the cell to be displayed is moved under view model where it has been tested.

New Payment View Model:

func setConfiguratorArray(){       
   configuratorArray.append(NewProductCellConfig.init(item: productModel)as Any)    
   configuratorArray.append(PersonalisedCellConfig.init(item:personalisedModel  )     
   configuratorArray.append(MatchGuaranteeConfig.init(item: matchGuaranteeModel)   
   configuratorArray.append(FAQInsertConfig.init(item: faqModely)   
}    
  func getConfigurator(data:AnyObject, indexPath:IndexPath)->CellConfigurator  {
     // Any logic can be added  
      let configurator:CellConfigurator = configuratorArray.safeObject(at: indexPath.section) as! CellConfigurator
      return configurator
  }

In the end…

Maintaining a strict boundary between our view layer and model layer often leads to a much more flexible and easier to reuse code. As our list evolves, we don’t need to constantly add on to the main controller code, but rather build upon the view model where we wish to have our logic. 

Does this mean that all UI code should always be fully generalised and ready to render any model? I personally don’t think so.

Creating specialised views is something we sometimes have to do – for example when creating very custom graphics or clusters of views that need each other in order to work. Attempting to generalise things in such situations could instead make us end up with code that is really complicated and hard to navigate. Like always, it’s all about tradeoffs, and attempting to strike the right balance for each situation.