UPDATE: Swift Package Manager release in Xcode 11 for all platforms 🎉. See ZamzamKit for a Swift Package example.
The title is a mouth-full, but so is creating cross-platform frameworks. In this post, I’d like to show you how to create a Swift framework for iOS, watchOS, and tvOS and get them distributed via Carthage and CocoaPods. It’s a technique I use to share frameworks across all my apps and with the community. Note this will only target iOS 8 above because of dynamic frameworks. Ready?
Creating the Project
First, let’s create an empty project. When I say empty, I literally mean empty. From Xcode, choose a template under “Other > 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” and “tvOS > Framework & Library”.
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 Swift 3 comes out 😉
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. Go into each platform folder created above and start appending the platform name after the “Info.plist” files. For example for iOS, rename the file to “Info-iOS.plist”.
Once you have done this for each platform, move them all into the “Sources” folder:
Now you can add the .plist files into the 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 with the appended platform name you did earlier:
Finally for the .plist files, delete the entry under “Build Phases > Copy Bundle Sources”. This was just a side-effect of adding the files into the Xcode project, but we don’t need to copy the bundle since it is taken care of in the previous step when we updated the path in the build settings. Here is the entry you must delete for each platform target:
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 and be cool again. 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 “ZamzamKitData_iOSVersionNumber” and “ZamzamKitData_iOSVersionString”. Save the file then rename it to remove ” iOS” from the file name. Next drag it into the “Sources” folder.
Go to Finder and you’ll notice it’s not really in the “Sources” folder, but still in the iOS target folder. So manually move it to the “Sources” folder from Finder. This will break your project, so go back to Xcode and update the location AND while you’re at it 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).
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 Carthage support, 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 for my apps I usually support a minimum of iOS 8.4, watchOS 2.0, and tvOS 9.0. The reason is because iOS 8.4 has some goodies not available in previous version, such as support for Apple Watch and security updates. Plus this is just some of the perks of developing for the Apple ecosystem instead of Android :wink:. Check out your app stats and don’t end up supporting older version just for one or two people. This setting should be configured under your “Project > Info > Deployment Target”. This will be inherited to the target frameworks. However, for the watchOS and tvOS targets, you’ll have to go “Build Settings > Deployment > watchOS/tvOS Deployment Target” and set it to 2.0/9.0. Don’t worry though, 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” 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 to remove them from the “Build Phases > Compile Sources” and “Build Phases > Copy Bundle Resources” since they don’t need to be compiled.
The Workspace
Are you still with me? Trust me, the end game is worth it… just a little bit longer…
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.
Also for convenience, add a Playground file so you can sketch some ideas out while dreaming up some code. Go to “File > New > Playground” and call it the same name as your workspace. Close the playground and add it to your workspace as a sibling, not a child, of your project.
The Tests
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
The Big Picture
I commend you for reading this far! Here’s how your workspace should look like:
I created some empty folders in the “Sources” folder as a convention for my frameworks, but of course add your own flavor.
Finally, add your workspace to git or some source control and add any dependencies you’d like your framework to use. Check out this excellent blog post for details on how to do that.
See below 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 💡
Conclusion
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… 😉
Happy Coding!!
What about localisation ? Can you give example of this ? thanks !!!
See this post: http://basememara.com/swifty-localization-xcode-support/
Why create three targets instead of just one that works with all platforms?
Many times, you do not want a source code file to target all platforms. For example, you might want to create a base UIViewController for iOS. This would not compile if targeting all platforms. Instead, you can create a base UIViewController for iOS, a base WKInterfaceController for watchOS, etc then attach protocols and extensions to put common code. There are many other scenarios such as dealing with notifications, connectivity, location, etc where this applies. It would save you the effort of creating a project per platform which would be more juggling. Instead, you can create a target per platform within the same project and have common code that targets one, some or all platforms. Also, when creating a new target, you must either select a framework from iOS, watchOS, tvOS, or OSX – which one would you create to make a single target work across all platforms btw?
Thanks for writing this post. I get most of it, but I have a couple of holes when it comes to getting the tests to work. Is there any chance you add this to a GitHub project? (Or I can if you’re happy to give some feedback.) It might be the latest version of Xcode that is causing me some problems.
Thanks
Scott
Hi Scott, I’m glad it helps. Check out one of my libraries that uses this method to see how it’s set up with real code and tests: https://github.com/ZamzamInc/ZamzamKit. I was heavily inspired by popular libraries out there so you might want to see some of your favorites. I wrote another post about some of the ones I use: http://basememara.com/top-10-swift-friendly-cocoapods/
Very nice summary!
But plists shouldn’t be added to the targets.
Target membership is only for compiler and linker.
Hi Thomas, can you please elaborate on why plists shouldn’t be added to the targets?
And of course, Thanks to Basem, great post! 🙂
Thanks for the feedback, Andrej. And thanks for the input Thomas – you are absolutely right! I did some more research to confirm and indeed it is not necessary since it’s already part of the build settings: http://stackoverflow.com/questions/18114413/xcode-which-files-need-to-be-members-of-my-target-target-membership, http://useyourloaf.com/blog/multiple-xcode-targets-and-infoplist/. Of course, I tried it as well and everything seemed fine. I’ve updated the post, much appreciated!
I have a library that has some dependencies on other libraries defined in the podspec. When I create my XCode project as you suggested, the compiler doesnt find those libraries (of course). I think I will need a podfile in some way. Can you tell me how to setup such a structure?
I try to minimize my use of CocoaPods as much as possible. It tends to hijack (hack?) your project and can leave you at its mercy for days banging your head against the wall. Don’t get me wrong, I love CocoaPods and iOS would not be where it is today without it. I think they served their time very well. However, Swift and dynamic frameworks shouldn’t be afterthought anymore and a new era of dependency management is required. I suggest using Carthage for the time being. It will keep your project clean and will be a good path forward to Swift Package Manager when it’s fully baked.
With all this said, in your framework define your dependencies via Carthage as outlined in this blog post: https://robots.thoughtbot.com/creating-your-first-ios-framework. This will allow you to code against the dependencies. Then make sure your podspec reflect the same dependencies so others consuming your framework and using CocoaPod can use it.
Then for testing, create an “Example CocoaPods” app in your framework project. Add a pod file to it with only your framework pod and try it out. With only your pod defined there, it should obey your podspec and pull all the CocoaPods your framework needs.
Ho yeah! That’s the stuff! Thank you for writing this. Very helpful article 🙂
Hey..thx for the info. I have one query . Suppose I have 4 frameworks like Framework1, Framework2, Framework3 and Framework4. I define a class file whose targets are these 4 frameworks. Now, i do some processing and archive this class instance in Framework1 and I want to access them in the other frameworks. But I am not able to do this. The cast fails and I get ‘Couldn’t cast Framework1.className’ to Framework2.className. Any help??
Hello. The tutorial is really nice.
I have tried this in XCode 8.3.3. and everything works “internally”.
My problem is: I can’t import the framework into my existing projects with this solution.
I have tried:
– Drag & drop (with and without copy if needed).
– Add files via Xcode
– Add from build phases
– Added public to all publicly available classes.
Import myFramework doesn’t also work in any case. If possible please add this final step in the tutorial =).
Hi, how are you importing this into your existing project? You don’t want to manually add your framework to your existing project, you’d want to pull it in via Carthage for example, such as your `Cartfile` having the git entry to your framework and add the built framework in the project’s “Linked Frameworks and Libraries” section. Let me know if you need help from here.
This is awesome! I’m having an issue getting live issues / errors to show while developing though.
Oh turns out I just had to select the scheme. D’oh! Thanks!
In Packaging you can use $(PROJECT_NAME) for all platforms. In this way you can use the project as a template.
Thanks for creating this tutorial, everything was super clear, the pacing was perfect, and at the end there were no errors to debug. Great work!
Thanks for your efforts Basem Emara, really a very skillful video and worth watching.
Please share the more links to such videos.