UITableView + Swift

Una de las primeras cosas que aprendes a hacer en iOS es crear tablas. Es muy difícil encontrar una aplicación en el App Store que no haga uso de UITableView de una u otra forma.

En este post, me enfocaré en el punto 3, y te mostraré cómo puedes aprovechar los genéricos de Swift para mejorar el API de UITableView.

Crear una tabla con UITableView es simple:

  1. Crea la instancia
  2. Asigna el delegate y el dataSource
  3. Registra el tipo de celdas que vas a usar
lazy var tableView: UITableView = {
    let t = UITableView(frame: .zero, style: .plain)
    t.delegate = self
    t.dataSource = self
    t.register(MyCell.self, forCellReuseIdentifier: MyCell.Identifier)
}()

Luego, hay que implementar los métodos en UITableViewDataSource, principalmente los siguientes:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return items.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.Identifier, for: indexPath) as? MyCell
    
    cell.title = items[indexPath].title
    
    return cell!
}

Pueden ser muy sutiles, pero hay un par de problemas pero son puntos propensos a error que nos pueden hacer la vida de cuadritos más a delante.

  1. La definición de los identificadores de celda no es segura, puesto que no hay forma de estar seguros cual identificador (un String arbitrario) corresponde a qué tipo de celda.
  2. El API nos permite registrar un mismo tipo de celda con varios identificadores. No es práctico que un mismo tipo de celda esté asociado con varios identificadores. En el caso de que en una misma tabla necesites tener varias celdas con configuraciones diferentes, lo mejor es encapsular cada una de ellas en su propio tipo de dato.
  3. Se necesita hacer type-casting al momento de hacer un dequeue de una celda en el método del data source. En el peor de los casos, tienes que hacer force-unwrapping para retornar la celda adecuada. Y eso está mal.

Mejorando UITableView con genéricos

Una de las características más poderosas de Swift es que tiene genéricos. Es decir, puedes definir funciones y métodos, clases, etc., que funcionen con un tipo de dato cualquiera.

A continuación, los genéricos nos ayudarán a mejorar el API de UITableView.

public extension UITableView {
    private static func reuseIdentifier<T: UITableViewCell>(class _class: T.Type) -> String {
      return (Bundle(for: _class).bundleIdentifier ?? "_P3BundleIdentifier") + String(describing: _class)
    }
}

En una extensión, definí una función que toma como parámetro una clase (mientras dicha clase sea a su vez subclase de UITableViewCell), y retorna un String.

Dicha cadena es el identificador único para ese tipo de dato, y está compuesto por el identificador del Bundle que la contiene, más la representación en cadena del nombre de la clase. Cuando el proyecto está configurado correctamente, este identificador está garantizado ser único.

UITableview.reuseIdentifier(MyCell.self) // #=> "com.aprendeios.DemoAppMyCell

En la misma extensión, ahora defino el método para registrar una celda nueva:

public extension UITableView {
    public func register<T: UITableViewCell>(cellClass: T.Type) {
      register(cellClass, forCellReuseIdentifier: UITableView.reuseIdentifier(class: cellClass))
    }
    
    // ....
}

Este nuevo método recibe un tipo de dato (MyCell.self) y lo registra con la tabla (como se hace regularmente) utilizando como identificador el que se genera a partir del nombre de la clase.

Ahora se puede registrar una nueva celda de la siguiente forma:

tableView.register(cellClass: MyCell.self)

Lo único restante por hacer, es definir el método que va a hacer el dequeuing de las celdas:

public extension UITableView {
    public func dequeue<T: UITableViewCell>(for indexPath: IndexPath, type: T.Type) -> T {
      guard let c = dequeueReusableCell(withIdentifier: UITableView.reuseIdentifier(class: type), for: indexPath) as? T else {
        fatalError("Cell \(String(describing: type)) not configured correctly for reuse.")
      }
    
      return c
    }
}

Ahora, lo podemos aplicar de la siguiente forma:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    # 1
    let cell = tableView.dequeue(for: indexPath, type: MyCell.self) 
    cell.title = items[indexPath].title
    return cell
}

Al utilizar el nuevo método para hacer el dequeuing, el resultado de la celda que se obtiene ya es del tipo correcto, y no se tiene que hacer más type-casting, ni force-unwrapping de opcionales.

Conclusiones

Lo que mostré en este post, aunque parece ser bastante simple, es solamente una probada de los beneficios que Swift nos puede brindar al momento de integrarlo en el desarrollo de nuestras aplicaciones.

Un simple wrapper del API oficial de UITableView puede ayudarnos a limpiar nuestro código de manera razonable.

Ojo: esto también funciona para UICollectionView fácilmente. Checa mi implementación aquí.

DEJA UNA RESPUESTA

Please enter your comment!
Please enter your name here