[iOs] Upload di immagini da app su server

Come primo post dell’anno, vi scrivo una soluzione per l’upload di foto su un vostro server (valida anche per qualsiasi altro tipo di file). Fino a poco tempo fa era possibile utilizzare il framework ASIHTTPRequest, che permetteva in modo agevole di eseguire questa operazione. Tuttavia, essendo tale framework “no-ARC compatibile”, vi consiglio un metodo nuovo che sfrutta la classe “nativa” NSURLConnection, oppure la libreria di terza parte URLConnection (per inserire una comoda progress bar).

Vi scrivo anche lo script PHP da richiamare lato app per l’upload della foto e che ho utilizzato come test (file image_upload.php):

/* image_upload.php */

<?php

//elenco delle estensioni permesse
$extensions_permitted = array("jpg", "png", "JPG", "PNG");

//path assoluto in cui si trova il file di script upload_image.php
$absolute_current_path = getcwd();

//recupero il nome del file dal parametro di input "filename"
$filename = $_POST['filename'];
$response = array();
$error = false;

//recupero l'estensione del file
$ext = pathinfo($_FILES['userfile']['name'], PATHINFO_EXTENSION);

//controllo il nome file
if(empty($filename)){
  $error = true;
  $response['code'] = '500';	
  $response['message'] = ('Nome file non valido!');	
  $response['newfile'] = $filename.'.'.$ext;
}

//controllo l'estensione
if (!in_array($ext, $extensions_permitted)) {
  $error = true;
  $response['code'] = '500';	
  $response['message'] = ('Estensione del file non permessa!');	
  $response['newfile'] = null;
}

if(!$error){
	//NOTA: la seguente directory deve esistere sul proprio server.
	//Assicurarsi di avere i chmod di scrittura
	$uploaddir = $absolute_current_path.'/img/';

	//percorso assoluto del nuovo file
	$uploadfile = $uploaddir . $filename.'.'.$ext;

	if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
    	$response['code'] = '200';	
  		$response['message'] = ('File valido e correttamente salvato!');	
        $response['newfile'] = $filename.'.'.$ext;
	} else {
    	echo "Errore nell'upload del file!\n";
        $response['code'] = '500';	
  		$response['message'] = ('Errore nell\'upload del file!');	
        $response['newfile'] = null;
	}

//echo 'Alcune informazioni di debug:';
//print_r($_FILES);

}

echo json_encode($response);

?>

Di seguito, una pagina di test PHP per controllare che il vostro script di upload sia stato configurato correttamente sul vostro server. NOTA. Vi troverete le immagini nella directory IMG relativa al file image_upload.php.

<html>
<head>
	<title>Upload a file</title>
</head>

<body>
<!-- Tipo di codifica dei dati, DEVE essere specificato come segue -->
<form enctype="multipart/form-data" action="upload_image.php" method="POST">

    <!-- MAX_FILE_SIZE deve precedere campo di input del nome file -->
    <input type="hidden" name="MAX_FILE_SIZE" value="30000" />

    <b>Nome file:</b>
    <input type="text" name="filename" value="" />

    <br/>

    <!-- Il nome dell'elemento di input determina il nome nell'array $_FILES -->
    <b>File:</b> <input name="userfile" type="file" />
    <br/>
    <input type="submit" value="Invia File" />
</form>

</body>

</html>

Ecco i codici sorgenti degli script PHP su riportati:

PHP upload script

upload
upload.rar (2 kB)

Template doesn't exists. Use default

Passiamo ora al codice Objective-C per la vostra app client. Ho creato la classe UploaderDelegate, che potete comodamente importare nei vostri progetti e che gestirà l’upload della foto. L’interfaccia è quella che segue:

//
//  UploaderDelegate.h
//
//  Created by Fr@nk on 07/01/14.
//  Copyright (c) 2014. All rights reserved.
//

#import <Foundation/Foundation.h>

@protocol UploaderProtocol <NSObject>

@required
-(void)successUpload;

@optional
-(void)unsuccessUpload;

@optional
-(void)updateProgressBar:(float)progress;

@end

@interface UploaderDelegate : NSObject<NSURLConnectionDataDelegate>{

    NSMutableData *receivedData;

}

//singleton
+ (id)sharedInstance;
-(void)sendImageToServerWithURLPath:(NSString*)pathImage withFilename:(NSString*)filename toServiceURL:(NSString*)serviceURL;

@property (nonatomic, weak) id<UploaderProtocol> delegate;

@end

L’implementazione dell’UploadDelegate è questa:

//
//  UploaderDelegate.m
//
//  Created by Fr@nk on 07/01/14.
//  Copyright (c) 2013. All rights reserved.
//

#import "UploaderDelegate.h"
#import "URLConnection.h"
#import "Configuration.h"

@implementation UploaderDelegate

@synthesize delegate;

static UploaderDelegate *sharedInstance = nil;

// Get the shared instance and create it if necessary.
+ (UploaderDelegate *)sharedInstance {
    if (sharedInstance == nil) {
        sharedInstance = [[super allocWithZone:NULL] init];
    }

    return sharedInstance;
}

- (id)init
{
    self = [super init];

    if (self) {

    }

    return self;
}

-(void)sendImageToServerWithURLPath:(NSString*)pathImage withFilename:(NSString*)filename toServiceURL:(NSString*)serviceURL{

    //controllo se il file esiste
    BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pathImage];

    if(!fileExists){
        NSLog(@"Error: file not exists!");
        //richiamo l'errorUpload sul Delegate in caso di success
        if (self.delegate && [self.delegate respondsToSelector:@selector(unsuccessUpload)]) {
            [self.delegate unsuccessUpload];
        }
        return;
    }

    //controllo la grandezza del file
    NSError *AttributesError = nil;
	NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:pathImage error:&AttributesError];
	NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
	long fileSize = [fileSizeNumber longValue];
	NSLog(@"File: %@, Size: %ld", pathImage, fileSize);
    if(fileSize<=0){
        NSLog(@"Error: file size incorrect!");
        //richiamo l'errorUpload sul Delegate in caso di success
        if (self.delegate && [self.delegate respondsToSelector:@selector(unsuccessUpload)]) {
            [self.delegate unsuccessUpload];
        }
        return;
    }

    //se la foto è troppo grande, la comprimo
    UIImage *originalImage = [[UIImage alloc]initWithContentsOfFile:pathImage];

    //NSData *dataImage = [NSData dataWithContentsOfURL:[NSURL URLWithString:pathImage]];
    //NSData *dataImage = [[NSFileManager defaultManager] contentsAtPath:pathImage];

    CGFloat compression = 0.9f;
    CGFloat maxCompression = 0.1f;
    int maxFileSize = MAXFILESIZE_IMAGE_UPLOAD;

    //comprimo l'immagine e la salvo in JPEG, se è troppo grande
    NSData *dataImage = UIImageJPEGRepresentation(originalImage, compression);

    while ([dataImage length] > maxFileSize && compression > maxCompression)
    {
        compression -= 0.1;
        dataImage = UIImageJPEGRepresentation(originalImage, compression);
    }

    NSMutableURLRequest *postRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serviceURL]];

    [postRequest setHTTPMethod:@"POST"];

    NSString *boundary = @"BVillage";
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
    [postRequest addValue:contentType forHTTPHeaderField: @"Content-Type"];

    NSMutableData *body = [NSMutableData data];

    [body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"filename\"\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[filename dataUsingEncoding:NSUTF8StringEncoding]];

    [body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[@"Content-Disposition: form-data; name=\"userfile\"; filename=\"tmpFile.jpg\"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[NSData dataWithData:dataImage]];
    [body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];

    [postRequest setHTTPMethod:@"POST"];
    [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

    // Setting a timeout
    postRequest.timeoutInterval = 60.0;
    [postRequest setHTTPBody:body];

    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

    /*[NSURLConnection sendAsynchronousRequest:postRequest
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
     */
    [URLConnection asyncConnectionWithRequest:postRequest completionBlock:^(NSData *data, NSURLResponse *response) {

        NSLog(@"Succeeded! Received %d bytes of data",[receivedData length]);
        [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

        NSDictionary *responseJSON = [NSJSONSerialization
                                      JSONObjectWithData:data
                                      options:kNilOptions error:nil];

        NSLog(@"Response JSON: %@",responseJSON);

        //if(!error){
            //richiamo il successUpload sul Delegate in caso di success
            if (self.delegate && [self.delegate respondsToSelector:@selector(successUpload)]) {
                [self.delegate successUpload];
            }
            return;
        /*}
        else{
            //richiamo l'errorUpload sul Delegate in caso di success
            if (self.delegate && [self.delegate respondsToSelector:@selector(unsuccessUpload)]) {
                [self.delegate unsuccessUpload];
            }
            return;
        }*/

    } errorBlock:^(NSError *error) {
        ////richiamo l'errorUpload sul Delegate in caso di success
        if (self.delegate && [self.delegate respondsToSelector:@selector(unsuccessUpload)]) {
            [self.delegate unsuccessUpload];
        }
        return;
    } uploadProgressBlock:^(float progress) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(updateProgressBar:)]) {
            [self.delegate updateProgressBar:progress];
        }

    } downloadProgressBlock:^(float progress) {
        //
    }

    ];

}

@end

Come potete vedere, ho utilizzato la libreria URLConnection, per poter mostrare durante il caricamento del file una “progress bar” (come spiegato in questo post: http://messagesenttodeallocatedinstance.wordpress.com/2012/04/10/nsurlconnection-with-blocks/).

Per poter usare la classe UploaderDelegate e procedere all’upload della foto, basta inserire nel vostro codice la seguente chiamata:

//chiamata al wrapper per l'upload dei file sul server
UploaderDelegate *uploaderDelegate = [UploaderDelegate sharedInstance];
uploaderDelegate.delegate = self;

NSString *pathFile = ABSOLUTE_PATH_OF_YOUR_PHOTO;
NSString *photoName = FILENAME_OF_YOUR_PHOTO;

    [uploaderDelegate sendImageToServerWithURLPath:pathFile withFilename:photoName toServiceURL:UPLOAD_IMAGE_URL];

NOTA. E’ importante settare che il pathFile sia il percorso assoluto alla vostra foto (comprensivo di nome del file e di estensione). La variabile photoName è il nome della foto (senza estensione) così come verrà salvata sul vostro server. Al posto dell’etichetta UPLOAD_IMAGE_URL ricordatevi di sostituire la URL del vostro servizio PHP di upload (per esempio, http://www.francescoficetola/upload/image_upload.php)

Ricordatevi di importare la classe suddetta e di settare la “delega” nel vostro ViewController, inserendo l’UploaderProtocol. Come ad esempio:

//
//  MyViewController
//
//  Created by Fr@nk on 07/01/14.
//  Copyright (c) 2014. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "UploaderDelegate.h"
#import "KKProgressToolbar.h"

@interface MyViewController : UIViewController<UploaderProtocol, KKProgressToolbarDelegate>{

    IBOutlet UIImageView *imageView;
    IBOutlet UIButton *imageUploadButton;
    KKProgressToolbar *progressBar;
}

/* metodi di delegate */
-(void)successUpload;
-(void)unsuccessUpload;
-(void)updateProgressBar:(float)progress;
-(IBAction)startProgressBarLoading;
-(IBAction)stopProgressBarLoading;

-(IBAction)sendPhotoToServer:(id)sender;

@property (nonatomic, strong) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) KKProgressToolbar *progressBar;
@property (nonatomic, strong) IBOutlet UIButton *imageUploadButton;

@end

Per poter gestire gli eventi relativi all’upload, occorre definire nel vostro ViewController i seguenti metodi di delegate:

  • successUpload: richiamato dall’UploaderDelegate in caso di upload con successo
  • unsuccessUpload: richiamato dall’UploadDelegate in caso di errore nell’upload
  • updateProgressBar: richiamato dall’UploadDelegate durante l’upload per aggiornare la percentuale di caricamento sulla progress bar
  • startProgressBarLoading: da richiamare, lato app, per inizializzare la progress bar
  • stopProgressBarLoading: da richiamare, lato app, per stoppare la progress bar

NOTA. Per gestire la progress bar ho utilizzato la seguente libreria: KKProgressToolbar

#pragma mark Uploader methods

-(void)updateProgressBar:(float)progress{
    self.progressBar.progressBar.progress = progress;
}

-(void)successUpload{
    NSLog(@"successUpload");
    //in caso di successo aggiorno rimuovo la progress bar
    [self stopProgressBarLoading];

}

-(void)unsuccessUpload{

    NSLog(@"unsuccessUpload");
}

#pragma StatusBarLoading

-(void)addProgressBar{

    //aggiungo la progress view:
    CGRect progressBarFrame = CGRectMake(0, self.view.frame.size.height-100, self.view.frame.size.width, 44);
	self.progressBar = [[KKProgressToolbar alloc] initWithFrame:progressBarFrame];
	self.progressBar.actionDelegate = self;
    self.progressBar.progressBar.progress = 0;
    self.progressBar.statusLabel.text = @"Loading from server...";
	[self.view addSubview:self.progressBar];

    [self startProgressBarLoading];

}

- (void)didCancelProgressBarButtonPressed:(KKProgressToolbar *)toolbar {
    [self stopProgressBarLoading];
    //TODO: eliminare la richiesta di upload...
}

- (IBAction)startProgressBarLoading  {

    [self.progressBar show:YES completion:^(BOOL finished) {
        //Code
    }];

}

- (IBAction)stopProgressBarLoading {
    [self.progressBar hide:YES completion:^(BOOL finished) {
        //code
    }];

}

Creative Commons License
This work by Francesco Ficetola is licensed under a Creative Commons Attribution 4.0 International License.
Based on a work at www.francescoficetola.it.
Permissions beyond the scope of this license may be available at http://www.francescoficetola.it/2014/01/06/ios-upload-di-immagini-da-app-su-server/.

[iOS] Come effettuare chiamate asincrone

La modalità sicuramente più performante per richiamare dei servizi online di back-end da una vostra app è quella delle chiamate asincrone.

1° METODO (con dispatch_async). Il classico metodo in iOS, che definirei “nativo”, è quello che utilizza il dispatch_async:

// Add to top of file
#import <;dispatch/dispatch.h>;

// Use it
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     NSData* responseData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://tuo-sito/servizio"]];

/* ... your code here ...*/

});
}

Non occorre importare nessuna libreria. Nella responseData vi è la risposta HTTP, da cui potete recuperare i vostri dati. Tale metodo richiama il Grand Central Dispatcher per eseguire la chiamata e ricevere una risposta (asincrona) dal server in background. Intanto, la vostra app continua ad eseguire sul thread principale il lavoro, senza bloccarsi in attesa dei dati del servizio richiamato.

2° METODO (con ASIHTTPRequest ). Il metodo che, invece, preferisco è quello che utilizza l’utilissima libreria ASIHTTPRequest, che ci permette di effettuare in modo semplice chiamate HTTP (sincrone e asincrone) e interagire con servizi REST (vedi l’articolo “La filosofia REST“, su questo blog). E’ una libreria particolarmente utile per effettuare anche upload e download di file, chiamate con autenticazione sicura HTTPS, e molto altro … e vi consiglio vivamente di provarla.

La potete scaricare direttamente dal sito ufficiale: ASIHTTP-request downloads

Come si spiega qui: Setup-Instructions, per installarla nel vostro progetto, occorre importare le seguenti librerie/framework (che trovate direttamente in xCode – click sulla root di progetto >> Build Phases >> Link Binary With Libraries):

  • CFNetwork,
  • SystemConfiguration
  • MobileCoreServices
  • libz.dylib (attualmente c’è libz.1.2.5.dylib)
Ecco un esempio di chiamata in POST ad un servizio REST che restituisce un JSON:
//import libs
#import "ASIFormDataRequest.h"

/* ... your code in controller ...*/

- (void)fetchedData:(NSData *)responseData {

    NSArray* json = [NSJSONSerialization
                     JSONObjectWithData:responseData
                     options:kNilOptions error:nil];

    NSMutableArray *postTMP = [[NSMutableArray alloc] initWithCapacity:[json count]];

    for (NSDictionary *dict in json) {
        Post *post = [[Post alloc] init];
        post.titolo = [dict objectForKey:@"titolo"];
        post.articolo = [dict objectForKey:@"articolo"];
        post.snippet = [dict objectForKey:@"snippet"];
        post.urlImage = [dict objectForKey:@"urlImage"];
        post.urlThumb = [dict objectForKey:@"urlThumb"];
        [postTMP addObject:post];
    }

    //mi assicuro che l'array non possa essere modificato.
    self.posts = [postTMP copy];
    NSLog(@"posts count: %d",[self.posts count]);

    [self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES];
}

- (void)requestFinished:(ASIHTTPRequest *)request {

    NSLog(@"Response %d ==>; %@", request.responseStatusCode, [request responseString]);
    NSData *responseData = request.responseData;

    [self performSelectorOnMainThread:@selector(fetchedData:) withObject:responseData waitUntilDone:YES];

}

- (void)requestFailed:(ASIHTTPRequest *)request
{
    //[progressAlert release];
    NSError *error = [request error];
    NSLog(@"ERROR %@",error);

    if ([[request error] code] == ASIConnectionFailureErrorType  ||
        [[request error] code] == ASIRequestTimedOutErrorType){
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"ERROR", nil) message:NSLocalizedString(@"CONNECTION_FAILED",nil)
            delegate:self cancelButtonTitle:NSLocalizedString(@"OK",nil) otherButtonTitles:nil];
        [alert show];
        [alert release];
        //return;

    }
    else{
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"ERROR",nil) message:
        NSLocalizedString(@"GENERIC_ERROR",nil)
            delegate:self cancelButtonTitle:NSLocalizedString(@"OK",nil) otherButtonTitles:nil];
        [alert show];
        [alert release];
        //return; 
    }  
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSURL *url = [NSURL URLWithString:@"http://francescoficetola.it/services/test.php"];

    ASIFormDataRequest *myRequest = [ASIFormDataRequest requestWithURL:url];

    [myRequest setDefaultResponseEncoding:NSUTF8StringEncoding];
    [myRequest setResponseEncoding:NSUTF8StringEncoding];
    [myRequest setRequestMethod:@"POST"];
    [myRequest setPostValue:@"valueMyParameter" forKey:@"myParameter"];

    [myRequest addRequestHeader:@"Accept" value:@"application/json"];
    [myRequest addRequestHeader:@"Content-Type" value:@"application/json; charset=UTF-8;"];
    [myRequest setDelegate:self];

    [myRequest startAsynchronous];

}
Nel metodo viewDidLoad, si prepara la richiesta asincrona in POST, settando i valori da inserire nell’header HTTP, come il Content-Type, lo User-Agent e la codifica da utilizzare. Con il comando startAsynchronous, la richiesta viene inviata al servizio specificato nella URL. Per effettuare una chiamata in GET, occorre modificare in setRequestMethod:@”GET”

 

Occorre definire poi i due metodi di success ed error callback:
  • requestFailed: è il metodo che viene richiamato se la risposta non è andata a buon fine. E’ possibile intercettare anche il relativo status code, per visualizzare i relativi messaggi di errore all’utente;
  • requestFinished: è il metodo che viene richiamato in caso di ricezione con successo della risposta, raccolta dalla variabile responseData. L’elaborazione di quest’ultima viene effettuata con un metodo (che non fa parte della libreria ASIHTTPRequest) che, in un thread di background, estrae dal JSON una lista di oggetti (in questo esempio, i post di un forum), ricaricandoli poi in una UITableView (con il metodo nativo reloadData).
In alternativa, è possibile dichiarare le funzioni di callback di successo ed errore direttamente prima della chiamata asincrona, definendo dei blocks:
/*  ...  */   

   [request setDelegate:self];
   [request setCompletionBlock:^{         
        NSString *responseString = [request responseString];
        NSLog(@"Response: %@", responseString);

        /* your code here in success case */

    }];
    [request setFailedBlock:^{
        NSError *error = [request error];
        NSLog(@"Error: %@", error.localizedDescription);

       /* your code here in error case */

    }];

   [request startAsynchronous];

/*  ...  */
Direi che entrambi i metodi sono abbastanza semplici, ma quello con la ASIHTTPRequest offre sicuramente maggiore controllo e un ricco set di funzionalità per la connettività ai nostri servizi. Eccole tutte: http://allseeing-i.com/ASIHTTPRequest/How-to-use
Creative Commons License
This work by Francesco Ficetola is licensed under a Creative Commons Attribution 4.0 International License.
Based on a work at www.francescoficetola.it.
Permissions beyond the scope of this license may be available at http://www.francescoficetola.it/2012/11/03/ios-come-effettuare-chiamate-asincrone/.