Come si mette in relazione una TableView ed il database Realtime di Firebase?

Creare e popolare una TableView con il linguaggio Swift è un passaggio relativamente semplice. Tutto si complica quando il riempimento della tabella deve avvenire in seguito al download asincrono di un contenuto (Ho parlato dei concetti di asincronia e thread in questo articolo).

Oggi te ne voglio parlare con esempio applicato al framework di Firebase.

Quello che noi faremo è relativamente semplice (a parole). L’app si connetterà al database realtime di Firebase e scaricherà una lista di elementi. Contemporaneamente all’iterazione degli elementi della lista, li trasformerà in oggetti malleabili con il linguaggio Swift e li aggiungerà alla TableView.

firebase-databaserealtime-e-tableview-ios-linguaggio-swift

Quindi, poco spazio alle parole, sei pronto?
Allora vediamo insieme come gestire una TableView ed il database Realtime di Firebase!

Il progetto

La nostra applicazione d’esempio è un raccoglitore di figure professionali. L’interfaccia è composto da un UIViewController che ha, al suo interno, una UITableView con una Prototype Cell con Style Subtitle. Il ViewController, infine, è collegato ad un UINavigationController (usato solo per ottenere la top bar).

tableview-ios-linguaggio-swift

Uno specialista è rappresentato da una struct che ci permetterà di gestire meglio la mole di dati trasferita da e verso Firebase. Quindi, assicurati di avere una struct o classe del genere:

struct Specialist {
    var name: String
    var profession: String
}

Per quanto riguarda la gestione della tabella, nel ViewController, crea un array vuoto del tipo [Specialist], crea la outlet tra l’interfaccia e la TableView, definisci il DataSource ed aggiungi i relativi metodi. Questo è il codice che dovresti avere:

import UIKit
import FirebaseDatabase

class ViewController: UIViewController, UITableViewDataSource {
    
    var specialists: [Specialist] = []


    @IBOutlet var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.tableView.dataSource = self
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.specialists.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        let user = self.specialists[indexPath.row]
        
        cell.textLabel?.text = user.name
        cell.detailTextLabel?.text = user.profession
        
        return cell
    }

}

In fondo alla pagina trovi il download del progetto completo.

Non dimenticare di importare il file GoogleService-Info.plist, di sbloccare la lettura/scrittura dal Database Realtime e tutte le altre sfaccettature che ho spiegato nel tutorial introduttivo al Database Realtime di Firebase.

Una volta definita la struttura dell’app, quella su Firebase sarà un semplice nodo con sotto nodi generati dinamicamente (childByAutoId) i quali conterranno i dati del professionista. Quindi, se vuoi seguire per filo e per segno il tutorial, assicurati di avere una struttura simile a questa:

struttura-database-realtime-firebase

Una volta definita la base dei nostri dati, vediamo come scaricarli ed inserirli nella tabella.

Download con observe(.value)

Procediamo con ordine ed analizziamo i vari sistemi con cui potrai scaricare i dati da Firebase. Il primo è indubbiamente quello di utilizzare un FIRDataEventType di tipo value.

Ti ricordo che un observer, di tipo .value, scarica il contenuto dell’intero nodo in cui si attacca. Il download è in differita. Ovvero il download avviene in asincrono, su una queue in background, ed il dato viene conservato in un oggetto di tipo FIRDataSnapshot restituito dalla closure (o per chi proviene dal web, una callback).

let root = FIRDatabase.database().reference()

root.child("specialists").observe(.value, with: { (snap) in
   // eseguito sulla DispatchQueue.main al termine del download
})

Quindi, il download del contenuto avviene su una queue di background, mentre la closure viene invocata sul main thread.

Se questi concetti ti sembrano nuovi, dai un’occhiata al mio tutorial sul Grand Central Dispatch prima di procedere.

Dato che l’observer con event type value restituisce tutto il contenuto dei nodi e sotto nodi, leggendo sul nodo specialists, dovremmo aver scaricato un dizionario. Un dizionario cui:

  • la chiave è la stringa univoca che rappresenta il sotto nodo
  • il valore associato è il dizionario con chiave name e profession

Il download avviene per intero, quindi, è lecito pensare che il passaggio dei dati in tabella debba avvenire tutto in una volta.  Questo significa che dovrai creare un array temporaneo, del tipo Specialist, a cui andrai ad inserire tutti i dati estrapolati dal dizionario scaricato.

Una volta completato questo passaggio, potrai aggiornare la tabella dal DispatchQueue.main.async:

root.child("specialists").observe(.value, with: { (snap) in
    // eseguito sulla main queue non appena finito il download
    
    // converto il contenuto dello snap nel dato che lo rappresenta, cioè un Dizionario con chiave String e valore Any
    guard let dictionary = snap.value as? [String : Any] else {
        return
    }
    
    // creo un array temporaneo per la conversione del dizionario in Specialist
    var specialistArray: [Specialist] = []
    
    for element in dictionary { // itero il dizionario
        
        guard
            let value = element.value as? [String : String], // converto il valore in un dizionario di String:String
            let name = value["name"], // estrapolo il nome
            let profession = value["profession"] // estrapolo la professione
        else { return }
        
        let specialist = Specialist(name: name, profession: profession) // creo un oggetto Specialist
        specialistArray.append(specialist) // lo passo all'array temporaneo
        
    }
    
    self.specialists = specialistArray // assegno l'array temporaneo all'array utilizzato dalla tabella
    self.tableView.reloadData() // aggiorno la tabella
    
})

firebase-e-tableview-ios-linguaggio-swift

Perché tutti quei guard? (se non ti ricordi cos’è, leggi questo mia lezione sul guard)

É sempre un bene estrapolare i valori scaricati utilizzando un’istruzione di tipo guard o if let, anche se a monte conosci già la struttura dei dati. Perché?

Se in futuro dovessi aggiornare la struttura del database, gli utenti che utilizzeranno una versione non ancora aggiornata a quel modello, non avranno crash ma un semplice download non completato (la pagina rimarrà bianca).

Una volta passato l’array temporaneo all’array che hai dato in pasto alla tabella, ricordati di invocare il reloadData() sulla tableView. Questo è un passaggio necessario in quanto richiamerà tutti i metodi per la gestione della tabella (numberOfRowInSection e cellForRowAtIndexPath).

[mailmunch-form id=”101287″]

Nel caso dovessi avere dei problemi, non esitare a chiedere. Chi fa domande è sempre avanti a chi non le fa ;) 

Download con observe(.childAdded)

Spesso, i dati da leggere, vengono modificati continuamente da altri utenti o da noi stessi. Pensa per esempio ad una chat, i dati potrebbero venir tirati fuori uno dietro l’altro nell’arco di poco tempo. Di conseguenze, l’utilizzo di un observer .value è poco consigliato in quanto, qualsiasi modifica al nodo osservato, comporterebbe il download dell’interno blocco.

In questi casi, o in tutti i casi in cui si aspetta l’aggiornamento di un nodo con un nuovo dato, conviene utilizzare l’observe con FIRDataEventType di tipo .childAdded.

L’observe di tipo .childAdded verrà invocato:

  • Al primo avvio per N volte quanti sono i nodi presenti (un po’ come se fossero stati inseriti in quel momento ed uno dopo l’altro)
  • Ogni qual volta in cui verrà aggiunto un nuovo nodo al nodo osservato

Di conseguenza, il FIRDataSnapshot attaccato al nodo specialists, conterrà il dizionario che ha per chiave name e profession. 

root.child("specialists").observe(.childAdded, with: { (snap) in

    // estraggo i dati del professionista
    guard
        let dictionary = snap.value as? [String : String],
        let name = dictionary["name"],
        let profession = dictionary["profession"]
    else { return }
    
    let specialist = Specialist(name: name, profession: profession)
        
    print(specialist)
})

Prima di farti vedere il codice completo, ragioniamo un attimo su quello che accade qui.

L’observe .childAdded, al primo avvio, legge uno per uno tutti i sotto nodi presenti nel nodo specialists. Per cominciare, quindi, lo snap conterrà solamente il sotto nodo “Giuseppe Sapienza“. Dato che ci sono più sotto nodi, una volta completata la prima lettura, verrà invocato un ulteriore observe sul nodo successivo. Continuerà così fin quando non finiranno tutti i sotto nodi.

In pratica, si comporta come una sorta di ciclo for che itera un array. Il contenuto della closure (cioè lo snap) rappresenta l’elemento iterato.

Per questo motivo, è logico pensare che l’inserimento del dato in tabella debba avvenire singolarmente. Dunque, ogni qual volta in cui l’observe tirerà fuori un nuovo nodo, dovrai convertire immediatamente il dato ed inserirlo in tabella. 

L’inserimento di un dato in maniera singola o per range, all’interno di una tabella, si esegue utilizzando la funzione insertRows il quale vuole come parametro un array di IndexPath (cioè di indici) e l’animazione da utilizzare. Questo metodo, deve essere inserito tra le chiamate dei metodi beginUpdates() ed endUpdates() invocati sulla tabelle, i quali eviteranno l’accesso multiplo alle risorse (ti basta sapere che si deve fare così :P ).

Prima di invocarlo, però, è necessario aggiungere il nuovo dato nell‘array utilizzato dalla tabella. Il motivo è che l’insertRows si comporta come un reloadData però invocato solo sugli indici passati. Quindi, se non c’è l’elemento nell’array, i metodi cellForRowAtIndexPath e numberOfRowsInSection genereranno un errore.

root.child("specialists").observe(.childAdded, with: { (snap) in

    // estraggo i dati del professionista
    guard
        let dictionary = snap.value as? [String : String],
        let name = dictionary["name"],
        let profession = dictionary["profession"]
    else { return }

    let specialist = Specialist(name: name, profession: profession)
        
    self.specialists.append(specialist) // aggiunto l'elemento all'array della tabella prima dell'aggiornamento
        
    let count = self.specialists.count // prendo il numero di elementi in tabella
    let indexPath = IndexPath.init(row: count-1, section: 0) // genero l'indexPath in cui andrà il nuovo elemento
        
    self.tableView.beginUpdates() // necessario prima dell'invocazione dell'insertRows
    self.tableView.insertRows(at: [indexPath], with: .left) // aggiunto una nuova riga alla tabella all'indexPath con animazione da left verso right
    self.tableView.endUpdates() // necessario subito dopo l'invocazione dell'insertRows
    
})

Considerazioni

Ho voluto mostrarti i metodi nudi e crudi per avvicinarsi a qualcosa di che nella sua banalità logica – del resto, che ci voleva? – ha qualcosa di molto complesso alle spalle.

Il passo sicuramente successivo è quello di wrappare questi metodi in ulteriori metodi presenti nelle tue classi che gestiscono, in maniera centralizzata, tutta la parte di download dell’applicazione. Ma questo dovrebbe venirti molto più semplice una volta capito il sistema che sta alla base dei metodi di Firebase.

A questo punto, dato che già dovresti avere le basi sull’uso del database realtime, ti indirizzo verso l’integrazione del login con Firebase.

Buona Programmazione!

Download del progetto

Trovi il download del seguente progetto, una volta sbloccato il seguente modulo:

[sociallocker]

Una volta scaricato, dato che non porta con sé i pod, dovrai eseguire un pod update dalla console sulla cartella del progetto. Ricordati anche di inserire il tuo file GoogleService-info.plist

Download Progetto

[/sociallocker]

Changelog

  • 29/03/2017 – Aggiornate alcune parti del tutorial.
  • 29/03/2017 – Prima versione del tutorial.