jueves, 8 de julio de 2010

WebServices solution for iPhone - Second Stage

The FileDownloadController class

Once I had the FileDownload class, it came to me the problem of using it many times and from many places. I could create as many FileDownload objects as needed, but this will create many variables and lines of code. My solution was to create a controller class (FileDownloadController), which I implement with a Singleton Pattern.

The concept is:
  • The FileDownloadController class will create and manage all needed FileDownload objects
  • Whenever an object needs to download a file, it should get the FileDownloadController instance and then asks for the download
  • Once the download is complete, or in case of error, a callback will be received by the requesting object

In order to maintain certain acceptable performance, because the download process works in the background and consumes resources, there should be a limited number of concurrent downloads, this number is set when the controller is created. In my tests, using an iPhone 3G, an acceptable number is 10.

Because of the limited number of download slots, we might face the problem of having more requests than slots, for this we have 2 possible solutions:
  1. Store requests in a queue and attend them once a slot is available
  2. Cancel the oldest download and use it for the requested

As one of the purposes of these classes were to download images, and those been requested from a scrollable table, the selected solution was number 2.

To attend this new requirement, an intermediary class was created to hold the FileDownload object and the time when the process was called. The interface of this class is:

@interface FileDownloadControllerItem : NSObject

{

int jobID;

FileDownload * downloadObject;

NSDate * startTime;

}


@property (nonatomic, retain) FileDownload * downloadObject;

@property (nonatomic, assign) int jobID;

@property (nonatomic, retain) NSDate * startTime;


-(FileDownloadControllerItem *)initwithDelegate:(id)delegate;

-(void)downloadFile:(NSInteger)fileID fileName:(NSString *)name callBack:(id<FileDownloadProtocol>)delegate;

-(void)stopDownload;


@end


  • jobID it holds the Download ID discussed in the previous post of this series.
  • downloadObject holds the FileDownload object that will be used to process the download
  • startTime holds the time when the download was called and will be used to identify the older download when a new download is requested and there are no available slots

Its implementation is pretty straightforward and in most cases are direct calls to the FileDownload object.

-(FileDownloadControllerItem *)initwithDelegate:(id)delegate;


Create objects and set the delegate for callbacks

-(void)downloadFile:(NSInteger)fileID fileName:(NSString *)name callBack:(id<FileDownloadProtocol>)delegate;


Is called when a file needs to be download, it set the self variables and call the FileDownload object to start the process. This class is not validating any available slot; it assumes that validation was made previously.

-(void)stopDownload;


Just call to cancel the current download.

About the implementation

/**

Initializtion given a delegate

**/

-(FileDownloadControllerItem *)initwithDelegate:(id)delegate

{

[self init];


self.jobID = FREESLOT;

self.downloadObject = [[FileDownload alloc] init];

self.startTime = [NSDate dateWithTimeIntervalSince1970:0];

return self;

}


/**

Download a file

**/

-(void)downloadFile:(NSInteger)fileID fileName:(NSString *)name callBack:(id)delegate;

{

self.jobID = fileID;

self.startTime = [NSDate dateWithTimeIntervalSinceNow:0];

[self.downloadObject setJobID:fileID];

[self.downloadObject startDownloadFile:name callBack:delegate];

}


/**

Cancel current download

**/

-(void)stopDownload

{

[self.downloadObject stopDownload];

}


Most of the logic is implemented in the Controller class, which interface is as follows:

@interface FileDownloadController : NSObject

{

int currentDownloads;

BOOL maintainQueue;

int totalJobs;

NSMutableArray * downloadArray;

NSMutableDictionary * callBackObjects;

}


@property (nonatomic, assign) int totalJobs;

@property (nonatomic, assign) BOOL maintainQueue;


+ (FileDownloadController *)getController;

+ (FileDownloadController *)getController:(NSInteger)jobs useQueue:(BOOL)queue;


- (FileDownloadController *)initWithJobs:(NSInteger)jobs;

- (FileDownloadController *)initWithJobs:(NSInteger)jobs useQueue:(BOOL)queue;


- (BOOL)downloadFile:(NSInteger)fileID fileName:(NSString *)name callBack:(id<FileDownloadProtocol>)delegate;


- (BOOL)findJobInQueue:(NSInteger)job;


- (void)cancelDownloadFile:(NSInteger)fileID;

- (void)stopAllDownloads;


@end


  • currentDownloads is used to keep track of the actual concurrent downloads
  • maintainQueue is not used, yet, but it intention is to identify if a download queue is used or if the older download might be cancelled
  • totalJobs maximum number of concurrent downloads
  • downloadArray this array holds all the download slots
  • callBackObjects is used to hold the download ids and the callback objects; this is needed because the FileDownload will call a callback within the controller, then the controller will call the requesting object

The available functions are:

+ (FileDownloadController *)getController;


This is used to retrieve a controller with default values, internally it calls initWithJobs with 10 jogs and not using the queue

+ (FileDownloadController *)getController:(NSInteger)jobs useQueue:(BOOL)queue;


Is used to create a controller with specific values, it also calls initWithJobs with the given parameters

- (FileDownloadController *)initWithJobs:(NSInteger)jobs;


Simple constructor which initialize the controller with the given jobs and not using the queue

- (FileDownloadController *)initWithJobs:(NSInteger)jobs useQueue:(BOOL)queue;


Initialize the controller with the given jobs and queue use, this function creates the required download slots

- (BOOL)downloadFile:(NSInteger)fileID fileName:(NSString *)name callBack:(id<FileDownloadProtocol>)delegate;


Called to download a file

- (BOOL)findJobInQueue:(NSInteger)job;


As each download required an ID, this function tells us if an ID is already in the download queue, this is, if it was found in any download slot

- (void)cancelDownloadFile:(NSInteger)fileID;


Call to cancel a given download

- (void)stopAllDownloads;


Called to stop all downloads, mainly called from the destructor

There are other internal functions used by the class, the implementation is:

/**

Get Singleton object

**/

+ (FileDownloadController *)getController

{

if (controller == nil)

{

controller = [[FileDownloadController alloc] initWithJobs:10 useQueue:NO];

}

return controller;

}


/**

Get Singleton object

**/

+ (FileDownloadController *)getController:(NSInteger)jobs useQueue:(BOOL)queue

{

if (controller == nil)

{

controller = [[FileDownloadController alloc] initWithJobs:jobs useQueue:queue];

}

return controller;

}


Both functions call the constructors.

/**

Initialization with job number

**/

-(FileDownloadController *)initWithJobs:(NSInteger)jobs

{

return [self initWithJobs:jobs useQueue:NO];

}


/**

Initialization with jobs and Queue handling

**/

-(FileDownloadController *)initWithJobs:(NSInteger)jobs useQueue:(BOOL)queue

{

[self init];

totalJobs = jobs;

maintainQueue = queue;

currentDownloads = 0;


downloadArray = [[NSMutableArray alloc] initWithCapacity:self.totalJobs];

for (int i = 0; i < self.totalJobs; ++i)

{

[downloadArray addObject:[[FileDownloadControllerItem alloc] initwithDelegate:self]];

}

callBackObjects = [[NSMutableDictionary alloc] init];

return self;

}


Within the constructor, the download slots are created and object’s variables initialized.

The downloadArray holds the FileDownloadControllerItem created for each slot.
The callBackObjects will hold the relation of each download ID and the object related for the callback

/**

Download files

**/

-(BOOL)downloadFile:(NSInteger)fileID fileName:(NSString *)name callBack:(id<FileDownloadProtocol>)delegate

{

// Validate file is not already in download queue, cero is not valid id

// Find a free download slot

// If none, clear one

// Configure slot to download file

BOOL error;

FileDownloadControllerItem * downloadItem;

error = [self findJobInQueue:fileID];

if (!error)

{

[self storeSession:fileID callBack:delegate];

downloadItem = [self getFreeSlot:NO];

if (downloadItem == nil)

{

downloadItem = [self getFreeSlot:YES];

}

[self increaseDownloadCounter];

[downloadItem downloadFile:fileID fileName:name callBack:self];

}

return downloadItem != nil;

}


To start a download process, first thing to do is validate it is not already been downloaded. Then we store the session in the dictionary and look for a free slot ([[self getFreeSlot:NO]), if there is none, we get one canceling the oldest ([[self getFreeSlot:YES]). Finally we increase the download counter and start the download ([downloadItem downloadFile:fileID fileName:name callBack:self]).

/**

Find if job is already in progress

**/

-(BOOL)findJobInQueue:(NSInteger)job

{

BOOL error = NO;

FileDownloadControllerItem * downloadItem;

for (int i = 0; i < [downloadArray count]; ++i)

{

downloadItem = (FileDownloadControllerItem *)[downloadArray objectAtIndex:i];

if (downloadItem.jobID == job)

{

error = YES;

}

}

return error;

}


To find if a job is already been downloaded, we loop through all the slots and compare the IDs

/**

Cancel docwnload given its fileID

**/

-(void)cancelDownloadFile:(NSInteger)fileID

{

[self cancelDownload:[self getSlot:fileID]];

}


To cancel a download, just get the slot a a given ID and call to cancel it using cancelDownload

/**

Call to cancel download activities

**/

-(void)cancelDownload:(FileDownloadControllerItem *)downloadItem

{

if (downloadItem != nil)

{

[self deleteSession:downloadItem.jobID];

[self decreaseDownloadCounter];

[downloadItem.downloadObject stopDownload];

downloadItem.jobID = FREESLOT;

}

}


cancelDownload cancel the downloadItem, decrease counters and clear sessions for the canceled download.

/**

Stop downloads

**/

-(void)stopAllDownloads

{

for (int i = 0; i < [downloadArray count]; ++i)

{

FileDownloadControllerItem * downloadItem = (FileDownloadControllerItem *)[downloadArray objectAtIndex:i];

[self deleteSession:downloadItem.jobID];

[self decreaseDownloadCounter];


[downloadItem stopDownload];

downloadItem.jobID = FREESLOT;

}

}


To stop all downloads, loop the slots and call each to be cancelled.

/**

Get a worker given a jobid

**/

-(FileDownloadControllerItem *)getSlot:(NSInteger)job

{

FileDownloadControllerItem * downloadItem = nil;

for (int i = 0; i < [downloadArray count]; ++i)

{

downloadItem = (FileDownloadControllerItem *)[downloadArray objectAtIndex:i];

if (downloadItem.jobID == job)

{

return downloadItem;

}

}

return downloadItem;

}


This function finds a downloadItem given the ID and returns it

/**

Get a free worker

**/

-(FileDownloadControllerItem *)getFreeSlot:(BOOL)force

{

FileDownloadControllerItem * downloadItem = nil;

FileDownloadControllerItem * downloadItemReturn = nil;

NSDate * minDate = [NSDate dateWithTimeIntervalSinceNow:0];

for (int i = 0; i < [downloadArray count]; ++i)

{

downloadItem = (FileDownloadControllerItem *)[downloadArray objectAtIndex:i];

if ( ( downloadItemReturn == nil && downloadItem.jobID == FREESLOT ) || force)

{

if (force)

{

if ([downloadItem.startTime timeIntervalSince1970] < [minDate timeIntervalSince1970])

{

minDate = [NSDate dateWithTimeIntervalSince1970:[downloadItem.startTime timeIntervalSince1970]];

downloadItemReturn = downloadItem;

}

}

else

{

downloadItemReturn = downloadItem;

}

}

}


if (downloadItem != nil)

{

[self cancelDownload:downloadItemReturn];

}

return downloadItemReturn;

}


getFreeSlot is used to retrieve an available downloadItem, in normal execution, it loop through all the slots looking for an empty one. When forcing the availability, it finds the oldest, which will be cancelled and then returned as a free slot.

The controller implements the FileDownloadProtocol and need to implement its functionality, this is:

- (void) downloadError:(NSError *)error downloadID:(NSInteger)jobID;

- (void) downloadSucceed:(NSMutableData *)data downloadID:(NSInteger)jobID;


In both cases, it deletes the session created for the download and return the information to the object which request the download using a callback.

With all this work done, when an object requires to download a file, it need to do some like these:

downloadController = [FileDownloadController getController];

[downloadController downloadFile:fileID fileName:url callBack:self];


Implementing the FileDownloadProtocol we should have:

#pragma mark -

#pragma mark FileDownloadProtocol


/**

Notify an error

**/

- (void) downloadError:(NSError *)error downloadID:(NSInteger)jobID

{

// Notify error to screen

}


/**

Notify download complete

**/

- (void)downloadSucceed:(NSMutableData *)data downloadID:(NSInteger)jobID

{

// Do something with the data, remember the jobID is the ID sent in downloadFile

}


Using this class, a file download is easiest than previous. Now just remember to release the downloadController object before exiting the application.

in a next post I'll complete this series with the Third Stage: calling WebServices.

jueves, 1 de julio de 2010

I used to read my eMail on my iPhone, but not any more...

When I got my iPhone I configure it to sync all my email account, at first, maybe because on the novelty of the device, I used the vibrate notification, but is was too annoying. When I deactivate the alarm, I start checking it too often.

After a couple of weeks I definitely delete all mail account from the iPhone. I got to the next conclusions:
  • No email is too important to be notified immediately, if those cases they can still contact me by phone
  • Checking mail took to much time, I can use that time in other activities
  • I rather prefer to read emails in my PC Screen

Recently I found this post from HBR: Why I Returned My iPad, is kind the same that happens to me.