One of the common developments in mobile applications is showing information using tables. This task can easily be done when you have a collection of homogeneous entities but it gets trickier when this collection has n-number of different entities. In this article, I’m going to show an example of how to display heterogeneous data in a table view using the adapter pattern. The example exposed is a messaging app with two types of messages; texts and images.

 

Display heterogeneous data in a table view

Display heterogeneous dataWhen we face the problem of displaying heterogeneous data in tables, the straightforward solution is to use pattern matching in the method “cellForRowAtIndexPath” to decide what cell we need to use to display the current entity in the collection.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
            let currentItem = itemsArray[indexPath.row] 
            switch currentItem { 
            case let item as TextMessage:< 
                 let cell = tableView.dequeueReusableCell(withIdentifier: "TextMessageCell", for: indexPath) as! TextMessageCell< 
                 cell.configure(viewModel: item) 
                 return cell 
            case let item as ImageMessage: 
                 let cell = tableView.dequeueReusableCell(withIdentifier: "ImageMessageCell", for: indexPath) as! ImageMessageCell 
                 cell.configure(viewModel: item) 
                 return cell 
            default: 
                 fatalError("unsupported cell for type: \(currentItem)") 
            } 
}

There are several drawbacks with this solution:

  • It is not scalable, meaning the switch cases will grow every time we need to display a new type of entity.
  • We are repeating the call to the method configure(viewModel:) that could be part of an interface for the cell.
  • Since we do not have a strictly typed number of entities, we need to provide a default case for the switch.

The Adapter Pattern

The adapter pattern is used to transform one interface into another that our system will use instead.

Let’s see a solution using the adapter pattern to make it scalable and avoid repeating code.

The solution will have 2 components:

  • Adapter interface
  • Adapter collection holder (Adapter map)

First we need to create an adapter interface, this interface will have basic cell configuration attributes and the configure.

protocol PostTableViewAnyCellAdapter { 
     var reuseIdentifier: String { get } 
     var preferredHeight: CGFloat { get } 
     func configure(cell: UITableViewCell, with viewModel: Post) 
}

The implementation of that interface will receive generic types for both model and cell.

class PostCellAdapter: PostTableViewAnyCellAdapter where CellType.ViewModelType == ViewModelType { 
        var reuseIdentifier: String { 
            return CellType.reuseIdentifier 
        } 
        var preferredHeight: CGFloat { 
            return CellType.preferredHeight 
        } 
        init(tableView: UITableView) { 
            register(into: tableView) 
        } 
        func register(into tableView: UITableView) { 
             switch CellType.registerMethod { 
             case .nib(let nib): 
                 tableView.register(nib, forCellReuseIdentifier: 
CellType.reuseIdentifier) 
             case .classReference(let cellClass): 
                 tableView.register(cellClass, forCellReuseIdentifier: 
CellType.reuseIdentifier) 
             } 
      } 
      func configure(cell: UITableViewCell, with viewModel: Post) { 
           let typedCell = cell as! CellType 
           let typedViewModel = viewModel as! ViewModelType 
           typedCell.configure(viewModel: typedViewModel) 
      }
}

Then we create a class that will hold a collection this adapter interface. This class will be called the Adapter map.

struct PostsAdapterMap { 
               typealias InternalMapType = [String: PostTableViewAnyCellAdapter] 
               private var internalMap = InternalMapType()

               subscript(viewModel: Post) -> PostTableViewAnyCellAdapter { 
                   let key = String(describing: type(of: viewModel)) 
                   return internalMap[key]! 
               } 
               mutating func add<CellType, ViewModelType>(adapter: PostCellAdapter<CellType, ViewModelType>) { 
                   let key = String(describing: ViewModelType.self) 
                   internalMap[key] = adapter 
               } 
}

Now that we have all the components needed, we can use it in our view controllers by simply creating a new instance of that Adapter map class.

var tableAdapter = PostsAdapterMap()

To register the cell adapters to the table view we create instances of the Adapter using a pair of UITableviewCell subclass and a model:

let textAdapter = PostCellAdapter<TextMessageCell, TextMessage>(tableView: tableView)
tableAdapter.add(adapter: textAdapter)

Finally, our new implementation of the method “cellForRowAtIndexPath” will look as follows:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let currentItem = itemsArray[indexPath.row]
       let currentAdapter = tableAdapter[currentItem]

       let cell = tableView.dequeueReusableCell(withIdentifier: currentAdapter.reuseIdentifier, for: indexPath)
       currentAdapter.configure(cell: cell, with: currentItem)

       return cell
}

We are no longer configuring cell view classes directly using pattern matching but instead using adapter interfaces that will receive the models and the recycled cell views. The adapter map will use the appropriate method for the given model to configure the current cell

How does this solve scalability? If we need to display another type of message in this chat application, let’s say a video message, we only need to create the cell view with it’s configure method, define the model and add it to the adapter map. The adapter map will handle everything and therefore, our table view datasource methods will remain unchanged.