Wednesday, April 1, 2015

Checking for Device Features - the Right Way

As an iOS Developer, you want as many people as possible to be able to use your application. You may support a variety of Operating System versions, hardware models and hardware versions. This article will make your job a little bit easier.

Consider the different screen sizes and resolutions of iPhone 5, iPhone 6, iPhone 6 Plus, iPad, iPad Retina, iPad Mini. Using auto-layout is often all you need to re-use a single storyboard that will create User Interface layouts that work for all screens. But what if your app needs to know more than just screen resolution, say, if there is a camera or not, and at what resolution?

In the past, it was easy to assume that certain hardware versions had specific features. But as more devices were released, tying code to specific hardware versions became increasingly difficult. The recommended (by Apple) best practice has always been to test for specific functionality, not for specific hardware versions. Let's see some examples.

The below table has three columns. The first is the question the developer wants answered. The second column is the wrong way to answer the question; the last column is a better way.


Desired Information
Wrong
Right
does this device have a front camera?
NSString* valueDevice = [[UIDevice currentDevice] model];
if(value==@"iPhone3,1

" ) {
// the iPhone 4 was the first device to 
// have a front facing camera
// future devices, bad luck
}

NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *captureDevice = nil;
for (AVCaptureDevice *device in videoDevices)
{
   if (device.position == AVCaptureDevicePositionFront)
   {
       captureDevice = device;
       break;
   }
}
does this device have a high resolution screen
NSString* valueDevice = [[UIDevice currentDevice] model];
if(value==@"iPhone4,1
" ) {
// the iPhone4S was first to have a 
// high res screen
// future devices, bad luck
}
BOOL hasHighResScreen = NO;
if ([UIScreen instancesRespondToSelector:@selector(scale)]) { 
// only if supporting iOS < 4.0
   CGFloat scale = [[UIScreen mainScreen] scale];
   if (scale > 1.0) {
       hasHighResScreen = YES;
   }
}
can this device use Passbook?
if ([[[UIDevice currentDevice] systemVersion] floatValue] > 6.0) {
   // any OS newer than iOS6 is
// *assumed* to have PassLibrary
}
- (BOOL) canUsePassLibrary {
   Class libraryClass = NSClassFromString(@"PKPassLibrary");
   if(libraryClass && [libraryClass  performSelector:@selector(isPassLibraryAvailable)]) {
       // the method isPassLibraryAvailable 
// returns a BOOL
       return YES;
   }
   return NO;
}
can I use a Core Location Activity Type
#if __IPHONE_6_0
CLLocationManager * manager = [[CLLocationManager alloc] init];
CLActivityType type = [manager activityType];

#endif
CLLocationManager * manager = [[CLLocationManager alloc] init];
   if ([manager respondsToSelector:@selector(activityType)]) {
       id typeOfActivity = [manager performSelector:@selector(activityType)];
       // CLActivityType is an enum that is 
// only defined in iOS 6 or higher
       // and we can't directly test if an enum 
// is defined
   }

When checking for certain features, Apple often has methods which will tell you whether or not that feature is available. Try looking for these methods before creating your own.
However, there is a caveat to using this method: if you try and use a method on a device which is running an older Operation System which does not recognize that method (because the framework is not available), a fatal error will result. This is not easy to check using the iOS simulator. Here is how you check for the existence of the required part of a framework before jumping and using it:



Desired code to use
How to test first
Class
Class someClass = NSClassFromString(@”PKPassLibrary”);
if(someClass) { //.....}
Method
if ([thisObject respondsToSelector:@selector(someMethod)]) { //..... }
Constant
BOOL isKeyboardBoundsKeyAvailable = (&UIKeyboardBoundsUserInfoKey != NULL);
if (isKeyboardBoundsKeyAvailable) {
 // UIKeyboardBoundsUserInfoKey defined
}
enum
unfortunately, there is no direct way to test if an enum exists, so instead find a method which should be available when the enum is available
e.g. the enum CLActivityType is a typedef for an enum, defined in iOS 6. Fortunately, the class also has a property which uses this type, called activityType, and properties can be tested as getters/setters

As the above table shows, if you want to know if a Class is available, try creating a class from a string. On devices where the Class is unavailable, the "if (someclass)" condition will fail, and the code inside the block will not be executed.

Likewise, if you want to know if a certain method is available, test using "respondsToSelector". Sometimes newer OS versions have constants which are not defined in earlier versions. The solution in this case is to test if the address of the constant is "NULL".  However, dealing with enumerations is trickier. As the table states, we cannot test directly for the definition of an enum, so instead our best guess it to look for something else (a property, method, or constant) which was released as part of the same group of technology as the enum.

As a professional iOS developer, you may be glad that applications require constant maintenance. But you want to protect your reputation that would be tarnished if your apps crashed or lost functionality when newer devices were released. No code is truly "future-proof" forever. However, by following the advice in this article, your code will be more reliable and require less maintenance.

No comments:

Post a Comment