/swift

Multi-platform Framework Targets in Xcode

Targeting multiple Apple platforms is becoming increasingly easier with the advent of inherently multi-platform technologies like SwiftUI and Catalyst. In 2019, Apple also introduced their new XCFramework binary format.

"The XCFramework format allows developers to conveniently distribute binary libraries for multiple platforms and architectures in a single bundle. For example, with XCFrameworks, vendors no longer need to merge (lipo) multiple architectures into a single binary, only to later have to remove the Simulator slice during the archive phase." — Rad Azzouz from PSPDFKit

Frameworks that target more than one Apple platform?

The key thing to remember about XCFramework is that it's a helpful tool for bundling multiple .frameworks together into a single binary bundle (with the .xcframework extension). You still need to build multiple instances of your frameworks when targeting more than one platform at once (e.g. iOS, but also tvOS, and macOS). Sadly, there is no such thing as a truly universal framework that runs on all of Apple's platforms. In the end, all we're doing is bundling disparate binaries in order to ship them as a coherent unit. xcodebuild will then select the right sub-binary at compile-time.

There is no such thing as a "universal" framework.

Single Xcode Target, Multiple Platforms

One way to approach this is to set up a separate Xcode target per platform, and deal with the [sometimes extreme] inconvenience of assigning source files to multiple targets. The number of targets balloons as you add more framework targets, test targets and app targets. It works, but it's not great.

The ideal solution, though, would be a single Xcode target that can be built for any or all of the platforms you want to support. Xcode's out of the box support for this is sorely lacking, as is evidenced by Xcode's UI:

Screenshot of Xcode New Target dialog

A framework or library target is suspiciously missing from Xcode's "New Project/Target" dialog. They're only available if you switch over to the platform-specific tabs.

Reconfiguring platform-specific targets to be multi-platform with the help of some .xcconfig magic

PointFree is my online learning resource of choice on functional programming in Swift. While they're largely a paid website that's very much deserving of the yearly fee they charge, a lot of their code and resulting libraries are available as open-source. They also publish all of the sample code, produced for their video content, on GitHub.

The sample code for ep. 0097 caught my eye a while ago because they were demonstrating a single framework capable of running simultaneously on iOS and macOS. They made this work by employing a set of xcconfig files that toggle a few key build settings. I have since tried these out on one of my own projects.

xcconfig?

In short, Xcode Build Configuration Files (xcconfigs) are plaintext formatted files that enable developers to centralize and share Xcode build settings across targets in an Xcode project. If this description comes across as vague, Mattt from NSHipster has an entire write-up describing the ins and outs.

Base.xcconfig

This base xcconfig shares the common parts for the following two configurations, and deals with setting the supported platforms, architectures and framework search paths:

SUPPORTED_PLATFORMS = appletvos appletvsimulator iphonesimulator iphoneos macosx watchos watchsimulator
VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s
VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64
VALID_ARCHS[sdk=macosx*] = i386 x86_64
VALID_ARCHS[sdk=watchos*] = armv7k arm64_32
VALID_ARCHS[sdk=watchsimulator*] = i386
VALID_ARCHS[sdk=appletvos*] = arm64
VALID_ARCHS[sdk=appletvsimulator*] = x86_64

LD_RUNPATH_SEARCH_PATHS[sdk=appletvos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=appletvsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=watchos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=watchsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'

Framework.xcconfig

If you apply this config to your framework targets, they will become multi-platform. It configures build settings related to signing, bitcode, and device families. Note how it includes the base config from above:

#include "Base.xcconfig"

CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
COMBINE_HIDPI_IMAGES[sdk=macosx*] = YES
ENABLE_BITCODE[sdk=macosx*] = NO
ENABLE_BITCODE[sdk=watchsimulator*] = YES
ENABLE_BITCODE[sdk=watch*] = YES
ENABLE_BITCODE[sdk=iphonesimulator*] = YES
ENABLE_BITCODE[sdk=iphone*] = YES
ENABLE_BITCODE[sdk=appletvsimulator*] = YES
ENABLE_BITCODE[sdk=appletv*] = YES
FRAMEWORK_VERSION[sdk=macosx*] = A
TARGETED_DEVICE_FAMILY[sdk=appletv*] = 3
TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2
TARGETED_DEVICE_FAMILY[sdk=watch*] = 4

Test.xcconfig

Every software project worth its salt has a test target. This config enables you to run your test suites across all of Apple's platforms too:

#include "Base.xcconfig"

FRAMEWORK_SEARCH_PATHS = $(inherited) '$(PLATFORM_DIR)/Developer/Library/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=watchos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=watchsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=appletvos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
LD_RUNPATH_SEARCH_PATHS[sdk=appletvsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'

.xcconfig + Xcode project

Applying these configuration files to your targets are a little unintuitive.

  • Make sure you've added the .xcconfig files to your project or workspace
  • Go to your project/workspace's Project Inspector
  • Highlight the actual project, not the targets themselves
  • Expand the "Configurations" section
  • Expand the Debug and Release subsections (and others)
  • Select the appropriate configuration
    • Framework for framework targets
    • Test for test targets
Screenshot of Xcode project inspector

Once complete you'll be able to compile and test across all platforms:

Screenshot of Xcode run menu
Tagged with: