Pagine

2014-09-01

NSManagedObject, Undo e dealloc

L'allegra compagnia definita nel titolo di questo post normalmente funziona felicemente assieme: anche se non si comprende bene bene perché, tutto va a posto e non c'è n'è dobbiamo preoccupare (per dirla alla Apple: it just works).
Ma la legge di Murphy ci dice che se qualcosa potrebbe non funzionare, certamente lo farà nel momento peggiore... Infatti...
Supponiamo di avere una sottoclasse di un NSManagedObject che ha bisogno di reagire al cambio di una delle sue proprietà; supponiamo di avere un attribute posizione che ad un certo punto viene modificato esternamente e del quale dobbiamo avvisare un'altra parte della nostra app, p.es. per aggiornare alcuni conteggi, tramite una notifica.
Questo sembra essere un compito perfetto per un accessor personalizzato, evitando l'inserimento di un observer dell'oggetto su una proprietà di se stesso:
- (void)setOrdine:(NSNumber *)ordine
{
    [self willChangeValueForKey:@"ordine";
    _ordine = ordine;
    [self didChangeValueForKey:@"ordine"];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"myNotification" object:nil];
}
Ogni volta che il valore di ordine viene modificato, la notifica viene spedita; il tutto senza observer.
Se però eliminiamo l'oggetto dal NSManagedObjectContext (l'interfaccia si aggiorna di conseguenza) e in seguito chiamiamo la voce di Undo del menu Edit, Core Data si premura di rifar comparire l'oggetto stesso, ma l'interfaccia non cambia! Dobbiamo esplicitamente fare azioni per aggiornare lo stato dei conteggi, quali p.es. forzare il ridisegno, tramite una chiamata... che però non sappiamo dove fare (l'Undo non mi avvisa di aver fatto qualcosa e la fa in modo asincrono).

Allora occorre cambiare strategia: introduciamo un observer sulla property e dovremo inserirlo sia in -awakeFromInsert che in -awakeFromFetch. Questi infatti sono i metodi chiamati rispettivamente alla creazione di un NSManagedObject e al suo recupero dallo store:

- (void)awakeFromInsert
{
    [super awakeFromInsert];
    [self setPrimitiveValue:... forKey:@"..."];
    [self addObserver:self forKeyPath:@"posizione" options:0 content:nil];
}

- (void)awakeFromFetch
{
    [super awakeFromFetch];
    [self addObserver:self forKeyPath:@"posizione" options:0 content:nil];
}

- (void)didTurnIntoFault
{
    [self removeObserver:self forKeyPath:@"posizione"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ( [keyPath isEqualToString:@"posizione"] ) {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"myNotification" object:nil];
    }
}

Notiamo che abbiamo dovuto togliere l'observer nel metodo -didTurnIntoFault: Apple infatti ci dice che è questo il metodo da usare con i NSManagedObject e che non dobbiamo lasciare in giro degli observer quando l'oggetto non c'è più, poiché potrebbero persino essere attaccati ad altri oggetti. In questo modo, ogni oggetto, che sia creato ex-novo o che sia caricato da disco, avrà un observer sulla property posizione e quando viene cancellato, l'observer viene tolto.
Bene: lanciamo, osserviamo che la notifica viene effettivamente spedita quando la property cambia; proviamo a cancellare l'oggetto: ok. Facciamo un Undo: voilà! L'aggiornamento non sembra funzionare più! Peggio: cancelliamo nuovamente l'oggetto (senza utilizzare il Redo) ed otteniamo una bellissima exception: "ho cercato di togliere un observer ma questo non c'era"!!!

Siamo arrivati al punto del problema: quando abbiamo cancellato l'oggetto la prima volta, è stato chiamato -didTurnIntoFault, che ha giustamente tolto l'observer. Quando però abbiamo fatto l'Undo, il nostro ManagedObject è stato riportato in vita ...artificialmente! Cioè, senza passare dall'-awakeFromFetch! Quindi non ha avuto occasione di rimettere a posto l'observer sulla property e questo spiega perché i conteggi non vengano eseguiti; quando poi cancelliamo di nuovo l'oggetto, il -didTurnIntoFault viene nuovamente chiamato, ma l'observer non esiste ed il codice si lamenta giustamente!
In modo un po' empirico, potremmo pensare di eliminare il -didTurnIntoFault, ma dopo aver cancellato un oggetto, all'uscita dell'applicazione otterremo un alto lamento del codice, che ci dice che abbiamo lasciato un observer vagante e che ciò non è cosa da farsi! Siamo in una strada senza uscita: se lo togliamo si arrabbia, se non lo togliamo si arrabbia lo stesso!

Intanto, dobbiamo capire il perché succeda una cosa simile; dopo averci ragionato un po', eccoci al punto! Quando un oggetto viene cancellato, è in effetti tolto dalla vista, ma viene messo nella coda di Undo e non viene distrutto; quando l'Undo viene chiamato, non fa altro che rimettere l'oggetto al suo posto, senza passare da nessuno dei metodi precedenti. Quindi è corretto non togliere l'observer alla cancellazione: l'oggetto non è cancellato, ma solo messo da parte. Quando l'Undo lo ripresenta, l'oggetto è pronto per continuare a funzionare.
Ma allora dove possiamo togliere l'observer? Quando l'oggetto verrà definitivamente distrutto e anche l'Undo non lo manterrà più! Ok, ma in pratica, dove??? Proprio nel metodo sconsigliato da Apple per i ManagedObject:
- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"posizione"];
}
Lo so, lo so: Apple dice di non usare questo metodo con i ManagedObject, perché probabilmente non verrebbe mai chiamato. Ma questo è proprio uno dei casi in cui è certamente chiamato: noi andiamo a togliere l'observer proprio quando l'oggetto sta per essere distrutto e non quando viene solo tolto dalla vista, con possibilità di essere reinserito.

In questo modo non lasciamo observer vaganti, non cerchiamo di toglierli quando non ci sono e, soprattutto, le notifiche verranno lanciate ogni volta che la property dell'oggetto cambia, proprio come volevamo.