Libraries are the future for sharing your code, but they’re also fraught with peril. Platforms, package managers and tests are all crucial for your successful library! In this talk from try! Swift, learn about all the tools and processes around shipping and maintaining a library in Swift.
A Library for Your Awesome Code (0:00)
Imagine you have some great piece of code – maybe a new style of programming like reactive programming or Promises or Results or maybe it’s just a wrapper around exiting API. It could be something like Address Book or Keychain, which just has not-so-great C API that you just want to make more Swifty.
You have this great code and you want to share it to the world. After all, it could benefit others, your fellow engineers. It could save them hours or even days of effort. Where do you start? What’s the best way to do this? Well, pretty much, there’s only one answer and that’s to create a library.
You create a new project and you put your code in it but that’s just the start because you’ll want this code to be widely accessible. You’ll want to run it on multiple platforms: tvOS, watchOS, iOS, OS X and with Swift being open source you want it to also run on Linux.
You might want to make it easy for developers to integrate your library. You have to make it easy for developers to install the library the way they like to. That means they’ll have to use CocoaPods, Carthage or the Swift Package Manager or sometimes even manually via Xcode sub-projects. There are still quite a few developers that like to do that for some reason.
Next, you’ll probably receive contributions from others so you’ll want to ensure some level of quality for your library. Usually this is through tests and you want to run these tests frequently and very often to reduce the chances of introducing regressions or breaking things in your library.
Finally, you need to manage releases. You need to publish to all these package managers that you support, across all these platforms you support in a consistent manner all while describing to developers that are using your library when is it appropriate for them to upgrade.
That’s all actually a lot of work. That’s a lot of work around the core which is your code. A lot of work that is not actually what you’re trying to distribute as part of your library. And it’s really just all the infrastructure around having a library started to get your library to get as many developers as possible, to make it a great user experience. That’s what this talk is really about. This isn’t about the awesome piece of code you have that you’re ready to share because it’s already awesome. I don’t need to do anything for that. It’s really about having the infrastructure around your code to make it a painless library to use and be a joy for other developers to integrate with.
Swift is a great programming language but having more third-party libraries available in the ecosystem is great for everyone. A developer today could accomplish way more than a developer a few decades ago. That’s all because we can build upon the work that’s been done in the past.
Library by Example (2:32)
Let’s go build the Swift library.
It’s easier to follow during an example because then all the concrete demonstrations and snippets that I’ll use will make a lot more sense. As with all good libraries, you need a really good name. So, I’m proud to announce Snorlax!
Get more development news like this
This library has one feature and only one feature. It is to sleep for a random amount of time. This library therefore only has one function: rest(). It’ll just sleep for a random amount of time. And it even has documentation so this is ready to be used and distributed for everyone. I think it’s production ready.
Like all good libraries, we have a test. In this case, I have just an asynchronous test. There’re many different ways and strategies you could write test for this. But, for the sake of this library, we’re just going to use an asynchronous test. It doesn’t really matter about the test itself because what’s more important is that this test will change over time as we support more platforms.
That’s Snorlax, that’s all the code that this library provides. Obviously your code will be more awesome than this.
Build Settings (4:29)
Let’s get started turning it into a library. We have this code and we’re going to put in new Xcode iOS framework. From the bare minimum that Xcode provides, there’s several built-in configuration settings we need to change so that it will be smooth when developers that use your library will integrate with and so the series of steps I’m going to show will apply to every framework you make in Xcode even though, for this case, I’m just doing it for iOS.
First, we’re going to make the scheme shared. This will allow us to run tests for the continuous integration in the command line and will ensure that those build configurations on how to run those tests is synchronized across anyone that’s working on this library.
Next, Snorlax is actually great for extensions. So there’s a little check box in the bottom over here that allows API extensions only that we’re going to check. This will allow it so that our library can be integrated with extensions such as widgets or watch apps and so on. This is just a checkbox in the General tab of the build setting. You just check that and you’re all set.
Finally, we’ll have to rename this one build setting which is Product Module Name or you might just see it as Module Name. We want it for all the frameworks to be unified under one name so that when a developer uses our library and they call Import; it’s all uniform. By default, it will give it whatever name it has and if you have other platforms, it might postfix some operating system name. You just want to change it to be uniform so that all your developers can say Import.
That was it! We got our iOS framework, woohoo! We could just expand it to all the other platforms but in effort of saving time and not repeating myself, you just do the same thing for all the other platforms.
It’s worth noting that watchOS does not have test bundles so you have to be a little bit more careful to make sure that you integrate it properly, such that you didn’t forget a source file.
Testing: Continuous Integration (6:43)
Next, testing, or more specifically, continuous integration. We want to set up our continuous integration service with our library so that we get great feedback of when something breaks when we’re making changes to our library. For open source, since this is primarily a library that you want to share and have contributions.
There’s really only one choice: Travis. It’s useful because it supports a bunch of different features that a lot of other CI systems don’t have. You could try using Jenkins but it’s a lot more work. It has a whole bunch of extra features specifically like if you want to target Xcode betas, you can do that or if you want to have multi-OS builds or different versions of SDKs to run on with matrix builds, you could do that. It’s also managed so I don’t have to run the server which is great.
For Travis, you just need one file which is a travis.yml file. For just Apple platforms, this is really all you need. You just need three lines that describe exactly how to run. So I’m running on Xcode 7.2. I’m using Objective-C, lie, it’s a Swift but it configures, Travis will configure everything correctly. Then I call out to scripts to run the tests. This, I prefer to run a script because then, if I need to emulate it locally, I can just easily run just the test script instead of having to copy, paste the commands that I’m telling Travis to run.
The test script isn’t that difficult at all. It looks like a lot but it’s mostly just running Xcode build test for each of the supported platforms. watchOS doesn’t have any tests, so it’s really just OS X, iOS and tvOS. And then there’s a shell script command to make it fail early. Not that complicated. And with that, we have Travis integration. So when someone sends a pull request to any project that has this, Travis will automatically build and run the tests on the platform. So that’s super convenient for when you’re just doing development and I like to do merge pull request via my phone so having another machine run tests is super nice.
Package Managers (9:11)
Next, okay so at this point, Snorlax can be used only if the developer integrates it manually. If they know how to integrate Snorlax as a sub project in their app. We should expand it to more package managers and there’s currently three popular package managers: CocoaPods, Carthage and the Swift Package Manager.
Let’s look at the most popular one first: CocoaPods. It’s the easiest to use because if you’re not very familiar with how Xcode builds projects, CocoaPods provides a hassle free way of installing third party dependencies. The way it does it is that in CocoaPods you define a pod spec file which is work that we have to do as library maintainers to specify what’s the proper way to set up and configure the library.
To get started, we just run one command and CocoaPods actually does reasonable defaults so this is pretty much all you need to do to create the file and then you just need to tweak some of the file settings and some of the defaults like what does this library do? And what’s the version of this library? What’s the name of this library? But more importantly, it also lists dependencies or any kind of build configuration that you need.
In this case, we need to list dependencies on Foundation. So in the pod spec, the only change really to add is add foundation. If you had other third-party dependencies, you would have to add it in the pod file but Snorlax doesn’t require any of that so we can move along.
Carthage requires more familiarity with how Xcode builds projects. So on the end users perspective, they have to do the actual file integration into their project. It will handle building dependencies and figure out which versions it needs for their project but in the end it is the developers job to integrate it into their application. It’s actually a really good alternative if you’re used to just manually adding Xcode projects because it kind of automates picking the versions for you. And so really, there’s not much to do with Carthage, you just need to run one command which will verify if Carthage can actually build your project and so you can just run this.
carthage build --no-skip-current
The no-skip-current flag indicates that it should just always build it instead of trying to be smart and saying, “Hey, I’m done. Don’t need to build it, I already built it before.” If we have dependencies though, we would actually need to specify the Carthage cart file in the repository. But since Snorlax doesn’t have any of that, we don’t need to do anything.
Swift Package Manager (11:37)
The Swift Package Manager is actually a lot more work than the other two package managers and it’s still relatively new and ever evolving. In fact I updated it yesterday. It’s always changing. What’s interesting though is that Swift Package Manager is the only one that supports Linux. We’ll have to also support Linux in conjunction of supporting Swift Package Manager.
The way Swift Package Manager works is that it has a Package Swift file, kind of like CocoaPods and you can define just what targets you have. By default, it will imply one target which is Snorlax and that’s really all you need. Because in the new Swift snapshots, it can infer the primary target you want to build and your tests and we’ll see how it works with the project structure. It’s also worth noting, if we have any dependencies, we also, like CocoaPods, have to list it in here. But we don’t.
Snorlax is a very basic project, it has two files and the Swift Package Manager requires a specific layout in order to infer how to build your project. So we have our Package Swift file under our Snorlax directory and we’re going to move our source code into the sources folder underneath Snorlax which is the target name that it defined in the Project Swift file. That’s where I’m just going to put all my source code.
The same thing applies to tests. I have one test file and I’m going to put it under an equivalent one under the Test folder. In Snorlax, the name comes exactly from the Package Swift again. Finally, for Linux, we need this extra Linux main file which tells the Swift Package Manager how to run tests in Linux because it doesn’t have the Objective-C runtime to do the introspection to figure out where are the tests.
Let’s look at this main file.
// LinuxMain.swift import XCTest @testable import SnorlaxTest // This is the entry point for tests on Linux XCTMain([ SnorlaxTests(), ])
It’s actually pretty simple. We import XCTest and we just call it XCTest main. And we just list all the XCTest case classes we have in the test. I only have one so we’re only going to run just one. Pretty straight forward. It’s also worth noting that LinuxMain should only be in your project and not be part of your Xcode project. Xcode does not want to know about this file at all. So, just make sure you don’t have it.
Next, we have to update our tests. First XCTest doesn’t support async for Linux, so we got to remove that and so I convert it to not be async. And the second part is that there’s extra code that we need to add to support the fact that Linux XCTest does not support Reflection because you can’t use the Objective-C runtime. So we have to manually list out all our tests by inheriting the XCTest Case Provider and specifying it there. But there’s a problem because XCTest Case Provider actually is only available on Linux Swift. So you actually have to do some poly fill which is what this example’s doing to define XCTest Case Provider for Apple’s platforms so that you don’t get a compiler error or have to, if/else your entire XCTest Case. Usually you just put this XCTest Case Provider definition somewhere and all your other tests can just work their merry ways.
With all that, we can actually build our code. We can run Swift Build which will build our library and our test bundle and then we could run Swift Test to actually run the tests.
But, wait, there are still some more problems. Swift Linux isn’t stable so the versions always change and there’s been a whole bunch of new versions released every week. Keeping up-to-date is extra work and it’d be nice if we optimized this workflow. This means that we have to synchronize the version between developers that are working on the library as well as our continuous integration library. It’s a lot of hassle just to constantly be updating it and coordinating with other contributors saying this is the version that we’re using.
Instead, I would recommend using a library from Kyle Fuller called swiftenv. If you’re familiar with Ruby or Python’s RVM or pyenv, this is the equivalent one, just for Swift. It basically allows you to use a specific version of Swift for a project and so with swiftenv we’ll install the latest snapshot of Swift and we’ll say local, use that version in the current working directory that we’re using for this version of Swift.
swiftenv install DEVELOPMENT-SNAPSHOT-2016-03-01-a swiftenv local DEVELOPMENT-SNAPSHOT-2016-03-01-a
And with that we can just run the regular Swift commands and it will automatically use the right Swift executable.
swift build swift test
Our CI’s going to change slightly. We now have to use the build matrix that I was talking about before. And so we’re going to target OS X and Linux and either you’re using Xcode to build the test suite or the Swift Package Manager. There’s this one line of install at the very bottom which is abbreviated. There’s more references later which installs swiftenv in a one liner.
Then I just run tests with the environment variable type which the test script is smart enough to know that if it sees SPM the string, it just runs the Swift Build Test versus all the XCTest ones that I listed before. And so in Travis, now we have three builds for every build or every commit and so that’s super nice to see which platform is actually failing. If someone makes a change, they’ll see if Linux will fail or OS X will fail or OS X Swift Package Manager will fail. So that is super nice.
I think we’re ready, almost, to create our release. We’re about to release this great, new library. And so we have everything configured and we just need to release it with a version. But what do we need to use? How do we set version numbers? You see all these version numbers, what do they mean?
Turns out, it’s very common for many libraries and in fact, it’s encoded in all the packet managers to use semantic versioning. So the first least most significant number is the patch version which is used to indicate if there’s only bug fixes. So if the patch version increments, only bug fixes have been applied to the library and no break in changes or new features have been added.
If you update the minor version, you’re indicating to developers that new features have been added and bug fixes may have occurred but no breaking changes have been made to the library.
Whereas if you increment the major version all bets are off. Everything could have changed. Everything could have broke. All new features could have been there and all the bugs were fixed, hopefully.
You have to choose specifically which version number that corresponds with what changes that we’re making. In this case we’re using, this is the first version so you can just set whatever version number you want. That’s fine by me and with that, we can start actually going through the process of releasing each version. If you want more information about semantic version, check out this page.
Creating a Release (19:21)
Now we can actually go through kind of a routine of creating our actual release for our library. Most of it is pretty straight forward. There is some explanation of why you need to do this.
Most of it is to support CocoaPods and so in the beginning, we’re going to find all the version numbers that we specify documentation or the pod spec because the pod spec will contain the version number and will just set it to this new version that we want.
We’ll commit it and then we’ll push it to GitHub or whatever source control you use. We’ll then tag the new version with the particular version number that we have and this is useful for Carthage and the Swift Package Manager because they look at tags to determine what version to pull in as a library and they’re following semantic versioning and so keep that in mind.
Finally, make sure you push it so that those package managers can see it when they look at the repository and then, after that, you need to push the pod spec to CocoaPod’s trunk and this will tell CocoaPods that a new version of your library’s been released. And so CocoaPod users can then use the new version that you’ve released.
Furthermore, if you want to add some extra sparkle, you can create a release on GitHub, just makes it look bigger and better in GitHub. And then just announce it to the world. “Hey, I got this new library. It allows me to sleep for a random amount of time!”
I actually have the whole library in all it’s glory on GitHub at Snorlax. It’s the world’s boring code but it actually has all of the infrastructure around setting up the library, the CocoaPods, updating for Swift Package Manager. I advise you to look at the commit history. The commit history actually is broken down by each of the steps that I’ve done to support all the different platforms.
Also, there’s supplementary to the supplementary material. There is SnorlaxSamples which is examples that integrate Snorlax as a dependency. This way you can see how it works under Carthage, CocoaPods and Swift Package Manager. With that, thanks!
Q: When versioning, should I add the V to the front of the version tag?
Jeff: The ever questions of life like tabs and spaces. I always put the V because it’s easier to then filter by the tag versions. So if you have other tags say you’re targeting a different branch for Swift, like an older version of Swift or older releases or having any other kind of uses for tags like for deployments, it’s better to have some way to categorize your tags. That’s why I prefer the V. If you feel like your library won’t ever need that, then that’s fine.
Q: Is there anything in particular you would recommend if my library depends on another library that’s say not Foundation?
Jeff: In the Snorlax repository, it actually includes examples of pulling in other dependencies as part of the library. There is extra work you need to do but it’s a little bit more difficult with the Swift Package Manager because just a lot of libraries just don’t support it yet. But for Carthage and CocoaPods, you could perfectly do that.
I will mention though that it’s not recommended to use CocoaPods to integrate your library because then you make a dependency on CocoaPods as a requirement for anyone using your library. So you should make sure that, if you’re integrating third party libraries you should use either Carthage or git sub modules and doing it manually instead of doing it through CocoaPods because CocoaPods changes your configuration so that it basically depends on it.
Q: How would you recommend creating an internal library within a company not to be completely open sourced?
Jeff: It depends entirely on what’s the current ecosystem that your company’s using. So if you’re company’s using exclusively CocoaPods then yes, make a private specs repo and store your dependencies there. But if you’re all just having some internal GitHub enterprise, you just use Carthage or git submodules instead.
You also don’t have to use all the examples. This is like a complete coverage of all possibilities of all platforms. Not every library’s going to have to support every platform that Swift is on. Because it might be a wrap around Objective-C API. In that case then maybe you don’t need support like Linux. So it’s perfectly okay to take bits and pieces of what I said in this talk, to cater to what the circumstances your library’s in.
About the content
This talk was delivered live in March 2017 at try! Swift Tokyo. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.