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: iOS