Pagine

Visualizzazione post con etichetta property. Mostra tutti i post
Visualizzazione post con etichetta property. Mostra tutti i post

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-04-24

Core Data Localization Dictionary

Core Data su Mac è indubbiamente per lo sviluppatore quando si tratta di gestire quantità di dati. Non starò qui a descrivere le possibilità di Core Data: esistono molte fonti in rete, a cui si aggiunge la documentazione di Apple, da cui è conveniente partire (meglio ricordare che Core Data, a dispetto della semplicità d'uso, è una delle tecnologie più profonde che abbia a disposizione uno sviluppatore).

Inoltre, Apple ha fornito tecniche interessanti da utilizzare.
Una di queste è la possibilità di localizzare (cioè presentare nella lingua dell'utente) persino le proprietà di Core Data (o, più correttamente, le proprietà delle Entity). In questo modo, è possibile far riferimento a tali proprietà nell'interfaccia visibile all'utente, per esempio durante l'emissione di un messaggio di errore. È infatti certamente più user friendly un messaggio che dice "il valore di 'Indirizzo' non è corretto" piuttosto che "il valore di 'Address' non è corretto"; infatti non ci dobbiamo attendere che tutti i nostri utenti conoscano l'inglese.

La documentazione ci dice che basta costruire un Localization Dictionary. Cioè?
In pratica è un altro file di tipo .strings, dove andremo ad inserire il nome dell'Entity e delle sue proprietà, indicandone la traduzione. Per esempio, nel caso di una Entity 'Home', il file potrebbe essere così costruito:
"Entity/Home" = "Casa";
"Property/address" = "Indirizzo";
"Property/Number" = "Numero";
dove vediamo che la chiave è composta da "Entity" o "Property"seguita da "/" e dal nome della proprietà come definita nel modello (dataModel). Se due Entity differenti avessero una proprietà con lo stesso nome, dovremmo costruire la chiave indicando anche l' Entity .

Per renderci le cose meno semplici, la documentazione ci dice che il nome di questo file .stringsdeve essere quello del nostro DataModel a cui si aggiunge la parole Model . Quindi, se il nostro data model di Core Data è stato da noi chiamato p.es. dataModel, il file che riporta le traduzioni si dovrà chiamare dataModelModel.strings. Questo, per quanto strano (in effetti Xcode genera un nuovo progetto usando proprio questo nome...) è spiegato esplicitamente e in effetti funziona.

Una volta che abbiamo costruito il file, possiamo richiamarlo dal codice semplicemente definendo il dizionario (che sarà riconosciuto dal codice) e prendendo la proprietà che ci serve:
NSDictionary *localDict = [self.managedObjectModel localizationDictionary];
NSString *localProperty = [localDict valueForKey:@"Property/Address"];
per cui dovremmo essere a posto.... Non è vero! Infatti la documentazione ci dice che, almeno fino al 10.6 Snow Leopard, il dizionario viene caricato lazily, cioè se proprio ce n'è bisogno. E se non ci son stati errori da presentare, il bisogno non c'è stato! Quindi sul 10.6 la stringa localPropertydell'esempio precedente è nulla!
Ma la soluzione è molto semplice: le stringhe da caricare sono dentro il file .strings, per cui carichiamole come se fossero proprio quello:
NSString *localProperty = NSLocalizedStringFromTable(@"Property/Address", @"dataModelModel", @"proprietà Indirizzo");
e possiamo proseguire felici e contenti!

Da notare che dal 10.7 in poi il problema del caricamento non c'è più, ma per il momento meglio usare la macro NSLocalizedStringFromTable, soprattutto se prevediamo di supportare il 10.6!

2012-03-11

Errore "property cannot pair"

Vi siete mai trovati davanti all'errore sibillino:
error: writable atomic property 'miaProperty' cannot pair a synthesized setter/getter with a user defined setter/getter
Bene: significa che state usando in modo disinvolto la definizione delle property di Cocoa!
Infatti, nella definizione
@property (retain,...) NSObject *miaProperty;
se non viene esplicitato nonatomic, i metodi saranno atomic per default. Allora non potrete usare @synthetize nell'implementazione della classe, fornendo anche una vostra implementazione degli stessi metodi.
Quindi, per eliminare il problema, potete fare una delle 3 cose seguenti:
- usare la direttiva @dynamic (invece di @synthetize);
- usare @synthetize, ma tenersi i metodi automatici, senza fornirne di propri;
- non usare alcuna direttiva e scrivere per proprio conto entrambi i metodi setter/getter.
Esiste anche un quarto metodo, che consiste nel dichiarare nonatomic la property, consigliabile solamente se non abbiamo necessità di settare/leggere la property da thread diversi.