Ahora que he tenido que hacer un empty view, voy a contar un poco como lo he llevado a cabo.

Cuando una tabla o una colección no tiene datos, dependendiendo del diseño, es muy común que mostremos una vista no? Estas vistas son los EmptyState de nuestras tablas o colecciones.

Mi primera necesidad es diseñar esa vista por interface builder. Para eso ya tengo esto, es una clase simple para poder hacer vistas desde interface builder e inicializarlas por código, IB… lo que tu quierah papi 🤗

No se si está bien o no, pero tardo 2 min en hacer una nueva y funciona como un tiro, lo único malo, nos añade una vista más a nuestra jerarquía de vistas. Pero tampoco es para tanto no?

Tratando de hacerlo.

La primera idea es, hazlo una vez y ni una más. ¿Que quiero decir con esto?, que no quiero tener que añadir mi EmptyStateView a ningun sitio por codigo cada vez que haga una distinta (Además de que todas sean diseñables por IB). Quiero que valga tanto para una UITableView como para una UICollectionView, bien… Y quiero meterlo por IB también, 0 código.

Lo único que quiero decidir por código es cuando mostrarla o no. Y en realidad tampoco me gustaría tener que hacer eso, pero no he encontrado nada para recibir un aviso de una tabla o una colección después del reloadData. He intentando observar el visibleCells, pero no ha sido satisfactorio 😂 así que por código. Por supuesto, si alguno teneis una idea o sabeis como se puede saber cuando se hace un reloadData, podeis decirmelo y así ni siquiera hay que llamar a la parte de enseñar o no el empty view.

Genial, pues con eso en mente, lo primero que he hecho ha sido una subclase de XibView para estos EmptyStateView. tienen una variable container: UIView? con un didSet, este llama a un bind por debajo y nos metemos como subvista de ese container, con un par de constraints para estar bien colocaditos 🍀💨.

class EmptyStateView: XibView {
    static let TAG = 9999

    @IBOutlet weak var container: UIView? {
        didSet {
            if let container = self.container {
                self.bind(to: container)
            }
        }
    }

    override func configure() {
        self.tag = EmptyStateView.TAG
        self.alpha = 0.0
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        if let container = self.container {
            self.bind(to: container)
        }
    }

    func show(animated: Bool = true) {
        if let scroll = self.container as? UIScrollView {
            scroll.isScrollEnabled = false
        }
        UIView.animate(withDuration: animated ? 0.3 : 0.0) {
            self.alpha = 1.0
        }
    }

    func hide(animated: Bool = true) {
        if let scroll = self.container as? UIScrollView {
            scroll.isScrollEnabled = true
        }
        UIView.animate(withDuration: animated ? 0.3 : 0.0) {
            self.alpha = 0.0
        }
    }

    private func bind(to view: UIView) {
        self.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(self)
        let views = ["view": self]
        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
                                                           options: [],
                                                           metrics: nil,
                                                           views: views))
        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
                                                           options: [],
                                                           metrics: nil,
                                                           views: views))
        view.addConstraint(NSLayoutConstraint(item: self,
                                              attribute: .height,
                                              relatedBy: .equal,
                                              toItem: view,
                                              attribute: .height,
                                              multiplier: 1.0,
                                              constant: 0.0))
        view.addConstraint(NSLayoutConstraint(item: self,
                                              attribute: .width,
                                              relatedBy: .equal,
                                              toItem: view,
                                              attribute: .width,
                                              multiplier: 1.0,
                                              constant: 0.0))
        view.layoutIfNeeded()
    }

}

Usémoslo

Ahora, deberíamos tener una referencia a nuestra empty view, y según la tabla tenga o no tenga datos mostrarla o no. En realidad para esto, no me gusta tener que llamar a esa vista (nuestro EmptyStateView), prefiero llamar a un método dentro de la tabla directamente.

Camarero, una de protocolos no?

Definimos un protocolo que nos diga que algo puede estar vacío, y como los nombres se me dan de culo, pues Emptyable. Después de eso, una extension de él para todo aquello que sea un UIView con el comportamiento por defecto. Y después, hacemos que tanto las tablas como las colecciones lo implementen.

Y queda esto:


protocol Emptyable {

    var emptyView: EmptyStateView? { get }

    func showEmptyView(animated: Bool)
    func hideEmptyView(animated: Bool)
}

extension Emptyable where Self: UIView {

    var emptyView: EmptyStateView? {
        return self.viewWithTag(EmptyStateView.TAG) as? EmptyStateView
    }

    func showEmptyView(animated: Bool = true) {
        self.emptyView?.show(animated: animated)
    }

    func hideEmptyView(animated: Bool = false) {
        self.emptyView?.hide(animated: animated)
    }

}

extension UITableView: Emptyable {}
extension UICollectionView: Emptyable {}


Conclusión

Asi de sencillito queda, creamos el fichero .swift y el .xib, diseñamos la vista, la metemos en el VC que esté la tabla. Por @IBOutlet le metemos la tabla como container y listo! Container example Tenemos nuestra tabla con su empty state view funcionando.