Pagine

2013-05-02

Bindings e model: come aggiornare

I bindings in Cocoa sono una benedizione dal cielo: evitano di scrivere montagne di codice per fare in modo che quello che vede l'utente sia sempre aggiornato con le variabili del modello, senza necessità di andare materialmente nell'oggetto grafico a impostare il nuovo valore.
In più permettono di separare il modello (dove normalmente avvengono le elaborazioni) dall'interfaccia, cosa altamente consigliabile per avere codice riutilizzabile con poco sforzo. Il bello dei bindings è che funzionano a doppio senso; per esempio:

  • se la variabile (property) del mio controller cambia per effetto dell'elaborazione, il campo di testo visibile all'utente e collegato a quella variabile via binding si aggiorna;
  • se l'utente cambia il valore del campo di testo scrivendoci dentro, posso star sicuro che il controller avrà immediatamente la propria property aggiornata con il nuovo valore.
Sempre che la property segua il quadro KVO.
Inoltre, se la property è di un certo tipo e voglio che nell'oggetto grafico ci vada una rappresentazione diversa (p.es. ho un intervallo di tempo che voglio rappresentare sotto forma di colore) basta prevedere un NSValueTransformer che si occupa della traduzione nei due sensi.

Tuttavia, ci sono momenti in cui qualche binding si presenta come la classica mosca al naso (che si vorrebbe distruggere senza esitazione)...
Esempio: voglio un campo di testo che, invece di permettere l'edit diretto, apra una piccola finestra dove l'utente inserisce alcuni ingressi (p.es. cognome, nome, secondo nome) e alla chiusura il tutto viene elaborato e inserito nel campo (p.es. i tre ingressi nella stessa stringa). Dobbiamo usare questo oggetto da più parti, per cui creo una sotto-classe di NSTextField che porti al suo interno tutto il necessario, in modo da poterlo utilizzare dove mi serve semplicemente inserendo la nuova sotto-classe.
Tutto il meccanismo è semplice da implementare; al termine inseriamo la stringa elaborata nel campo, l'utente vede il risultato, ma... la property a cui il campo è collegato tramite binding non si aggiorna! Se invece volessimo aggiornare il modello, bene (anzi male), tale modello non è raggiungibile dalla nostra sottoclasse!

È come se fosse differente inserire a mano (da interfaccia) un valore e inserirlo via codice. E infatti è proprio una cosa diversa, perché da interfaccia il campo riceve un valore e lo passa al binding, mentre se questo avviene da codice non facciamo altro che inserirlo nell'interfaccia e basta!
A pensarci, una volta che si sa, è ovvio! Ma arrivarci... è un'altra cosa.

La soluzione è che dobbiamo scrivere una (piccola) parte del codice che il binding normalmente tratta in automatico, ma che qui non può funzionare perché gli stiamo cambiando i valori da sotto il naso.
Ogni sottoclasse di NSControl (quindi campi di testo, pulsanti, campi data,...) ha un parametro value, che poi è quello che compare nell'inspector di Xcode nella sezione binding. Il nome effettivo è diverso a seconda che sia un numero, una data, ecc...; per un campo di testo è objectValue.
Il codice complessivo è il seguente:
self.objectValue = myNewValue;
NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding];
NSObject *boundObject = [bindingInfo valueForKey:NSObservedObjectKey];
NSString *keyPath = [bindingInfo valueForKey:NSObservedKeyPathKey];
[boundObject setValue:self.objectValue forKeyPath:keyPath];
Quindi, per prima cosa dobbiamo assegnare il risultato che vogliamo inserire proprio a questa proprietà della nostra sottoclasse (riga 1). Una volta fatto, dobbiamo essere noi ad aggiornare la property del modello, visto che il binding non si accorge che gli abbiamo cambiato il valore.
Quindi cominciamo col recuperare informazioni sul binding: in riga 2 otteniamo le info volute (attenzione: se il binding non fosse collegato al value ma ad un'altra proprietà, dovremmo mettere quella al posto della chiave NSValueBinding - le troviamo nella documentazione Apple).
Alla riga 3 otteniamo dal NSDictionary l'oggetto che gestisce il modello, mentre alla riga 4 otteniamo il keyPath (cioè la property) a cui è legato il nostro campo.
A questo punto siamo a posto! Abbiamo l'oggetto e la proprietà: non dobbiamo fare altro che cambiarla: cosa che facciamo nell'ultima riga!
Ora abbiamo cambiato il modello nel modo corretto, per cui eventuali altri binding si aggiorneranno di conseguenza, senza fare altro e se questo valore facesse parte di quanto si andrà a scrivere su disco, i valori registrati saranno corretti!

2013-05-01

ARC e Snow Leopard

L'Automatic Reference Counting (ARC) è molto comodo: praticamente, ci possiamo quasi dimenticare della gestione della memoria (...il quasi è molto importante, ma per oggi ce ne dimentichiamo...) poiché possiamo essere sicuri che il compilatore gestirà le cose per noi e in modo migliore di come potremmo fare, anche rispetto alla Garbage Collection.
Però... una percentuale di utenti attorno a 20-30% sta ancora usando la versione 10.6 di Mac OS e ci piacerebbe che anche questi utenti potessero usare le nostre app.

La cosa che indispettisce di più è compilare il nostro software, portare il risultato su una macchina con 10.6... e scoprire che non riesce nemmeno a partire: un paio di saltelli nel Dock e chiusura, per di più senza lasciare traccia nella Console di sistema.
Sono i momenti in cui è meglio non essere vicini ad una finestra: si potrebbe rischiare di farci passare il nostro Mac con graziosa parabola...

Quindi, calma e gesso. Allora, non è possibile compilare una applicazione con ARC su Snow Leopard (in quanto Xcode 4.2, l'ultima versione che accetta il 10.6); infatti, per compilare in ARC occorre almeno il 10.7. Se proviamo a farlo su 10.6 otteniamo infatti un errore che ci avvisa di non trovare un file, che è proprio quello che permette di usare l'ARC.
Quindi l'unico modo è quello ufficiale: lavorare su 10.7 o 10.8, impostando l'SDK all'ultima versione, ma ponendo il deployment SDK a 10.6. Ma... il problema di mancato avvio su 10.6?

Dopo un lungo giro su Google, raccogliendo piccoli pezzi da vari post (impossibile listarli tutti, anche se buona parte sono su StackOverflow), alla fine ecco la soluzione!

Occorre selezionare il nostro target, andare sui Build Setting e impostare:

Implicitly Link Objective-C Runtime Support

a NO. Questa impostazione corrisponde alla variabile CLANG_LINK_OBJC_RUNTIME.

App compilata, portata su 10.6 e lanciata: funziona!

Ho cercato in giro per capire il perché sia necessaria questa impostazione, ma non ho trovato nulla di convincente. L'unica spiegazione è sulla repository di Sparkle: "il link implicito all'SDK 10.7 potrebbe inavvertitamente chiamare dei simboli che sul 10.6 non esistono".
L'importante è che funzioni!