Pagine

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!

2013-04-20

XIB non compilati?

Questo è abbastanza divertente: dopo più di un mese passato a lavorare su un progetto piuttosto complesso, un giorno apro Xcode, archivio ed esporto l'applicazione e... a un certo punto un bel crash!

Che roba è? Da dove esce? Il punto di crash non è stato modificato dall'ultima commit, per cui... Fantasmi? Maledizioni?

Apro il bundle dell'applicazione: sembra tutto a posto. Entro nella cartella di localizzazione e... eccolo! Anzi, eccoli!
Mentre tutti i file di interfaccia compaiono come documenti generici con estensione nib, due di essi si mostrano come xib!

In quel momento ero particolarmente fresco, dato che avevo da poco ripassato l'uso dei tool di Apple per la localizzazione delle applicazioni ( genstring e ibtool, per capirsi). Sapevo quindi che in un bundle in esecuzione non dovrebbero esistere gli xib.
Ma perché erano rimasti 'non compilati'?

Diciamo subito che il motivo non è stato trovato: ad un certo punto la situazione era quella. In più, era anche spuntato l'errore che affermava che "l'auto-layout non è supportato per sistemi inferiori al 10.7". Va bene, in effetti il progetto deve funzionare anche sul 10.6, ma il fatto è che non stavo usando l'auto-layout (vade-retro, Satana!).

Prima cosa: facciamo scomparire questo finto avviso. Cioè ho dovuto impostare 10.7 su entrambi gli xib (nel pannello di destra, dopo aver selezionato il file a sinistra); tanto è solo un dato che serve appunto per controllare gli errori.

Poi ho cominciato a girare a caccia di punti 'strani': gli xib facevano parte delle risorse da copiare nel bundle (impostazioni del target); i file erano dove effettivamente dovevano essere, cioè nella cartella di localizzazione,... Bah!
Alla fine, imbeccato da un problema diverso, ho risolto: selezionare il file sulla sinistra, aprire l'inspector a destra e cercare il menu popup che riporta il tipo di file:


Normalmente, come deve essere, è impostato a "Default - Interface Builder"; questo dovrebbe dire al progetto cosa fare di questo file. L'impostazione è corretta, ma evidentemente non rispecchia la verità.
Per cui ho modificato il valore, aprendo il menu e scegliendo un valore a caso (nel mio caso ho scelto "C Preprocessed Source", ma dovrebbe andar bene qualunque). Salvato tutto il progetto con ⌘⌥S, ho poi riportato il menu all'impostazione originale "Interface Builder".
Salvato nuovamente, compilato... Eureka! È tornato a funzionare!!!
---
E, visto che ci sono, segnalo anche come fare in modo che i nib compilati siano accessibili da ibtool: basta impostare nei Build Setting il parametro Flatten Compiled XIB Files a NO per lo schema di Debug (si lascia invece a YES per lo schema di Release, in modo che l'applicazione da distribuire sia più leggera possibile e non siano leggibili direttamente dagli utenti).

2013-04-01

Due target nello stesso progetto

Ok, le App a pagamento sono spesso sinonimo di garanzia: se riceve soldi, lo sviluppatore sarà più invogliato a mantenere la propria applicazione ed a correggerne i bachi.
Però l'utente, prima di acquistare, vorrebbe provare l'applicazione, spesso se questa è costosa. Mantenere una App gratis, con funzionalità ridotte può spesso valere la spesa: una volta che si è verificato che risponde a quello che stavamo cercando, possiamo acquistare quella completa.

Per lo sviluppatore, questo significherebbe portare avanti due progetti, con tutti i difetti del caso: doppio debug, doppia correzione di bachi,... A meno che non sfruttiamo Xcode! Possiamo infatti ottenere due o più applicazioni, solo utilizzando i target di Xcode (qui utilizziamo XCode 4, ma funziona con tutte le versioni).

Cominciamo quindi con l'aprire un progetto esistente (uso un progetto di prova chiamato CocoaApp); nella parte sinistra della finestra clicchiamo sul progetto, rivelando le sue impostazioni: nella seconda colonna vediamo una sezione PROJECT ed una TARGETS: i target indicano i prodotti del progetto, cioè le applicazioni (ora ne vedremo una sola con lo stesso nome del progetto):


Ora, facciamo un bel click destro e nel menu che ne esce scegliamo Duplicate. Il risultato sarà di avere un nuovo target CocoaApp copy, che viene selezionato:



Vediamo che le info del nuovo target sono identiche a quello di partenza, salvo il nome: certo, non rilasceremo mai una App con un copy in fondo al nome! Per cui, per prima cosa clicchiamo in alto su Build Settings e poi scriviamo PRODUCT_NAME nel campo di ricerca. Verranno visualizzate solo le righe che contengono la stringa; un doppio click sul campo corrispondente a questo parametro e inseriamo il nome del prodotto che vogliamo nel popup che viene fuori:


Ora cliccando su Summary, vediamo che l'Identifier è quello corretto: com.dominio.CocoaApp-Free.
Lo spazio viene automaticamente riempito con un trattino.
Tuttavia, nella colonna a fianco, il nome del target è ancora quello vecchio: niente paura, un click sul nome del target, entriamo in edit e lo modifichiamo.
Diamo ora un'occhiata alla colonna contenente i file: vediamo che in fondo è stato creato un nuovo Info.plist file: ovvio, visto che si tratta di una nuova App; però anche questo ha il nome sbagliato. Facciamo come prima: click sul nome e inseriamo quello corretto; poi portiamo il file nel gruppo giusto, con un drag&drop:


Non abbiamo ancora finito di mettere in bella il nuovo target: infatti, guardiamo in alto a sinistra, dove c'è un menu CocoaApp - My Mac 64 bit: aprendolo troviamo una nuova voce che si chiama ancora CocoaApp copy! Allora, dallo stesso menu, scegliamo la voce Manage Schemes...: si aprirà una finestra, in cui vediamo lo schema incriminato: click e lo editiamo al nuovo nome:


Vedremo che nel menu ora compare il nome giusto! Quindi possiamo dedicarci al codice!
Di sicuro vorremo togliere funzionalità nella versione free, ma vogliamo anche tenere lo stesso codice, in modo da correggere i bachi una volta sola, giusto?
Per fare questo, abbiamo bisogno di dire al compilatore cosa inserire e cosa no: è proprio un lavoro per le macro preprocessor! Si tratta quindi di definire una macro per la versione Free e poi nel codice verificare se tale macro è definita o meno.

Per la definizione di questa macro (la chiameremo FREE) il posto più semplice sono parametri di compilazione; per cui click sul progetto a sinistra, poi click su Build Settings. Nel campo di ricerca in alto scriviamo PROCESSOR e alla voce Preprocessor Macro inseriamo FREE (ovviamente va bene qualsiasi nome, ma questo identifica bene cosa vogliamo ottenere):


Naturalmente dovrà essere inserito sia per la configurazione Debug che Release!

Finalmente siamo pronti per passare al codice.
Certamente vorremo che la finestra rispecchi il nome dell'applicazione, pur usando un solo xib. Nel nostro xib la finestra avrà titolo "CocoaApp" e nel delegate inseriremo il codice:


- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
#ifdef FREE
[self.window setTitle:@"CocoaApp Free"];
#endif
}


Il codice di impostazione del titolo sarà eseguito solo quando andremo a compilare la versione free; la compilazione di una o dell'altra versione avverrà selezionando il target corrispondente e lanciando (o archiviando) il progetto!

NOTA: ogni tanto si trova su internet il consiglio di partire creando un nuovo target; solo che poi dovrete associare al nuovo target tutti i file, ovviamente dimenticandone qualcuno... La cosa più semplice è invece di duplicare il target iniziale: dovrete soltanto occuparvi di cambiare alcuni nomi, come abbiamo fatto e basta.
Anche la definizione della macro può essere fatta inserendola in un header comune, ma questa via mi sembra la più semplice.