Making Private, Cross-Platform Swift Frameworks With CocoaPods

Goal: Make a private, cross-platform Swift framework with dependencies using Github and CocoaPods. The framework will work with iOS, macOS, tvOS, and watchOS.

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. We'll use Github and CocoaPods to make the framework private, so you can include it as a managed dependency in your project without exposing the source.

The framework we're going to build will do two things, fetch an image from the network and provide a random color. This may not seem like much, but it'll demonstrate important concepts 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 UIImage and UIColor where UIKit is available and NSImage and NSColor on macOS? Also, how can we include a dependency like Alamofire to help us fetch images from the network with our own framework using CocoaPods?

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 "Other". 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.

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, let's change all of these to import Foundation.

#import <Foundation/Foundation.h>

Next, create a folder in your project's root directory called "Framework" with a matching group in Xcode. This is where our framework's code is going to go.

CocoaPods Setup

We're going to be using CocoaPods to manage our framework and its dependencies. The first step in this process is to create a podspec file, the CocoaPods standard for telling the dependency manager about our framework, its dependencies, supported platforms, and where to find the source code. Here is an example podspec for our framework:

Pod::Spec.new do |s|
    s.name = 'ExampleFramework'
    s.version = '1.0.0'
    s.summary = 'This is an example of a cross-platform Swift framework!'
    s.source = { :git => '[REPO URL]', :tag => s.version }
    s.authors = '[NAME / COMPANY NAME]'
    s.license = 'Copyright'
    s.homepage = '[WEBSITE URL]'

    s.ios.deployment_target = '9.0'
    s.osx.deployment_target = '10.9'
    s.tvos.deployment_target = '9.0'
    s.watchos.deployment_target = '2.0'

    s.source_files = 'Framework/**/*.swift'
    s.dependency 'Alamofire', '3.1.1'
    s.dependency 'AlamofireImage', '~> 2.2.0'
end

OK let's break this down. The first section is pretty self-explanatory, just note the areas you need to change for your own framework and consider which license is appropriate for you. Also, note that in the "s.source" line we promised to tag a release with the version number we specified (1.0.0). This will be important later.

Next we specified the deployment targets for each platform. Here just make sure to use the minimum versions that make sense for your framework.

In the last section, we specify where to find the source files. Here we tell the podspec to look for any Swift files in the "Framework" folder and recursively in any of its subfolders. We also specify that the framework has two dependencies, Alamofire and AlamofireImage.

Now we're ready to create the Podfile, which will use our podspec to pull in the Alamofire and AlamofireImage dependencies. The Podfile will need to know where to get dependencies for each target. Here's how it should look:

use_frameworks!

target 'ExampleFrameworkMobile' do
    platform :ios, '9.0'
    podspec :path => 'ExampleFramework.podspec'
end

target 'ExampleFrameworkMac' do
    platform :osx, '10.9'
    podspec :path => 'ExampleFramework.podspec'
end

target 'ExampleFrameworkTV' do
    platform :tvos, '9.0'
    podspec :path => 'ExampleFramework.podspec'
end

target 'ExampleFrameworkWatch' do
    platform :watchos, '2.0'
    podspec :path => 'ExampleFramework.podspec'
end

Run pod install, close the Xcode project, and open the workspace that CocoaPods generated for you. You should now be able to build each framework target.

Github Setup

You're going to need to create two Github repos, both private. In our example the first would be for the framework itself, which we've already started creating in the top-level "ExampleFramework" folder. The second repo is for managing private pods, referred to as a "podspecs" repo. CocoaPods has a master podspecs repo where all of the public and open-source pods live. Since we want our framework to be private, we need to create a separate repo where CocoaPods will be able to find the podspec we created above. In our example we might call this "ExamplePodspecs".

Framework Code

After all of this setup, it's finally time to write the framework code. We're just going to need one public class for the framework, called "ExampleClass", which will initially look like this:

import Foundation
import Alamofire
import AlamofireImage

public class ExampleFramework {

    public class func getNetworkImage(url: String, completion: (Image? -> Void)) -> Request {
        return Alamofire.request(.GET, url).responseImage { response in
            guard let image = response.result.value else {
                completion(nil)
                return
            }
            completion(image)
        }
    }

}

Make sure to add the class to all four targets in the File Inspector under Target Membership.

We've imported Foundation, Alamofire, and AlamofireImage and defined a public class function to fetch an image from the network. The function takes a URL string and a completion closure as arguments, and returns an Alamofire Request object so the request can be stored and cancelled if necessary.

The most interesting part of this class for our purposes is the "Image" type that Alamofire gives us instead of UIImage or NSImage. "Image" is actually a type alias, and it allows our framework to be cross-platform while still giving us UIImage and NSImage objects to work with on the appropriate platforms. Let's take a look at how Alamofire is doing this:

#if os(iOS) || os(tvOS) || os(watchOS)
    import UIKit
    public typealias Image = UIImage
#elseif os(OSX)
    import Cocoa
    public typealias Image = NSImage
#endif

As you can see, if we're on iOS, tvOS, or watchOS, the code is importing UIKit and defining the Image type as an alias to UIImage. If we're on macOS, it imports Cocoa and aliases to NSImage.

Let's try this ourselves by using a type alias to work with UIColor and NSColor where appropriate. Add the following right above our public class declaration:

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

Now we can add a new public class function to our ExampleFramework class that returns a random color using the "Color" type alias we just defined.

public class func getRandomColor() -> Color {
    let colors = [Color.redColor(), Color.orangeColor(), Color.yellowColor(), Color.greenColor(), Color.blueColor()]
    let randomIndex = Int(arc4random_uniform(UInt32(colors.count)))
    return colors[randomIndex]
}

You should now be able to build all four targets. With our framework built, it's time to tag a release as we promised to do in our podsepc file. Commit the framework to the repo you made for it, then in Terminal, run the following:

git tag 1.0.0
git push origin 1.0.0

Using The Framework

We have our cross-platform framework, so now it's time to use it. First we need to push the podspec file we created to our private podspecs repo. This podspecs repo is where our apps that use the framework will look to find pods that aren't defined in the CocoaPods master repo. To do this, run the following in Terminal, replacing "ExamplePodspecs" with the name you gave to your podspecs repo:

pod repo push ExamplePodspecs ExampleFramework.podspec

Pushing the podspec can take time, so you may want to add "--verbose" so you can see what's happening. Also, CocoaPods will validate your podspec and by default won't push it if there are warnings. It's important to resolve these warnings, but if you want to try publishing the podspec to your private repo anyway, you can add "--allow-warnings".

Once the podspec has been pushed, the last step is to add the pod to your app's Podfile. Since you will likely be pulling dependencies both from the CocoaPods master repo and your own, you will have to define these podspec sources in your Podfile. An example iOS Podfile might look like this:

source '[PRIVATE PODSPECS REPO URL]'
source 'https://github.com/CocoaPods/Specs.git'

platform :iOS, '9.0'
use_frameworks!

target 'MyTarget' do

#Internal
pod 'ExampleFramework', '1.0.0'

#Public
#other publicly available pods you want to include

end

When you run "pod install", CocoaPods will find ExampleFramework in your private podspecs repo and pull it along with the Alamofire and AlamofireImage dependencies. You now should be able to use the framework to fetch images from the network and get a random color!

In this tutorial we've created a private framework that will work on all of Apple's platforms and used Github and CocoaPods to manage it alongside our public dependencies. Frameworks are a great way to make sure you're never repeating code. Where possible, it pays to bundle code into a framework, and we've even seen how code that may not look like it could work on multiple platforms can use selective importing and type aliases to make it platform-agnostic.

The source code for this project is available in the "ExampleFramework" folder here.