Skip to content

Instantly share code, notes, and snippets.

@nicklockwood
Last active March 28, 2022 08:16
Show Gist options
  • Star 50 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nicklockwood/d63c69ba2f40a33d7aa4 to your computer and use it in GitHub Desktop.
Save nicklockwood/d63c69ba2f40a33d7aa4 to your computer and use it in GitHub Desktop.
Writing Objective-C framework code that works on multiple OS versions AND can be compiled using multiple SDK versions without warnings can be a PITA. Here's my approach:

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:

  1. The new API will crash if we call it on iOS 7
  2. The new API won't compile if we build it using the iOS 7 SDK
  3. 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:

  1. We can avoid calling the new API on an old OS version by using runtime detection (e.g. respondsToSelector:)
  2. We can avoid compiling new APIs on old SDKs using the __IPHONE_OS_VERSION_MAX_ALLOWED macro
  3. 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.

@mbigatti
Copy link

this is a very interesting writeup. I personally prefer to opt for more clarity in the control flow and to use straight expression in conditionals (no !) even if this makes it necessary to repeat the actual application logic, but this is matter of personal preference.

In this case, where app logic is just one line, the two alternatives shouldn't be too different in complexity but I imagine that my way would not scale well with more complex application logic; in this case I'll probably need some other tricks to simplify the code (common methods, macros)

@nicklockwood
Copy link
Author

Yeah, that was my thinking - this is a simple example but for multiple lines, repeating the logic would be very bad practice, especially since a mistake would only show up on certain build targets or OS versions.

@mike-nelson
Copy link

Very good write up. Not as simple as you would first think. Well thought through solution.

@isadon
Copy link

isadon commented Dec 7, 2017

Xcode 9 just added Swift's availability call via @available. Will you update this to reflect changes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment