As part of the Workflow team, Conrad Kramer maintains dozens of dependencies and SDKs in his projects, as well as contributing to and open-sourcing his own. In this AltConf 2016 talk, Conrad shares the critical lessons he learned in the process, picking apart what makes iOS (and other) SDKs great, and how to avoid common pitfalls when creating one. Covering the language, build system, environment, platform, and many more you likely didn’t know about, Conrad provides an invaluable resource to teach you how to share your code with others in the most useful and friendly way possible.
Sharing Your Code (0:00)
Hi! I’m Conrad. Many of you might be thinking that writing an iOS SDK is pretty simple: you just put some code in a project, then compile it. However, there’re a lot of small nuances that many end up getting wrong, so that’s why I have “not” in the title; I’m going to go through a long list of things you should do and should not do when building an iOS SDK.
I work on an app called Workflow, and we use a lot of third-party apps and services. As a result, we integrate a lot of third-party SDKs. We have something like 30 sub-modules. A lot of those we have to end up forking or even writing our own versions, simply because they don’t meet our needs.
But we’re not like most developers because we actually will put in the effort to do that. In contrast, most developers will just jump ship if your code doesn’t suit their needs, so you really need to support a wide number of configurations.
Furthermore, some SDKs aren’t open-sourced, so if it doesn’t work for them, they just can’t modify it; they can’t even use it if they wanted to.
(Not) Making Assumptions (1:05)
A primary take-away from this talk is to not make assumptions. Developers use a large number of configurations, and your SDK needs to be flexible to handle all of them.
To help you with this, I will go through some common ways to make your SDK as flexible as possible.
Using a Modern iOS SDK Language (1:22)
The most important thing is the language you use. There are two popular languages right now for writing iOS apps, as I’m sure you know (Swift and Objective-C). Some developers use only Objective-C, and some use only Swift, and a lot use something in between. As such, it’s critical that you write your SDK in Objective-C so that the people that don’t ship Swift can actually use your SDK.
It’s also important that you use nullability and generics within Objective-C such that you can use your SDK from Swift. For example, the Dropbox SDK that came out recently was only written in Swift, and we couldn’t actually integrate it into Workflow. We haven’t spent the time yet to rewrite it into Objective-C.
It’s also important that you support your Swift developers with a first-class SDK, so you should write a wrapper around your Objective-C SDK that makes your APIs really Swifty. You should also support all active versions of Swift, which are currently versions 2 and 3. A great example of this is the Realm SDK, they have a Realm wrapper around their Objective-C core in Swift, that makes their API really Swifty.
Supporting Many Build Systems (2:26)
One more helpful (yet opinionated) aspect of SDK development is the build system. It’s really important that you support every build system available, such that anyone can integrate your SDK. For example, the most important thing is to have an Xcode project.
You should have an Xcode project that compiles your app, both dynamic and static libraries, such that people can just drag your Xcode project into theirs and compile it into their app.
You should also make your dynamic framework scheme shared, and tag your release with semantic versioning on GitHub, so that you support Carthage. For those of you unfamiliar with Carthage, it basically uses
xcodebuild to build your framework.
Lastly we have the Swift Package Manager. That’s still very new, but actually, they recently added support for C, Objective-C, and C++, so you can, today, be using Swift 3, make a Package.swift file, that specifies how to build your SDK, and you can add that support today.
Supporting Multiple Environments (3:34)
The the environment in which your SDK runs is also important. This is different from the platform it runs; it’s the context in which it is used. This is a little bit more nuanced, but as extensions are increasingly important to the core experience of iOS, you need to make sure your SDK works great in extensions as well as the main app.
This means things like making your file paths configurable. You can’t hardcode
Documents if you want to work in an extension if you share data between the two. You want to make sure developers can put their files in app groups if they want to.
Also, have reasonable defaults for people that don’t care what an app group is.
Furthermore, avoid reliance on
UIApplication. This is really important because
UIApplication does not exist in extensions. If you must, you can actually mark APIs that use
UIApplication inside as
NS_EXTENSION_UNAVAILABLE and this means that it will still compile for extensions, you just can’t use those APIs inside the extension.
Lastly, if you’re using
UIApplication for background tasks in your SDK, you can actually use a newer API called
NSProcessInfo. It has an expiring task API that allows you roughly around three minutes of background time, for both extensions and the main app.
Handling Inter-Process Communication (4:48)
Next I’ll cover inter-process communication and coordination., which is really hard. The idea is that if your SDK saves data in both the main app and the Today extension, you don’t want them to clobber each other. For example, if you have a logging SDK that just logs events to a file, and you want to share that file between your main app and your extension, the main app could overwrite the extensions logs and vice versa, over and over again.
Commonly both an extension and the app run at the same time. For instance, the Today widget often runs so when you swipe down from the notification center with your app open the two are running simultaneously. You can’t just use an
NSLock for example, because your application has its own
NSLock, and the widget has a different
NSLock which can’t communicate with the other. Thus, you could have the lock simultaneously and still be overwriting data.
Now, all of those are pretty low level and not very well documented, which is really unfortunate, but you can use some higher level abstractions around these to integrate them. At Workflow, we wrote this thing called
WFNotificationCenter, and it’s API-compatible with
NSNotificationCenter. It works with an app group, so you just have to initialize it with your app group and it will work across all of your extensions and your app.
We use this in our widget to notify when events happen between our widget and our main app.
Supporting All Apple Platforms (iOS, macOS, tvOS, watchOS, carOS) (6:23)
It’s also important that you support all platforms. There are a lot of platforms these days, watchOS, tvOS, iOS, macOS, probably carOS. If you use something like networking, there’s no reason you shouldn’t support tvOS. tvOS and iOS are very similar, so it’s likely that if your SDK works on the iOS, it does work on tvOS, so you should make sure you build and test on that platform as well.
This is often a problem for dependencies. If you depend on something and you have to get them to update to watchOS, or add tvOS support, it’s often very simple but frustrating. Also, if you have like iOS specific UI, but a networking core like many analytics libraries do, you can make the UI compile optional and still support Mac, for example.
Setting the Deployment Target (7:14)
Typically the rule of thumb is to support at least one iOS version back, because many apps have this constraint in which they support both. Right now, this is at least iOS 8 and iOS 9. Nevertheless, if it’s not too hard, you should try to go back as far as you can.
For example, if you use simply
NSURLSession, and nothing else, you should be able to support back to iOS 7, or macOS 10.9.
(Not) Pulling in Dependencies (7:45)
Up next, let’s talk dependencies. In most cases, you want to avoid them. There are many strong reasons behind this, but the primary argument is to keep your SDK lightweight and flexible.
If you want to change your SDK, or a new platform comes out, or any of these things happen, you don’t have to go in and make sure that all of your dependencies also update.
This is especially a problem with source-level compatibility in Swift. If you write a wrapper in Swift 3 and it depends on something that is still in Swift 2, you have to update the entire thing to Swift 3. If you only use a little small part of that SDK, that’s some wasted effort that you don’t want to have to go through.
Therefore, you could likely avoid many wrappers like keychain wrappers, Alamofire or AFNetworking and more. Now you may update your SDK even if the dependencies don’t. Moreover, a lot of the functionality that they provide isn’t necessary, you probably only use one small bit, so you can probably just take that and implement it yourself.
Documenting Your APIs (8:49)
Also, you want to document your API, but it’s important not just to document the methods using the Xcode method documentation, but you also want to do a high-level overview of your SDK and your README. You want to make sure that they understand the basic concepts that are involved in your SDK and how to set it up, but both are very important.
I often have to look at the source code for a lot of SDKs to figure out what this method is doing, and it would be really cool if I could just read the documentation without having to look at the source code. With things like nullability, it makes it really easy to gauge whether I can pass nil to a method now, for example.
Testing Your Code (9:26)
Although tests seem hard and annoying, but they seriously help when working on open-source projects. They allow you to merge contributions from the community with confidence that nothing will break. This means that you can merge a huge number of contributions and support a community of developers. For example, ComponentKit has a very huge test library. They test every single component with snapshot tests, so if anything changes or breaks, you know about it. This makes it simple for them to merge pull requests because they know that nothing is broken.
Also, hooking up to continuous integration makes testing seamless.
Optimizing & Communicating SDK Performance (10:07)
As part of helping users of your library understand the performance of their calls, you want to document expensive API calls. For example, if you’re writing out a file, uploading something, processing images, or loading web content behind a method, you definitely want to tell the developer that that’s what you’re doing. They might end up putting it in a tight loop, or putting it in some performance critical code and not realize what they’re doing.
Also, if you’re doing a lot of processing or intensive work, make sure you put it on a GCD queue, with a low background priority, unless it’s really important. For example, a logging library should just be on background queue, and it should only access the network or a file on this very low priority background queue since you don’t want to affect the main thread for your app.
A different key aspect you may not have considered is memory utilization. Now this is a little bit more subtle, because you might not be testing your SDK in memory-constrained environments, but there are a lot of them on iOS now. Widgets used to have 16 megabytes of memory, but now they have 26, but it’s still nothing like the 650 that your main app has.
As such doing anything that loads a lot of stuff in
objc_copyClassList, or using
UIWebView or a
CGBitmapContext, can just crash your process without realizing it. If you try to render a big image into a bitmap context in a widget, it will just crash instantly.
Putting It Together: WFOAuth2 (11:41)
Putting this all together, I’ve done a lot of talking, but I actually wanted to implement something like this, so I wrote a library called WFOAuth2. WFOAuth2 supports watchOS, macOS, iOS, tvOS. It supports CocoaPods, Carthage and Swift Package Manager, and it’s what we use inside Workflow to authenticate with everything.
We use WFOAuth2 to authenticate with Slack and Dropbox and Box, and many other different services. It’s also documented, lightweight, and does not have very many dependencies.
Q: Have you had any experiences with the automatically generated SDKs from AWS APIs, or other web service APIs that auto-generate SDKs?
Conrad: I actually haven’t played with those at all, but as long as the tool itself just doesn’t generate anything that breaks any of these we discussed, you should generally be fine.
Q: Would you recommend distributing a static library in a framework as well as support Carthage and CocoaPods?
Conrad: Definitely. If you have an open-source library, though, I would, you can provide binaries, but you can also just provide an Xcode project that allows them to build a static library or framework.
Q: Does Swift support distributing closed-source frameworks as binaries despite being open-source itself?
Conrad: Yes. If you don’t have an open-source library, then you do need to ship binaries. However, it’s a tad more of a hassle currently as you would need to ship different ones for Swift 2 and 3.
Q: Do you have a suggestion on distributing a private repo through CocoaPods or Carthage?
Conrad: You can include private repos in CocoaPods or Carthage fairly easily. I do not have experience maintaining the same public and private repos simultaneously, but it seems like you could just have two separate repos and use both.
Q: Why do you opt to use sub-modules over Carthage or CocoaPods in Workflow?
Conrad: The primary reasons we use sub-modules is for flexibility and because we can’t use anything else on occasions. We actually end up using a lot of libraries that don’t support CocoaPods or Carthage. We use Carthage to build some of the things that we don’t want to build with every run, but it’s really just down to flexibility, and sub-modules give us the flexibility we needed.
Some of the frameworks we use do not even have Xcode projects, so we have to just drag the source files into our project and build it as part of something else. It would be great if we could use a build system, but with the number of things we integrated, it’s not feasible right now.
Q: What are SDKs that you deem to set good examples, or follow good conventions and standards?
Conrad: There are a few I enjoy using consistently. Mantle handles many of these very well, but is specialized for Objective-C. Nevertheless, in terms of build system, it uses Xcode and a framework, and is lightweight. This allowed us to use it on watchOS since release without much effort.
Realm is also really good about always supporting all platforms and all versions of Swift.
It’s much easier to remember the bad ones rather than the good ones. We’ve had quite a few issues with dependencies and AFNetworking. Some ship static libraries that use AFNetworking and we want to use another thing that uses AFNetworking and they conflict.
In terms of other SDKs that are a pleasure to use, another that comes to mind is MailCore. Despite being big with a C++ core, it supports building on iOS, Mac, Android, and more, simplifying sending email.
Q: You recommended writing an Objective-C core with a Swift shell. What’s your opinion of using a Swift core and writing an Objective-C shell?
Conrad: You can definitely do that, write the framework in Swift and make it compatible with Objective-C. But, unfortunately that necessitates shipping the Swift runtime with your app. In Workflow, we currently don’t ship the Swift runtime, so we wouldn’t be able to use the Swift SDK. It just comes down to supporting all of those configurations. One day the Swift runtime will be included in the OS, and at that point, writing Swift stuff that works from Objective-C would be perfectly viable.