An essential part of creating scalable apps is code organization. Low-level, reusable code should be kept in a separate framework instead of the app project. This makes maintenance, deployment, and code sharing between apps possible. In this post, we will create a Swift framework in Xcode and use it in our iOS, watchOS, and tvOS apps.
Creating the Project
First, let’s create an empty project called “MyModule”. When I say empty, I literally mean empty. From Xcode, choose a template under “Cross-platform > Empty”:
From here, you can start creating your targets per platform. You can do this under “File > New > Target”. Choose the “Cocoa Touch Framework” template under “iOS > Framework & Library”. You can call it “MyModule iOS”. Do not check “Include Unit Tests”, we will do this later.
Now do the same for “watchOS > Framework & Library > Watch Framework” and “tvOS > Framework & Library > TV Framework”.
Next, create an empty folder called “Sources” and add it to the project. This is where all your code will go. This convention is meant to be forward-compatible with the Swift Package Manager when available for iOS.
So far, your project should look something like this:
The Info.plist Files
Now that we have our foundation to our project, it’s time to fix it up so the platforms play nice together against the same code base. Let’s take care of the “Info.plist” files. Move the “Info.plist” file from “MyModule iOS” into the “Sources” folder. We will use share this “Info.plist” across all the platforms.
Now let’s add the .plist file into the Xcode project by right-clicking on your “Sources” folder in Xcode and select “Add files”. Uncheck “Copy items if needed”, select “Create groups”, and make sure none of the Target Memberships are selected. Your Xcode project should look like this so far:
Now we need to update the “Build Settings” to point to the respective .plist file name and location for each platform target. So for the iOS target, go to “Build Settings > Packaging > Info.plist File”. From here, put in the relative path to the .plist file you just placed in the “Sources” folder:
The Header Files
Unfortunately, we have to live with Objective-C for awhile, so let’s handle our header file so Objective-C projects can consume our Swift framework. Go to the “.h” file Xcode created for you under the iOS folder and remove the platform name from the names in the source code:
Above, I removed “_iOS” from “MyModule_iOSVersionNumber” and “MyModule_iOSVersionString”. Save the file then rename it to remove ” iOS” from the file name. Next drag it into the “Sources” folder.
Highlight the file and select all of the “Target Memberships” and select “Public”:
Now you can delete the platform folders from the project and “Move to Trash” when prompted. Our code will go in the “Sources” folder going forward, not these target folders. Remember, your framework targets are still available to us, we just don’t need the folders Xcode created for us. At this point, your project should look a lot cleaner:
Go ahead and add a Swift code file in the “Sources” folder to try it out. You’ll be able to toggle which “Target Memberships” this code file is for (iOS, watchOS, tvOS, or all of them).
You may also need to embed images into the framework so it can be shared amongst apps. Simple add an asset catalog called “Media.xcasset” and we can use it later in images by specifying the bundle. Be sure to select all target memberships as well.
Also for a bit of clean up at this point, create a new group (without folder) called “Supporting Files” and drag the “MyModule.h” and “Info.plish” files in there so they’re out the way.
The Build Settings
Let’s update our “Build Settings” to accommodate the cross-platform architecture we created. For each of the platform targets, go to “Build Settings > Packaging > Product Name” and remove the appended platform name, so it will be an identical name for all the platforms so they are packaged as one product:
For allowing other projects to consume this framework via Carthage, you’ll have to make your targets “Shared”. To do this “Manage Schemes” and check the “Shared” areas:
These next steps aren’t necessary, but I highly recommend them:
- Set “Require Only App-Extension-Safe API” to “Yes”. This will allow your framework to be used in extensions like the Today Widget, which have tighter restrictions. If you do something in your code that breaks this restriction, you’ll get a compile error right away so you can think of a different approach to your code. This is better than later finding out that you need to use your framework in an extension and have to re-architect some parts of your code.
- This is more of a business/management decision, but usually the consensus for iOS version support is the latest version down to the last jailbroken version (hey, just because you don’t jailbreak your phone doesn’t mean we should ignore this segment of users ;). Check your app stats to get a better idea if you should support older versions. This setting should be configured under your “Project > Info > Deployment Target”. Now go to your iOS build settings and delete the iOS Deployment Target version so it will be inherited from the project level. However, for the watchOS and tvOS targets, you’ll have to go “Build Settings > Deployment > watchOS/tvOS Deployment Target” and set it to only the x.0 of the latest versions, i.e. 4.0 and 11.0. Don’t worry about all these older versions, you’ll be coding against the latest SDK versions across the board using the “Base SDK” setting. You are just supporting older versions with the “Deployment Target” and will get warned by the compiler if something in your code is not supported in an older version you’re trying to support.
The Meta Data
Let’s create a “Metadata” group (without folder) and add some miscellaneous files such as a read me, license, podspec, etc. This is what I have:
When you add these files to your project, make sure unselect all the target memberships since they don’t need to be compiled into the framework.
Save your project as a workspace by going to “File > Save As Workspace”. Call it the same as your project and save it in the root of your project folder. Now close the project and open this new workspace.
Add a new target to your Xcode project. I like to add these templates for unit testing and sample demos:
- iOS > Test > iOS Unit Testing Bundle
- iOS > Application > Tabbed Application
- watchOS > Application > WatchKit App
- tvOS > Test > TV Unit Testing Bundle
- tvOS > Application > Single View App
Manage the schemes and make all these “Shared” as well so they are in source control.
The Big Picture
Finally, your workspace should look like this:
Be sure to immediately add your workspace to git or some source control (include a .gitignore).
Now let’s see how you can select which platform to target per file:
Also notice you can even have more granular control within the code using Swift Conditional Compilation *if needed*. I advise against it since segmenting your file into different platforms is not very elegant and can be messy. Instead, use protocol extensions to segment code 💡
It was a long journey, but now you’re ready to rock some code and support multiple platforms with a single code base. When adding new code files, just select the “Target Memberships” you’d like to support for that particular code file. And don’t forget to unit test… 😉
Watch the Video for this Post!
See the “Creating Cross-Platform Swift Frameworks” webinar; it will change the way you build iOS apps!