With the advent of XCFrameworks in 2019, Apple made it simple to deliver a single framework package that catered for multiple platforms. However, producing a multi-platform framework still involved managing different per-platform configurations in the framework's Xcode project. This duplication of configuration could become repetitive and error prone, leading to potentially misconfigured framework versions. Now, thanks to Multi-Platform Frameworks in Xcode 13, this duplication problem has been resolved once and for all.
To demonstrate how to convert an Xcode project with multiple targets to a single "multi-platform framework" target project, we will rework the Hopoate framework's Xcode project step-by-step, until it contains just one single framework target. Hopoate supports iOS, watchOS, and tvOS, and currently has a separate framework target for each platform:
We will begin the process of transforming the project to a multi-platform framework by editing the build settings of the existing "Hopoate iOS" target.
The first step we need to take is to set the ALLOW_TARGET_PLATFORM_SPECIALIZATION
build setting to YES
:
This allows the Xcode build process to build the target for multiple platforms.
Currently, the target is only configured to support iOS. We need to expand this to include watchOS and tvOS:
We must make sure to include both the hardware and simulator versions of the supported platforms. Having done this, we are thwarted in our attempts to build the framework for one of these new platform's simulators by Xcode's destination menu:
This occurs because the target's "Targeted Device Families" setting only contains iPhone and iPad:
We need to update this to include Apple Watch and Apple TV:
Now we can build for Apple Watch and Apple TV simulators:With 3 separate framework targets comes 3 separate umbrella headers and Info.plist files:
We can replace these with a single umbrella header and Info.plist that works for all of the platforms supported by the framework. For Hopoate this is simple, as the only system framework it replies on is Foundation. However, for frameworks that provide user-interface elements, different framework imports will be needed based on the target platform e.g UIKit on iOS and WatchKit on watchOS. This can be catered for by importing TargetConditionals.h:
#import <TargetConditionals.h>
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#elif TARGET_OS_WATCH
#import <WatchKit/WatchKit.h>
#endif
Once we have our unified umbrella header and Info.plist, make sure that the new umbrella header is included as a public header in the framework target, and that the Info.plist entry is updated with the location of the universal Info.plist file. Finally, remove the platform specific umbrella headers and Info.plist files. This nicely simplifies the project sidebar:
Our "iOS" target is now configured to build for all of our supported platforms, so we can rename the target to remove the "iOS" suffix:
The build bar still shows "Hopoate iOS" as the target to be built:
This is because the scheme used to build the framework is still called "Hopoate iOS". We rename it to "Hopoate" to reflect the new target name.Now that we have a multi-platform framework target configured, we no longer need the watchOS and tvOS specific versions. As such, we can delete the targets and associated schemes from the project. Our target list now looks like this:
You may have used different bundle IDs for the different builds of your framework for each of the supported platforms, perhaps suffixing the framework's bundle ID with the platform name. Double check the value of the bundle ID to ensure that this is no-longer the case:
Updating your framework's Xcode project configuration to take advantage of multi-platform framework builds is a relatively straightforward process that reduces duplication and the possibility of misconfiguring your framework. For more information, watch WWDC21 session 10210 Explore advanced project configuration in Xcode.