Pagine

2013-12-15

Test della ricevuta e Mavericks

È cosa normale che durante lo sviluppo di una nuova versione si facciano test intermedi sulla ricevuta; dato che la versione deve essere la stessa definita in iTunesConnect, mi succede sempre di fare questi primi test utilizzando la versione presente in quel momento sullo Store (una app non può restare un tempo indefinito nello stato Waiting for upload e all'inizio non si ha ancora una stima precisa di quando si finirà lo sviluppo).
Per cui, si effettua il solito procedimento per scaricare la ricevuta dall'AppStore per l'utente di test, fino ad arrivare a quando siamo molto vicini al rilascio della versione finale.

In quel momento, si entra in iTunesConnect, si crea la nuova versione e la si porta nello stato di attesa dei binary. Poi si modifica la versione in Xcode, si esporta l'eseguibile e si esce dall'utenza normale dell'AppStore.

Bene. Prima dell'avvento di Mavericks, il procedimento andava a buon fine senza problemi: all'avvio dell'app c'era un attimo di attesa, poi compariva la richiesta di utente/password; una volta forniti, la ricevuta era scaricata, l'app veniva riavviata, il check della receipt effettuato e, a scanso di bachi inattesi, alla fine si otteneva all'interno del bundle l'attesa cartella _MASReceipt.
La stessa cosa dovrebbe succedere con Mavericks.

Invece, dopo aver cambiato la versione in Xcode e aggiunta su iTunesConnect, il lancio dell'app esportata terminava con un bel dialogo con cui il Finder mi avvisava che l'app era danneggiata e che non poteva essere lanciata (se non nel cestino...).
Dato che la procedura seguita era la stessa, l'unico modo era di andare in debug e osservare cosa accidenti stava succedendo. Risposta: la versione dell'app era diversa da quella registrata nella receipt! Cioè, la ricevuta riguardava la versione vecchia, mentre l'Info.plist conteneva la nuova!
Ma in iTunesConnect la versione era proprio la nuova!

Il caso ha voluto che il giorno successivo prevedessi di non usare il Mac: invece del solito Stop ho dato il comando Spegni. Quando, due giorni dopo, l'ho riacceso, il problema era scomparso: la ricevuta veniva scaricata e la versione era quella giusta!

Dopo qualche giro sulla rete e dopo aver fatto una prova simile su un'altra app, sono arrivato alla soluzione: senza dire nulla (o per lo meno, io non ho trovato indicazioni), Apple ha aggiunto in Mavericks la possibilità di tenere in cache una ricevuta e di riutilizzarla se la stessa applicazione la richiede (fino a quando questa cache è disponibile). Di solito questa è una cosa buona, poiché si evita di fare una nuova richiesta, una nuova attesa e un nuovo download. In questo caso invece crea questo problema.

La soluzione (oltre al riavvio, ma è esagerato!) è di killare lo StoreAgent:

$ killall -KILL storeagent

e dopo mettere l'app nel cestino e svuotarlo. A questo punto, esportiamo una nuova copia dell'app da Xcode, lanciamola, lo StoreAgent viene rilanciato da zero, forniamo utente test/password e... magicamente tutto torna a posto!

2013-12-07

Firmare app e framework

Una delle novità di Mavericks è che non sia più possibile firmare un'applicazione con il proprio certificato sviluppatore se all'interno si trova un framework non firmato.
Ho scoperto questa "bella" novità quando ho deciso di re-introdurre l'utilizzo dell'utilissimo Sparkle per l'aggiornamento automatico delle applicazioni, per rilasci fuori Mac App Store. Giusto per completezza, la stessa situazione si trova comunque ogni volta che utilizziamo un framework esterno o mini-applicazioni per estendere le funzionalità della nostra app.
A pensarci, è anche giusto che sia stata introdotta questa sicurezza aggiuntiva: in effetti un framework potrebbe effettuare azioni potenzialmente dannose e nascondersi dentro alla nostra app; tuttavia, avrei preferito un bel banner che avvisasse di questo problema, prima di sbatterci il naso per un po' di tempo...

Il sintomo del problema compare quando tentiamo di compilare con il nostro bravo framework importato; il passaggio della firma fornisce un errore del genere:

codesign build/release.../myApp.app: code object is not signed at all
in subcomponent build/release..../Sparkle.framework 
codesign failed with exit code 1

La parte "in subcomponent" ci indica anche quale framework (o app) è responsabile della mancata firma.
Il suggerimento di alcuni siti di inserire il comando --deep non risolve: purtroppo questa opzione provoca sì la firma di tutti i bundle ai vari livelli, ma applicando le stesse proprietà richieste dall'app principale. Il risultato è che il procedimento di firma va a buon fine, ma quando un utente lancia la nostra applicazione, il Finder gli mostra un bel messaggio che lo invita a cestinarla, perché danneggiata. Il problema viene mostrato anche tramite il comando da Terminale spctl:

spctl --verbose=4 --assess myApp.app 
myApp.app: a sealed resource is missing or invalid

La soluzione, tutto sommato, è abbastanza semplice (una volta che la si conosce): basta infatti firmare, con lo stesso certificato dell'App, anche il framework, dopo che questo è stato copiato durante i passaggi della compilazione.
Per far questo, si seleziona il progetto e si va nella pagina delle Build Phases, dove avremo già aggiunto in precedenza una Copy Phase, per fare in modo che il framework sia copiato nel bundle dell'applicazione (precisamente, nella cartella Framework, che verrà quindi creata se non esiste). Dal menu Editor, scegliere Add Build Phase  Add Run Script Build Phase; lasciando la shell di default, scriviamo nel riquadro dedicato il seguente script (riferendosi al framework Sparkle):

LOCATION="${BUILT_PRODUCTS_DIR}"/"${FRAMEWORKS_FOLDER_PATH}" 
IDENTITY="Mac Developer: abcdef ghijk" 
codesign --verbose --force --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A"

dove nella variabile IDENTITY andiamo ad inserire il nome del certificato da utilizzare (che dovrà essere lo stesso utilizzato per l'applicazione, altrimenti l'app verrà vista come danneggiata).
A questo punto il problema è risolto! Da notare che se avessimo altri framework o applicazioni da inserire nel bundle, avremo script aggiuntivi, uno per ciascuno di essi, da eseguire dopo la copia nel percorso adatto.

Per comodità, aggiungo a questo post anche i comandi shell per il controllo della firma.
Per verificare che tutto è a posto:

codesign --verify --verbose=4 myApp.app

Per essere sicuri che Gatekeeper accetti la nostra app (fuori dal Mac App Store):

spctl --verbose=4 --assess --type execute myApp.app

Per esaminare i requirement di una app:

codesign --display --requirements - myApp.app

2013-08-15

Il 'locale' personalizzato

Mac OS X (e ovviamente iOS) ha un'ottima gestione della internazionalizzazione delle applicazioni: basta usare le cartelle magiche per ogni lingua che vogliamo supportare, un paio di macro di compilazione e siamo a posto. Il risultato è un'applicazione che si presenta differente a seconda della lingua scelta nel sistema dall'utente; la documentazione Apple al proposito è sufficientemente chiara: per l'interfaccia usiamo degli xib differenti (ciascuno nella propria cartella della lingua) e per le stringhe nel codice usiamo sempre la macro NSLocalizedString, che andrà a pescare dal file localizableString della lingua giusta.

C'è però una cosa più sottile ed è rappresentata da tutte le varie convenzioni tipiche di ogni lingua; ciò che fa si che la data 5/4/2013 significhi '5 aprile 2013' in Italia, mentre negli Stati Uniti stia per '4 maggio 2013'. Oppure che il le cifre 1,234 indichino un numero appena più grande di 1 in Francia, ma per gli inglesi superi il migliaio.
Per vederle tutte basta aprire Preferenze di Sistema e cliccare su Lingua e Testo e poi scegliere il tab Regione.
Quindi l'utente è abituato (p.es. in Italia) a leggere e scrivere numeri con la virgola per indicare i decimali e così via; quindi la nostra app deve metterlo a suo agio, altrimenti dopo pochi minuti finirà nel cestino, seguita da alcune maledizioni al nostro riguardo...

Per fortuna è molto semplice tenerne conto: di solito è sufficiente impostare nello xib un formatter legato al campo che riceverà il numero e impostare il formatter come ci interessa: in automatico le impostazioni saranno quelle usate nel sistema.

Ci sono però dei momenti in cui è utile conoscere da codice p.es. cosa viene usato come separatore decimale in una certa lingua: nulla di più facile! Per questa operazione ci viene in aiuto l'oggetto NSLocale: basta prima ottenere la lingua impostata sul computer dell'utente usando currentLocale e poi utilizzare l'oggetto così ottenuto per ricavare il carattere usato e molte altre cose. Detto in 'codicese':
NSLocale *myPresentLocale = [NSLocale currentLocale];
//ricaviamo il separatore decimale della lingua dell'utente
NSString *punto = [myPresentLocale objectForKey:NSLocaleDecimalSeparator];
//oppure otteniamo il simbolo di valuta in uso
NSString *valuta = [myPresentLocale objectForKey:NSLocaleCurrencySymbol];
//o per esempio il nome della lingua dell'utente
NSString *lingua = [myPresentLocale objectForKey:NSLocaleIdentifier];
//se vogliamo strafare, questo è il codice della lingua
NSString *codice = [myPresentLocale objectForKey:NSLocaleLanguageCode];
//questa invece è il nome di una lingua... nella lingua dell'utente!
NSString *nome = [myPresentLocale displayNameForKey:NSLocaleIdentifier value:@"fr_FR"];
//se siamo in italiano, il risultato sarà 'Francese'
In questo modo possiamo ottenere tutte le informazioni che ci servono ed utilizzarle per mettere l'utente a proprio agio.
Tuttavia, il nostro caro utente ha molta più libertà che la sola scelta della lingua: p.es. qualcuno potrebbe preferire l'utilizzo del punto decimale al posto della virgola, ma continuare a tenersi il sistema in italiano per avere le date nel formato corretto. Se guardiamo quanto disponibile nelle Preferenze di Sistema, vediamo che l'utente può personalizzare tutto (e non chiediamoci perché dovrebbe farlo: dato che può, qualcuno lo farà certamente!) e questo sarebbe un bel guaio per il povero programmatore. Però... ci viene in aiuto di nuovo il NSLocale: possiamo infatti ottenere un oggetto NSLocale che contiene tutti i valori del locale attuale ma corretti con le eventuali personalizzazioni che il nostro caro utente ha ritenuto opportuno inserire:
NSLocale *actualLocale = [NSLocale autoupdatingCurrentLocale];
NSString *punto = [actualLocale objectForKey:NSLocaleDecimalSeparator];
//se l'utente è un italiano che usa il punto al posto della virgola, otterremo '.'
Da notare che la stessa funzione esiste anche nella Foundation (se dovessimo per qualche motivo lavorare in quel modo); il codice diventerebbe:
CFLocaleRef userLocale = CFLocaleCopyCurrent();
CFTypeRef decimalPoint = CFLocaleGetValue(userLocale, kCFLocaleDecimalSeparator);
//più difficile da leggere, ma con lo stesso risultato!
Quindi, da oggi i nostri utenti saranno felici e contenti di poter usare le loro impostazioni anche nelle nostre app!

2013-07-06

Controllo della receipt Mac App Store

Pur non essendo obbligatorio per pubblicare applicazioni sul Mac App Store (MAS), Apple consiglia di effettuare un controllo sulla ricevuta (receipt) per evitare che tramite una semplice copia da un Mac all'altro la nostra applicazione si trovi ad avere 2 copie vendute e 500 utenti!
Si tratta quindi di una sicurezza aggiuntiva per lo sviluppatore (l'utente è già al sicuro: firma del codice e registro degli acquisti sullo Store permettono di identificare lo sviluppatore - che quindi non inserirà codice cattivo - e di essere sicuri che l'app sul proprio Mac è proprio la stessa rilasciata sul MAS).
Allora, meglio procedere!

Ma come si fa a controllare un documento che esisterà solo sul Mac dell'utente dopo l'acquisto e non su quello dello sviluppatore?

Tempo fa Apple forniva un certificato campione per i test, ma ora la procedura è cambiata: una volta terminato lo sviluppo, si aggiunge il codice di controllo, si lancia l'applicazione, il codice nota l'assenza di ricevuta e ne richiede una apposita al MAS. Una volta ottenuta, la usiamo per testare a fondo il codice di controllo.

Bene, lineare, facile a dirsi... non a farsi!
Dopo vari tentativi, tripla lettura della documentazione ufficiale, ricerche su Google... sono arrivato al procedimento corretto, o per lo meno, quello che funziona! Occorre seguire passo passo le seguenti istruzioni.
  • Per prima cosa, andiamo su iTunes Connect, entriamo con le credenziali di sviluppatore e andiamo nella sezione di gestione degli utenti. Vedremo due grandi pulsanti: uno è per gli utenti normali, l'altro per gli utenti di test; entriamo su quest'ultimo e generiamo un utente di test. La creazione è simile a quella di un utente normale: dovremo fornire una mail apposita (deve essere vera, perchè ci verrà richiesto di validarla) con password e tutti i vari dati. Per sicurezza li ho messi uguali al mio utente standard (a parte la mail, ovviamente!). Naturalmente, questo utente è collegato quello di sviluppatore: in questo modo, saranno disponibili tutte le nostre applicazioni registrate.
  • Mentre siamo su iTunes Connect, registriamo la nostra applicazione come se stessimo per farne l'upload; cioé, il sistema deve avere in pancia il bundleID e la versione dell'app su cui stiamo lavorando. Questo è indispensabile se la nostra app è nuova e non è mai stata registrata; se invece abbiamo già una versione precedente registrata, non c'è bisogno di fare nulla: basta usare per le nostre prove la versione registrata
  • Usciamo da iTunes Connect, apriamo l'applicazione App Store e, dal menu Store, usciamo dal nostro utente; così, quando necessario, il sistema ci chiederà utente e password per accedere. Questo è un punto critico: occorre infatti che Mac OS cerchi di chiamare l'AppStore e si trovi disconnesso: solo così potremo usare l'utente di test.
  • Chiudiamo l'App Store e torniamo a Xcode. Inseriamo una bozza del codice che controllerà la presenza della ricevuta. Come, non serve che sia completo: è sufficiente che controlli la presenza della ricevuta e, in caso manchi, effettui un'uscita immediata con il codice exit(173), come dice la documentazione Apple. Anzi, meglio che si tratti di un codice molto semplice: sarà così più facile evitare gli errori e quindi di dover ripetere la procedura.
  • Apriamo i Build Settings del progetto, cerchiamo la voce Code Signing Identity e impostiamola a Mac Developer. Controlliamo che l'impostazione sia sul target della nostra applicazione e non sul progetto. Questa selezione è un punto critico: se ne scegliamo un'altra, non funzionerà! Notiamo che quando faremo l'upload definitivo su iTunesConnect dovremo riportarla a 3rd Party Developer, altrimenti ci verrà segnalato un errore e l'upload non sarà accettato.
  • Verifichiamo nel Info.plist della nostra applicazione che il BundleID (CFBundleIdentifier) e la versione (CFBundleShortVersionString) siano proprio quelli registrati ufficialmente sull'App Store; se non lo sono, non otterremo alcun risultato.
  • Ora dobbiamo esportare l'applicazione, poiché il tutto funziona solo se l'applicazione è lanciata dal Finder, non da Xcode. Quindi eseguiamo un Archive (menu Product) e verremo portati nella schermata Archive dell'Organizer.
  • Selezioniamo l'archivio e clicchiamo sul pulsante Distribute. Selezioniamo Export As - Application, click Ok.

  • Nella scheda successiva (Code Signing Identity) dobbiamo scegliere la firma indicata con Mac Development (di solito è quella selezionata di default). Dimentichiamoci del triangolino giallo che ci comunica che stiamo sbagliando... e clicchiamo Next. Anche questo è un punto critico: un altro tipo di firma non scatenerebbe l'evento corretto, ma ci ritroveremmo solo con un triste messaggio in Console (App not signed), per di più fuorviante.


  • Ci verrà chiesto dove salvare l'app così esportata e su Xcode abbiamo finito.
  • Ora passiamo al Finder: troviamo la nostra app appena esportata e lanciamola! Se abbiamo fatto tutto correttamente, entro qualche secondo ci comparirà il dialogo standard di accesso all'App Store con richiesta di utente (mail) e password. Inseriamo la coppia, usando le credenziali dell'utente di test. Senza che ce ne accorgiamo, il Finder avrà scaricato una ricevuta, l'avrà inserita nel percorso <miaApp.app/Content/_MAS/> e avrà rilanciato l'applicazione. Questa volta, il codice verificherà che la ricevuta esiste, quindi la nostra app partirà normalmente! Infatti, usciamo dall'app, entriamo dal Finder nel pacchetto e vedremo che nel Content è comparsa la cartella _MAS, contenente la ricevuta!
  • Da Finder, prendiamo questa cartella e mettiamola in salvo da qualche parte (anche se poi potremmo scaricarla quante volte vogliamo).
  • Da Xcode, facciamo una compilazione normale (basta dare un Run oppure Build for Running).
  • Da Finder troviamo l'app appena costruita (è al percorso DerivedData/<nomeApp>/Build/Products/Debug), apriamo il bundle e copiamo dentro Content la cartella _MAS: da ora in poi potremo costruire il nostro codice finale per il controllo della ricevuta e ad ogni Run la ricevuta sarà trovata al suo posto, per cui possiamo inserire tutti i controlli che vogliamo!
Attenzione: se per qualche motivo faremo un Clean del progetto, anche la ricevuta sarà eliminata! Ecco il motivo per cui conviene farne una copia in zona sicura, per riprenderla ogni volta che serve (altrimenti dovremo rifare il processo di esportazione e lancio dal Finder).
Un consiglio: Apple fornisce codice solo per il controllo del GUID del Mac (e conviene utilizzarlo così come è, senza modifiche), ma nulla sugli altri controlli. Non è pigrizia da parte di Apple: il controllo della ricevuta è proprio dove un eventuale pirata va a fare reverse engineering per introdurre del codice che salti i controlli. Se tutte le applicazioni usassero lo stesso codice, trovata una soluzione, sarebbero craccabili tutte le applicazioni con quel codice; per cui è consigliabile che ciascuno trovi la propria soluzione.
Per lo stesso motivo, conviene usare codice di Foundation in C puro e non metodi Objective-C, che si portano nel codice compilato una messe di informazioni in chiaro. Volendo si può procedere a offuscare il codice, in modo che anche un reverse engineering diventi difficile. In ogni caso, un pirata motivato e con tempo a disposizione prima o poi riuscirà nel proprio intento...

Informazioni: la documentazione Apple si trova a questo link; come esempi, consiglio Roddi, AlanQuatermain e Sazameki. Colgo l'occasione per ringraziarli tutti e tre: lo studio del loro codice mi ha fatto imparare molto!

2013-06-20

Doppio click su una NSTableView

Può capitare di voler aggiungere una funzionalità piuttosto banale: la possibilità di aprire un dialogo o di far comparire un editor tramite un doppio click su una tabella.
Le motivazione possono essere diverse: vogliamo che un valore sia controllato prima di essere insere inserito definitivamente; oppure poter mostrare un po' di dati non presenti nella tabella ma relativi alla riga selezionata.

Bene: la cosa sembrerebbe molto semplice e in effetti è vero, spesso... ma non sempre.

La cosa che si consiglia di fare di solito è di collegare un outlet della tabella (chiamiamo la property theTable) e poi di impostare il target e la doubleAction come segue in qualche metodo chiamato all'inizio (awakeFromNib, per esempio):
[self.theTable setTarget:self];
[self.theTable setDoubleAction:@selector(doubleClickOnTable:)];
Se nel metodo doubleClickOnTable inseriamo un NSLog(@"doppioCliccato") e lanciamo il tutto, vedremo che in effetti nella console di Xcode viene lasciata la traccia del messaggio ogni volta che andiamo a fare un doppio click. Poi, usando i messaggi clickedRow e clickedColumn otterremo la cella che è stata doppio-cliccata (di solito ci basta sapere la riga, ma se serve...).
Ottimo, ma questo è un caso semplice...
Supponiamo di voler mantenere la comodità dell'azione sul doppio click, ma di voler lasciare la libertà all'utente di editare il contenuto di una colonna, ma non di altre: l'utente si aspetterà che un doppio click su quella colonna metta in edit la cella, mentre un doppio click su altre colonne chiamino la funzione di prima. Ok, impostiamo l'edit della colonna voluta tramite il metodo del delegate:
- (BOOL)tableView:(NSTableView *)table shouldEditTableColumn:(NSTableColumn *)col row:(NSInteger)row
{
  if (tableColumn == [[tableView tableColumns] objectAtIndex:[tableView columnWithIdentifier:@"daEditare"]]) {
    return YES;
  }
  return NO;
}
Lanciamo: vedremo che il doppio click continua a "rubare" l'azione: ovunque sia il doppio click, compresa la colonna che vorremmo editare, viene sempre interpretato come doppio click! Doppio sigh!
L'unico modo di editare la cella voluta è di selezionare la riga e dopo un attimo fare un altro click sulla cella e solo allora potremo editarne il contenuto.
Questo non è bello: l'utente si aspetta che un doppio click gli lasci editare il contenuto e se stiamo bene a sentire, udremo i suoi improperi, piuttosto coloriti, ad ogni evento...

Cercando bene nella documentazione di Apple, scopriamo, molto nascosto, che l'impostare la doubleAction via codice è cosa diversa da impostarla via binding!! Anzi, ci avvisa che se le impostiamo entrambe, verranno chiamate tutte e due, una dopo l'altra!
Allora... tremendo sospetto... Eliminiamo l'impostazione da codice, andiamo nello xib, selezioniamo la tabella (non la scrollView...), apriamo l'inspector dei binding e clicchiamo su Double Click Target: gli metteremo l'oggetto che contiene il metodo da chiamare, nel Model Key Path lasciamo self, mentre scriveremo il nome del metodo nel campo Selector Name.

Salviamo e lanciamo: vedremo che un doppio click sulla cella aprirà il modo di edit, mentre un doppio click sulla cella di un'altra colonna, trovando impedito l'edit dal delegate, verrà chiamato il metodo cercato!

La spiegazione... non c'è! Anzi: Apple ci dice che la doubleAction da binding viene chiamata prima di quella da codice, ma questo spiegherebbe il contrario... Bah, visto che funziona, siamo a posto. Speriamo solo che un giorno Apple non cambi qualcosa...

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!

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.

2013-01-26

Il nostro caro NSUndoManager

Chiunque abbia sviluppato una applicazione usando Core Data, sa che questi gestisce automaticamente le azioni di Undo. In parole povere, ogni cambiamento effettuato dall'utente viene tracciato in automatico dal NSUndoManager, che abilita e disabilita le voci di Undo e Redo nel menu Edit; tutto questo in automatico, senza che il povero programmatore debba scriversi tutto il codice. Ok?
Beh, non è proprio tutto così semplice, qualcosa il programmatore di cui sopra deve scriverla, se vuole che la propria applicazione compaia più professionale.

Nomi delle Azioni

Prima di tutto, Vorremmo che il menu cambiasse nome a seconda dell'azione che viene annullata; cioè, se abbiamo appena cancellato una immagine, il menu dovrebbe apparire come "Annulla Cancella Immagine". Questo è semplicissimo: basta trovare il punto nel codice in cui stiamo per cancellare l'immagine (non staremo mica cancellando un oggetto usando i metodi automatici del NSArrayController, vero?) e aggiungere la piccola linea di codice:
[undoManager setActionName:NSLocalizedString(@"Cancella Immagine",@"")];
E da lì in poi il menu presenterà il nostro titolo, diverso a seconda della lingua dell'utente (posto che staremo costruendo il nostro bravo file di localized strings).
Ma... e il Redo? Nessun problema: il nostro NSUndoManager si occupa di far passare gli stessi nomi delle azioni anche al Redo, non dobbiamo preoccuparci di questo, tutto viene in automatico

Costringere l'undoManager

Esistono casi in cui questo tracciamento automatico ci dà un po' fastidio.
Supponiamo che l'utente stia spostando un oggetto col mouse e che le sue coordinate siano gestite da Core Data; se lasciamo l'automatismo, Core Data vedrà ogni minimo cambio delle coordinate come una variazione di dati e l'undoManager farà il suo compito, registrando la serie delle variazioni. Il risultato sarà che, al termine dello spostamento ci troveremo una infinita serie di Undo, il cui numero dipende da quanto velocemente l'utente avrà spostato l'oggetto; scegliendo Undo l'oggetto verrà spostato solo dell'ultima quantità registrata e per fare un Undo totale l'utente dovrà scegliere il menu un notevole numero di volte, mandando al programmatore un buon accidente per ciascuna.
Ma la soluzione è ancora semplice: quando scopriamo che l'utente sta spostando il mouse tenendo premuto il pulsante per spostare l'oggetto, diciamo all'undoManager che gli stiamo per comunicare una serie di azioni che lui dovrà considerare come una cosa unica, fino a che non gli diciamo di smettere. Detto in codice:
[undoManager beginUndoGrouping];
e quando l'utente ha finalmente smesso di spostare l'oggetto diciamo all'undoManager che può smetterla e di chiudere l'azione:
[undoManager endUndoGrouping];
[undoManager setActionName: NSLocalizedString(@"spostaOggetto", @"")];
Il risultato sarà che scegliendo il menu, l'oggetto sarà riportato nella posizione di partenza in un salto solo!

Non annullare quell'azione!

Qualche volta può succedere che non vogliamo che l'utente possa annullare una certa azione, per esempio per evitare che la nostra applicazione non venga lasciata in uno stato non consistente (di solito si tratta di operazioni su file, soprattutto in presenza della Sandbox). Posto che è meglio essere gentili ed avvisare il nostro utente che tale operazione non sarà annullabile, la cosa è semplice, ma bisogna ricordarsi la sequenza obbligata: prima bisogna avvisare il NSManagedObjectContext che stiamo per bloccare l'undo e che quindi deve sbrigarsi ad usarlo se ha qualche cosa a mezzo (infatti Apple ci avverte che alcuni oggetti potrebbero non cambiare quando previsto, ma solo in seguito). Glielo diciamo spedendogli il messaggio
[managedObjectContext processPendingChanges];
e solo allora potremo dire all'undoManager di smetterla di ascoltare i cambi, con il messaggio
[undoManager disableUndoRegistration];
Da quel momento, l'undoManager si ritira in buon ordine e dorme fino a che non vogliamo che ritorni ad ascoltare, tramite il messaggio:
[undoManager enableUndoRegistration];
Tutto qui. Non dobbiamo dimenticarci il -processPendingChanges: se ci succedesse, potremmo trovarci la voce Undo attiva al termine, ma che si riferisce ad un'azione precedente e allora l'utente, assieme con il nostro codice, non ci capirebbe più nulla!
Esiste anche il messaggio -removeAllActions che dice all'undoManager di dimenticarsi di tutte le azioni di undo che ha registrato, ma non è una bella cosa fare la sorpresa all'utente di non poter tornare indietro su nulla! Al massimo lo si può fare dopo un salvataggio, ma personalmente lo evito.

Se poi dovessimo accorgerci che l'undoManager di Core Data non fa quello che vogliamo noi ed è troppo difficile convertirlo a quello che vogliamo, possiamo sempre eliminarlo dal gioco con
[managedObjectContext setUndoManager:nil];
e poi potremo costruire un NSUndoManager come ci serve, naturalmente ricordandoci di implementare tutto per conto nostro.