Suppose we want to add support for a new iOS 8 API in our framework that replaces an older iOS 7 API. There are a few problems we might face:
- The new API will crash if we call it on iOS 7
- The new API won't compile if we build it using the iOS 7 SDK
- The old API will raise a deprecation warning if built with a deployment target of iOS 8 and up
These three problems require three different technical solutions:
- We can avoid calling the new API on an old OS version by using runtime detection (e.g.
respondsToSelector:
) - We can avoid compiling new APIs on old SDKs using the
__IPHONE_OS_VERSION_MAX_ALLOWED
macro - We can avoid compiling deprecated code on new SDKs by using the
__IPHONE_OS_VERSION_MIN_REQUIRED
macro
So let's tackle the problems individually. Supposed we want to write code that will use the new API if available, but the old API if not. In this case we want to set the priority of an NSOperation
in a queue:
//set NSOperation thread priority
if ([operation respondsToSelector:@selector(setQualityOfService:)])
{
//use the new API if available
[operation setQualityOfService:NSQualityOfServiceUserInteractive];
}
else
{
//fall back to the old API
[operation setThreadPriority:1.0];
}
This solution is perfectly acceptable in an ordinary app, where we control the SDK version and deployment target. As long as the SDK is set to iOS 8 or above, and the deployment target is set to iOS 7 or below, this will work as intended.
But for framework code, we don't control those settings, and if we are releasing code around the time of a new iOS release, it's reasonable to assume that not everyone will be using or targeting the same SDK and OS versions. But if we try to compile the code above using the iOS 7 SDK it will fail to compile because the setQualityOfService:
method is undefined. So how do we fix that?
We use conditional compilation:
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
if ([operation respondsToSelector:@selector(setQualityOfService:)])
{
//use the new API if available
[operation setQualityOfService:NSQualityOfServiceUserInteractive];
}
else
#endif
{
//fall back to the old API
[operation setThreadPriority:1.0];
}
We've used __IPHONE_OS_VERSION_MAX_ALLOWED
to check if the SDK is >= iOS 8. Now, if we are using the iOS 8 SDK, the code will still do a runtime check for setQualityOfService:
and fall back to setThreadPriority:
if it doesn't exist. But if we compile using the iOS 7 SDK, the code to do that check is omitted completely.
This arrangement of having the else
inside the #if
block may seem a bit weird, but it means we don't have to duplicate the fallback code between the run-time and compile-time checks. Technically the {
and }
in the else clause aren't needed here, but if "goto fail;
" means anything to you, you'll know why they're there.
OK, so that handles the late adopters who are still using iOS 7 SDK. But what about the early adopters who want to drop support for iOS 7 completely? If they set their deployment target to iOS 8, they'll get a warning for the setThreadPriority:
line because it's deprecated. It will never actually be called at runtime, but the compiler isn't smart enough to figure that out. So how do we supress the warning (without cheating using #pragma GCC diagnostic ignored …
)?
We use conditional compilation again:
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
if (![operation respondsToSelector:@selector(setQualityOfService:)])
{
//if the new API is not available, use the old API
[operation setThreadPriority:1.0];
}
else
#endif
{
//use the new API
[operation setQualityOfService:NSQualityOfServiceUserInteractive];
}
This time we've used __IPHONE_OS_VERSION_MIN_REQUIRED
to check if the deployment target is >= than iOS 8. If it is, then there's no way the code can be running on iOS 7, which means we don't need to do the runtime check. We've also inverted the respondsToSelector:
test so that we avoid doing an unnecessary runtime check when we already know we must be on iOS 8.
OK, so what if we want to support both the late adopters and the early adopters at once? This is a little bit more challenging, if we don't want repeat ourselves. Here's the best solution I could come up with:
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
if (![operation respondsToSelector:@selector(setQualityOfService:)])
{
#endif
[operation setThreadPriority:1.0];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
}
else
#endif
#endif
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
[operation setQualityOfService:NSQualityOfServiceUserInteractive];
#endif
}
This approach repeats the __IPHONE_OS_VERSION_MAX_ALLOWED
test three times, but avoids repeating the actual application logic, which seems like the lesser of two evils (especially if the logic spanned multiple lines, unlike this trivial example).
of course, if you were doing this check a lot, you could define a shorter macro for "is SDK >= than iOS 8", but that wouldn't improve things very much. I'm open to better alternatives.
Very good write up. Not as simple as you would first think. Well thought through solution.