Pagine

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.