Keep the UI fluid: Test On-device
It’s tempting with modern mobile toolchains and workflows to stick with the simulator/emulator during development, but there are pitfalls to delaying testing on real hardware!
Move fast and break things
We’re developers, we like to move fast and see the results of our labours instantly - it’s who we are and how we work. When developing native apps for mobile platforms it’s so easy to just get the app running in a simulator, it’s quick and efficient.
Of course, generally it makes perfect sense to do so:
- in most cases it’s faster to hit RUN and see the results of your latest changes on the simulator than on a connected device (a note on this later!)
- you don’t need a phone/tablet connected to your machine, or even need to own one!
You can of course take the middle-ground and develop using a simulator, then build the app and distribute it to testers for installation on actual hardware.
However, it’s inevitable; you will get tripped up at some point. There’s so much to consider when developing for mobile devices (from now on, I’ll lump phone handsets and tablets under this descriptive) :
- actual mobile hardware is invariably less performant than a desktop/laptop (things that take almost no time in the simulator on your development machine can take a lot longer to complete on a mobile device)
- you don’t suffer intermittent connectivity when running apps on a desktop/laptop (as you will do in the real world on a cellular network)
- data transfer speed (and size considerations) must always be kept in mind for when the user is on a cellular network and using their data plan
- you have many times more memory available on a development machine
- you’re less likely to hit low-storage conditions when installing/running apps on a desktop/laptop
Personally, I do develop using the simulator, and can burn through adding features to apps very quickly as a result, but I always, without fail have an actual device available. Often this is connected to my development machine while I am developing, so it’s easy for me to tell the IDE (XCode, for example) to run my code in the simulator OR on the device.
Fluid UI turns sticky
Using this approach I was able to catch and resolve a potential issue with an app I am working on very early. I’m using a 3rd party PDF library that has SDKs for iOS, Android and Windows Phone. One of the features I was implementing involved giving the user the ability to search a PDF - sounds simple. Indeed the library supports this natively, but the intended use is to find the first match and show this visually in the PDF, then let the user navigate with previous/next buttons through the rest of the matches.
The design we wanted was to present the matches to the user in a list which showed the context of the match as well as the page it appeared on. If the user tapped an item in the list they would be taken to the matching page. It wasn’t apparent how to achieve this with the 3rd party SDK so a quick look on the support forums for the library yielded some sample code which I amended slightly to create this:
for (NSInteger i = 0 ; i < totalPDFPages ; i++) {
PDFPage *currentPage = [m_doc page:i];
[currentPage objsStart];
PDFFinder *mFinder = [currentPage find:textToFind :FALSE :FALSE];
if (mFinder) {
NSInteger finds = [mFinder count];
for (NSInteger j = 0 ; j < finds ; j++) {
NSInteger foundIndex = [mFinder objsIndex:j];
NSInteger phraseStartIndex = foundIndex - charsBeforeMatch < 0 ? 0 : foundIndex - charsBeforeMatch;
NSInteger phraseEndIndex = (foundIndex + [textToFind length]) + charsAfterMatch < [currentPage objsCount] ? (foundIndex + [textToFind length]) + charsAfterMatch : [currentPage objsCount] - 1;
// adjust phrase start and end so that they observe word boundaries (no partial words)
phraseStartIndex = [currentPage objsAlignWord:phraseStartIndex :-1];
phraseEndIndex = [currentPage objsAlignWord:phraseEndIndex :1];
// get the matching string
NSString *phrase = [currentPage objsString:phraseStartIndex :phraseEndIndex + 1];
// clean the matching string
phrase = [phrase stringByTrimmingCharactersInSet:charactersToRemove];
phrase = [phrase stringByReplacingOccurrencesOfString:@"\r\n" withString:@" "];
// STORE THE MATCHING STRING - CODE OMITTED FOR BREVITY
}
}
}
I ran this in the simulator and on a 135+ page PDF the search completed sub-second so appeared instant. This was running the simulator on my development 2013 iMac. The results looked good, all seemed well.
Prepare for the smackdown!
Then for completeness I tested on an iPhone 4; oh dear, what’s this?! The search took 6 seconds for the same PDF and what’s worse, it locked the UI!
If I had carried on with developing on the simulator only, I’d have had some very unhappy testers when they eventually got to test the app on a real device.
Fortunately this was caught early, so I could take steps to remedy the behaviour.
Sticky UI turns fluid (again)
First things first, let’s sort out the UI locking. In many cases it’s perfectly okay to loop through data collections in code that is executing on the UI thread, but when you’re not the owner of the code - in this situation a 3rd party library - you might fall foul of performance issues.
The solution in iOS is pretty painless - use an NSOperationQueue, then create and add NSOperation items to it. These will get executed in turn and on a different thread to the UI - it’s all handled using Grand Central Dispatch (GCD) but is all nicely wrapped up for you.
Unlike dispatch_async
which also uses GCD, you have more control over NSOperationQueues
and for example, can cancel items on the queue (or create dependencies between items). This is ideal for the situation I am in where I want the search to execute on a different thread, but want to be able to cancel it under several conditions - like if the user moves away from the PDF view.
So here’s the refactored code. Note that I changed from FOR to WHILE loops so I could break out easily if the NSOperation
was cancelled ([operation isCancelled]
) - this would happen if the NSOperationQueue
cancelAllOperations
method was called. Which it is under several circumstances - dependent on what the user is doing with the UI.
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSCharacterSet *charactersToRemove = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
NSInteger i = 0;
while ((i < totalPDFPages) && (![operation isCancelled])) {
PDFPage *currentPage = [m_doc page:i];
[currentPage objsStart];
PDFFinder *mFinder = [currentPage find:textToFind :FALSE :FALSE];
if (mFinder) {
NSInteger finds = [mFinder count];
NSInteger j = 0;
while ((j < finds) && (![operation isCancelled])) {
NSInteger foundIndex = [mFinder objsIndex:j];
NSInteger phraseStartIndex = foundIndex - charsBeforeMatch < 0 ? 0 : foundIndex - charsBeforeMatch;
NSInteger phraseEndIndex = (foundIndex + [textToFind length]) + charsAfterMatch < [currentPage objsCount] ? (foundIndex + [textToFind length]) + charsAfterMatch : [currentPage objsCount] - 1;
// adjust phrase start and end so that they observe word boundaries (no partial words)
phraseStartIndex = [currentPage objsAlignWord:phraseStartIndex :-1];
phraseEndIndex = [currentPage objsAlignWord:phraseEndIndex :1];
// get the matching string
NSString *phrase = [currentPage objsString:phraseStartIndex :phraseEndIndex + 1];
// clean the matching string
phrase = [phrase stringByTrimmingCharactersInSet:charactersToRemove];
phrase = [phrase stringByReplacingOccurrencesOfString:@"\r\n" withString:@" "];
// STORE THE MATCHING STRING - CODE OMITTED FOR BREVITY
j++;
}
}
i++;
}
if (![operation isCancelled]) {
// Get hold of main queue (main thread) and call the complete method
[[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
[self searchComplete];
}];
}
}];
Note that you can subclass NSOperation
if you so wish, or alternatively (as in my case) use an NSBlockOperation
if you’re not going to be making extensive use of your subclass or your circumstances are pretty simple - as they are here.
All’s well that ends well (as Shakespeare wrote)
So, there you have it, just one instance where testing on device highlighted performance issues that needed to be immediately addressed. I’ve also covered one simple way to ensure your UI remains fluid and doesn’t inhibit your user experience by leveraging the power of NSOperationQueue
.
Android Simulator
One last note - about the Android simulator: the performance of this - at least on OSX - leaves a lot to be desired. I could not get this to behave anywhere near as quickly or conveniently as the iPhone simulator from XCode.
My recommendation here is to always use an actual Android device; it’s simply much quicker IMHO. Of course, YMMV!
*UPDATED*
Note that we’ve found some ways to improve the performance of the Android emulator on Mac OS X and blogged about them in this article: Turbocharge the Android emulator