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!

2012-11-18

I segreti degli NSSplitView - II

Nella scorsa puntata abbiamo esaminato come sia possibile limitare le dimensioni delle viste di uno NSSplitView ed anche come fare in modo che l'utente possa far scomparire una vista, senza che questa si offenda e distribuisca a caso gli oggetti contenuti.
Ora continuiamo nel nostro viaggio per scoprire altre cose interessanti.

Ridimensionare educatamente

Il comportamento di uno SplitView quando la finestra in cui è contenuto si ridimensiona è generico e spesso irritante: non fa altro che mantenere le proporzioni tra le viste. Così, se a schermo pieno abbiamo ridotto una delle viste a 30 pixel, quando usciamo dal full screen potremmo trovare che la stessa vista è ora di 5 pixel o meno! Non c'è differenza se le dimensioni della finestra sono cambiate da codice o a mano dall'utente: il signor SplitView non ne vuol sapere di controllare il suo delegate per sapere se può stringere una vista.
Considerando l'esempio della prima parte, con uno SplitView fatto di 3 viste, noi vorremmo che la prima (di solito una SourceList) e l'ultima (di solito un Inspector) mantenessero costanti le proprie dimensioni, variando solamente la vista centrale.
Con MountainLion, Apple ha introdotto la priorità delle viste, con cui si può risolvere il problema, ma al momento non ce la sentiamo di tagliar fuori tutti gli utenti che stanno utilizzando Lion e SnowLeopard, per cui dobbiamo di nuovo rimboccarci le maniche e cominciare a battere sulla tastiera.
Il metodo che ci viene in aiuto è sempre del delegate:
- (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
Questo viene continuamente chiamato durante il ridimensionamento dello SplitView (conseguente a quello della finestra) e propone una nuova dimensione (oldSize) che il delegate può accettare o meno; dobbiamo fare in modo che la prima e la terza vista restino costanti, modificando solo quella centrale.
Cominciamo col registrarci alcuni dati che ci torneranno utili nel metodo:
- (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
{
    NSView *leftView = [[sender subviews] objectAtIndex:0];
    NSView *centerView = [[sender subviews] objectAtIndex:1];
    NSView *rightView = [[sender subviews] objectAtIndex:2];
    float dividerThickness = [sender dividerThickness];
    NSRect splitFrame = [sender frame];
    NSRect leftFrame = [leftView frame];
    NSRect centerFrame = [centerView frame];
    NSRect rightFrame = [rightView frame];
    BOOL collapsed = [sender isSubviewCollapsed:rightView];
Notiamo che alla riga 11 teniamo conto se la vista di destra è collassata o meno.
Ora impostiamo le cose più semplici: per ogni sotto-view impostiamo che l'altezza sia sempre quella dello SplitView e che la view a sinistra resti sempre incollata all'angolo sinistro:
. leftFrame.size.height = splitFrame.size.height;
 centerFrame.size.height = splitFrame.size.height;
 rightFrame.size.height = splitFrame.size.height;
 leftFrame.origin = NSMakePoint(0, 0);
Ora arriva la parte importante: modifichiamo la larghezza della view centrale, calcolando quanto deve essere grande se le altre viste restano costanti, ma tenendo anche conto della condizione di collapsed (e ricordiamoci della larghezza dei divider!):
. centerFrame.size.width = !collapsed ? (splitFrame.size.width - leftFrame.size.width - rightFrame.size.width - 2*dividerThickness) : (splitFrame.size.width - leftFrame.size.width - dividerThickness);
 centerFrame.origin = NSMakePoint(leftFrame.size.width + dividerThickness, 0);
Ora osserviamo che in questo modo la vista centrale potrebbe diventare più piccola di quanto abbiamo impostato: non viene impedito dal delegate, per cui lo dobbiamo impedire noi! Se succede, impostiamo la view centrale al valore minimo ammesso e passiamo a cambiare la dimensione della view di sinistra, sempre tenendo conto se la terza view è collassata o meno:
. if (centerFrame.size.width <= kMinWidthTable)
 {
  centerFrame.size.width = kMinWidthTable;
  leftFrame.size.width = !collapsed ? (splitFrame.size.width - 2*dividerThickness - kMinWidthTable - rightFrame.size.width) : (splitFrame.size.width - kMinWidthTable - dividerThickness);
  centerFrame.origin = NSMakePoint(leftFrame.size.width + dividerThickness, 0);
 }
Ricordiamoci di impostare la posizione della vista destra (se non collassata):
. if ( ! collapsed )
 {
  rightFrame.origin = NSMakePoint(splitFrame.size.width - rightFrame.size.width, 0);
 }
e concludiamo dando i nuovi frame alle viste:
. [leftView setFrame:leftFrame];
 [centerView setFrame:centerFrame];
 if (! collapsed) [rightView setFrame:rightFrame];
}
Compiliamo e divertiamoci con uno splitView ora beneducato!

2012-11-04

I segreti degli NSSplitView - I

È difficile trovare un'applicazione per MacOS X che non faccia uso degli oggetti NSSplitView: in questo modo si lascia all'utente la scelta di quanto mostrare delle varie viste necessarie per le attività.
Come al solito, quando una cosa è semplice per l'utente, aumentano le difficoltà per il programmatore! A questo si aggiunge che Cocoa fornisce tutti i mezzi necessari ad un funzionamento di base, ma se stiamo cercando di costruire un'interfaccia all'altezza delle aspettative, allora dobbiamo rimboccarci le maniche e cominciare a scrivere del codice aggiuntivo.

Creazione dello split

La cosa più semplice è, nell'IB, di creare le le view che saranno contenuti nello split, selezionarli tutti e scegliere il menu Editor > Embed In > splitView. Oppure si può inserire prima lo split e poi riempire le view che questo si porta dietro, eventualmente aggiungendone di nuove. Il numero di NSView al primo livello dello split determina quanti saranno i divisori e quindi le view gestite.
Nel seguito supporremo sempre di avere uno split view con viste affiancate: nel caso in cui le volessimo sovrapposte basterà scambiare le larghezze con le altezze.

Dimensioni massime - minime

Diciamo subito che un NSSplitView, proprio perché usato per suddividere una finestra, di solito la riempie tutta; allora le dimensioni dello split seguono allegramente quelle della finestra (posto di aver messo i giusti vincoli nell'inspector dell'XCode per il dimensionamento automatico), per cui basta impostare il massimo ed il minimo per la finestra, tramite l'inspector di XCode.
Il problema invece riguarda le view contenute nello split: di default un divisore può essere spostato fino a che non arriva ad un altro divisore, azzerando la dimensione della vista che contiene; in questo modo scopriamo subito che le viste non amano essere ridotte a zero: quando vengono riaperte gli oggetti contenuti vagano senza ritegno in posti impensati. Purtroppo la classe NSSplitView non possiede metodi per impostare l'ampiezza massima e minima di una view e di conseguenza non possono nemmeno essere inseriti da Interface Builder. È il momento di scrivere codice!
Tutte i metodi interessanti si trovano nel protocollo del delegate. Quindi, per prima cosa dobbiamo informare lo splitView che da ora possiede un delegate: questo può essere fatto da XCode (collegando il suo outlet delegate all'oggetto delegate) oppure direttamente da codice con il metodo -setDelegate: seguito dall'oggetto eletto a questo rango.
Personalmente, preferisco dedicare un oggetto apposito per queste cose, per cui creiamo una nuova classe, sottoclasse di NSObject e dichiariamo che seguirà le leggi previste per il delegate di uno splitView. In termini semplici, il nostro header si presenterà come:
interface mySplitDelegate : NSObject <NSSplitViewDelegate>
end
e non dichiariamo alcun metodo, poiché l'essere un delegate ne prevede già una serie, tra cui quelli che servono a noi. In particolare, quelli preposti alla sorveglianza delle dimensioni sono:
- splitView:constrainMinCoordinate:ofSubviewAt:
- splitView:constrainMaxCoordinate:ofSubviewAt:
dove la prima gestisce il minimo, la seconda il massimo.
Questi metodi sono chiamati dallo split quando vuole sapere se permettere uno spostamento di un divisore e per saperlo passa al delegate lo split in questione (primo parametro), la proposta di valore minimo nelle coordinate dello splitView (secondo parametro, è quello che userebbe se questi metodi non fossero implementati), il divisore che si sta muovendo (i divisori sono numerati da 0, come se si riferissero alla vista alla loro sinistra).
Come esempio, consideriamo il caso di uno split a 3 viste (2 divisori) in cui vogliamo che ciascuna di esse non possa essere azzerata; definiamo con delle macro queste dimensioni:
#define kMinWidthView1   100   //minimo vista 1, a sinistra
#define kMinWidthView2   200   //minimo vista 2, centrale
#define kMinWidthView3    50   //minimo vista 3, a destra
Supponiamo di muovere il divisore 0 (tra la prima e la seconda vista). La proposta che arriva al delegate è per un valore minimo di 0 (divisore tutto a sinistra, collassando la prima vista); noi invece ritorneremo un valore di 100 pixel. Se invece muoviamo il secondo divisore, la proposta non sarà 0 ma sarà uguale alla larghezza della prima vista a cui viene aggiunta la larghezza del divisore. Ma noi siamo inflessibili e gli rispondiamo che a questa posizione dovrà aggiungere 100. Sotto forma di codice:
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex
{
  CGFloat constrained = proposedMinimumPosition;
  float dividerThickness = [splitView dividerThickness];
  switch (dividerIndex) {
    case 0:
      constrained = proposedMinimumPosition + kMinWidthView1;
      break;
    case 1:
      constrained = proposedMinimumPosition + dividerThickness + kMinWidthView2;
      break;
  }
  return constrained;
}
I casi sono tutti quelli possibili, dato che abbiamo solo 2 divisori. Ma come controllo la larghezza della terza view? Ovvio: inserendo un limite sulla dimensione massima della seconda view! Perciò implementiamo anche il secondo metodo (in effetti, devono sempre andare in coppia). La trovata infatti è che la dimensione massima di una vista è quella che non costringe la vista vicina a scendere sotto la sua dimensione minima. In pratica:
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex
{
  CGFloat constrained = proposedMaximumPosition;
  float dividerThickness = [splitView dividerThickness]; 
  switch (dividerIndex) {
    case 0:
      constrained = proposedMaximumPosition - 200 - dividerThickness;
      break;
    case 1:
      constrained = proposedMaximumPosition - 50;
      break;
  }
  return constrained;
}
Ora lanciamo l'applicazione e scopriremo che in effetti lo split si comporta come previsto, qualunque cosa facciamo con i divisori!
Notiamo che non abbiamo avuto bisogno di utilizzare la dimensione dello splitView: è lui stesso che ne tiene conto, passandoci la sua proposta di dimensione massima (che infatti non può andare oltre il collasso della vista vicina oppure oltre la dimensione della finestra che lo contiene).

La vista che scompare

Abbiamo ancora un problema: se l'utente decidesse che la terza vista è troppo noiosa, potrebbe volerla togliere di mezzo, ma non potrebbe perché il delegate lo impedisce! Ma è lo stesso delegate che si riscatta, mettendo a disposizione un altro metodo, che può coesistere con i due precedenti: si tratta di -splitView:canCollapseSubview:. Se si tratta della vista che vogliamo lasciar chiudere, ritorniamo TRUE altrimenti FALSE:
#define kViewToCollapse  2
- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview
{
  NSView *rightView = [[splitView subviews] objectAtIndex:kViewToCollapse];
  return [subview isEqual:rightView];
}
dove abbiamo approfittato del fatto che la proprietà -subviews ritorna un NSArray contenente le viste dello split.
Ora l'utente può far collassare la terza vista continuando a trascinare il divisore oltre il limite minimo: se mantiene la pressione sul mouse, quando arriva a circa la metà dell'ampiezza della vista, la vista stessa collassa e scompare! Per tornare indietro, basta andare col mouse sul bordo e spostare il divisore verso sinistra: la vista comparirà nuovamente, prendendo la dimensione minima prevista.
Notiamo che questo non crea problemi agli oggetti all'interno della vista: in effetti il collasso della vista non riduce la sua dimensione a 0, ma semplicemente la fa scomparire; quando la riapriamo, la ritroviamo con la stessa dimensione di prima, con gli oggetti ordinatamente al loro posto.

2012-08-19

Oggetti Booleani

Questa potrebbe sembrare molto semplice, ma poi finisce sempre che non me la ricordo mai!
Supponiamo di dover costruire un nuovo NSDictionary, tramite uno dei due metodi della classe
dictionaryWithObjectsAndKeys
dictionaryWithObjects:forKeys:
dove nel primo si fornisce una serie di coppie oggetto-chiave, mentre nel secondo si forniscono due NSArray, il primo con gli oggetti ed il secondo con le corrispondenti chiavi.
Ho detto "oggetti": infatti, se dobbiamo creare un elemento Booleano, non dobbiamo farci prendere dalla tentazione di inserire la coppia
YES, @"miaChiave"
Infatti, mentre @"miaChiave" è un oggetto, altrettanto non possiamo dire per YES! Per come sono definiti nel C, i Booleani sono interi, non oggetti!
La scrittura corretta della coppia è
[NSNumber numberWithBool:YES], @"miaChiave"
In questo modo, entrambi sono oggetti e la definizione è salva.
Come mai mi capita di dimenticarla? Perché il compilatore non esce con un errore nel caso sbagliato! Al massimo un warning. Anzi, compila perfettamente e lancia l'applicazione senza dire nulla! Salvo poi dare errori strambi, difficili da comprendere, se non dopo aver capito di cosa si tratta.
Nell'ultimo caso, per esempio, funzionava tutto, i valori venivano usati (erano property legati a dei checkbox), ma ad certo momento, completamente slegato dal loro utilizzo, il software andava in crash, senza nessun messaggio di errore! Tempo di debugging: quasi una giornata!
Quindi, occhio agli oggetti!

2012-06-04

Scroll di una NSView

Di solito le scroll-view funzionano da sole: per avere un campo di testo che permetta lo scroll basta inserire da Interface Builder l'oggetto completo e tutto funziona senza fare nulla; lo stesso per una tabella: quando le righe aumentano oltre le dimensioni previste, la NSTableView si porta dietro una scroll automatica.
Se poi vogliamo lo scroll di qualcosa non previsto, basta inserire l'oggetto nella finestra, selezionarlo e poi scegliere il comando XCode Editor > Embed In > ScrollView e vengono inseriti automaticamente gli oggetti giusti e legati fra loro.
Cosa succede invece se devo inserire una scroll su una view generata dinamicamente? In questo caso c'è qualche problemino da risolvere.

Per prima cosa, prepariamo la scrollView che useremo dal codice: aprendo lo xib giusto, trasciniamo dall'elenco degli oggetti una NSScrollView nella finestra; lasciamo tutte le sue impostazioni invariate.
Prepariamo poi una NSView (come consiglia Apple, in un nuovo xib, supponiamo col nome di "myView"): questa sarà la view che caricheremo da codice e che vogliamo abbia la possibilità di avere lo scroll.
Nel codice dell'oggetto che controlla la finestra (di norma sarà l'AppDelegate, ma non è necessario lo sia) avremo un NSViewController, che si occuperà di caricare la view e supponiamo che questo controller sia già inizializzato (come outlet o da codice), chiamiamolo loadedViewController. Nello xib, avremo impostato il File's Owner come NSViewController e collegato l'outlet view alla NSView che caricheremo.
Nel file appDelegate.h avremo qualcosa del tipo:
@property (retain) NSViewController *loadedViewController;
@property (assign) NSScrollView *myScrollView;

- (IBOutlet)caricaView:(id)sender;
mentre nel appDelegate.m supponendo che il caricamento avvenga tramite un click su un pulsante:
- (IBOutlet)caricaView:(id)sender
{
    [NSBundle loadNibNamed:@"myView" owner:loadedViewController];
    NSView *addedView = [loadedViewController view];
    [myScrollView setDocumentView:addedView];
}
La parte importante è stata fatta! Se lanciamo l'applicazione, quando premiamo il pulsante la vista viene caricata (per semplicità non c'è nessun controllo di errore) e se riduciamo la dimensione della finestra, escono fuori le scroll bar laterali, che ci permettono di fare lo scroll!
Ma: attenzione! Proviamo ad allargare molto la finestra e... vedremo che la nuova view resta attaccata al bordo inferiore della scroll view e non al superiore, come sarebbe logico attendersi! Potremmo anche lasciare tutto così, ma gli utenti Mac si aspettano che ogni cosa funzioni in modo logico, per cui questo posizionamento in basso dà fastidio e l'impressione che la nostra applicazione sia buttata lì, senza grande cura. Per cui dobbiamo fare in modo che la view resti in alto.
Cercando in giro per il web, ho trovato l'unico modo è di fare una sottoclasse di NSClipView (che è l'oggetto creato in automatico che fa da tramite tra la NSScrollView e la documentView (cioè quella che viene caricata dinamicamente) e fare in modo che risponda YES alla property Flipped (cioè, facciamo finta che le coordinate siano invertite).
Quindi creiamo i file della sottoclasse:
------- File myClip.h:
#import <Cocoa/Cocoa.h>
@interface myClip : NSClipView
@end
------- File myClip.m:
#import "myClip.h"
@implementation myClip

- (BOOL)isFlipped {return YES;}
@end
L'unico metodo è in fatti isFlipped, che ritorna sempre TRUE e quindi la scroll agisce di conseguenza.
Ora però dobbiamo dire alla nostra scroll di usare questa classe: da Interface Builder non si può, perché la clipView non è accessibile e viene create in automatico durante l'esecuzione. Però... c'è ovviamente un comando che fa la stessa cosa: setContentView.

Riassumendo, il metodo dell'appDelegate diventa:
- (IBOutlet)caricaView:(id)sender
{
    [NSBundle loadNibNamed:@"myView" owner:loadedViewController];
    NSView *addedView = [loadedViewController view];
    [myScrollView setContentView:[[[myClip alloc] init] autorelease]];
    [myScrollView setBackgroundColor:[NSColor windowBackgroundColor]];
    [myScrollView setDocumentView:addedView];
}
dove è stato anche aggiunto il colore di sfondo per la scrollView (altrimenti restava bianco). Abbiamo fatto l'autorelease della clipView, perché viene già ritenuta dalla scrollView.
Se ora lanciamo l'applicazione, vedremo che la view viene caricata in alto e lì resta!

2012-05-19

Tabelle dipendenti

Supponiamo di avere un'applicazione Core Data con due Entity, Persona e Figlio. Queste abbiano le seguenti proprietà:

Persona {nome, cognome, figli}
Figlio {nome, eta, padre}


dove figli e padre sono relationship, la prima è one-to-many (un padre può avere più figli), la seconda è one-to-one (un figlio ha un solo padre) e ciascuna indica l'altra come relazione inversa.
Vogliamo far comparire nella prima tabella il nome di tutte le persone, mentre nella seconda dovranno comparire non tutti i figli (che sarebbe semplice), ma solo quelli il cui padre è selezionato nella prima tabella.

Inseriamo tutti i normali oggetti richiesti dall'architettura Core Data: nel nib mettiamo due NSArrayController (PersoneController e FigliController), entrambi avranno la proprietà ManagedObjectContext legata via binding (in InterfaceBuilder) alla proprietà ManagedObjectContext dell'AppDelegate.
Le prime due colonne della prima tabella (quella che deve contenere le Persone) avranno i seguenti binding:

Prima colonna (nome):

Value
Bind to: PersoneController
Controller Key: arrangedObjects
Model Key Path: nome


Seconda colonna (cognome):

Value
Bind to: PersoneController
Controller Key: arrangedObjects
Model Key Path: cognome


Analogamente, per la colonna nome della seconda tabella, quella dei Figli:

Value
Bind to: FigliController
Controller Key: arrangedObjects
Model Key Path: nome


Ora però si tratta di dire a Cocoa che il contenuto di FigliController non deve essere tutta la serie delle Entity Figlio, ma solo una parte.
Tenendo conto che una relationship one-to-many è descritta tramite un NSSet, la cosa si risolve effettuando il binding del Content Set del controller; per cui selezioniamo FigliController nel nib e imponiamo:

Content Set
Bind to: PersoneController
Controller Key: selection (consideriamo la relazione della persona selezionata)
Model Key Path: figli


Compiliamo e lanciamo: inseriamo un po' di dati e vediamo se abbiamo fatto tutto giusto!

NOTA: verrebbe spontaneo (almeno a me!) collegare il Value della colonna nome della seconda tabella ad un percorso tipo:
PersoneController.selection.figli.nome
ma se provate vedrete che non funziona: infatti alla colonna arriverebbe un NSSet, che il Value non può rappresentare.

2012-04-17

Liberarsi della documentazione Xcode che non ci serve

Con l'ultimo aggiornamento di Xcode, Apple ha trasformato il tool di sviluppo in una sola applicazione, per rendere più semplice il processo di aggiornamento tramite Mac App Store. Per facilitare questa piccola grande transizione, già dalla versione precedente, era possibile scegliere quali componenti dovevano essere installati e scaricarli direttamente da dentro l'applicazione.


Il problema

La documentazione scaricata direttamente sull'hard disk, oltre ad offrire una velocità indubbiamente superiore al download, ne permette la visualizzazione anche in assenza di una connessione ad Internet. Tuttavia, anche se ormai le dimensioni degli hard drive odierni non pongono quasi più questo problema, può dar fastidio, ad uno sviluppatore che scrive applicazioni solo per Mac, trovare gigabyte occupati per la documentazione di ben due versioni di iOS (mi è ancora ignoto per quale motivo Xcode permetta di scaricare la documentazione di iOS 5.0 e quella di iOS 5.1: la seconda non dovrebbe essere un aggiornamento della prima?); allo stesso modo, a coloro che sviluppano solo per il sistema operativo mobile, non serve a nulla la documentazione di Mac OS X 10.7.

La soluzione

La prima cosa che può venire in mente a chiunque è: "perché non posso semplicemente cancellare la documentazione che non utilizzo da Xcode?". Purtroppo, chi ha già tentato questa vita, avrà già scoperto che l'applicazione, per quanto metta a disposizione un pulsante "-", lo tiene rigorosamente disabilitato se sono selezionate documentazioni che vengono installate di default.

Per fortuna, ci viene in aiuto un dettaglio che gli sviluppatori Apple hanno pensato di mettere, forse per farci un favore: facendo click sul pulsante nell'angolo sinistro della tabella (la sua figura rappresenta un rettangolo con una freccia dentro) verrà aperta una sezione, normalmente nascosta, dove vengono fornite ulteriori informazioni sulla libreria selezionata, tra cui anche il suo indirizzo sul nostro hard disk. A questo punto, non ci basterà che farvi click sopra per vederlo apparire nel Finder.

Metto a disposizione, qui di seguito, le posizioni di default dove vengono installate le librerie di iOS 5.0, iOS 5.1 e Mac OS X 10.7:

iOS 5.0 (557,4MB)
~/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.AppleiOS5_0.iOSLibrary.docset 

iOS 5.1 (559,3MB)
~/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.AppleiOS5_1.iOSLibrary.docset 

Mac OS X 10.7 (996,9MB)
/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.AppleLion.CoreReference.docset 

Fate attenzione al fatto che, mentre la documentazione di iOS è situata nella Libreria dell'utente (simboleggiata dal carattere ~), quella di Mac OS X viene installata nella Libreria di sistema.

(Nota: su Mac OS X Lion e superiori, la cartella Libreria dell'utente è nascosta per default. Potete raggiungerla dal collegamento fornito da Xcode, oppure aprendo il menù Vai del Finder mentre si tiene premuto il tasto Option, e scegliendo la voce "Libreria")


Una volta selezionato il file che ci interessa, non dovremo far altro che spostarlo nel Cestino. Il Finder richiederà l'autorizzazione di un amministratore per eseguire tale operazione, ma una volta fornita la password, il file sarà spostato nel Cestino e sarà possibile eliminarlo.

Si consiglia di eseguire l'operazione di eliminazione mentre Xcode è chiuso. Nel caso in cui si desideri riaccedere alla documentazione cancellata, è possibile riscaricarla dal pannello "Downloads" delle Preferenze di Xcode.

2012-04-02

Quando il PopUp cambia

Come reagire ad un cambio della selezione di un menu popup?
La cosa è piuttosto semplice, ma spesso me la dimentico, per cui ora la inserisco qui sul blog!
Non parlo dei popup costruiti direttamente nell'Interface Builder, ma di quelli costruiti dinamicamente da codice.
Per prima cosa, occorre introdurre un NSArrayController, che conterrà i valori che saranno presenti nel menu del popup. Tali valori possono trovarsi in un file plist, in modo da essere caricati all'inizio (p.es. nel metodo AwakeFromNib) o generati direttamente da codice con il metodo AddObject dell'ArrayController.
Creiamo un ArrayController nel nib, scegliendolo nella libreria dell'Interface Builder e trascinandolo nel nib, come al solito. È conveniente dargli un nome specifico, giusto per poterlo distinguere facilmente.
Poi nell'header del nostro oggetto (nel seguito supponiamo che sia il delegate dell'applicazione) creiamo un Outlet:
IBOutlet NSArrayController *myArrayCtrl;
Selezioniamo il nib, facciamo un ctrl-drag dall'icona dell'arrayController al delegate e dal menu che ne esce scegliamo myArrayCtrl: abbiamo quindi unito il controller all'outlet del delegate.
Inserendolo nel nib e legandolo all'Outlet, siamo sicuri che venga creato; se così non fosse, dovremmo crearlo noi per esempio nel metodo init.
Ora selezioniamo il menu popup che avremo aggiunto, andiamo nell'inspector dei bindings, sulla destra dell'Xcode e leghiamo le seguenti property dei popup ai corrispondenti percorsi del controller:

content → myArrayCtrl.arrangedObjects
selectedIndex → myArraCtrl.selectionIndex


Se l'array contiene un NSDictionary, collegheremo anche

contentValues → myArrayCtrl.arrangedObjects.theName

avendo supposto che la chiave theName contenga quello che vogliamo compaia nel menu.
Nell'Interface Builder abbiamo finito; ora andiamo nel .m del delegate; in un metodo di inizio (p.es. awakeFromNib oppure applicationDidFinishLaunching) dobbiamo registrare il delegate come osservatore della chiave selectionIndex:
 
[myArrayCtrl addObserver:self forKeyPath:@"selectionIndex" options:NSKeyValueObservingOptionNew context:nil];
 
Il valore NSKeyValueObservingOptionNew ci mette in condizioni di conoscere il nuovo valore.
Ora il delegate sta in ascolto delle variazioni cercate, ma occorre che sappia rispondere al metodo
 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
 
Tutte le volte che il popup cambia selezione (quindi nessuna chiamata se viene soltanto aperto e rilasciato senza cambiare), viene chiamato questo metodo: nella sua implementazione potremo decidere cosa fare quando la selezione cambia.
Da notare che la chiamata avviene per qualunque oggetto la cui chiave selectionIndex è cambiata: quindi, se abbiamo altri observer sul selectionIndex dovremo controllare nel codice del metodo se si tratta del nostro popup: in questo caso l'oggetto object nel NSDictionary *change sarà contenuto il nuovo valore; questo ci permette di monitorare più di un menu e prendere decisioni diverse. In caso di necessità, possiamo settare l'oggetto context nell'AddObserver, in modo che nel metodo possiamo controllare se la situazione è quella voluta: per esempio, potremmo far scattare l'azione solo se il menu è cambiato in una certa condizione e non far nulla negli altri casi.

2012-03-24

Metodi deprecati e nuove funzionalità

Dispiace dover sospendere il supporto al 10.5, ma in Snow Leopard e Lion sono state introdotte in Cocoa molte possibilità aggiuntive.
La più semplice da implementare senza problemi è certamente la nuova modalità di Full Screen nel 10.7: basta semplicemente aprire in Xcode il nib, selezionare la finestra a cui vogliamo aggiungere questa funzionalità e scegliere nel menu drop-down Full Screen la voce Primary Window.
Il risultato sarà che in Lion la finestra potrà andare a schermo pieno, mentre nei precedenti sistemi la cosa non sarà disponibile ma comunque non creerà problemi.

Più difficile invece gestire alcune funzioni che nel 10.6 e 10.7 sono deprecate, mentre nel 10.5 devono essere usate. In questo caso, il procedimento, pur essendo semplice, deve essere ragionato.

Base + Deployment

L'SDK usato come base deve contenere entrambi metodi (quello attuale e quello deprecato), per cui conviene impostarlo all'SDK più recente (nell'esempio corrente può andare anche quello del 10.6, che contiene il metodo che funziona anche sul 10.7. Per far questo, nel navigatore di sinistra di Xcode clicchiamo sulla prima riga (dove c'è il nome del progetto); nella schermata che viene fuori, altro click sul nome del Project; altro click su Build Settings. Nella lista di impostazioni, cerchiamo la voce Base SDK e impostiamo Mac OS X 10.6; poi cerchiamo la voce Mac OS X Deployment Target e impostiamo a Mac OS X 10.5.
Ora ripetiamo le stesse impostazioni dopo aver cliccato a sinistra sul target del nostro software.
Per verificare che tutto sia a posto, andiamo sul tab Summary in alto, da cui dovremmo vedere la conferma che il Deployment è sul 10.5.

Implementazione

Ora la parte più concettuale: dobbiamo trovare una funzionalità introdotta dal 10.6, cioè che sul 10.5 non esisteva. Il caso che andremo ad esaminare è legato all'apertura di un file tramite un NSOpenPanel, dove vogliamo che l'utente possa selezionare solo alcuni tipi di file.
Il problema è che sul 10.5 si usa il metodo runModalForDirectory:file:types:, mentre nel 10.6 e seguenti tale metodo è deprecato (esiste ancora e funziona, ma in seguito potrebbe essere eliminato) e occorre usare il metodo runModal e in precedenza usare altri metodi per definire la directory di partenza ed i tipi permessi.
Il tutto è complicato dal fatto che il metodo setAllowedFileTypes esiste anche nel 10.5, ma non ha alcun effetto! Anzi, chiamandolo si annulla l'eventuale precedente impostazione! Occorre trovare un altro metodo che ci permetta di discriminare.
Io ho scelto setDirectoryURL, introdotto nel 10.6 per impostare la directory di partenza. Per verificare se questo metodo esiste (e allora siamo sul 10.6) userò il metodo comune respondsToSelector.
Inoltre, noi siamo puristi della programmazione per cui non vogliamo per nessun motivo duplicare inutilmente del codice, usando un IF per scegliere una via o l'altra.

Per prima cosa, settiamo il pannello di apertura e costruiamo il vettore che contiene i tipi permessi:
NSOpenPanel *myPanel = [NSOpenPanel openPanel];
[myPanel setCanChooseFiles:true];
[myPanel setAllowsMultipleSelection:YES];
[myPanel setCanChooseDirectories:NO];
[myPanel setTitle:@"titolo"];
[myPanel setMessage:@"spiegazione"];
NSArray *allowedTypes = [NSArray arrayWithObjects:@"png",@"gif"];
[myPanel setAllowedFileTypes:allowedTypes];
Ora la pensata: usiamo il costrutto IF nella forma implicita, dove avviene l'apertura del pannello con uno dei due metodi, a seconda del sistema:
BOOL ok = ( [myPanel respondsToSelector:@selector(setDirectoryURL:)] ) ? 
          ( [myPanel runModal] == NSFileHandlingPanelOKButton ) : 
          ( [myPanel runModalForDirectory:nil file:nil types:allowedTypes] == NSOKButton );
Notiamo che la scelta viene fatta sul risultato del metodo respondsToSelector:.
A questo punto il pannello è ormai chiuso e a noi basta controllare se la variabile ok è vera (cioè l'utente ha fatto una scelta e cliccato su ok:
if ( ok ) {
   <...facciamo qualcosa...>
}

Nota: avremmo anche potuto calcolare la versione del sistema su cui ci troviamo, ma questo implica operazioni non semplici e qualche volta con risultati strani; in questo modo, la sequenza è semplicissima.

2012-03-11

Errore "property cannot pair"

Vi siete mai trovati davanti all'errore sibillino:
error: writable atomic property 'miaProperty' cannot pair a synthesized setter/getter with a user defined setter/getter
Bene: significa che state usando in modo disinvolto la definizione delle property di Cocoa!
Infatti, nella definizione
@property (retain,...) NSObject *miaProperty;
se non viene esplicitato nonatomic, i metodi saranno atomic per default. Allora non potrete usare @synthetize nell'implementazione della classe, fornendo anche una vostra implementazione degli stessi metodi.
Quindi, per eliminare il problema, potete fare una delle 3 cose seguenti:
- usare la direttiva @dynamic (invece di @synthetize);
- usare @synthetize, ma tenersi i metodi automatici, senza fornirne di propri;
- non usare alcuna direttiva e scrivere per proprio conto entrambi i metodi setter/getter.
Esiste anche un quarto metodo, che consiste nel dichiarare nonatomic la property, consigliabile solamente se non abbiamo necessità di settare/leggere la property da thread diversi.

2012-03-04

Aprire un file con l'applicazione di default

La cosa è molto semplice, ma se non si sa dove cercarla si rischia di girare a vuoto nella documentazione. Chi ha esperienza di Applescript, tende a cercare la soluzione nel Finder (il famoso comando tell Application...).
Apple invece ha nascosto la funzionalità in un oggetto tutto-fare, chiamato NSWorkspace, di cui esiste normalmente una sola istanza, ottenuta col metodo sharedWorkspace.

Quindi, come esempio, vediamo come far aprire dall'applicazione di default un file di testo mioFile che si trova sulla scrivania:
NSString *percorso = [NSString stringWithString:@"/Users/utente/Desktop/mioFile.text"];
NSWorkspace *istanza = [NSWorkspace sharedWorkspace];
if ( ! [istanza openFile:percorso] ) NSLog(@"Non ci sono riuscito");
Nella stringa percorso abbiamo immagazzinato il path al file, nome compreso, che diamo al metodo openFile dell'NSWorkspace. Il ritorno del metodo viene usato per controllare che l'apertura sia andata a buon fine.

Bindings via codice: i falsi amici

I Binding sono una benedizione del cielo, non lo si può nascondere! Con pochi click, e senza scrivere una riga di codice (o comunque scrivendone pochissime, se dobbiamo impostare qualche property aggiuntiva) possiamo aggiornare campi e controlli dell'interfaccia grafica, senza andare ad intaccare le performance dell'applicazione.

La magia che sta dietro ai Binding è ancora oscura ai più, per quanto sia largamente spiegata nella Cocoa Bindings Reference. L'importante è che, in qualsiasi condizione, questi sono in grado di semplificarci di molto il lavoro. Ma, purtroppo, la possibilità di abilitarli e configurarli direttamente dall'interfaccia grafica dell'Xcode, preclude molte possibilità ed, in alcune condizioni, non è abbastanza personalizzabile per essere utilizzata.

Per questo, ci viene in aiuto il codice, che ci permette di ottenere lo stesso risultato che si ottiene collegando i Binding dalla GUI di Xcode, tramite un metodo Cocoa. Il problema è che questo metodo non è affatto spiegato bene, e nella documentazione dedicata a lui vengono riportate informazioni al quanto generiche.
- (void)bind:(NSString*)bind toObject:(id)object withKeyPath:(NSString*)keyPath options:(NSDictionary*)options;
A prima vista, non sembrerebbe una cosa complicata: il fatto è che, appena ci si immerge nella compilazione dei parametri del metodo, ci si accorge che il valore previsto dal parametro "bind" è una semplice stringa. La domanda sorge, quindi, spontanea: cosa devo metterci lì?

La documentazione Apple non aiuta per niente. Ma gironzolando per Internet si viene a scoprire che esistono delle costanti da utilizzare per i binding predisposti da Apple: queste costanti sono elencate in questa pagina, dove è anche elencato il metodo proposto più sopra, ma le due cose non vengono collegate. Le costanti si trovano in fondo alla pagina, ma sarete voi a dover capire quali di quelle sono supportate dall'oggetto di cui dovete fare il bind.

Un ulteriore metodo molto interessante, è l'antagonista di quello precedente, ovvero:
- (void)unbind:(NSString*)bind;
Anche qui, il valore della stringa bind va ricercato tra le costanti che vi ho indicato in precedenza e, chiaramente, serve per annullare l'effetto causato dal metodo bind:toObject:withKeyPath:options:, eliminando così il binding

2012-01-11

Compilare per PowerPc con Xcode 4 - Aggiornato

E continuo con Xcode 4: con questa versione dello strumento di sviluppo, Apple ha deciso di far dismettere lo sviluppo di applicazioni per PowerPc; questo coincide con l'abbandono della piattaforma Rosetta (che permetteva appunto ai software per PowerPc di girare anche sui processori Intel) con l'arrivo di Lion.
Non è stato un abbandono improvviso: la coesistenza di Intel e Rosetta durava dal MacOSX 10.5. Tuttavia sappiamo che i Mac, anche se vecchi, possono ancora avere un loro utilizzo, per cui esiste ancora richiesta per software per PowerPc. Ma chi vuole rilasciare applicazioni Universal (cioè per entrambi i processori) ed contemporaneamente utilizzare le ultime versioni degli strumenti di sviluppo, deve adattarsi ad avere sia la versione 3 che la 4 di Xcode, installati sullo stesso computer (a meno di non dover dedicare un Mac apposta allo sviluppo con Xcode 3.
Ho già spiegato in un post precedente che comunque sono costretto a tenermi anche una versione di Xcode 3, almeno per costruire un nuovo progetto, per alcuni bachi presenti nella versione 4. Tuttavia, avere il 4 e dover usare il 3, non è proprio una cosa soddisfacente.
Dopo aver fatto un po' di ricerche, mi sono imbattuto quasi per caso nella soluzione, che riporto qui di seguito, giusto per avere tutto in un solo posto e per condividere quanto ho ottenuto. I siti che mi hanno suggerito come procedere sono soprattutto due: il primo è un post su Stackoverflow ed il secondo sul blog bdunagan; servono entrambi, in quanto alcune sono condizioni sono cambiate con le diverse versioni di Xcode 4.
Prima di cominciare: si tratta di operazioni da effettuare da terminale, in cui anche gli spazi potrebbero essere significativi, per cui consiglio il procedimento a chi si trova a suo agio nella shell di unix. In caso di errori potreste essere costretti a re-installare Xcode, per cui tenetevi pronto il file di installazione e fate comunque un backup prima di partire. Mi raccomando: non mi prendo responsabilità per eventuali danni!

Requisiti

- ultima versione di Xcode 3 (3.2.6);
- ultima versione di Xcode 4 (4.2.1); vedere in fondo per la 4.3;
- utente amministratore da cui eseguire tutti i comandi;
- Snow Leopard o Lion (io sono su 10.7, ma dovrebbe funzionare anche su 10.6).

Installazione Xcode 3, Xcode 4

I post suggeriscono di disinstallare Xcode (qualunque versione) e di partire installando Xcode 3 in una cartella diversa da quella di default, p.es. scegliendo /Xcode3; non installate i System Tools/Unix development. Fatto questo, installiamo Xcode 4, stavolta nella cartella /Developer, come da default; installate i System Tools: non indispensabili, ma saranno certamente utili in seguito. Personalmente, ho copiato la cartella della versione 3 da un backup di SnowLeopard e poi installando la 4.

Supporto al 10.5 (Leopard)

Potremmo copiare l'SDK del 10.5; ma per modificare solo l'indispensabile (e per risparmiare 300MB di spazio disco) è sufficiente mettere al suo posto un link. Dal terminale, andiamo nella cartella degli SDK della 4 e creiamo un link alla versione 3:
cd /Developer/SDKs
sudo ln -s /Xcode3/SDKs/MacOSX10.5.sdk
Verrà chiesta la password di amministratore (comando sudo). Per verificare che tutto sia a posto, facciamo una lista della cartella, con il comando ls -l ed otterremo:
lrwxr-xr-x  1 root  wheel   33  8 Gen 17:04 MacOSX10.5.sdk -> /Xcode32/SDKs/MacOSX10.5.sdk
drwxr-xr-x  7 root  wheel  238  4 Gen 22:37 MacOSX10.6.sdk
drwxr-xr-x  7 root  wheel  238 22 Set 08:34 MacOSX10.7.sdk
dove la prima riga ci indica il link alla corrispondete cartella di Xcode 3.
Se ora creiamo un nuovo progetto in Xcode 4, vedremo che è disponibile anche la 10.5 come versione base! L'abbiamo ottenuto tramite un link: se l'avessimo copiato, avremmo anche dovuto cambiare i permessi, che invece ora sono quelli giusti.

Supporto PPC per il GCC 4.2

Ora c'è solo più da attivare il supporto per PPC per il compilatore CGG 4.2. Questa parte può essere diversa a seconda della versione di Xcode 4 e dalla presenza o meno di Lion. Nel mio caso (Xcode 4.2.1 su Lion), è necessario impostare un altro link dopo essere passati nella posizione indicata:
cd /Developer/usr/libexec
sudo ln -s /Xcode3/usr/libexec/gcc gcc
Ora è necessario ancora un ultimo passaggio per attivare il default llvm, spostandosi nella cartella:
cd /Developer/usr/llvm-gcc-4.2/bin
sudo ln -s /Xcode3/usr/llvm-gcc-4.2/bin/powerpc-apple-darwin10-llvm-gcc-4.2 powerpc-apple-darwin11-llvm-gcc-4.2
dove abbiamo impostato un nuovo link all'Xcode 3, ma stavolta cambiando il nome in darwin11 ed il link si riferisce questa volta solo ad un file, non ad un'intera cartella.

Nel caso di SnowLeopard, non è necessario il link al gcc (penultimo passaggio), ma occorre invece sostituire (facendo un backup) il file as:
cd /Developer/usr/libexec/gcc/powerpc-apple-darwin10/4.2.1
sudo mv as as.bak
sudo ln -s /Xcode3/usr/bin/as
NOTA: quest'ultima parte è riportata dai siti precedenti e non è stata da me verificata, essendo già su Lion. Se qualcuno ha occasione di verificarla, può lasciare un commento, utile per chi si trova nella stessa condizione.

Ora, potete aprire Xcode 4, preparare un minimo software di prova; se non abbiamo usato funzioni deprecate, possiamo compilare anche per OSX 10.5, impostandolo come SDK di base; per compilare anche per PPC, occorre variare in due punti nei Build Settings dell'applicazione: alla voce Architectures e poi ancora alla voce Valid Architectures, un po' più sotto. In entrambi i casi, è consigliabile inserirlo solo per Release, in quanto come Debug può non essere necessario. Per entrambe le voci, cliccando sul valore si apre un menu, dal quale si sceglie Other...: nella tabella risultante si aggiunge la voce ppc.
Compiliamo il nostro programmino (azione Archive) e una volta estratta l'applicazione, selezioniamola e apriamo le sue proprietà dal Finder (cmd-I): vedremo che alla voce Tipo sarà riportata la scritta "Applicazione (Universal)". Eureka!
E se vogliamo particolari in più, possiamo usare il comando lipo da terminale:
lipo -info ~/Desktop/miaApp.app/Contents/MacOS/miaApp
che risponderà:
Architectures in the fat file: ~/Desktop/miaApp.app/Contents/MacOS/miaApp are: x86_64 i386 ppc7400
cioè: Intel 32/64bit e PowerPc!

Aggiornamento Xcode 4.3

Tutta la procedura indicata, sia supporto al 10.5 che la compilazione per ppc vale anche per la versione 4.3 di Xcode con però un'importante modifica: Xcode 4.3 è ora un'applicazione a tutti gli effetti, per cui tutti gli strumenti, compilatori, ecc... si trovano all'interno del pacchetto dell'applicazione. Quindi tutti i percorsi che prima partivano con /Developer/... ora devono tenere conto della nuova posizione:
/Applications/Xcode.app/Contents/...
Il tutto è complicato dal fatto che la nuova struttura interna di cartella è diversa da quella precedente esterna, per cui bisogna andarsele a cercare! In particolare: per attivare il supporto al 10.5, cioè per inserire il link all'SDK dell'Xcode 3, occorre spostarsi nella cartella:
cd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs
sudo ln -s /Xcode3/SDKs/MacOSX10.5.sdk
L'inserimento del link per attire il gcc avviene invece con i seguenti comandi:
cd /Applications/Xcode.app/Contents/Developer/usr/libexec
sudo ln -s /Xcode3/usr/libexec/gcc gcc
mentre l'attivazione di llvm necessita dei seguenti:
cd /Applications/Xcode.app/Contents/Developer/usr/llvm-gcc-4.2/bin
sudo ln -s /Xcode3/usr/llvm-gcc-4.2/bin/powerpc-apple-darwin10-llvm-gcc-4.2 powerpc-apple-darwin11-llvm-gcc-4.2
dove notiamo che viene mantenuto darwin11, come per la versione 4.2.1.
Non posso verificare che la stessa cosa avvenga con Snow Leopard (sono su Lion), oltre tutto non sono sicuro che Xcode 4.3 possa esservi installato.

Nota

Sia per Xcode 4.2 che 4.3, in caso di aggiornamento tutte le modifiche sono perse! Quindi la procedura deve essere ripetuta.

2012-01-08

Xcode 4 e l'Help del progetto

Sempre a proposito di problemi di Xcode 4.2: la versione precedente aveva già collegato il menu Help, per cui bastava inserire le chiavi corrette nel file nomeProgetto.plist e si era a posto. Xcode 4 no!
Per fortuna, qui la soluzione è semplice: basta collegare la voce del menu Help nomeProgetto Help con il metodo showHelp del First Responder!
Facciamo per precauzione un Clean, compiliamo e... ecco che il menu Help è attivo e funzionante!
Naturalmente, ricordiamoci di inserire le chiavi CFBundleHelpBookName e CFBundleHelpBookFolder, che indicano il nome del file di partenza e della cartella in cui è inserito.

PS: tutto questo è riferito al metodo vecchio per registrare l'Aiuto, di default fino a MacOSX 10.5, ma supportato ancora anche in Lion.

2012-01-03

Core Data e Xcode 4.2

Ho incontrato questo problema recentemente: fino ad allora avevo usato Xcode 4 solo per aggiornare progetti nati su Xcode 3; ma da poco sono partito da zero con un nuovo progetto basato su Core Data. Ho provato e riprovato, re-installato da zero Xcode 4 dopo aver rimosso tutto con lo script da terminale consigliato da Apple, ma il risultato ora è definitivo: Un progetto Core Data nato su Xcode 4.2.1 non è completo e genera un'applicazione con i seguenti problemi:

I comandi Undo/Redo non funzionano

Le azioni di Undo e Redo, che Core Data fornisce in automatico, non sono disponibili; o meglio: lo stack di Undo/Redo viene mantenuto correttamente, ma le voci di menu sono sempre disabilitate. Ho provato a collegare il binding Enable al metodo canUndo dell'UndoManager: in effetti il menu si abilita, ma selezionandolo non succede nulla. L'ultimo tentativo è stato di collegare il menu ad una azione che chiamasse esplicitamente il metodo giusto:
[[self.managedObjectContext undoManager] undo]
Funziona, ma quando si va su un'altra finestra, per esempio con possibilità di editing testuale, quel menu continua (giustamente) a chiamare l'UndoManager di Core Data, rischiando seriamente il crash dell'applicazione e comunque ottenendo una cosa diversa.

Non è possibile nominare le azioni di Undo

Poca cosa, visto comunque che l'Undo non funziona...; comunque il messaggio
[[self.managedObjectContext undoManager] setActionName:@"Edit"]
non ha alcun effetto, anche se abilito il menu in qualche modo. Probabilmente è conseguenza del primo punto.

Il menu Save non è collegato all'azione SaveAction

Questo si risolve velocemente, collegando il menu all'IBAction; in Xcode 3 questo arrivava già pronto. Un volta collegato, il menu si abilita correttamente quando è necessario salvare i dati.

Il Persistent Store punta alla Libreria dell'utente, invece che all'Application Support

Anche questo potrebbe essere corretto in modo semplice: nel metodo dell'AppDelegate
- (NSURL *)applicationFilesDirectory
basta sostituire il parametro NSLibraryDirectory con NSApplicationSupportDirectory.

Il vero problema, non risolvibile per quanto ne so, è comunque quello del funzionamento dell'Undo; la prova del nove l'ho avuta riesumando Xcode 3, creando un progetto nuovo e poi aprendo lo stesso progetto da Xcode 4: tutto è tornato a funzionare!
Quindi per ora non gettate via Xcode 3; oltre tutto possono convivere entrambi sullo stesso hard disk (prima bisogna installare la versione 3 e poi la 4, altrimenti la cosa non funziona).

Ho posto il problema ad alcuni forum, ma non ho trovato risposte, nemmeno chiamando in aiuto Google; potrebbe anche essere un problema di concomitanza tra qualche baco e la versione italiana di Lion.
Se qualcuno ha notizie in proposito, lasciate un commento.
--------
Aggiornamento (dicembre 2012)
La versione attuale di Xcode ha finalmente risolto tutti i problemi elencati, escluso il mancato collegamento del menu Save; per fortuna questo è risolvibile impostando il collegamento mancante.
Nel frattempo, si sono rivelati altri bachi in Xcode, non certo al normale livello di qualità dei software Apple; per citarne uno, scordatevi di impostare una animazione usando l'interfaccia: non potrete più toglierla (se lo faccio, Xcode emette un ottimo errore e si chiude...)

2012-01-02

Inizio

Questo nuovo blog nasce il 2 gennaio 2012 semplicemente dall'esigenza di avere un quaderno di appunti su Cocoa e Objective-C, gli strumenti per la programmazione su MacOS X. Nessuna pretesa di completezza, ma solo un luogo in cui segnare i problemi incontrati e superati durante l'utilizzo di MacOS.
Esistono blog simili a migliaia, ma la stragrande maggioranza è in inglese, veramente pochi in italiano; spero che questo possa servire per diffondere ulteriormente queste conoscenze senza la barriera della lingua. Molti di questi problemi vengono spesso superati pescando dalla rete: in questi casi inserirò i link ai siti originali, sia per correttezza che per ricordare a me stesso dove ho trovato la soluzione.
Ogni lettore si senta libero di inserire commenti e suggerimenti, ampliando quindi le mie descrizioni. Eventuali commenti, a mio giudizio, offensivi o di spam verranno eliminati.
Buon Cocoa a tutti!