Making A Cross-Platform Swift Framework

Originally posted 2/20/2018. Updated for Swift 5 and Xcode 10.2.1

With technologies like handoff and continuity, a unified developer program, and many of the same system frameworks available on all four of its platforms, Apple has been moving over the past few years to more seamlessly integrate its ecosystem of devices. This presents a great opportunity for us to bring our apps to each platform while reusing a lot of the app logic we've already written.

In this tutorial we'll build a cross-platform framework that will work on iOS, macOS, tvOS, and watchOS. All it will do is provide a random color. This may not seem like much, but it'll demonstrate an important concept that will be useful in building more advanced frameworks. For example, while UIKit is available on iOS, tvOS, and watchOS, it isn't available on macOS. How do we set up our framework to use UIColor where UIKit is available and NSColor on macOS?

Let's get started by creating the project.

Project Setup

The first step is to create framework targets for each of the supported platforms. We're going to start by creating an empty Xcode project. You'll find this in the Xcode New Project screen under "Cross-Platform". Call the project "ExampleFramework". The reason we're creating an empty project instead of starting with one of the framework targets is that we want the project name to be general and each of the target names to refer to their corresponding platforms.

Next let's create four targets (File > New > Target). For each platform (iOS, macOS, tvOS, watchOS), choose "Framework & Library", and select the corresponding "Framework" option. Call them "ExampleFrameworkMobile", "ExampleFrameworkMac", "ExampleFrameworkTV", and "ExampleFrameworkWatch" respectively and ensure the "Include Unit Tests" box is checked.

Now we have our four platform-specific frameworks, but when we ultimately use them in our apps, we don't want to have to import "ExampleFrameworkMobile" or "ExampleFrameworkMac". We just want to write:

import ExampleFramework

So let's fix that. For each of the new targets, go into Build Settings, find "Product Name", and change it to "ExampleFramework".

If in Build Settings you don't see the list of targets in a sidebar, click the button in the upper left of the window to show the sidebar.

You'll notice that each framework target has a header file (e.g. ExampleFrameworkMobile.h). For iOS and tvOS, the header imports UIKit. For watchOS, WatchKit. And for macOS, Cocoa. Since our framework is cross-platform, we want the header to import Foundation only, and it would be great to use one header for all four targets. Let's convert the mobile header into a universal one.

First, change the import from UIKit to Foundation:

@import Foundation;

Next, replace the rest of the file with the following:

FOUNDATION_EXPORT double ExampleFrameworkVersionNumber;
FOUNDATION_EXPORT const unsigned char ExampleFrameworkVersionString[];

Note that we changed the name here from "ExampleFrameworkMobile" to just "ExampleFramework". Let's do the same for the filename, which should be changed to "ExampleFramework.h". Finally, in the File Inspector under "Target Membership", add the header to all four framework targets and change the scope in the dropdown menu from "Project" to "Public" where necessary.

We will actually set the target membership again below when we reorganize these files, but it's good to do it correctly each time anyway.

Organization

We now have four targets and a universal header, but our framework directory has seven sub-directories, one for each framework target and one each for the Mobile, Mac, and TV test targets. Since our code will be shared between the targets, let's merge these into a single directory for source files and another for test files.

In the framework's root directory, create two sub-directories, one called "Sources" and another called "Tests". In Finder, move the two files in ExampleFrameworkMobile (ExampleFramework.h and Info.plist) into Sources. Next, still in Finder, delete all four target directories (leave the test directories for now). Now delete the references in the Xcode Project Navigator.

Next, drag the Sources directory into the Xcode Project Navigator. Leave "Copy items if needed" checked, choose "Create Groups", and ensure all targets are unchecked. The reason for this is that the Info.plist should not be added to targets and we will want to ensure the header has the correct scope anyway. Now, once again, add the ExampleFramework.h file to all four framework targets and set the scope to "Public".

Our universal Sources directory is now set up, but in order to use the Info.plist for all four targets we need to set it's path in Build Settings. In the Build Settings search bar, enter "Info.plist". Now for each target, set "Info.plist File" to "Sources/Info.plist".

You should now be able to successfully build each target. Next let's do the same for the test targets. In Finder, go to "ExampleFrameworkMobileTests" and rename the file "ExampleFrameworkMobileTests.swift" to "ExampleFrameworkTests.swift". Then move that file and "Info.plist" into the "Tests" directory you created earlier. Now delete the three tests directories, remove the references in Xcode, and drag the Tests directory into Xcode with the same options as before.

Add "ExampleFrameworkTests.swift" to each of the three test targets and replace "ExampleFrameworkMobile" with "ExampleFramework" everywhere it appears in the file. Finally, in Build Settings, set the Info.plist path in the same way as before, but this time in each test target to "Tests/Info.plist".

You should now be able to run unit tests for the Mobile, Mac, and TV targets.

Framework Code

Now that all of the setup and organization is done, it's finally time to add the code! We're just going to need one public class for the framework, called "ExampleClass". Add a file "ExampleClass.swift" to the Sources directory using the "Swift File" template and make sure the boxes for all four framework targets are checked.

This class will have one static function for generating a random color. As mentioned above, the Mac target will need to use NSColor and the other three targets will use UIColor. So before declaring the class, add the following to the top of the file:

#if os(iOS) || os(tvOS) || os(watchOS)
    import UIKit
    public typealias Color = UIColor
#elseif os(OSX)
    import Cocoa
    public typealias Color = NSColor
#endif

Here we declare a typealias called Color, which refers to UIColor and NSColor as appropriate. We also import UIKit and Cocoa for the relevant platforms.

Next, declare ExampleClass as follows:

public class ExampleClass {

    public static func getRandomColor() -> Color {
        let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue]
        let randomIndex = Int(arc4random_uniform(UInt32(colors.count)))
        return colors[randomIndex]
    }

}

The code here is pretty straightforward, but it demonstrates how we can take advantage of overlap between UIKit and Cocoa APIs to write cross-platform logic using type aliases.


With each release of its four operating systems, Apple is making it easier to provide a consistent user experience across its devices. Writing cross-platform frameworks is a great way to take advantage of this opportunity efficiently. And if you're writing an open-source framework, support for all platforms is one more reason to use your framework over a platform-specific one with similar functionality.

Next Steps