I love to browse through Github and see the sorts of frameworks people build. Pretty frequently I come across repositories that make a good effort to provide a cross-platform experience by offering iOS, watchOS, and tvOS versions. Sometimes there’ll even be the odd macOS version too!

One thing I notice in pretty much every single one of these cases is that the platform-specific code is implemented as an entirely separate target. While this will certainly work, it typically leads to disparities as one target gets updated and another forgotten.

Fortunately, there is a better way! In this post, we’ll learn how to build a true cross-platform framework from only a single target, by altering a single build setting.


Supported Platforms

There is one overriding build setting that allows for the creation of cross-platform frameworks: SUPPORTED_PLATFORMS. Typically in an iOS framework, this build setting is defined like this:

SUPPORTED_PLATFORMS = 

When the SUPPORTED_PLATFORMS value is empty, its value is derived from the Base SDK (SDKROOT) value. So, when you have a Base SDK set to iphoneos, the supported platforms is inferred to be the platforms supported by that SDK (iphoneos and iphonesimulator). For a macOS SDK, this value is macosx.

So the simple extension to this is that, instead of leaving this value blank, we can manually specify the SUPPORTED_PLATFORMS value to include all known platform identifiers:

SUPPORTED_PLATFORMS = iphonesimulator iphoneos watchos watchsimulator appletvos appletvsimulator macosx

In Xcode, this value can be set in this build setting popup:

Setting the Supported Platforms build setting

(Of course, if you don’t want to support tvOS or watchOS or macOS, leave out the corresponding values.)

A question arises then: if we do this, what happens with the SDKROOT value? Is there a problem with having all these supported platforms, but an SDKROOT still limited to a single value?

The answer is “no”; there is not a problem. From what I can tell, the SDKROOT build setting is somewhat special. It seems to be somehow derived from the run destination. So, if the run destination is an iOS simulator, then the SDK ends up being the latest iOS SDK. If the run destination is “My Mac”, the SDK will be the macOS SDK.

This extends to when the framework is being included within another target. The final run destination is propagated down through the entire tree of products being built, and they all use the same SDK.

So, by setting the SUPPORTED_PLATFORMS value to all known platform values, we are actively indicating that the target can be built for all known platforms.

At this point, all we need to do now is unify the target structure, which typically involves deleting a whole bunch of extraneous stuff. We still have the full power of conditional compilation at our disposal for the odd time when we need to do something unique for a particular platform.

Once we’ve done that, we’re basically done. We have a target that will build for whichever platform we want. We can conditionalize any build setting we need to make platform-specific adjustments, and we have vastly simplified the adoption of the framework by others.

All with a single build setting.


For more info on building a universal target, check out this great Github repository of xcconfig files: https://github.com/mrackwitz/xcconfigs (Kudos to @SmileyKeith for the link)