Over the last year, I built a number of iOS applications such as yogaspot, iPlexLotto and OptionsHouse (still in development), which are backed by the network services. I am documenting a few idioms and best practices that I have used in these applications along with some lessons learned:
Separation of concerns
The iOS APIs are based on MVC design pattern, which encourages separating the non-GUI code such as Models, Services, Helper classes from your views and uses Controllers to communicate between GUI and non-GUI code. As with building any other kind of application, keeping your design and code clean would make your application more maintainable and extendable. The iOS APIs make heavy use of protocols for interface segaration principle. Here are some of the usages of protocols in iOS APIs:
Data Sources
The iOS API use protocols for data sources, which then populate views such as table views, pickers and other views. In most application, your controller will be the data source but I used the data source protocols to create Wizard like behavior by implementing the data source interface in multiple classes and displaying different contents based on the state of application.
Delegates
The delegates are callback interface that are generally used for communication between classes or threads. As, protocols in Objective C can define both mandatory and optional behavior, delegates are also used to provide customized UI behavior. The iOS SDK uses delegates for customizing UI in a number of classes such as UISearchDisplayDelegate, UISearchBarDelegate, UIGestureRecognizerDelegate, UITableViewDelegate, etc. Here is an example of delegate for communication between classes:
@protocol OrderCallbackDelegate
@required
- (void) addedOrder: (Order *)order;
@end
Implementing Delegate
@protocol OrderCallbackDelegate
@interface OrderCallbackDelegate : NSObject {
}
@end
Referencing Delegate
@interface SomeObject : NSObject {
id delegate;
}
@property(nonatomic, retain) id delegate;
@end
Encapsulation and Extensions
The iOS provide several options for hiding private details or extending existing classes. For example, you can use @private keyword in the class declaration to enforce encapsulation such as:
@interface MyClass : NSObject {
@private
int myVar;
}
@end
The myVar attribute can only be accessed by the instance of MyClass.
Objective C supports inheritance and composition as other object-oriented languages, which I won’t cover here. However, the Categories feature of Objective C language is a more powerful way to extend an existing class without modifying it. For example, you can extend UIColor to provide additional colors as follows:
@interface UIColor(MyBlueColor)
+(UIColor *)myBlueColor;
@end
@implementation UIColor(MyBlueColor)
+(UIColor *)myBlueColor {
return [UIColor colorWithRed:0.2274 green:0.4666 blue:0.8235 alpha:1.000];
}
@end
Other examples include JSON parsing library, which extends NSString to support JSON parsing.
In addition to extension, you can also use categories for encapsulation by using categories syntax in the implementation file and adding private method and properites, e.g.
@interface MyClass(private)
//my private methods only visible in the implementation file
@end
Code Structure
As with any other application, keeping your code organized in your iOS project helps readability and maintenance. One thing I don’t like about XCode is that when you create groups for organizing classes in your XCode project, it doesn’t create folders, so you will find all classes in Classes directory. So, I prefer creating folders on the file system and then add folders as groups to XCode. Here is an example of different groups I used in my projects:
- Models
- Network
- Controllers
- Utils
- OtherSources
- Parsing
- Views
- Resources
Dynamic Language Support
Objective C supports both static and dynamic language syntax and has powerful support of reflection. This gives you powerful tool to write concise code. For example, I could not find any XML or JSON binding library for Objective C, so I built helper classes that parsed XML and JSON and automatically bound the properties to the classes that used same names, e.g.
typedef id (^objectFactoryBlk_t)();
@interface JsonParser : NSObject {
}
+ (void)bindJson:(NSData *)data toObject:(NSObject *) object;
+ (void)bindJson:(NSData *)data toArray:(NSMutableArray *) array mandatoryFields:(NSArray *)fields withFactory:(objectFactoryBlk_t) objectFactoryBlk;
@end
And
@interface XmlParser : NSObject {
xmlDocPtr doc;
}
- (id)initWithXML:(NSData *) document;
+ (void)bindXml:(NSData *)xml toObject:(NSObject *) object;
+ (void)bindXml:(NSData *)xml toArray:(NSMutableArray *) array mandatoryFields:(NSArray *)fields withFactory:(objectFactoryBlk_t) objectFactoryBlk;
@end
I am skipping more details but you can download the implementation from Parser.zip.
Also, as Objective-C is dynamic languages, the compiler won’t complain if you call methods that are not defined, so you need to pay special attention to warnings when using dynamic language constructs or better turn warnings into errors by configuring your build. You can also use CLANG static analyzer to find improper dynamic method.
Network Services
Any network based iPhone app would rely on the backend network services and you would make these easier if these services are REST based. I recommend ASIHTTPRequest library, which supports asynchronous communication and concurrency.
Loading Status
When designing an modal dialog that interacts with the backend, it helps if you show loading message or use UIActivityIndicator when making network request (even though the network request would be asynchronous). For example, the OptionsHouse app shows loading status when executing trades as it may take a couple of seconds to communicate with the server.
XML vs JSON vs Binary
When you are developing an iPhone application, you will learn that despite the increasing powerful hardware it’s not same as desktop, laptops or servers we have in the office. Thus, you will have to learn how to optimize every aspect of the application and one of the key factor for network application is the protocol for the payload. I used both XML and JSON based services and found that JSON parsing was at least twice as fast compare to XML. Optionally, you can use Binary format such as binary plist, which further improves the performance of parsing.
Network Latency
An iPhone user might be connected on Edge, 3G, 4G or WiFi network and the network speed would vary. Though, you can make a general assumption that WiFi would be faster than 3G/4G, which would be faster than Edge, but I suggest that you test the speed within the app instead of relying on the connection. For example, you might be using WiFi at a local coffee shop or a conference, where the speed is slower than the edge or 3G.
Network Outages
On websites, the network services are often behind the web server so when they are down it’s easy to post a page for network outage. You will have to handle such outages in the application and notify the users about the outages.
Network Data Requests
A network based application requires fetching the data from the server and it’s best that you communicate with the services without blocking user interface. Both ASIHTTPRequest and NSURL supports asynchronous communcationions, where you register callback methods when the data arrives or network failure occurs. I built a thin layer on top of ASIHTTPRequest so that I could use blocks:
typedef void (^requestCompletedBlk_t)(id payload);
typedef void (^requestFailedBlk_t)(NSError *error);
@interface ServiceBlocks : NSObject {
requestCompletedBlk_t requestCompletedBlk;
requestFailedBlk_t requestFailedBlk;
ASIHTTPRequest *request;
NSTimeInterval started;
int retries;
}
@property (copy, nonatomic) requestCompletedBlk_t requestCompletedBlk;
@property (copy, nonatomic) requestFailedBlk_t requestFailedBlk;
@property (retain, nonatomic) ASIHTTPRequest *request;
@property (assign, nonatomic) NSTimeInterval started;
@property (assign, nonatomic) int retries;
@end
@implementation ServiceBlocks
@synthesize requestCompletedBlk, requestFailedBlk, request, started, retries;
-(id)initWithRequest:(ASIHTTPRequest *) arequest completedBlock:(requestCompletedBlk_t) arequestCompletedBlk failedBlock:(requestFailedBlk_t) arequestFailedBlk {
if(self = [super init]) {
self.requestCompletedBlk = arequestCompletedBlk;
self.requestFailedBlk = arequestFailedBlk;
self.request = arequest;
self.started = [[NSDate date] timeIntervalSince1970];
self.retries = 0;
}
return self;
}
- (void)dealloc {
[requestCompletedBlk release];
[requestFailedBlk release];
[request release];
[super dealloc];
}
@end
Here is how you can use ASIHTTPRequest to make REST POST and GET requests:
- (void)postJson:(NSData *) json completedBlock:(requestCompletedBlk_t)requestCompletedBlk failedBlock:(requestFailedBlk_t) requestFailedBlk {
NSURL *url = [NSURL URLWithString:[[Configuration sharedConfiguration] getJsonServerUrl]];
NSString *jsonBuffer = [[[NSString alloc] initWithData:json encoding:NSASCIIStringEncoding] autorelease];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
request.connectionCanBeReused = YES;
[request setUseKeychainPersistence:YES];
[request setTimeOutSeconds:60];
[request setValidatesSecureCertificate:NO];
[request setDelegate:self];
[request setShouldPresentAuthenticationDialog:YES];
[request setDidFinishSelector:@selector(downloadCompleted:)];
[request setDidFailSelector:@selector(downloadFailed:)];
[request appendPostData:json];
[request addRequestHeader:@"Content-Type" value:@"application/json"];
[request setRequestMethod:@"POST"];
[request buildPostBody];
ServiceBlocks *blocks = [[ServiceBlocks alloc] initWithRequest:request completedBlock:requestCompletedBlk failedBlock:requestFailedBlk];
[requests setValue:blocks forKey:[self toKey:request]];
[blocks release];
[request startAsynchronous];
}
- (void)get:(NSString *) uri completedBlock:(requestCompletedBlk_t) requestCompletedBlk failedBlock:(requestFailedBlk_t) requestFailedBlk {
NSURL *url = [NSURL URLWithString:uri];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setUseKeychainPersistence:YES];
[request setDelegate:self];
[request setShouldPresentAuthenticationDialog:YES];
[request setDidFinishSelector:@selector(downloadCompleted:)];
[request setDidFailSelector:@selector(downloadFailed:)];
ServiceBlocks *blocks = [[ServiceBlocks alloc] initWithRequest:request completedBlock:requestCompletedBlk failedBlock:requestFailedBlk];
[requests setValue:blocks forKey:[self toKey:request]];
[blocks release];
[request startAsynchronous];
}
Above methods setup downloadCompleted as a callback handler for successful completion of the request and downloadFailed for service failure, which are defined as follows:
- (void)downloadFailed:(ASIHTTPRequest *)theRequest {
ServiceBlocks *blk = [requests valueForKey:[self toKey:theRequest]];
if (blk != nil) {
NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - blk.started;
NSLog(@"#####downloadFailed for %@ in %f seconds", [self toKey:theRequest], interval);
if (blk.requestFailedBlk != nil) {
blk.requestFailedBlk([theRequest error]);
}
[requests removeObjectForKey:[self toKey:theRequest]];
} else {
NSLog(@"#####downloadFailed could not find blocks for %@, available %@", [self toKey:theRequest], [requests allKeys]);
}
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}
- (void)downloadCompleted:(ASIHTTPRequest *)theRequest {
ServiceBlocks *blk = [requests valueForKey:[self toKey:theRequest]];
if (blk != nil) {
NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - blk.started;
NSLog(@"################################downloadCompleted for %@ in %f seconds", [self toKey:theRequest], interval);
NSData *content = [theRequest responseData];
if (blk.requestCompletedBlk != nil) {
blk.requestCompletedBlk(content);
}
[requests removeObjectForKey:[self toKey:theRequest]];
} else {
NSLog(@"################################downloadCompleted could not find blocks for %@, available %@", [self toKey:theRequest], [requests allKeys]);
}
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}
Both methods lookup the user’s block that was passed upon request and then callback the proper block. In certain cases, I extended downloadFailed method to automatically retry the request upon certain failure types such as timeout errors.
Gateway and Proxy Patterns
I used Gateway and Proxy patterns to wrap remote services in a local class that defined methods using local Model classes and automatically converted Models into proper XML/JSON format for requests and responses. This kept the code clean and in a few cases, I had to switch from XML to JSON, which was made in these classes and rest of application remained unchanged.
Check-If-Modified
As both iPlexLotto and OptionsHouse refreshed data continuously in background, I needed an efficient way to check if the data is updated on the server. HTTP provides a great way to reduce bandwidth by using E-Tag or Last-Modified headers, and we used similar techniques to send modified timestamp as part of the response and used it in future requests. The server then compared the timestamp in the request with local state and sent response only if the data was modified.
Network Errors
A critical aspect of network based applications is handling network errors. There can be various reasons for the network errors such as phone might be in Airplane mode so no network requests should be made. In other cases network connection might be too slow or the service is down. Also, you may be continuously updating data in background and in some cases occasional network interruptions can be ignored. This requires careful consideration of the UI so that the application is smart about the environment and retries when possible instead of annoying users with popup alerts.
Caching
A quick way to improve performance is to cache contents for a certain period of time depending on the data. As iOS devices have limited memory, I used in-memory cache for small objects and used file based cache for larger objects. For example, I cached lottery results for a couple of hours, on the other hand for trading application I cached orders and positions for a few seconds. I didn’t’ cache quotes data as it changed frequently and showing stale quotes can result in financial loss. In addition to the user data, I also cached chart images or other meta-data that is not frequently changed. Here is a simple implementation of file based cache:
#define K_CACHE_TIMEOUT_SECS 3600
@interface FileCache : NSObject {
}
+(BOOL)saveObject:(NSString *)name withData:(id)entry;
+(id)loadObject:(NSString *)name expired:(BOOL*)expired;
@end
#import "FileCache.h"
@implementation FileCache
+(id)loadObject:(NSString *)name expired:(BOOL*)expired {
*expired = YES;
NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [docs stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.obj", name]];
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *attrs = [fileManager attributesOfItemAtPath:filePath error:&error];
id obj = nil;
if (attrs != nil && [attrs count] > 0) {
NSDate *date = [attrs valueForKey:@"NSFileModificationDate"];
if (date != nil) {
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date];
if (interval <= K_CACHE_TIMEOUT_SECS) {
*expired = NO;
}
obj = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
}
}
return obj;
}
+(BOOL)saveObject:(NSString *)name withData:(id)entry {
NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [docs stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.obj", name]];
return [NSKeyedArchiver archiveRootObject:entry
toFile:filePath];
}
@end
Using Timers for Repeatable Tasks
Both iPlexLotto and OptionsHouse continuously updated the data in background. For example, the OptionsHouse application fetched stock/option quotes, order transactions, positions in background. I used GCD's based Timers to schedule periodic download of the data:
dispatch_time_t now = dispatch_walltime(DISPATCH_TIME_NOW, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
int kPOLL_INTERVAL_SECS = 5;
dispatch_source_set_timer(timer, now, [kPOLL_INTERVAL_SECS * NSEC_PER_SEC, 5000ull);
dispatch_source_set_event_handler(timer, ^{
// fetch data and update
});
Above timer runs every 5 seconds and polls for new data and download updates for the user. In my applications, I activated the timer when the application comes in foreground (applicationDidBecomeActive method) and disable it when it goes in background (applicationWillResignActive method). Note that this syntax is much simpler compared to prior syntax of NSTimer and NSRunLoop.
Multi-tasking
Apple added support for multi-tasking in iOS SDK 4.0, which means an application can now be in one of these states
Active
When an application is in foreground and interacting with the user.
InActive
When an application is on its way to background and stops receiving the events.
Background
When an application is running in background.
Suspended
When application is still in memory but not executing.
Callback Methods
The iOS SDK added a number of callback methods for the state transfer such as:
- (void)applicationWillResignActive:(UIApplication *)application {
/*
Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone
call or SMS message) or when the user quits the application and it begins the transition to the background state.
Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
*/
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
/*
Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to it
s current state in case it is terminated later.
If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
*/
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
}
On the older devices without multi-tasking support, applicationWillTerminate is called after applicationDidEnterBackground.
Multi-Threading
The iOS SDK provides multiple ways to add concurrency support to your application.
NSThread
You can create NSThread as follows
NSThread* myThread = [[NSThread alloc] initWithTarget:self
selector:@selector(myThreadMainMethod:)
object:nil];
[myThread start]; // Actually create the thread
However, there is an easier way to create a thread by invoking a custom method on an object as follows:
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
Alternatively, you can start a background task as follows:
[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
You will then define your method for doing the work as follows:
- (void)myThreadMainRoutine
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
// Do thread work here.
[pool release]; // Release the objects in the pool.
}
Note that you will have to create an auto-release pool before doing the actual work. Though, these APIs are simple to use but managing threads manually is harder and you have to synchronize shared data, cleanup upon termination or when the app goes in background. See Apple's documentation for more details.
NSOperationQueue
NSOperationQueue is much easier way to manage concurrency as you don't have to create threads manually. For example, you can create an operation to save data asynchronously as follows:
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(saveData:) object:data];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
Instead of using NSInvocationOperation, you can define an operation class as follows:
@interface SaveOperation : NSOperation {
NSData *data;
}
@property(retain) NSData *data;
- (id)initWithData:(NSData*)data;
@end
@implementation SaveOperation
@synthesize data;
- (id)initWithData:(NSData*)theData;
{
if ([super init]) {
self.data = theData;
}
return self;
}
- (void)dealloc {
[data release], data = nil;
[super dealloc];
}
- (void)main {
// saving
}
@end
You will then add the operation as follows:
SaveOperation *op = [[SaveOperation alloc] initWithDataL:data];
[queue addOperation:op];
[op release];
You can also control the number of concurrent operations via:
[queue setMaxConcurrentOperationCount:2];
Grand Central Dispatch
By far, the easiest way to add concurrency is to use Grand Central Dispatch (GCD) syntax and libraries. The GCD allows you to define tasks via function or blocks and schedules them to the available processors and automatically uses threads behind the scene. Though, it's similar to NSOperation, but the syntax is very terse and the performance is much better than using threads directly.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// save data
NSLog(@"finished saving data");
});
You can also nested blocks such as
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// save data
NSLog(@"finished saving data");
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]
});
});
In addition to using predefined queues, you can also create your own queues, which execute the tasks in serial order such as:
dispatch_queue_t myQueue = dispatch_queue_create("com.plexobject.myqueue", NULL);
dispatch_async(myQueue, ^{
// save data
// ...
// now release the queue (optionally)
dispatch_release(myQueue);
});
Note that if we need to delete the queue, we must do it after all tasks are finished executed. See Apple's documenation for more details on queues.
Final word on Multi-threading
When using multi-threading explicitly or implicitly, you have to synchronize shared data. The IOS SDK provides several constructs such as @synchronized keyword, mutexes, locks, etc. to protect shared data. Nevertheless, this can be error-prone and can slowdown your application, so eliminate shared data if possible. Also, pay special attentions to exceptions within the thread method and cleanup threads upon app termination or when it becomes inactive. See Apple's documentation for more details. Also, any operations that require UI interaction must be run via main thread, though there are some exceptions such as some QuickTime APIs can be run in background.
Event Notifications
The iOS supports notifications for intercommunication between classes and threads, which promote loosely coupled design. In my applications, I extended notification by creating my own events such as:
@interface MyEvent : NSObject {
NSString *type;
}
@property(nonatomic, copy) NSString *type;
-(void)fireEvent;
@end
#import
#import "MyEvent.h"
@implementation MyEvent
@synthesize type;
-(void)fireEvent {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:type object:self];
});
}
@end
Note that the event is being fired on the main thread so that UI controllers can respond to the event and update UI without any changes. The controller class would register for the event (typically in viewDidLoad) as follows:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMyevent:) name:@"MyEvent" object:nil];
Similarly, controller would unregister for the event in viewDidUnload as follows:
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"MyEvent" object:nil];
The controller would then define the method handler for the event as follows:
- (void) handleMyevent:(NSNotification *)notif {
MyEvent *event = (MyEvent *) [notif object];
}
You would then generate the event as follows:
MyEvent *event = new MyEvent();
event.type = @"MyEvent"
event.fireEvent;
sending bug reports Automatically
Though, you can download crash reports from the Apple website, I added automatic reporting mechanism for bug reports and crashes. I used UncaughtExceptionHandler class to handle unexpected exceptions and automatically send bug reports. I don’t try to recover from unexpected exceptions as often the memory corruption leave the application in such unstable condition that it’s better to restart the application.
- (void)validateAndSaveCriticalApplicationData:(NSException *)exception {
NSMutableString *message = [[[NSMutableString alloc] initWithCapacity:200] autorelease];
[message appendFormat:@"{name:'%@', ", exception.name];
[message appendFormat:@"reason:'%@', ", exception.reason];
[message appendFormat:@"userInfo:'%@', callStack:[", exception.userInfo];
int count=0;
for (NSObject *obj in exception.callStackReturnAddresses) {
if (count > 0) {
[message appendString:@","];
}
[message appendFormat:@"callStack_%d:'%@'", obj];
}
[message appendString:@"], callSymbols:["];
count=0;
for (NSObject *obj in exception.callStackSymbols) {
if (count > 0) {
[message appendString:@","];
}
[message appendFormat:@"callSymbol_%d:'%@'", obj];
}
[message appendString:@"]}"];
[self sendBug:@"Crash report" withMessage:message];
}
Persistence
The iOS SDK provides several options for persistence such as user preferences, file I/O, serialization, sqlite and CoreData. I used file based serialization for caching resources and objects that implemented NSCoding protocol and used CoreData for more complexed data. I will skip file based serialization as it’s fairly straightforward, but would list some tips on CoreData.
- Setup versions for your data mapping files so that you can easily upgrade schema. You can add the version by selecting xcdatamodel file, choosing Design and then selecting “Add Model Version” from “Data Model” submenu. In order to auto-upgrade migration, define persistentStoreCoordinator accessor as
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSURL *storeUrl = [NSURL fileURLWithPath: [docs stringByAppendingPathComponent: @"MyData"]];
NSError *error = nil;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return persistentStoreCoordinator;
}
- Use a single NSManagedObjectContext per thread or queue. For example, I used different NSManagedObjectContext for background timer that updated the database than the NSManagedObjectContext that displayed the table using NSFetchedResultsController.
- Merge changes when using multiple NSManagedObjectContext objects, e.g.
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserverForName:NSManagedObjectContextDidSaveNotification object:insertManagedObjectContext
queue:nil usingBlock:^(NSNotification *saveNotification) {
[[self readManagedObjectContext] mergeChangesFromContextDidSaveNotification:saveNotification];
}];
// update here
[dnc removeObserver:self name:NSManagedObjectContextDidSaveNotification object:ctx];
- In found problems with caching NSManagedObjects when using multiple NSManagedObjectContexts as iOS invalidate existing references when new updates come and you get wonderful error:
The NSManagedObject with ID:#### has been invalidated.
I ended up creating transient copy of the object for caching purpose. Another factor you will have to consider is what information should be kept in memory and what information can be stored in the database. In general, if the data is small or frequently updated such as stock quote, I kept it in memory, however if the data is large and less often updated such as stock positions, I kept it in the database. In addition, I used the database as interface between UI and network, so the data would get updated asynchronously in background and stored in the database. The network code would then fire an event, which the UI subscribes and would then update the view from the database. This provided fast and efficient way to view large amount of data without waiting.
Performance
Performance is a key part of usage experience. Apple provides a number of tools such as Shark and Instruments to measure performance bottlenecks as well as investigate memory leaks and CPU activity, file I/O, database and other issues. You need to start measuring the performance on real devices early in the application lifecycle so that you can design the application properly.
Memory Management
As, Apple does not support garbage collection on iOS devices, the memory management is one of most painful aspects of the development. I felt that pain as I have been using language such as Java, Ruby, Erlang and Python for past many years that come with garbage collection. Here are some of the tips for memory management:
Use Memory-Leak detection feature of Instruments to find the objects that are not properly released. You can set NSZombieEnabled flag to true on your executable, which can help track the memory leaks when the app crashes. Finally, multi-threading further complicates the memory management. For example, you may pass auto-released object to another thread and by the time that the thread uses it, the first thread’s auto-release pool released it. So, you must retain the object in separate thread before processing it.
Testing
Though, iPhone Simulator is fast and easy way to test the application during the development, but there are significant difference between the iPhone Simulator and real devices. First, many of the APIs such as Location, Apple Store, Accelerator, Email are not supported on the Simulator, but most importantly the Simulator runs on blazing fast MacPro machine, whereas real devices are slow and limited. I highly recommend start your testing with real devices early in your app lifecycle. There is also a big difference in hardware between different generation of iPod touch, iPhone and iPad. For example, iPhone 3G has 620Mhz processor with 128M memory, iPhone 3GS has 833Mhz with 256M memory and latest iPhone 4 has 1Ghz with 512M memory. I suggest using older devices to really find performance and stability issues with the application. Also, you want it to run on most devices so you should target lowest device that you can, which you can configure it by setting target OS property of the build.
Though, Apple provides basic Unit Test support, but I found it very painful to use. We have used the UI Automation tool for functional testing with some success, but it lacks good documentation.
Conclusion
In this blog, I listed several techniques for designing and developing iOS applications, though most of the techniques can be easily applied to other mobile platforms. Though, hardware on mobile devices is becoming more powerful, but it is still constrained with limited memory. You have to pay an exceptional attention to the memory usage, performance and stability of the app and users are going to hate if it’s slow or crashes. The network apps have to further address the availability and speed of the network so that apps is not completely unusable in airport mode and with slow connection. In the end, testing on real devices and real environment is the key to create effective network based applications.