UICollectionView y UICollectionViewCell en UIKit con Swift en Español - Curso iOS

SwiftBeta

Tabla de contenido

Aprende a crear UICollectionView en UIKit con Swift

Hoy en SwiftBeta vamos a ver otra vista muy usada al crear aplicaciones iOS. Es muy común usar UICollectionView cuando usamos el framework UIKit. Esta vista nos permite representar datos en un Grid. Ya vimos que con la vista UITableView podemos representar una colección de datos, pero una de las principales diferencias entre UITableView y UICollectionView, es que usamos las primeras para representar un listado, mientras que con UICollectionView entra en juego un elemento clave llamado Layout, con él podemos customizar el grid que queremos mostrar. Es decir, podemos especificar cuántas columnas queremos que aparezcan en nuestro grid del UICollectionView, qué espacio queremos entre filas, espacio entre los items del Grid, etc, pero no te preocupes que todo esto lo veremos en el post de hoy!

Creamos el proyecto en Xcode

Lo primero de todo que vamos hacer es crear el proyecto en Xcode. Acuérdate que estamos usando UIKit, es por eso que debes seleccionar Storyboard en Interface.

Una vez hemos creado el proyecto nos vamos al ViewController. Allí dentro vamos a crear nuestra primera instancia de UICollectionView.

import UIKit

class ViewController: UIViewController {
    
    private let swiftBetaCollectionView: UICollectionView = {
        let collectionView = UICollectionView()
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        swiftBetaCollectionView.backgroundColor = .blue
        view.addSubview(swiftBetaCollectionView)
        
        NSLayoutConstraint.activate([
            swiftBetaCollectionView.topAnchor.constraint(equalTo: view.topAnchor),
            swiftBetaCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            swiftBetaCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            swiftBetaCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}
Creamos un UICollectionView por código

Vamos a compilar a ver qué ocurre. Si compilamos tenemos el siguiente error:

Thread 1: "UICollectionView must be initialized with a non-nil layout parameter"
Error que nos aparece al compilar

Esto es justo lo que os comentaba al empezar, ahora con los UICollectionView tenemos un elemento clave que es el Layout que queremos de nuestro Grid, debemos especificarlo, y para hacerlo podemos utilizar una instancia de la clase UICollectionViewLayout. Tan solo debemos modificar la línea donde instanciamos el UICollectionView

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
Añadimos como parámetro la instancia UICollectionViewLayout

De esta manera, si compilamos otra vez, vemos como no crashea nuestra aplicación y el UICollectionView ocupa toda la pantalla del device (lo sabemos por que hemos indicado que tenga un backgroundColor de blue).

Creamos la estructura de datos a mostrar en nuestra UICollectionView

Ahora tenemos la vista, nuestro UICollectionView, pero nos faltan datos para poder representar de forma visual en nuestro UICollectionView. Para ello, vamos a crear una struct llamada Device:

struct Device {
    let title: String
    let imageName: String
}
Creamos estructura de datos llamada Device

Ahora que ya tenemos la struct, vamos a crear un array con información:

let house = [
    Device(title: "Laptop", imageName: "laptopcomputer"),
    Device(title: "Mac mini", imageName: "macmini"),
    Device(title: "Mac Pro", imageName: "macpro.gen3"),
    Device(title: "Pantallas", imageName: "display.2"),
    Device(title: "Apple TV", imageName: "appletv")
]
Creamos un array de tipo Device

La constante house contiene un array que va a representar información de los devices que tengo en casa. Cada elemento de este Array será un item en nuestro UICollectionView, ¿pero cómo hacemos para conectar los datos con la vista? Para hacerlo necesitamos usar la propiedad dataSource de nuestro UICollectionView.

Conformamos el protocolo UICollectionViewDataSource

La propiedad dataSource del UICollectionView espera que se le asigne una instancia de una clase, y que esa clase conforme el protocolo UICollectionViewDataSource, ¿por qué? porque al conformar este protocolo es obligatorio implementar un mínimo de métodos y al implementarlos la vista puede mostrar la información de cada item (de cada celda) que se mostrará en nuestro UICollectionView.

Si nosotros especificamos que nuestro ViewController conforme el protocolo UICollectionViewDataSource, podemos asignarle self a la propiedad dataSource del UICollectionView. Pero si compilamos el siguiente código ¿qué ocurre?

struct Device {
    let title: String
    let imageName: String
}

let house = [
    Device(title: "Laptop", imageName: "laptopcomputer"),
    Device(title: "Mac mini", imageName: "macmini"),
    Device(title: "Mac Pro", imageName: "macpro.gen3"),
    Device(title: "Pantallas", imageName: "display.2"),
    Device(title: "Apple TV", imageName: "appletv")
]

class ViewController: UIViewController, UICollectionViewDataSource {
    
    private let swiftBetaCollectionView: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        swiftBetaCollectionView.backgroundColor = .blue
        swiftBetaCollectionView.dataSource = self
        view.addSubview(swiftBetaCollectionView)
        
        NSLayoutConstraint.activate([
            swiftBetaCollectionView.topAnchor.constraint(equalTo: view.topAnchor),
            swiftBetaCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            swiftBetaCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            swiftBetaCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}

Falla por que no estamos implementando los métodos que te comentaba hace un momento, el compilador se queja de que no estamos implementando los métodos que son obligatorios sí o sí al conformar UICollectionViewDataSource. Si arreglamos los fallos que nos comenta Xcode, nos aparecen dos métodos nuevos

Error al no implementar los métodos de UICollectionViewDataSource
Error al no implementar los métodos de UICollectionViewDataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        <#code#>
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        <#code#>
    }
Métodos de UICollectionView que vamos a implementar
  • El primer método espera el número de items que queremos mostrar en nuestro UICollectionView (para que quede claro, el número de celdas)
  • El segundo método espera cómo representar visualmente la información en una celda de nuestro UICollectionView

Vamos a rellenar estos dos métodos, pero ¿por qué solo estos dos? estos son los obligatorios que debemos rellenar si conformamos el protocolo UICollectionViewDataSource. Si navegamos hasta este protocolo podemos ver todos los métodos que podríamos implementar en nuestra clase ViewController.
Spoiler: veremos algunos de estos más adelante en el video.

Rellenamos método numberOfItemsInSection

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        <#code#>
    }
Número de items en cada sección

Debemos dar un valor del número de elementos que queremos mostrar en nuestro UICollectionView. Este es muy sencillo, bastaría con poner el número de elementos de nuestro Array house:

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        house.count
    }
Retornamos el número de elementos de nuestro Array house

Rellenamos información de la celda UICollectionViewCell

Vamos a ver el segundo método que debemos rellenar:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        <#code#>
    }
Método que retorna la representación visual de nuestros datos, es decir, la celda

Aquí, lo que debemos hacer es preguntar a nuestro UICollectionView qué mostrar por cada elemento de nuestra constante house (el Array de devices que hemos creado hace uno momento). Y para ello nuestro UICollectionView nos retorna una celda vacía y la tenemos que rellenar con la información que queremos. Un detalle importante es que las celdas que no están en pantalla, se liberan para no ocupar memoria, es por eso que siempre las tenemos que setear con los valores que queremos.

De momento vamos hacer algo muy simple. Vamos a registrar la vista UICollectionViewCell en nuestra instancia de UICollectionView. Esta celda es una muy sencilla que ya viene dentro del framework UIKit pero que sepas que más adelantes crearemos una celda propia (con las subvistas que queremos). Seguimos, justo debajo de donde asignamos el backgroundColor de nuestro UICollectionView, podemos añadir el registro de la celda que usaremos:

swiftBetaCollectionView.backgroundColor = .blue
swiftBetaCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
Registramos la celda que usaremos en nuestro UICollectionView

Y una vez hemos registrado esta celda, podemos rellenar el método de la siguiente manera:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for: indexPath)
        cell.backgroundColor = .red
        return cell
    }
Rellenamos el método cellForItemAt indexPath

Si ahora compilamos, vemos que no ocurre nada. Sigue mostrándose el UICollectionView pero no aparece ningún item en él, ¡y eso que hemos conformado el protocol UICollectionViewDataSource!. ¿Qué ocurre? debemos especificar unas propiedades del Layout de nuestro UICollectionView. Justo en esta línea instanciamos UICollectionViewLayout, pero no hacemos nada más:

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())

Vamos a ver cómo lo arreglamos. Debemos crear una constante que guarde una instancia de UICollectionViewFlowLayout, y vamos a asignarle un tamaño a todos los items que queremos que se muestren en nuestro UICollectionView:

    private let swiftBetaCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = .init(width: 200, height: 200)
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()
Damos un valor a cada item de nuestro UICollectionView

Si ahora compilamos, vemos que aparecen 5 items en nuestro UICollectionView!

Resultado que obtenemos al compilar nuestro código
Resultado que obtenemos al compilar nuestro código

Un dato interesante, es que podemos especificar si queremos un UICollectionView horizontal o vertical, tan solo debemos cambiar una propiedad de nuestra instancia de UICollectionViewFlowLayout. Una de las diferencias principales es que en lugar de hacer un scroll vertical, lo tendrás que hacer de forma horizontal para poder desplazarte por el UICollectionView

        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
Cambiamos el scrollDirection de nuestro UICollectionViewFlowLayout
Cambiamos la orientación de nuestro UICollectionViewFlowlayout
Cambiamos la orientación de nuestro UICollectionViewFlowlayout

También podemos modificar estas dos propiedades:

  • minimumLineSpacing, mínimo espacio entre líneas del Grid
  • minimumInteritemSpacing, mínimo espacio entre items de la misma fila del Grid

Es decir, vamos a modificar estas dos propiedades por separado, así vemos exactamente el cambio:

private let swiftBetaCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = .init(width: 200, height: 200)
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 200
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()
Modificamos la propiedad minimumLineSpacing
Resultado al modificar la propiedad minimumLineSpacing de nuestro UICollectionViewFlowLayout
Resultado al modificar la propiedad minimumLineSpacing de nuestro UICollectionViewFlowLayout

Vemos que se ha incrementado el espacio entre las filas. Y por último, vamos a ver la siguiente propiedad que podemos modificar:

private let swiftBetaCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = .init(width: 200, height: 200)
        layout.scrollDirection = .horizontal
        layout.minimumInteritemSpacing = 200
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()
Modificamos la propiedad minimumInteritemSpacing
Resultado al modificar la propiedad minimumInteritemSpacing de nuestro UICollectionViewFlowLayout
Resultado al modificar la propiedad minimumInteritemSpacing de nuestro UICollectionViewFlowLayout

En este caso vemos como la separación entre items se ha incrementado. Para continuar con el post, vamos a borrar estas dos propiedades minimumLineSpacing y minimumInteritemSpacing solo quería comentarlas para que veas que se puede customizar nuestro UICollectionView

¡Vamos a continuar! Ahora que ya hemos conseguido mostrar el número de items en nuestro UICollectionView, vamos a mostrar la información correcta (en lugar de simples cuadrados rojos)

Rellenamos información de la celda UICollectionViewCell

Para poder mostrar la información de cada item, vamos a cambiar el método cellForItemAt (tal y como vimos en el post de UITableView)

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for: indexPath)
        cell.backgroundColor = .red
        let model = house[indexPath.row]
                
        var listContentConfiguration = UIListContentConfiguration.cell()
        listContentConfiguration.text = model.title
        listContentConfiguration.image = UIImage(systemName: model.imageName)
        
        cell.contentConfiguration = listContentConfiguration
        return cell
    }
Añadimos un UIListContentConfiguration a nuestro UICollectionView

Si ahora compilamos, vemos como aparece toda la información por cada item que se muestra en nuestro UICollectionView. ¡Pero! lo que vamos hacer ahora, es que en lugar de utilizar un UICollectionViewCell, vamos a crear nuestra propia vista.

Al compilar vemos como se muestra la información en cada celda
Al compilar vemos como se muestra la información en cada celda

Creamos una custom UICollectionViewCell

Pulsamos CMD+N y creamos un fichero nuevo, en el template escogemos Cocoa Touch Class

Creamos una Cocoa Touch Class en Xcode

Vamos a crear una clase que sea una subclase de UICollectionViewCell

Sobretodo, que sea una subclase de UICollectionViewCell
Sobretodo, que sea una subclase de UICollectionViewCell

Le damos a Next y una vez creada ya podemos añadir las vistas que queremos en nuestra nueva celda que representará la información del Item.
Vamos a crear una serie de subvistas, y ya que vimos en el anterior video los StackViews, vamos a crear uno y así evitamos añadir algunas constraints a mano.

import UIKit

class SwiftBetaCollectionViewCell: UICollectionViewCell {
    private let swiftBetaStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.layer.borderColor = UIColor.black.cgColor
        stackView.layer.borderWidth = 1
        return stackView
    }()
    
    private let deviceImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    private let deviceNameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 24)
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        
        addSubview(swiftBetaStackView)
        swiftBetaStackView.addArrangedSubview(deviceImageView)
        swiftBetaStackView.addArrangedSubview(deviceNameLabel)
        
        NSLayoutConstraint.activate([
            swiftBetaStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            swiftBetaStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
            swiftBetaStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(model: Device) {
        deviceImageView.image = UIImage(systemName: model.imageName)
        deviceNameLabel.text = model.title
    }
}
Creamos la nueva celda, será muy parecida a la anterior

Fíjate que al final hemos añadido una función, un método para poder rellenar la información de las subvistas. Es decir, la clase que cree esta celda llamará al método configure(model:) y le pasará los valores necesarios para que se pueda configurar. En este caso el nombre de la imagen y un título.

Lo siguiente que vamos hacer, es modificar la celda que se está mostrando por cada item de nuestro UICollectionView.

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SwiftBetaCollectionViewCell", for: indexPath) as! SwiftBetaCollectionViewCell
        cell.backgroundColor = .red
        let model = house[indexPath.row]
                
        cell.configure(model: model)
        return cell
    }
Usamos la nueva celda para que aparezca en nuestro UICollectionView

Muy importante registrar la celda, nos vamos a la propiedad donde cambiábamos el backgroundColor de nuestro UICollectionView y añadimos la siguiente línea:

        swiftBetaCollectionView.backgroundColor = .blue
        swiftBetaCollectionView.register(SwiftBetaCollectionViewCell.self, forCellWithReuseIdentifier: "SwiftBetaCollectionViewCell")
Registramos la nueva celda

Lo siguiente que vamos hacer es modificar nuestra instancia de UICollectionViewFlowLayout y vamos a modificar el tamaño, en lugar de:

        layout.itemSize = .init(width: 200, height: 200)

vamos hacerlo más pequeño, sustituimos el height por 60

        layout.itemSize = .init(width: 200, height: 60)

Y por último, vamos a modificar la top constraint de nuestro UICollectionView:

            swiftBetaCollectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),

De esta manera evitaremos que la primera celda aparezca detrás del notch del iPhone. ¡Y ahora compilamos para ver cómo se ve en el simulador!

Simulador mostrando nuestro UICollectionView
Simulador mostrando nuestro UICollectionView

Vamos a añadir más información al Array de nuestra constante house, así veremos mejor como se comporta nuestro UICollectionView.

Simulador mostrando nuestro UICollectionView con muchos más elementos
Simulador mostrando nuestro UICollectionView con muchos más elementos

Conclusión

Hoy hemos aprendido a cómo crear un UICollectionView en UIKit. Hemos creado una instancia de UICollectionFlowLayout, y hemos asignado varias propiedades para poder customizar el Grid.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple