Creating a single-target, cross-platform framework for iOS and macOS

Written by Anthony

Even though Apple is working on allowing developers to use UIKit to build macOS apps, there is and will continue to be a need for creating frameworks that can be shared between iOS, macOS, watchOS, and tvOS. Many of the recommended approaches to building these shared frameworks encourage developers to create separate targets for each platform. In other words, if we wanted to support Apple’s four major platforms, we would need to create SharedFramework, SharedFrameworkMac, SharedFrameworkWatch, and SharedFrameworkTV. I’ve found that this approach is difficult to maintain in practice because of a number of issues:

  • It causes an explosion in the number of targets in our project (four framework targets, four unit testing targets, and, potentially, four UI testing targets).
  • It requires the developer to make sure to add each new source file to each of the four framework targets.
  • It makes import statements far more complex because we’ll need to add the correct framework for the current build target to each source file that uses it. In other words, every time we use the framework, we’ll need to write code like:
#if os(iOS)
  import SharedFramework
#elseif os(macOS)
  import SharedFrameworkMac
#elseif os(watchOS)
  import SharedFrameworkWatch
#elseif os(tvOS)
  import SharedFrameworkTV
#endif

In my experience, a better approach is to create a single framework target that supports each of the platforms that we want to support. This approach solves all of the above problems and results in a considerably simpler Xcode projeect. As an additional advantage, it’s also very easy to accomplish. It only requires a few changes to the framework’s build settings:

First, add all of the platforms we want the framework to support.

SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator
Screenshot of XCode with the SQLite project selected and showing the Build Settings with the Support Platforms build setting value expanded

Second, set the appropriate deployment targets for each of the platforms we wish to support.

IPHONEOS_DEPLOYMENT_TARGET = 10.0
MACOSX_DEPLOYMENT_TARGET = 10.12
TVOS_DEPLOYMENT_TARGET = 11.0
WATCHOS_DEPLOYMENT_TARGET = 4.0
Screenshot of XCode with the SQLite project selected and showing the Build Settings including the deployment target variables

Third, update the runpath search paths so that the dynamic loader can find the frameworks correctly.

LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks @executable_path/../Frameworks @loader_path/Frameworks @loader_path/../Frameworks
Screenshot of XCode with the SQLite project selected and showing the Build Settings with the Runpath Search Paths build setting value expanded

After following these three steps, our framework should be able to be used by iOS, macOS, watchOS, or tvOS apps.

In order to avoid the hassle of manually changing these project settings, feel free to use the Xcode Configuration files Nathan and I use in our apps. Check out the Gist here.