惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
量子位
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
B
Blog
U
Unit 42
C
Check Point Blog
I
InfoQ
aimingoo的专栏
aimingoo的专栏
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
爱范儿
爱范儿

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger
Efficient iOS Version Checking | Peter Steinberger
Peter Steinberger · 2016-07-06 · via Peter Steinberger

Few apps have the luxury to support only the most recent version of iOS. It’s often necessary to set a lower deployment target and branch in code based on specific versions of iOS. There are various ways to accomplish this and even Apple’s message is a bit conflicted. I recently saw this tweet where someone basically warns to not do this:

#define IsIOS7 ([[[[UIDevice currentDevice] systemVersion] substringToIndex:1] intValue]>=7)

A GitHub search shows that there are over 8000 results calling substringToIndex:1. All of that code will break with iOS 10. It will instead assume iOS 1, where the only apps that existed were jailbreak apps.

It’s the same old story again. Windows 9 became Windows 10 because there was too much code out there checking for Windows 95 and 98 via if (name.startsWith("windows 9")).

New API

It took surprisingly long for this problem to be recognized and better API to be offered. With iOS 8, we finally got some improvements! There’s now a new operatingSystemVersion method on NSProcessInfo, and more importantly the - (BOOL)isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version method to check:

if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9, .minorVersion = 1, .patchVersion = 0}]) {
    NSLog(@"Hello from > iOS 9.1");
}

// Using short-form for the struct, we can make things somewhat more compact:
if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,3,0}]) {
    NSLog(@"Hello from > iOS 9.3");
}

What we do at PSPDFKit

PSPDFKit is an SDK to view, annotate and fill forms on PDF documents. It was originally written when iOS 4 was around and has continued to evolve as new iOS versions have been released. Back then there was no dedicated API to check for the version, and many apps did something like this:

if ([[[UIDevice currentDevice] systemVersion] isEqualToString:@"7.0"]) {
    //do stuff
}

This was never a good idea so we never used it. A string compare might be fast but it’s the wrong choice in this case. To get it right, you’d need to do something like this:

#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) \
  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)

if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9.0")) {
    ...
}

Clunky. Easy to get wrong. There’s a simpler way. We can use the NSFoundationVersionNumber (or the kCFCoreFoundationVersionNumber) to compare. The system check reduces to a simple if compare. No method calls required, so it’s extremely efficient, even in tight loops.

if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_9_0) {
    // do stuff for iOS 9 and newer
}
else {
    // do stuff for older versions than iOS 9
}

In fact, this is exactly what Apple suggested at the 2013 Tech Talks in the Architecting Modern Apps, Part 2 session.

Caveat: Sometimes the constants are missing. NSFoundationVersionNumber is defined in NSObjCRuntime.h and as part of Xcode 7.3.1 we have constants ranging from iPhone OS 2 to #define NSFoundationVersionNumber_iOS_8_4 1144.17 - not 9.0-9.3. Same for kCFCoreFoundationVersionNumber. Note that while these numbers are similar, they are not the same. So use one or the other.

If you do macOS development, you can also use the NSAppKitVersionNumber which is usually kept more up to date.

In SDK 10 (Xcode 8) Apple added the missing numbers and even some future ones.

#define NSFoundationVersionNumber_iOS_9_0 1240.1
#define NSFoundationVersionNumber_iOS_9_1 1241.14
#define NSFoundationVersionNumber_iOS_9_2 1242.12
#define NSFoundationVersionNumber_iOS_9_3 1242.12
#define NSFoundationVersionNumber_iOS_9_4 1280.25
#define NSFoundationVersionNumber_iOS_9_x_Max 1299

Will there be an iOS 9.4? Considering iOS 10 will be released in three months and 9.3.3 is in beta, I wouldn’t expect it but it is still nice to have a placeholder. In PSPDFKit, we define the missing version numbers using the following pattern below. If our code is built with a higher minimum deployment target, the code is automatically compiled out, which helps a lot when we drop iOS versions.

// iOS 9 compatibility
#ifndef kCFCoreFoundationVersionNumber_iOS_9_0
#define kCFCoreFoundationVersionNumber_iOS_9_0 1223.1
#endif

#define PSPDF_IS_IOS9_OR_GREATER (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_9_0)

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000
#define PSPDF_IF_IOS9_OR_GREATER(...) \
if (PSPDF_IS_IOS9_OR_GREATER) { \
PSPDF_PARTIAL_AVAILABILITY_BEGIN \
__VA_ARGS__ \
PSPDF_PARTIAL_AVAILABILITY_END }
#else
#define PSPDF_IF_IOS9_OR_GREATER(...)
#endif

#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 90000
#define PSPDF_IF_PRE_IOS9(...)  \
if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_9_0) { \
PSPDF_PARTIAL_AVAILABILITY_BEGIN \
__VA_ARGS__ \
PSPDF_PARTIAL_AVAILABILITY_END }
#else
#define PSPDF_IF_PRE_IOS9(...)
#endif

Notice the partial availability macros. This is a warning that was added with SDK 9. It’s not really useful when we already wrap things in version blocks, so we disable it. We have those as standalone macros since sometimes this is useful in code that uses other kinds of availability checks. We used it, for example, when implementing interactive transitions in UICollectionView to implement dragging of tabs or pages on iOS 9 while also also supporting iOS 8.

#define PSPDF_PARTIAL_AVAILABILITY_BEGIN \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wpartial-availability\"")

#define PSPDF_PARTIAL_AVAILABILITY_END \
_Pragma("clang diagnostic pop")

Why these macros?

Since we dropped iOS 7 a while ago, we could easily switch to the new isOperatingSystemAtLeastVersion:. Its implementation calls operatingSystemVersion internally and is quite efficient. It would produce a bit more code and would still be slightly slower than our current implementation. I don’t see any upside compared to the foundation check, but it’s certainly nicer to use if you’re not using macros.

Things get ugly if we look at the implementation of operatingSystemVersion directly. It’s cached, but it does generate the version via calling _CFCopySystemVersionDictionary() and then looking up kCFSystemVersionProductVersionKey (which is ProductVersion) and then calling componentsSeparatedByString: on that string. I somehow expected this to be hardcoded, but reading it from an external dictionary file is probably more flexible.

Swift

As of Swift 2.0 there’s support for version checking built right into the language. So instead of calling:

if NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 10, minorVersion: 0, patchVersion: 0)) {
    // modern code
}

You can now do the same with much less code:

if #available(iOS 10.0, *) {
    // modern code
} else {
    // Fallback on earlier versions
}

Swift also correctly applies the partial availability checks for API called within these blocks, so we get compile-time safety that we cannot easily copy in Objective-C. In “Swift in Practice” (WWDC 2015, Session 411) starting 08:40 an Apple engineer explains this in great detail.

So, what is Swift using under the hood? Good thing that it’s open source and well structured! Let’s take a look at Availability.swift:


/// Returns 1 if the running OS version is greater than or equal to
/// major.minor.patchVersion and 0 otherwise.
///
/// This is a magic entry point known to the compiler. It is called in
/// generated code for API availability checking.
@_semantics("availability.osversion")
public func _stdlib_isOSVersionAtLeast(
  _ major: Builtin.Word,
  _ minor: Builtin.Word,
  _ patch: Builtin.Word
) -> Builtin.Int1 {
#if os(OSX) || os(iOS) || os(tvOS) || os(watchOS)
  let runningVersion = _swift_stdlib_operatingSystemVersion()
  let queryVersion = _SwiftNSOperatingSystemVersion(
    majorVersion: Int(major),
    minorVersion: Int(minor),
    patchVersion: Int(patch)
  )

  let result = runningVersion >= queryVersion

  return result._value
#else
  // FIXME: As yet, there is no obvious versioning standard for platforms other
  // than Darwin-based OS', so we just assume false for now.
  // rdar://problem/18881232
  return false._value
#endif
}

Now, much more interestingly, what is _swift_stdlib_operatingSystemVersion() and how is it defined? For that we have to leave the cozy Swift world and dig into the madness that is Objective-C++. Enter Availability.mm:

/// Return the version of the operating system currently running for use in
/// API availability queries.
_SwiftNSOperatingSystemVersion swift::_swift_stdlib_operatingSystemVersion() {
  static NSOperatingSystemVersion version = ([]{
    // Use -[NSProcessInfo.operatingSystemVersion] when present
    // (on iOS 8 and OS X 10.10 and above).
    if ([NSProcessInfo
         instancesRespondToSelector:@selector(operatingSystemVersion)]) {
      return [[NSProcessInfo processInfo] operatingSystemVersion];
    } else {
      // Otherwise load and parse from SystemVersion dictionary.
      return operatingSystemVersionFromPlist();
    }
  })();

  return { version.majorVersion, version.minorVersion, version.patchVersion };
}

Swift uses the new API that we got in iOS 8 but falls back to horrible things on < 8, opening the @"/System/Library/CoreServices/SystemVersion.plist" file instead. The result here is cached however, so the version check does hit the disk, but only once. My first reflex was to send a pull request with a change to simply use existing public API (systemVersion), however Xcode 8 sets the minimum deployment target to iOS 8 and it’s unlikely that we see another Xcode 7.3.x release with an updated Swift version, so the code will likely just go away completely.

More on backwards compatibility

It’s worth noting that Apple has worked to make these kind of version checks unnecessary. Of course, there’s respondsToSelector: and instancesRespondToSelector: which is an integral part of Objective-C. I assume that you already know about these. It’s perfectly fine to use those, and we do that occasionally in some cases. However, there are also reasons why this is not always appropriate. For example, sometimes Apple makes API public that existed in some form or another internally before but with different characteristics. This was the case with appStoreReceiptURL which was added in iOS 7 but also existed in iOS 6. In such cases, being explicit about the version is more reliable. In addition, it also makes it easier to clean up code as you hopefully drop older versions of iOS. All you need to do is remove your compatibility macros and fix the resulting build errors.

Weak Linking

In the very early days, using a class not available on all versions meant using this pattern:

Class cls = NSClassFromString (@"NSRegularExpression");
if (cls) {
    // Create an instance of the class and use it.
} else {
    // Alternate code path to follow when the
    // class is not available.
}

With the addition of weakly linked classes in iOS 4.2, this is now much simpler:

if ([UIPrintInteractionController class]) {
    // Create an instance of the class and use it.
} else {
    // Alternate code path to follow when the
    // class is not available.
}

Greg Parker shares more about this in his Hamster Emporium archive, including this gem:

Weak import for Objective-C did not make Snow Leopard for scheduling reasons. Assuming it ships in Mac OS X 10.7 Cat Name Forthcoming, you won’t be able to use it until Mac OS X 10.8 LOLcat.

Weak linking can be extended to a whole framework. In PSPDFKit, we do this for SafariServices, which contains SFSafariViewController (added in iOS 9).

// Part of our .xcconfig file:
-weak_framework SafariServices

Weak linking incurs a small performance penalty on startup so only use this when you have to. To learn more about it, check Apple’s SDK Compatibility Guide.

Conclusion

For most apps, using isOperatingSystemAtLeastVersion: in Objective-C and #available() in Swift is the way to go. It’s still interesting to know what’s going on under the hood - and everything is better than string comparison. If you enjoy digging deep, then PSPDFKit is the place for you to be.

Update

After posting this article, Devin Coughlin, who wrote the #available feature replied why systemVersion can’t be used in the Swift implementation:

@steipete Also: why not “systemVersion”? Because UIDevice is not present on macOS and we wanted the same code path on all platforms.