Best Practices for supporting iOS 7 and 8


Maintaining early iOS programs can be a nightmare. Oftentimes, they were littered with macros such as:

#define SYSTEM_VERSION_EQUAL_TO(v)                  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
#define SYSTEM_VERSION_GREATER_THAN(v)              ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v)                 ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v)     ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

or:

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
  //at least iOS 8 code here
#else
  //lower than iOS 8 code here  
#endif

It seemed like every program had one, and none of them were the same! Fortunately, Apple has come to the rescue with the iOS 8 SDK, adding a method to NSProcessInfo to return operatingSystemVersion, thus saving us from macro hell! They even added a method to determine if the operating system is at least a certain version directly.

(- (BOOL)isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version)

 This allows for easy conditional execution based on the version of iOS. Thus, we can have two execution paths based on the result of isOperatingSystemAtLeastVersion and run all our iOS 8 only code there. That would be easy enough, right? But not so fast-we can do better!

By checking for a fixed version of the operating system, we are vulnerable to changes made by Apple. If Apple were to backport a feature to earlier an earlier version of iOS, your conditional code wouldn’t execute even though the feature was available. Or, Apple could deprecate or remove a feature entirely in a future release. If an API call you relied upon is removed in the future, you’ll find yourself in a tricky situation where you have to rewrite part of or your entire app to fix the issue.

Instead of using the system version to see if the feature you would like to use is available, it’s far better to ask the SDK directly. Apple provides several methods for this.
        
         The easiest way to do so is to ask an object if it responds to a selector. This is best used when a new method is added to an already existing API. For example, one of the new features of iOS 8 is always vs. when in use authorization of location services. It’s far better to use the following example rather than to check the operating system version to see if we can ask for always authorization.

    if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
        [self.locationManager requestAlwaysAuthorization];
    }

         But sometimes Apple will add a brand new class to a version of iOS, even deprecating the old version. We don’t want to use the old version in the project, as eventually the deprecated version will be removed from the SDK and our program will no longer work. We could check if we are above a certain version of iOS to determine which class we should use, but if we use NSClassFromString, it’s much clearer what we would like use, and should one or both classes be removed from the API, our code will not fail. For example, if we want to use UIAlertController on iOS 8 and above, and the deprecated UIAlertView on iOS 7, we would use:
        
        if (NSClassFromString(@"UIAlertController")) {

             This is much clearer and more future-proof than simply checking what version of the OS we are running.

         What if Apple expands a protocol in the latest version of iOS? You can’t just see if the class from string exists, unless the object that defines that protocol is also new. You also can’t ask the object that implements that protocol if it responds to that selector-it will return YES no matter what version of iOS the app is running on-even if you can’t ask the object to perform that selector! There IS a way to that without checking what version of iOS you’re running-but you have to be willing to work with C structs to do so.

         Since Objective-C compiles down to C, you can leverage those C methods and structs to determine if a method is part of a protocol at runtime. You do so using a function called protocol_getMethodDescription, which is part of the Objective-C runtime. The function takes 4 arguments, the protocol you’re checking, the selector you would like to know if it is included, and 2 Booleans, one indicating if it’s a required method and one indicating if it’s an instance method. For example, if we want to know if swipe to edit is available on the current version of iOS, we could use this method:

    struct objc_method_description hasMethod = protocol_getMethodDescription(@protocol(UITableViewDelegate), @selector(tableView:editActionsForRowAtIndexPath:), NO, YES);
   
    if ( hasMethod.name != NULL ){
        return YES;
    }
    else{
        return NO;
    }

         This ensures that on all versions of iOS that support swipe to edit, we are leveraging the UITableViewDelegate protocol to do so.


         Writing readable, maintainable code should be a goal of all developers. Although Objective-C is a very expressive language, there are still a myriad of traps and pitfalls that make that ideal not quite the reality. But by following the above methods to determine if a feature you would like to use is available on the running version of iOS, your code will be far more clear, easier to maintain, and far less likely to break when Apple releases a new version of iOS.

Labels: