Uno de los desarrollos más frecuentes en aplicaciones móviles es mostrar la información mediante tablas. Esta tarea se puede hacer de forma relativamente sencilla cuando dispones de una colección de entidades homogéneas, pero la cosa se complica cuando esta misma colección consta de una cantidad n de entidades distintas. En este artículo os mostraré mediante un ejemplo cómo mostrar esta información heterogénea en formato tabla (UITableViews) usando el patrón de diseño adapter. El ejemplo en concreto en que me centraré es una aplicación de chat con dos tipos de mensaje: textos e imágenes.

 

Cómo mostrar información heterogénea en UITableViews

Display heterogeneous dataA la hora de hacer frente al problema de cómo mostrar una información heterogénea en forma de tabla existe la solución más directa: usar la pattern matching en el método “cellForRowAtIndexPath” para decidir qué celda de la tabla usaremos para mostrar la entidad correspondiente de la colección.

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)") 
            } 
}

Esta solución, no obstante, cuenta con un seguido de inconvenientes:

  • No es escalable, lo que significa que los casos del switch aumentarán de tamaño a medida que necesitemos mostrar nuevos tipos de entidad.
  • Estamos repitiendo la llamada al método configure(viewModel:) que podría ser parte de la interfaz de la celda.
  • Al no tener un número específico de entidades deberemos marcar un case por defecto para el switch.

 

Apium Academy

 

El Patrón Adapter

El patrón adapter se usa para transformar una interfaz en otra que va a usar nuestro sistema.

Veamos una solución escalable usando éste patrón, con la que además evitaremos la reiteración de código:

Esta solución consta de dos componentes:

  • Adapter interface
  • Adapter collection holder (Adapter map)

Primero debemos crear la interfaz del adapter, la cual tendrá los atributos básicos de configuración de celdas y el método configure.

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

La implementación de esta interfaz recibirá tipos genéricos de modelo y celda.

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) 
      }
}

Acto seguido creamos una clase que tendrá una colección del tipo de la interfaz. Llamaremos esta clase “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 
               } 
}

Ahora que tenemos todos los componentes necesarios, podemos usarlos en nuestros UIViewControllers simplemente creando una nueva instancia de la clase “Adapter map”.

var tableAdapter = PostsAdapterMap()

Para registrar los cell adapters a la UITableView, creamos instancias de Adapter con una celda y su modelo correspondiente:

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

Por último, la nueva implementación del método “cellForRowAtIndexPath” se verá así:

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
}

Hemos dejado de configurar celdas directamente usando pattern matching y ahora las configuramos mediante el adapter que recibirá el modelo y la celda reciclada. El adapter map usará el método apropiado para el modelo correspondiente de cara a configurar la celda actual.

Y a todo esto ¿Cómo solventa éste método el problema de la escalabilidad? Pongamos por ejemplo que necesitáramos mostrar otro tipo de mensaje en esta aplicación de chat, digamos un video, pues lo único que tendremos que hacer es crear la celda con su método de configuración, definir el modelo y añadirlo al adapter map, el cual se encargará de todo y, de este modo, no tendremos que modificar los métodos de datasource de la tabla.