Invariante   Algumas coisas nunca mudam

Uma collection view cell to rule them all

Feliz ano novo, feliz post novo!


Um dia desses o caro Vinicius estava reclamando da curva de aprendizado do UICollectionView, para quem não conhece é uma UITableViewController com esteroides.
Você pode usar layouts customizados, transições de animações e muitas outras coisas que eu nem consigo imaginar. Por coincidência nesse mesmo dia eu estava implementando minha primeira UICollectionView em Swift. Nesse post não vou falar sobre essa classe mas de sobre suas células UICollectionViewCell.

No último mês venho brigando muito com generics (ou genéricos) e consegui montar um exemplo interessante de uso aplicado à UICollectionViewCell. Esse classe não tem um label como a UITableViewCell, apenas uma contentView, isso dificulta exemplos mais simples pois implica que 100% da vezes você vai ter que customizar as células.

A UICollectionViewCell precisa de uma ou mais UIView que vão ser adicionadas à contentView para essa customização. Então seria natural que eu uma célula genérica dependesse desse tipo:

class CollectionViewCell<View: UIView>: UICollectionViewCell {

    private(set) var customView: View

    override init(frame: CGRect)
    {
        customView = View()

        super.init(frame: frame)
        
        contentView.addSubview(customView)
        
        customView.frame = contentView.bounds
        customView.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
    }
}

Nesse caso específico não vejo necessidade de usar AutoLayout. Aqui temos algo bem simples, na criação da célula uma instância da View é criada adicionada à contentView de forma a ter sempre o seu tamanho. Para customizar essa view em collectionView(_: cellForItemAtIndexPath:) seria apenas utilizar a referência a ela em customView.

Mas se começarmos a pensar na linha do MVVM seria interessante que essa view aceitasse configuração via um ViewModel. Nesse caso teríamos um protocolo para tipos que possuem um model:

protocol HasModel {
    typealias Model
    var model: Model { get set }
}

Aqui temos um protocolo que possui um tipo associado (Associated Type), isso significa que o tipo da propriedade model pode ser diferente para cada tipo que adotar esse protocol. Entretanto isso tem alguns efeitos colaterais não relevantes para o exemplo corrente.

Vamos supor que eu não queira acessar a customView , mas passar o view model diretamente para a célula, como o modelo depende de cada view nossa célula vai passar a ter dois parâmetros, View e ViewModel. Essa classe então ficaria:

class CollectionViewCell<View: UIView, ViewModel where View: HasModel, View.Model == ViewModel>: UICollectionViewCell {

    private(set) var customView: View

    override init(frame: CGRect)
    {
        customView = View()

        super.init(frame: frame)
        contentView.addSubview(customView)

        customView.frame = contentView.bounds
        customView.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
    }

    var model: ViewModel {
        get {
            return customView.model
        }

        set(newModel) {
            customView.model = newModel
        }
    }
}

Agora a coisa fica mais interessante, note a definição do generics <View: UIView, ViewModel where View: HasModel, View.Model == ViewModel>. Ele define um tipo View que é subclasse de UIView e um tipo ViewModel, o where aplica restrições a esse tipos, o View adota o HasModel e o tipo associado Model da View é o mesmo do ViewModel.

Isso é suficiente para que qualquer UIView que adote o HasModel seja usada em uma collection view. Vamos supor que eu queira usar um UILabel para isso, uma extension de poucas linhas isso está resolvido:

extension UILabel: HasModel {

    var model: String {
        get {
            return text ?? ""
        }
        set(newModel) {
            text = newModel
            textAlignment = .Center
        }
    }
}

Para usar isso numa collection view precisamos primeiro registrar a classe da célula genérica (no viewDidLoad por exemplo):

collectionView.registerClass(CollectionViewCell<UILabel, String>.self, forCellWithReuseIdentifier: reuseIdentifier)

E então configurar a célula:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)

    if let cell = cell as? CollectionViewCell<UILabel, String> {
        let max = collectionView.numberOfItemsInSection(indexPath.section)
        cell.model = "Cell \(indexPath.row+1)/\(max)"
    }

    return cell
}

Um exemplo completo pode ser encontrado no repositório GenericCollectionViewCell. Criticas, sugestões e comentários são sempre bem vindos, é só me pingar no @diogot ou no slack do iOS Dev BR.


Diogo Tridapalli
@diogot