Skip to content
Fancy Pixel Fancy Pixel
Torna al blog

Condividere dati tra WatchKit e la tua app con Realm

Condividere il database della tua app iOS con la sua estensione WatchKit usando Realm e gli app groups. Una piccola app TODO open source come scusa per mettere insieme i pezzi.

Andrea Mazzini 7 min di lettura

Versione italiana dell’articolo originale del 29 marzo 2015; traduzione a cura del team.

È stato un anno intenso per gli sviluppatori iOS. Abbiamo avuto a disposizione un’enormità di nuovi giocattoli: una nuova versione di iOS, nuovi framework, un nuovo linguaggio, nuove dimensioni di schermo e dell’hardware nuovo da metterci al polso — proprio quando pensavamo che gli orologi fossero anacronistici. La tecnologia nuova è sempre entusiasmante, ma stare al passo è un compito tosto quando le deadline incalzano. Ultimamente mi sono preso un po’ di tempo per approfondire questi temi e tirar fuori una piccola app (che pensiamo di rilasciare open source). Mentre scrivo l’app è ancora in attesa di review, quindi ci concentriamo su una cosa che ho imparato lungo la strada e che può essere utile a molti altri dev iOS: come condividere dati tra la tua app e la sua estensione WatchKit, usando Realm.

Perché Realm

Realm è un’ottima alternativa sia a SQLite che a Core Data, e offre un modo rapido e indolore per persistere i dati nella tua app iOS (e Android). La cosa che lo rende così figo è la facilità d’uso: riduce il boilerplate quasi a zero (ti guardo, Core Data), mantenendo le cose a un buon livello di astrazione (ti guardo, SQLite) e… è veloce. È davvero un gran pezzo di software, e ti consiglio di dare un’occhiata alla documentazione ufficiale se ti serve un framework di persistenza per la tua app.

L’app di esempio

Impareremo a condividere dati tra un’app e la sua estensione WatchKit, e quale modo migliore se non con un’app di esempio? Non sono un fan delle demo di app TODO, mi sembrano sempre… senza fantasia… ma devo ammettere che sono perfette per un task come questo. Quindi abbi pazienza: ti presento un’altra TODO app di esempio: Done!, una semplice lista in cui aggiungere item e marcarli come fatti dal nostro Apple Watch.

Setup di Realm

Le estensioni WatchKit sono semplicemente un altro target all’interno della tua app, esattamente come le Today extension: vengono impacchettate insieme all’app ma hanno vita propria, e soprattutto un proprio bundle identifier. Questo significa che i dati salvati dall’app non sono visibili all’estensione e viceversa. Apple offre un modo per risolvere il problema con gli app group. Gli app group, come i bundle identifier, sono definiti da un reverse URI e hanno il prefisso group.. Permettono di condividere dati tra app e sono strettamente legati al provisioning profile (per fortuna Xcode gestisce la loro creazione nel developer portal con facilità). Possiamo sfruttarli per creare il nostro database Realm in uno spazio condiviso, visibile (e scrivibile) sia dall’app che dall’estensione WatchKit. Iniziamo creando il group nella sezione capabilities del progetto:

App groups

Tolto questo, possiamo dire a Realm di salvare il database dentro il group. Lo facciamo nella application(application: didFinishLaunchingWithOptions launchOptions:) del nostro AppDelegate:

let directory: NSURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.it.fancypixel.Done")!
let realmPath = directory.path!.stringByAppendingPathComponent("db.realm")
RLMRealm.setDefaultRealmPath(realmPath)

Ed ecco: Realm è pronto, dobbiamo solo definire un model da persistere:

// Entry.swift
import Realm

class Entry: RLMObject {
  dynamic var title = ""
  dynamic var completed = false
}

ViewController

Il ViewController che gestisce la TODO list sarà piuttosto semplice: una UITableView con un header custom che presenta la UITextField per l’input dell’utente. Nulla di sofisticato:

// ViewController.swift
import Realm
import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {

    @IBOutlet var tableView: UITableView!
    var dataSource: RLMResults!

    override func viewDidLoad() {
        super.viewDidLoad()
        reloadEntries()
    }

    func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let container = UIView(frame: CGRectMake(0, 0, self.view.frame.size.width, 60))
        let textField = UITextField(frame: CGRectMake(10, 10, self.view.frame.size.width - 20, 40))
        textField.delegate = self
        textField.textColor = UIColor.whiteColor()
        let placeholer = NSAttributedString(string: "Add an item", attributes: [NSForegroundColorAttributeName: UIColor.lightGrayColor()])
        textField.attributedPlaceholder = placeholer
        container.addSubview(textField)
        return container
    }

    func reloadEntries() {
        dataSource = Entry.allObjects()
        self.tableView.reloadData()
    }

    func textFieldShouldReturn(textField: UITextField) -> Bool {
        textField.resignFirstResponder()

        let realm = RLMRealm.defaultRealm()
        realm.beginWriteTransaction()
        let entry = Entry()
        entry.title = textField.text
        entry.completed = false
        realm.addObject(entry)
        realm.commitWriteTransaction()
        reloadEntries()
        return true
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return Int(dataSource.count)
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell
        let entry = dataSource[UInt(indexPath.row)] as Entry
        cell.textLabel!.text = entry.title
        cell.accessoryType = entry.completed ? .Checkmark : .None
        return cell
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let entry = dataSource[UInt(indexPath.row)] as Entry
        let realm = RLMRealm.defaultRealm()
        realm.beginWriteTransaction()
        entry.completed = !entry.completed
        realm.commitWriteTransaction()
        reloadEntries()
    }
}

Come vedi, recuperiamo gli item della table view da Realm con una semplice chiamata:

Entry.allObjects()

e salviamo le modifiche con un paio di righe:

realm.beginWriteTransaction()
let entry = Entry
entry.title = textField.text
entry.completed = false
realm.addObject(entry)
realm.commitWriteTransaction()

Se hai lavorato con Core Data in passato, ora dovresti capire meglio cosa intendo con “meno boilerplate”.

WatchKit

Ok, nuova tecnologia, nuovo framework, c’è tanto da imparare, vero? A quanto pare, Apple ci ha pensato: per quel che ho visto, sviluppare un’app per Watch sembrerà familiare a molti dev Cocoa e Cocoa Touch, anche se con alcuni cambiamenti filosofici significativi. Per prima cosa creiamo un nuovo target per l’app Watch:

WatchKit target

Poi il layout. Non useremo Autolayout, ma qualcosa che ricorda una versione semplificata dei linear e relative layout di Android (senza dover toccare grossi file XML però: gli storyboard sono ancora qui). Il layout sarà una WKInterfaceTable con un TableRowController custom, che conterrà una WKInterfaceImage e una WKInterfaceLabel:

WatchKit storyboard

Il TableRowController custom è una semplice classe con un paio di property, partiamo da lì:

class EntryTableRowController: NSObject {
    @IBOutlet var imageCheck: WKInterfaceImage!
    @IBOutlet var textLabel: WKInterfaceLabel!
}

Lineare: pensalo come l’equivalente di una UITableViewCell custom. Vediamo come popolare la table:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)

    let directory: NSURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.it.fancypixel.Done")!
    let realmPath = directory.path!.stringByAppendingPathComponent("db.realm")
    RLMRealm.setDefaultRealmPath(realmPath)
    reloadTableData()
}

func reloadTableData() {
    let realm = RLMRealm.defaultRealm()
    let dataSource = Entry.allObjects()

    watchTable.setNumberOfRows(Int(dataSource.count), withRowType: "EntryRow")

    for index in 0..<Int(dataSource.count) {
        let entry = dataSource[UInt(index)] as Entry
        if let row = watchTable.rowControllerAtIndex(index) as? EntryTableRowController {
            row.textLabel.setText(entry.title)
            let imageName = entry.completed ? "check-completed" : "check-empty"
            row.imageCheck.setImageNamed(imageName)
        }
    }
}

Un po’ diverso da quello a cui siamo abituati con UIKit. Niente delegatedataSource: in WatchKit definiamo esplicitamente ogni row prima di mostrare la table. Inoltre, come vedi nella awakeWithContext, stiamo settando Realm come avevamo fatto nell’app principale. Nota a margine: i pezzi di codice condivisi tra i due target (nel nostro caso tutto ciò che riguarda il model) andrebbero spostati in un framework custom per evitare duplicazione. Per questo sample ho preferito duplicare l’inizializzazione di Realm e condividere il model tra i due target, ma in un’app più strutturata sceglierei sicuramente la strada del framework. Se buildiamo e lanciamo, possiamo aggiungere un item sul telefono e, dopo aver ricaricato l’app, lo vedremo anche nell’estensione watch. Non si sente molto responsive, però, vero? Lo sistemiamo a breve, ma prima aggiungiamo la possibilità di marcare un item come done dal watch:

override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
    let dataSource = Entry.allObjects()
    let entry = dataSource[UInt(rowIndex)] as Entry
    let realm = RLMRealm.defaultRealm()
    realm.beginWriteTransaction()
    entry.completed = !entry.completed
    realm.commitWriteTransaction()
    reloadTableData()
}

Più o meno è tutto. Ora è ancora più evidente che, anche se le due app comunicano, manca un vero senso di interazione: dobbiamo ricaricare l’app per vedere i cambiamenti.

Sincronizzare i dati

Dobbiamo in qualche modo segnalare che la nostra watch app o l’app principale hanno fatto qualcosa di significativo. Ci sono modi ufficiali per farlo, e puoi leggerne di più in questo post di Natasha The Robot, ma in questo caso voglio mostrarti come una brillante libreria chiamata MMWormHole possa astrarre questo compito per noi.

MMWormHole

MMWormhole crea un ponte tra un’app e le sue estensioni. L’API è chiara e concisa, e il nome è deliziosamente nerd. Funziona come un sistema pub/sub: un’estremità registra interesse per un certo tipo di messaggio (identificato da una stringa), e l’altra può fare broadcast di un nuovo evento. Quando l’evento viene generato, l’handler del subscriber si attiva. Sembra qualcosa che possiamo sfruttare nella nostra app. Partiamo registrando interesse per gli aggiornamenti watch sull’app principale:

self.wormhole.listenForMessageWithIdentifier("watchUpdate", listener: { (_) -> Void in
    self.reloadEntries()
})

e nella watch app ascolteremo gli aggiornamenti main:

self.wormhole.listenForMessageWithIdentifier("mainUpdate", listener: { (_) -> Void in
    self.reloadTableData()
})

Tutto qui. Quando qualcosa cambia nel nostro model, basta chiamare queste funzioni:

// From the watch
self.wormhole.passMessageObject("update", identifier: "watchUpdate")

// From the main app
self.wormhole.passMessageObject(someObject, identifier: "mainUpdate")

Realm notifications

Update

Una volta che il post è andato live, Tim Anglade di Realm mi ha contattato e ha avuto la gentilezza di farmi notare che Realm ha un sistema di notification built-in, quindi in questo caso MMWormHole non è davvero necessario. L’API è facilissima da usare e non richiede configurazione, funziona così:

realmToken = RLMRealm.defaultRealm().addNotificationBlock { note, realm in
    self.reloadEntries()
}

Finché manteniamo un riferimento strong al token, il sistema invoca il notification block non appena viene committata una write transaction. Trovi il codice aggiornato nell’app di esempio.

Demo

Ecco la demo app in tutto il suo splendore:

Done

Come vedi è davvero facile mettere in piedi una piccola app per WatchKit, e grazie a un paio di ottime librerie open source aggiungere persistenza e interattività è uno sforzo indolore. Un grosso grazie sia al team di Realm sia a Mutual Mobile per l’ottimo lavoro su MMWormHole.

Come al solito, trovi il codice sorgente della demo sulla nostra pagina GitHub. Sentiti libero di lasciare un commento, ci farebbe piacere sentire il tuo feedback.