Gestão Dev & TIARTIGO

Uma collection view cell to rule them all

Um dia desses o caro Vinicius estava reclamando da curva de aprendizado do UICollectionView, para quem não conhece é uma UITableViewController com esteróides.

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 SwiftSwift13 conteúdosSwift 6: Segurança na Corrida de Dados — AtoresDev (Back & Front) · jan 2025Swift no Server-Side: o crescimento do Swift além do iOSDev (Back & Front) · mai 2026IA e Machine Learning com SwiftDev (Back & Front) · out 2025Ver tudo em Dev (Back & Front) . 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. Críticas, sugestões e comentários são sempre bem vindos, é só me pingar no @diogot ou no slack do iOS Dev BR.

Doutor em Física, faz parte da comunidade de iOS desde 2009. Trabalhou em uma ad network e teve seu código perturbando os usuários com anúncios em mais de 200 milhões de devices, também ajudou as pessoas a encontrar um táxi, quando desenvolvia o app iOS da 99Taxis. Já passou por novos projetos na Movile e hoje desenvolve no Nubank. Nas horas vagas gera entropia nas organizações do CocoaHeadsSP e de AppsterdamBR

Ver perfil