Pagine

2012-12-14

Sandbox per vecchie app

Ogni sviluppatore sa che il MacAppStore di Apple ha inaugurato da un po' di mesi una replica delle forche caudine: la Sandbox!
Lo scopo è buono: far sì che, nell'improbabile caso in cui un codice malintenzionato riesca a prendere il controllo di un'applicazione, i danni si riducano al minimo indispensabile (che, detto in parole povere, significa: al massimo perdiamo i dati interni della stessa applicazione, senza andare a toccare punti importanti del sistema).
Come stato raggiunto questo risultato? Mantenendo nascosti tutti i dati dell'applicazione! E l'unico punto nascosto è risultato essere la cartella Library, che, a meno di sapere come fare, non è più visibile.
Dentro la Library utente sono nascosti i dati delle applicazioni che onorano la sandbox (chiamate, in modo orribile, sandboxate).
Però c'è un problema: quando si aggiorna alla nuova versione, la nostra app cercherà i dati all'interno del suo orticello (appunto, la sandobox) e non troverà i dati della versione precedente, che verranno a trovarsi in un punto per lei inaccessibile. Questo significa che, una volta lanciata, tutti i dati originali sembreranno scomparsi. Ciò non sarebbe bello!
In effetti, anche Apple ha visto il problema e ha inserito una soluzione nel meccanismo della sandbox: basta inserire nella versione sandboxata un file XML, dal nome obbligatorio di container-migration.plist. In questo file andremo a dire alla nuova versione quali file e cartelle trasferire nel proprio orto; così, al primo lancio, l'applicazione sposterà i dati nella propria sandbox, proprio dove si aspetta di trovarli, li leggerà e l'utente sarà contento di non aver perso tutto.
Il meccanismo è molto semplice e nella documentazione ufficiale viene anche riportato un esempio, comprensibilissimo.
Quello che invece non viene detto (e che mi ha fatto perdere mezza giornata di prove) è che tale meccanismo non funziona semplicemente lanciando l'applicazione dall'interno di Xcode, nella configurazione di debug! Dopo una ventina di prove diverse e di insulti molto particolareggiati all'indirizzo della signora Sandbox, in un ultimo sprazzo di lucidità, ho fatto un archivio seguito da una esportazione in locale, lanciato un'applicazione dal desktop e... voilà! I dati sono stati trasferiti nella nuova posizione! Eureka!
C'è solo da ricordare che questo avviene solamente al primo lancio; se in seguito dovessimo ripetere l'esperimento, dobbiamo ricordarci di cancellare il container della nostra applicazione, altrimenti questa saprà di essere già stata lanciata almeno una volta e quindi non effettuerà lo spostamento.

Avvertimento: ricordiamoci di fare una copia dei dati originali, poiché questi non verranno copiati, bensì spostati. Quindi, se dovessimo ripetere l'esperimento, dovremo averne una copia.

I segreti degli NSSplitView - III

Nella prima puntata abbiamo visto come comunicare allo SplitView le dimensioni massime e minime delle proprie viste, mentre nella seconda puntata abbiamo implementato un metodo per associare il cambio delle dimensioni solo alla vista centrale (nell'ipotesi di tre viste).
In questa breve ultima puntata vediamo qualche finezza, che può far pensare all'utente che ci siamo sbattuti molto per farlo lavorare bene.

La vista che scompare sul serio

Abbiamo visto che con il metodo del delegate splitView:canCollapseSubview possiamo lasciare all'utente la possibilità di far scomparire la vista sulla destra, pur avendo il vincolo sulla sua dimensione minima. Se però non vogliamo lasciare questa libertà, potremmo pensare che non implementando questo metodo (oppure fornendo in ritorno sempre false), scopriremo presto che passando con il mouse sul bordo destro a vista collassata riusciamo ancora a prendere la vista invisibile e trascinarla, scoprendo gli altarini: cioè, se non facciamo nulla, si apre uno spazio vuoto: ricordiamo infatti che per evitare il caos abbiamo deciso di togliere letteralmente la vista da dentro lo SplitView.
Dobbiamo quindi convincere l'utente a non fare quella operazione e l'unico modo è... di impedirglielo.
Infatti, con il metodo SplitView:shouldHideDividerAtIndex: possiamo far scomparire definitivamente il divisore e senza quello l'utente non potrà più fare nulla di dannoso. È sufficiente verificare che il divisore sia quello incriminato e poi ritornare un risultato true:
- (BOOL)splitView:(NSSplitView *)splitView shouldHideDividerAtIndex:(NSInteger)dividerIndex
{
 NSInteger dividerToHide = 1;
 return (dividerIndex == dividerToHide);
}
Dato che parliamo della vista di destra, il divisore da far scomparire è quello indicato con il numero 1 (i divisori partono dal numero 0).

Azione finale

A questo punto abbiamo fatto in modo che l'utente non possa chiudere la vista e nemmeno riaprirla se è chiusa: ma allora... cosa può fare? Gli veniamo in soccorso e stabiliamo che l'apertura e la chiusura può essere fatta da un controllo esterno: un pulsante nella Toolbar della finestra è un buon candidato.
Per prima cosa colleghiamo il pulsante ad una IBAction del tipo:
- (IBAction)toggleInspectorPane:(id)sender
{
    if (collapsed) [self uncollapseView];  
    else [self collapseView];
}
In pratica, se la vista non c'è, la mettiamo noi, mentre se c'è la togliamo! Ed ora dedichiamoci al compito semplice ma piuttosto noioso di inserire o togliere la vista e di modificare la dimensione della sotto-vista dello SplitView.
Il nocciolo sta nel giocare con i frame, cambiando le dimensioni ed il punto di partenza di ogni view (ricordiamo che per default in Cocoa le view hanno la coordinata Y=0 nell'angolo in basso, mentre le X sono come ce le aspettiamo: crescono verso destra).
Supponendo che theSplit sia un IBOutlet legato allo NSSplitView che ci interessa:
- (void)collapseEditorView
{
 NSView *centerView = [[_theSplit subviews] objectAtIndex:1];
 NSRect centerFrame = [centerView frame];
 NSRect leftFrame = [[[_theSplit subviews] objectAtIndex:0] frame];
 NSView *rightView = [[_theSplit subviews] objectAtIndex:2];
 NSArray *viste = [rightView subviews];
  
 centerFrame.size.width = [_theSplit frame].size.width - leftFrame.size.width - [self.theSplit dividerThickness];
 centerFrame.origin = NSMakePoint(leftFrame.size.width + [self.theSplit dividerThickness], 0);
    [centerView setFrame:centerFrame];
    NSRect newCollapseRect = NSMakeRect([_theSplit frame].size.width, 0, 0, [_theSplit frame].size.height);
    [rightView setFrame:newCollapseRect];
    if ([viste count] != 0)
 {
  [(NSView*)[viste objectAtIndex:0] removeFromSuperview];
 }
 _editorViewController = nil;
 [_theSplit display];
}
In pratica andiamo ad impostare il frame della vista centrale in modo che copra tutto il restante splitView, togliendo dalla larghezza totale di questi la vista sinistra e lo spessore del divisore (righe 9-11). Per la vista destra, invece, impostiamo un frame con larghezza 0 e con origine nell'estremo destro dello splitView (righe 12-13).
Le ultime righe servono solamente a togliere la vista di mezzo, impostare il corrispondente NSViewController a nil e a rinfrescare lo splitView.

Le cose sono molto simili per il metodo che fa apparire la vista destra:
- (void)uncollapseEditorView
{
 NSView *leftView = [[_theSplit subviews] objectAtIndex:0];
 NSView *centerView = [[_theSplit subviews] objectAtIndex:1];
 NSView *rightView = [[_theSplit subviews] objectAtIndex:2];
 NSRect leftFrame = [leftView frame];
 NSRect centerFrame = [centerView frame];
 NSRect editorFrame;
 NSRect splitFrame = [_theSplit frame];
 CGFloat dividerThickness = [_theSplit dividerThickness];
 
 _editorViewController = [[editorViewCtrl alloc] initWithNibName:@"editorView" bundle:nil];
 editorFrame = [[_editorViewController view] frame];
 if ((splitFrame.size.width - editorFrame.size.width) < (leftFrame.size.width + 250))
 {
  NSRect windowFrame = [self.window frame];
  windowFrame.size.width += editorFrame.size.width;
  [self.window setFrame:windowFrame];
  splitFrame = [_theSplit frame];
  leftFrame = [leftView frame];
 }
 [rightView setHidden:NO];
 
 centerFrame.size.width = splitFrame.size.width - leftFrame.size.width - editorFrame.size.width - 2*dividerThickness;
 centerFrame.origin.x = leftFrame.size.width + dividerThickness;
 editorFrame.origin = NSMakePoint(leftFrame.size.width + centerFrame.size.width + 2*dividerThickness, 0);
 
 [centerView setFrame:centerFrame];
 [rightView setFrame:editorFrame];
}
Prima carichiamo il viewController con il nib che contiene la vista da inserire (riga 12), da cui otteniamo lo spazio necessario da riservare (13). L'idea è di rubare lo spazio necessario alla vista centrale, ma... dobbiamo fare attenzione che questa non scenda sotto la dimensione minima che abbiamo impostato (la vista lo farebbe, poiché i limiti non entrano in gioco da modifiche via codice, ma siamo noi a non volerlo!). Per cui, se la vista centrale dovesse diventare troppo piccola, la lasciamo come è e allarghiamo al suo posto la finestra, splitView compreso (in IB avremo fatto in modo che le sue dimensioni siano legate a quelle della finestra): questo è ciò che avviene nelle righe 14-20. Facciamo comparire la vista (22) e posizioniamo in modo corretto dimensioni e origine della vista centrale (24-25); poi imponiamo che la posizione della terza vista sia proprio dopo il divisore, come ci si attende (26).
Non ci resta che assegnare i nuovi frame a chi di dovere... e siamo a posto!
Volendo, potremmo animare il movimento del divider, in modo che la vista compaia scivolando da destra e scompaia chiudendosi dolcemente; ma questo è un compito abbastanza semplice con le possibilità di animazione di Cocoa!