MaCocoa 065

Capitolo 065 - Cocoa e Unix

In questo capitolo mi ingegno a far interagire un programma Cocoa con Unix. Nella fattispecie, esamino un comando per scaricare file da internet, e lo incorporo all'interno di una interfaccia grafica in Cocoa (piuttosto brutta, va detto).

Sorgente: copio un esempio Apple (Moriarity), e non solo.

Prima stesura: 10 dicembre 2004.

CURL

Curl, che i più esperti scrivono cURL, è un programma per effettuare trasferimenti di file tramite internet, utilizzando diversi protocolli quali HTTP, FTP, e molti altri. Si tratta di un programma a linea di comando, ed è disponibile per molti sistemi operativi. In particolare, è già installato all'interno di Mac OS X (il sito afferma a partire dalla versione 10.1); è un progetto open source, e sono quindi disponibili i sorgenti.

figura 01

figura 01

Per poterne vedere gli effetti, occorre normalmente aprire una finestra con l'applicazione Terminale, e scrivere il comando apposito, curl, corredato da varie opzioni.

La cosa più semplice che si può fare con curl è scaricare un file da internet, ad esempio con il protocollo http. La sintassi in questo caso è la seguente:

curl -o file-destinazione http://url-file-da-scaricare

figura 02

figura 02

Ad esempio, volendo scaricare i capitoli 1-31 di Macocoa, si può scrivere:

curl -o dst.pdf http://macocoa.altervista.org/download/MaCocoa.pdf

Tante e variegate sono le opzioni e le funzioni messe a disposizione da cURL.

figura 03

figura 03

Si possono imparare leggendo banalmente il manuale messo a disposizione in linea, tramite il comando unix man o più comodamente con programmi quali ManOpen, o direttamente dall'interno di XCode (c'è una apposita voce sotto il menu Help) ; per il momento, mi interessano solo le funzioni base, visto che intendo focalizzare l'attenzione altrove.

L'idea è infatti di fornire una interfaccia grafica per questo programma, in grado di evitare ad un utente poco smaliziato l'apertura del Terminale e la scrittura dei comandi necessari.

L'interfaccia

Definisco un nuovo progetto; è una semplice Cocoa-Application, dal momento che non saranno gestiti documenti. Per il momento, l'interfaccia è di una bruttezza unica, tanto da far rimpiangere la linea di comando.

figura 04

figura 04

C'è un capo di testo dove scrivere l'indirizzo del file da trasferire, un campo dove indicare la directory locale dove salvare il file; la directory di destinazione può essere scelta attivando il pulsante Choose Dir, che attiva un pannello di selezione. Quando i due campi sono pronti, si può attivare il trasferimento con il pulsante Get File. Un ulteriore campo di testo mostra il risultato del comando.

Ho definito una classe per la gestione dell'interfaccia, chiamandola curlCtrl. Qui sono presenti gli outlet verso i vari controlli ed i vari campi di testo, ed i metodi che rappresentano le action attivate dai controlli stessi. Effettuo tutti i collegamenti necessari in Interface Builder, e poi comincio a scrivere un po' di codice.

All'apertura della finestra è eseguito il metodo awakeFromnib

- (void)
awakeFromNib
{
    // url di default
    [ theUrl setStringValue: THEFILENAME ] ;
    [ self setUrl2download: [ theUrl stringValue ]];
    // directory destinazione di default
    [ self setSaveFilePath: [ NSHomeDirectory( ) stringByAppendingString: STARTDIR ]];
    [ theDst setStringValue: saveFilePath ] ;
}

Con questo metodo predispongo dei valori di default per i campi che contengono lo URL e la directory di destinazione. In questo secondo caso utilizzo la funzione NSHomeDirectory per ottenere una NSString che rappresenta il percorso della home directory dell'utente (cui poi aggiungo una sotto directory giusto per mia comodità).

Curioso il metodo per selezionare la directory (lanciato da un clic sul pulsante Choose Dir) dove salvare il file trasferito, che utilizza, contrariamente alle credenze popolari, un pannello di apertura anziché di salvataggio.

- (IBAction)
nameDstFile:(id)sender
{
    // per scegliere uan directory, paradossalmente si usa il pannello
    // per scegliere un file... opportunamente configurato
    int            risposta ;
    NSOpenPanel    *sPanel = [ NSOpenPanel openPanel ] ;
    // preparo il pannello di salvataggio file
    [ sPanel setTitle:@"Directory destinazione" ];
    [ sPanel setPrompt:@"Scegli la directory di destinazione" ];
    [ sPanel setCanChooseDirectories: YES ];
    [ sPanel setCanChooseFiles: NO ];
    risposta = [ sPanel runModal ] ;
    if ( risposta == NSOKButton )
    {
        // se e' andato bene, prelevo il nome del file
        NSString *aFile = [ sPanel filename ] ;
        // e lo assegno alla variabile interna
        [ self setSaveFilePath: aFile ];
        [ theDst setStringValue: saveFilePath ] ;
    }
}

Fino a qui, però, non ci sono concetti nuovi. Ma ora, conviene rispolverare i Thread ed alcuni concetti di base di Unix.

Wrapping e Pipe

L'idea di avere una interfaccia grafica attorno ad un comando Unix non è nuova, anzi, è talmente diffusa da avere un nome proprio: wrapping (to wrap significa in inglese, incartare, avvolgere). L'idea stessa di eseguire un wrapping di cURL non è per nulla nuova. Ci sono almeno due programmi, decisamente più belli del mio: cURL GUI e iCurl. Ovviamente il mio scopo non è di fare concorrenza a questi programmi, ma di imparare ad utilizzare i comandi Unix all'interno di Cocoa. Quando ho parlato di Thread, avevo accennato anche all'esistenza dei Task, programmi del tutto autonomi ma in grado di comunicare con altri task secondo metodi ben definiti. Bene: l'interfaccia grafica è un task, e cURL un altro task. Tutto molto semplice e chiaro.

I problemi cominciano quando il task cURL deve essere lanciato dall'interno del task di interfaccia grafica, e quando i due task devono scambiarsi informazioni.

Come esiste la classe NSThread per la gestione dei thread, esiste la classe NSTask per la gestione dei task. Questa classe fornisce tutta una serie di funzionalità adatte al lancio ed alla gestione di task.

All'interno di Unix, lanciare un task equivale a scrivere una linea di comando: occorre conoscere il nome del programma, la directory in cui lanciare il programma, gli argomenti di lancio. Automaticamente, ad un comando sono associati tre file: lo standard input (dove il programma legge eventualmente ulteriori caratteri per operare), lo standard output (dove scrive i risultati delle operazioni svolte) e lo standard error (dove scrive le informazioni di errore). Questo è vero sempre, con alcuni piccoli accorgimenti. Ad esempio, lo standard input può essere la tastiera, in modo che il programma possa operare interattivamente (ma lo standard input non è un file! Invece sì, in Unix, qualsiasi cosa è un file, oppure è truccato per apparire tale...); per la applicazioni che non sono lanciate da linea di comando, si utilizzano file di default (console.log, ad esempio, visibile con l'omonima applicazione Console). Oppure, e finalmente, questi tre file possono essere dei pipe, ovvero, tipi speciali di file che servono per scambiare informazioni tra task (in inglese, pipe è un tubo). Con un pipe, lo standard output di un programma può essere utilizzato come standard input di un altro programma (come appunto un tubo mette in comunicazione due serbatoi, dove il primo pompa liquido verso il secondo): questo è il metodo tipico di Unix per far parlare tra loro due task. Per avere comunicazione bidirezionale, sono necessari due pipe (uno di andata e l'altro di ritorno), ma spesso uno solo è sufficiente. Ancora una volta, Cocoa definisce una classe, NSPipe, per rendere disponibile questa caratteristica ad alto livello.

CurlWrapper

Riassumendo: mi occorrono due concetti. Il primo concetto è la classe NSTask, che incapsula i meccanismi per lancio e gestione di un task; il secondo concetto è la classe NSPipe, che nasconde al suo interno i meccanismi di pipe per la comunicazione tra due task.

Comincio allora a scrivere una classe, CurlWrapper, che gestisca il lancio e la gestione del comando cURL.

Il metodo di inizializzazione è il seguente:

- (id)
initWithCaller: (id <CurlWrapperController>) aCaller inDir: (NSString* ) workDir
{
    self = [ super init ];
    // mi metto da parte un riferimento al chiamante
    theCaller = aCaller ;
#if    0
    // ricavo il path al programma curl all'interno del bundle
    curlPath = [ [ NSBundle mainBundle ] pathForResource: @"curl" ofType: @"" ] ;
#else
    // uso il path di default del comando nel sistema
    curlPath = [ NSString stringWithString: @"/usr/bin/curl" ];    
#endif
    [ curlPath retain ];
    // metto da parte la directory di lavoro
    workingDir = workDir ;
    [ workingDir retain ];
    return self;
}

Discuto dopo lo strano costrutto <curlwrappercontroller>; per ora basti sapere che aCaller è un riferimento alla classe che intende utilizzare il comando cURL (nella fattispecie, curlCtrl). All'interno del metodo metto da parte due stringhe importanti; la prima è il percorso completo del comando. Nel caso, il comando cURL si trova all'interno della directory /usr/bin, come si può verificare con il Terminale ed il comando whereis curl. Ora, il comando cURL è già presente nell'installazione standard di Mac OS X, quindi sono ragionevolmente sicuro di trovarlo. Se invece il programma Unix non è comunemente disponibile, ci sono due possibilità. La prima consiste nell'installazione separata dell'applicazione interfaccia grafica e del comando Unix (e questo, in una posizione opportuna). Tuttavia, conviene sfruttare il fatto che una applicazione Cocoa è in realtà una cartella speciale, al cui interno sono presenti tutte le risorse necessarie all'esecuzione.

Si potrebbe quindi aggiungere a XCode l'eseguibile all'interno del progetto, come una risorsa tra le altre. In questo modo, durante le operazioni di linking, il programma curl è copiato all'interno dell'applicazione finale cocoacurl.app. Così facendo, il suo path è facilmente noto: basta utilizzare la classe NSBundle ed i suoi metodi per recuperare il path completo, indipendentemente dalle particolarità dell'installazione dell'applicazione da parte dell'utente.

Nel caso di cURL, tuttavia, si pone un problema ulteriore, dovuto al fatto che di default il comando è costruito in modo da utilizzare librerie dinamiche piuttosto che statiche... insomma è un po' un pasticcio fare il tutto, per cui mi sono ridotto ad utilizzare l'installazione standard di cURL ed andare avanti.

Stabilito il programma da eseguire, occorre indicare la directory corrente; è lo scopo dell'argomento workDir del metodo, che è copiato in una variabile d'istanza per averlo disponibile successivamente.

A questo punto, mi trovo nella situazione di conoscere quale programma lanciare, e dove lanciarlo. L'effettivo lancio del programma avviene però in corrispondenza di un ulteriore metodo (perché? per nessuna ragione specifica; avrei in effetti potuto raccogliere tutto nel metodo di inizializzazione...):

- (void)
startDownloadWithArgs: (NSArray *) args
{
    NSPipe            * pipe= [ NSPipe pipe ] ;    
    NSFileHandle    * handle;

    // dico al chiamante che stiamo partendo
    [ theCaller downloadStarted ];
    // costruisco il task
    curltask = [[NSTask alloc] init];
    // predispongo i parametri di lancio del task
    [ curltask setLaunchPath: curlPath ];
    // la directory di lavoro e' quella di destinazione
    [ curltask setCurrentDirectoryPath: workingDir ];
    [ curltask setArguments: args ];
    // dico che l'uscita del comando va su una pipe (tipo speciale di file)
    // ridirigo sia lo standard output che lo standard error
    [ curltask setStandardOutput: pipe ] ;
    [ curltask setStandardError: pipe ];
    // piglio un riferimento al file - pipe
    handle = [ pipe fileHandleForReading ] ;
    // e dico al sistema operativo di dirmi quando ci sono dati all'interno del file
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(getData:)
        name: NSFileHandleReadCompletionNotification
        object: handle ];
    // e di farlo in ackground, con calma
    [ handle readInBackgroundAndNotify];
    // lancio il download
    [ curltask launch];
}

Qui risiede il cuore di tutto il meccanismo. Le operazioni svolte consistono sostanzialmente nella creazione e configurazione di una istanza di NSTask, o, in parole più brutali, nella costruzione di una linea di comando da mandare in esecuzione. Il metodo setLaunchPath indica, tramite la stringa che avevo già inizializzato, quale programma utilizzare; il metodo setCurrentDirectoryPath: specifica in quale directory il comando va lanciato, il metodo setArguments: specifica quali sono gli altri argomenti sulla linea di comando. A questo punto, invocando il metodo launch, potrei far partire il comando (e quindi il trasferimento dei file specificato dai parametri). Ho però il problema di scambiare informazioni con questo task, o meglio, mi piacerebbe che il comando cURL mi restituisse qualche informazione su ciò che sta facendo. Insomma, ho bisogno di far parlare tra loro due task: ecco il cuore del problema, dove entra il gioco il concetto di pipe.

Poiché nella configurazione del task ho già specificato ogni parametro, non ho bisogno di immettere ulteriori informazioni tramite lo standard input; mi interessa solamente trasferire informazioni dal task cURL al task di interfaccia grafica. Costruisco quindi un unico oggetto della classe NSPipe e gli faccio fare da ponte tra i due task. Per farlo, dico che sia lo standard output che lo standard error di cURL siano ridiretti sul pipe, piuttosto che utilizzare i valori di default. Su questo pipe (che, ribadisco, come quasi ogni cosa in Unix, è un file), definisco e configuro un meccanismo di notifica. Utilizzo la classe NSFileHandle, che è un meccanismo di comodo fornito per gestire file aperti o canali di comunicazione (appunto). Se il file è un file normale, ci sono metodi di più alto livello per gestirlo; ma se il file è, come in questo caso, un pipe, è meglio scendere a più basso livello. La cosa interessante che si può fare con questo NSFileHandle è di ricevere una notifica quando sono presenti nuovi dati all'interno del file stesso.

In pratica, faccio in modo che il File Manager di Mac OS X mi costringa ad eseguire il metodo getData: ogni volta che il pipe presenta dei dati.

- (void)
getData: (NSNotification *)aNotification
{
    // recupero i dati presenti
    NSData *data = [[aNotification userInfo] objectForKey:NSFileHandleNotificationDataItem];
    // se ci sono effettivamente i dati...
    if ([data length])
    {
        // li estraggo e li passo intonsi al chiamante
        NSString    * newdata = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease] ;
        [ theCaller appendOutput: newdata ];
    }
    else
    {
        // quando non ci sono piu' dati, il processo e' finito
        [self stopDownload];
    }
    // rifaccio ripartire la notifica
    [[aNotification object] readInBackgroundAndNotify];
}

Inoltre, il File Manager è così gentile da fornirmi, all'interno della notifica, un dizionario che contiene proprio i dati che sono presenti all'interno del file. Prendo allora questi dati (che poi sono una serie di caratteri) e li rispedisco indietro alla classe chiamante curlCtrl. Da notare che il meccanismo di notifica è a colpo singolo, ovvero, la notifica viene data una volta sola. Per poter proseguire le operazioni di lettura dati dal pipe, occorre far ripartire il meccanismo con il metodo readInBackgroundAndNotify.

L'ultimo metodo della classe CurlWrapper deve essere eseguito al termine del trasferimento, e serve a fare le pulizie di casa:

- (void)
stopDownload
{
    NSData *data;
    NSFileHandle    * handle;
    // ripiglio un handle alla pipe
    handle = [ [curltask standardOutput] fileHandleForReading ] ;
    // cancello le notifiche
    [[NSNotificationCenter defaultCenter] removeObserver:self
            name:NSFileHandleReadCompletionNotification
            object: handle ];
    // termino il task
    [ curltask terminate ];
    // estraggo gli eventuali dati presenti nel file
    while ((data = [handle availableData]) && [data length])
    {
        [ theCaller appendOutput: [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]];
    }
    // dico che ho finito le operazioni
[ theCaller downloadFinished ];
theCaller = nil;
}

Qui è infatti rimossa l'osservazione delle notifiche sul NSFileHandle, e si estraggono gli eventuali ultimi dati pendenti all'interno del file (nel caso in cui il trasferimento del file sia stato interrotto prima dell'effettivo completamento; occorre lasciare il pipe pulito).

Questioni di protocollo

A questo punto ritorno alla classe curlCtrl ed al fatto che questa classe deve rispettare un protocollo. Infatti (all'interno del file CurlWrapper.h) ho definito un protocollo cui devono rispondere tutte le classi che vogliono interagire con la classe CurlWrapper:

@protocol    CurlWrapperController
// quando c'e' qualcosa da riportare indietro al task principale
- (void)appendOutput:(NSString *)output;
// callback alla partenza di un download
- (void)downloadStarted ;
// callback al termine di un download
- (void)downloadFinished ;
@end

Con un protocollo indico esplicitamente quali metodi una classe deve realizzare per poter effettuare con successo degli scambi di dati o di funzioni con un'altra classe. In questo caso, per poter lavorare con il comando cURL sono indicati tre metodi. Il metodo downloadStarted è utilizzato per notificare l'avvenuta partenza di un trasferimento, il metodo downloadFinished per indicare la conclusione (eventualmente anticipata) del trasferimento, mentre il metodo appendOutput trasferisce indietro una stringa di informazioni.

In altre parole, se una classe A intende utilizzare la classe CurlWrapper, conosce i metodi propri di CurlWrapper descritti nella sua interfaccia pubblica:

@interface CurlWrapper : NSObject
...
// inizializzazione della classe con gli argomenti
- (id)initWithCaller: (id <CurlWrapperController>) aCaller inDir: (NSString*) workdir ;
// partenza delel operazioni
- (void) startDownloadWithArgs: (NSArray *) args ;
// fine delle operazioni
- (void) stopDownload ;
@end

Tuttavia, la classe A deve anche adeguarsi al protocollo CurlWrapperController, realizzando i tre metodi appendOutput, ... sopra descritti.

In effetti, per questo semplice esempio, tutta l'architettura impostata è piuttosto sovrabbondante: si poteva conglobare in una unica classe tutti i metodi di curlCtrl e CurlWrapper, semplificando di parecchio la programmazione. Ma il buon programmatore orientato agli oggetti deve lavorare per il futuro; scrivendo la classe CurlWrapper, ho separato la parte di gestione del comando cURL dal resto dell'applicazione, con la quale interagisco in modi ben definiti (l'interfaccia pubblica ed il protocollo). Oggi il comando cURL è utilizzato dalla semplice interfaccia grafica sopra vista, ma nulla impedisce inserire la classe CurlWrapper all'interno di applicazioni più complicate.

Ritorno a curlCtrl

C'è da descrivere come ho realizzato il protocollo CurlWrapperController all'interno della classe curlCtrl.

Utilizzo il metodo downloadStarted per predisporre l'interfaccia in modo che indici attività di trasferimento:

- (void)
downloadStarted
{
    NSMutableAttributedString    * appStr ;
    NSDictionary    * dd ;
    NSTextStorage    * txtSto = [messageField textStorage] ;
    curStyle = [ NSMutableDictionary dictionaryWithCapacity: 2 ] ;
    [ curStyle retain ];
    dd = [ txtSto fontAttributesInRange: NSMakeRange(0, [txtSto length]) ] ;
    [ curStyle addEntriesFromDictionary: dd ] ;
    dd = [ txtSto rulerAttributesInRange: NSMakeRange(0, [txtSto length]) ] ;
    [ curStyle addEntriesFromDictionary: dd ];    
    downloadInProgress = YES;
    [theGetButton setTitle:@"Stop"];
    appStr = [[NSMutableAttributedString alloc] initWithString: @"Downloading...\n"] ;
    [ appStr setAttributes: curStyle range: NSMakeRange(0, [appStr length])];
    [ txtSto setAttributedString: appStr ];
}

La parte più corposa consiste nel leggere gli attributi del campo di testo e salvarli in una variabile d'istanza; lo stile del testo è poi utilizzato per scrivere una stringa che segnala l'inizio lavori. Le altre due istruzioni presenti impostano l'una una variabile interna per indicare un trasferimento in corso, e l'altra modifica l'aspetto del pulsante in modo che presenti l'etichetta Stop.

L'etichetta è riportata al suo valore originario dal metodo downloadFinished

- (void)
downloadFinished
{
    downloadInProgress=NO;
    [ curStyle release ];
    [theGetButton setTitle:@"Get File"];
}

Il metodo reimposta anche le altre variabili d'istanza.

Ho lasciato per ultimo il metodo più importante, quello che è attivato dal clic dell'utente sopra il pulsante Get File (o Stop durante il trasferimento): risulta adesso di molta più semplice comprensione.

- (IBAction)
getFile:(id)sender
{
    // se il download e' in corso, lo fermo
    if ( downloadInProgress )
    {
        // fermo il task di download
        [ curlWrapper stopDownload];
        // lo distruggo del tutto
        [ curlWrapper release ];
        // e metto a nil per sicurezza
        curlWrapper=nil;
        // fine delle operazioni
        return;
    }
    else
    {
        NSString        * theNewUrl = [ theUrl stringValue ];
        // qui bisogna lanciare le operazioni di download
        NSString        * dstFile = [ theNewUrl stringByStandardizingPath ] ;
        // costruisco l'elenco degli argomenti
        NSMutableArray    * argList = [ NSMutableArray arrayWithCapacity: 1 ];
        [ self setUrl2download: theNewUrl ];
        dstFile = [ dstFile lastPathComponent ] ;
        // il mio primo argomento e' il nome del file
        [ argList addObject: [ NSString stringWithFormat: @"-o %@", dstFile ] ];
        // ...
        // a quanto pare, le opzioni non abbreviate con argomento van messe su due
        // argomenti successivi...
#if    0
        // la versione di default in Mac Os X 10.3 non gestisce
        // correttamente questa opzione...
        [ argList addObject: @"--limit-rate" ];
        [ argList addObject: @"50K" ];
#endif
        [ argList addObject: @"--progress-bar" ];        
        // l'ultimo argomento e' l'URL
        [ argList addObject: url2download ];
        // mi premunisco in caso di problemi
        if (curlWrapper !=nil)
            [curlWrapper release];
        // costruisco il task di download
        curlWrapper= [ [CurlWrapper alloc] initWithCaller:self inDir: saveFilePath ];
        // lancio il task di download
        [ curlWrapper startDownloadWithArgs: argList ];
    }
}

Comincio dalla parte else dell'istruzione if, che corrisponde all'operazione di lancio del trasferimento.

Prelevo lo URL completo del file da trasferire, e ne estraggo l'ultima parte, che uso anche come nome del file locale di destinazione. Preparo poi nel vettore argList l'insieme di tutti gli argomenti necessari al lancio del comando; col primo argomento, indico il nome del file destinazione, come secondo argomento faccio in modo che il comando scriva sullo standard output una sorta di barra di avanzamento piuttosto che brutali informazioni numeriche, infine aggiungo il percorso completo del file da trasferire. Costruisco poi un elemento della classe CurlWrapper, che è subito configurato passandogli la directory destinazione nel metodo di inizializzazione. Finalmente, si comanda l'esecuzione del trasferimento.

A questo punto, l'oggetto CurlWrapper appena creato, esegue il metodo startDownloadWithArgs sopra visto, costruendo il task e lanciando ill comando cURL passando gli argomenti indicati. Così facendo, si mette in moto il meccanismo di trasferimento; man mano che il procedimento va avanti, cURL scrive qualcosa nel pipe (la barra di avanzamento che ho specificato). Allora si scatena la notifica, che provoca l'esecuzione del metodo getData:, il quale a sua volta provoca l'esecuzione del metodo appendOutput:

- (void)
appendOutput:(NSString *)output
{
    float    perc ;
    NSMutableAttributedString    * appStr ;
    NSDictionary    * dd ;
    NSTextStorage    * txtSto = [messageField textStorage] ;    
    appStr = [[NSMutableAttributedString alloc] initWithString: output] ;
    [ appStr setAttributes: curStyle range: NSMakeRange(0, [appStr length])];
    [ txtSto appendAttributedString: appStr ];
    [messageField scrollRangeToVisible:NSMakeRange([[messageField string] length], 0)];
}

Questo metodo piglia la stringa passatagli come argomento e l'aggiunge, con l'opportuna formattazione, al campo di testo.

figura 07

figura 07

Il procedimento di trasferimento può andare avanti fino al suo naturale compimento; in tal caso l'oggetto CurlWrapper provoca direttamente l'esecuzione del metodo downloadFinished, che termina il tutto. Alternativamente, può essere l'utente stesso che, stufo di aspettare o per qualche altro motivo, interrompe il trasferimento facendo clic sopra il pulsante che adesso si chiama Stop.

Viene così forzata l'esecuzione del metodo stopDownload della classe CurlWrapper, il quale, a sua volta, richiede l'esecuzione di downloadFinished, e tutto termina pulitamente anche questa volta (ma il file, pur presente nella directory di destinazione, è presente solo in modo parziale...).

L'applicazione che ho sviluppato è veramente grezza; è addirittura meno semplice da usare del comando cURL da linea di comando, ed è estremamente limitata nelle funzionalità proposte. Si comporta male in caso di errore, è brutta a vedersi, insomma, una vera schifezza. Però mi ha permesso di fare una cosa veramente importante: due task, uno scritto in Cocoa e l'altro preesistente, si sono scambiati informazioni. Tutto sommato, un buon punto di partenza.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).