Stop Being So Dependent
Get rid of unnecessary Swift dependencies
--
When starting a new Swift project, developers tend to plan ahead: “Oh, I need networking, I’ll make sure to add Alamofire.”, or: “We want to use Functional Reactive Programming, let’s add RxSwift/ReactiveSwift.”. Maybe you need to parse some JSON and have always used SwiftyJSON, so why not add that as well? However, starting a new project (or performing an update) also gives you an opportunity to evaluate your options again: does my project actually need all these third party dependencies? Let’s find out.
The Use of External Frameworks
There’s a myriad of reasons why you would want to use external frameworks in your apps and package managers such as Cocoapods, Carthage and more recently Swift Package Manager have made it very easy to add dependencies with just a few lines of code or clicks.
A lot of external frameworks are born out of necessity: either because functionality isn’t available in Foundation or any of the other system frameworks (e.g. RxSwift), or because there is no “nice” Swift-y overlay available (looking at you AVFoundation!). However, we have to remember that Swift has not been around for that long, most system frameworks in the early days were leaning a lot on old Objective-C style interfaces and that programming paradigms have shifted significantly over the last couple of years, preferring aspects of reactive and functional programming over the older imperative way of programming.
Apple has been steadily building on top of the Swift foundation with new and improved frameworks making their debut primarily at WWDC, but also updating existing frameworks and adding new overlays, such as the nicer vDSP overlay introduced in iOS 13, or the addition of Diffable Data Sources for tables and collections, essentially breathing new life into UIKit components that have existed for many years.
Every year, Apple cleans up more legacy and introduces big new features along the way (e.g. SwiftUI, Combine, etc.). You, as a developer, should stay on top of this as well so you can make informed decisions regarding your application’s architecture and its dependency on external frameworks.
Handling dependencies
As a rule of thumb, I suggest you only start adding dependencies when you actually need them. This may seem trivial, but it’s very tempting to set up a new project, and while you’re at it, just start adding frameworks just because you already “know” you are going to need them.
Whenever I think I might need an external framework for a certain task I tend to ask myself a couple of questions:
- What problem does this external framework solve for me?
- What parts of the external framework will I be using?
- What kind of risks are associated with using this external framework?
- Is there a system framework I could use instead?
- If so, does that system framework provide a satisfactory API?
Answering these questions helps me make an informed judgment on whether this framework is actually worth adding to the project. We’ll look at this using some real-life examples.
Example 1: Firebase
In some cases answers to the above-mentioned questions are clear: if I’m integrating with a service like Firebase I would not hesitate to add an (official) external framework to provide the integration. No way am I going to write my own integrations with third party service providers if an officially supported SDK exists. There are no system frameworks that handle this, you would basically have to construct and send HTTPS requests to the third party service and implement their API yourself. This is not something you would normally consider, and sometimes third parties even require you to use their SDK instead of rolling your own.
The same applies to frameworks for other third party services, such as Facebook or Amazon Web Services. The official framework probably is the best bet and (relatively) low risk. Just make sure to keep them up-to-date so they do not introduce unwanted legacy.
Example 2: SwiftyJSON
Apart from “official” SDKs, there are a couple of frameworks out there that historically have provided functionality that Swift was lacking, but has been added to the language since then. Take a look at SwiftyJSON for example: this is a framework that made dealing with JSON data (usually in the form of raw Data structures) easy and accessible back when there were no real substitutes in the Foundation framework itself.
This changed when Swift 4 introduced the Codable protocols (Encodable & Decodable), essentially rendering SwiftyJSON obsolete for most practical purposes. By making your data objects conform to these protocols you could get JSON (de)serialization essentially for free (as well as other data formats). Sure, the API is entirely different, and it’s conceptually a different approach to solving this problem (with SwiftyJSON essentially letting you manually drill down through raw JSON objects, whereas Codable prefers a more declarative way), but it is very clear which approach Apple prefers and how they think developers should look at deserializing their data objects moving forward.
For me, this would be a major factor in deciding: Apple has added its own interpretation of how (de)serialization should work, it integrates nicely with other Swift idioms, and the functionality I would lose (manually drilling down into JSON structures) is not really something I would need anyway. Furthermore, Codable is available in Swift 4 and up, making it universally available.
Adding this all up: I would no longer add SwiftyJSON as an external dependency for new applications and always prefer the Codable protocols. Furthermore, I would also take active steps in any maintained application to migrate towards the latter and reduce the amount of legacy.
Example 3: Alamofire
Alamofire grew out of the very popular AFNetworking library. Alamofire essentially started as the Swift counterpart of the Objective-C oriented AFNetworking, but has seen a lot of development that moved it into its own direction.
The obvious benefit from Alamofire is the easy access to HTTP requests without having to worry about managing sessions and requests yourself. However, if that is all that your app is doing, it might still be worthwhile to ditch it and switch to plain old URLSession and URLSessionTask. Simply including a (big!) external dependency just to do basic HTTP requesting is a bit like using an elephant gun on a mosquito. Fetching data from remote services is no longer the chore that it used to be and Apple’s developer documentation has improved considerably over the years. Apart from that, Alamofire requires you dip into URLSession and siblings anyway if you need to do advanced stuff with configuring sessions, headers, caches or requests, so better make sure you’re up on your reading!
The main advantage of staying with Alamofire is if you’re using a lot of the convenience methods and features they include, e.g. certificate or public key pinning, or extensions of UIImageView. In any case, it mostly depends on how much of the framework you actually plan on using. If you’re just doing basic HTTP requesting I would recommend against it. However, if you’re entire project is currently built on top of Alamofire, it does not make sense to immediately spend a lot of time and effort to remove it: it is a well-maintained open source project with plenty of contributions and has been around for several years, making it a low-risk option.
Example 4: RxSwift
RxSwift provides a reactive programming style for Swift that is currently very popular. In fact, it is so popular that Apple has decided to adopt certain principles of it and implement them in their own system framework: Combine.
The main benefit of RxSwift is that it provides a very generic and re-usable way to reason over the flow of data in your app and how to handle changes in that data over time. Apart from that, there is a shared idiom between different Rx implementations, that provides developers a cross-platform shared vocabulary regarding the implementation, which can be really benificial if your app is available on multiple platforms.
RxSwift has been around for many years, is well-maintained, provides backwards compatibility all the way back to iOS 8 and has bindings to a lot of UI controls (via RxCocoa) making it low risk.
So, what then might be a reason to not add this dependency? As said, Apple introduced it’s own version called Combine. While not a “true” implementation of Rx, Apple has taken a lot of inspiration from these ideas, and provided its own elegant API. It has included bindings to a lot of Foundation classes (e.g. Timer, NotificationCenter, URLSession) to make it easy to adopt the new framework without having to write a lot of wrapper code and it integrates nicely with SwiftUI. This is the way that Apple envisions going forward and that should account for quite a bit.
The downside? It is only available on iOS 13 and up, making this only a candidate if the following conditions hold:
- Your app is only available on those platforms that support it (iOS 13 and up)
- You don’t need to worry about cross-platform development
- You have no pre-existing dependency on RxSwift (or any other similar framework)
Does it make sense to remove this dependency from existing apps? If you’ve invested significantly in RxSwift I would say not, also because of the platform requirements. However, as soon as you move your target platform up to iOS 13 and above, you might want to reconsider, given that Apple has chosen its own path and historically it makes more sense to following the platform than stick to third party frameworks.
Example 5: “risky” frameworks
There is one final category I would like to mention here, which is that of “risky” frameworks. This is a factor I always take into account when introducing new dependencies and it’s a bit of a soft metric. I’ve mentioned it a couple of times now, but for me risk boils down to a couple of things:
- Is this framework well-maintained, when was the last code committed?
- Is this a one person job, or are there multiple people contributing to the project?
- Are people writing about this framework, is there some sort of community you can turn to for help?
- Does the project have a proper versioning system in place?
This list is by no means exhaustive, for one, it doesn’t go into code quality or testing of code. I don’t pretend to read every line of code of every framework in much detail, but a lot of potential “risk” can already be inferred from the aforementioned questions. You yourself can define your own notion of risk, adding perhaps other questions, such as: what kind of impact would it have if there was a bug in the framework? Might be more risky if it’s a library for handling in-app purchases than some string-formatting library.
That said, weighing the risk is not something that gives you an absolute number, but it is an important part of signalling potential issues in the use of the framework. If at any point you find yourself spotting red flags it might be a good idea to reconsider this framework. This also applies to frameworks that are already in your code base! If the framework you’re using has not seen relevant updates, such as updating to latest Swift version, or has a history of not fixing bugs, you might want to set aside some time to research alternatives. After all, the world keeps moving forward, and you should decide whether to move along with it!
The Opportune Moment
So, what is the right moment to think about your dependencies? There’s no point in dropping all your work at hand and start removing dependencies. Also, it doesn’t make sense to think about this every time you make a minor update to your app. In general I like to have a distinction between feature releases and maintenance releases (insert obligitary “bug fixes and performance enhancements” release notes here). In my opinion moments such as updating the iOS SDK, changing your platform requirements, or updating to a newer Swift version are good candidates, because they will almost certainly impact your dependencies anyway.
Updating to a newer version of the iOS SDK or Swift will most likely trigger code changes. This is also a good moment to see if the developers of the third party frameworks are on top of it and have updated their code accordingly (see the “risk” section). Sometimes this will trigger incompatibilities, e.g. you’re simply updating to a newer Swift version, while the framework author decides to simultaneously update to only support a newer iOS version, or decides to release a new major version with breaking API changes. Chances are, you are going to have to deal with external dependencies anyway, so why not make the best out of it and give your application a bit of a clean-up? While this may not seem as the most interesting or rewarding part of your app to work on, updating and/or removing your dependencies at this point will make your life easier in the future and help prevent legacy issues.
In conclusion
External frameworks can make your life as a developer much easier, simply by providing functionality that would otherwise not be available, or by making existing functionality easier to use.
External frameworks can also make your life as a developer much harder, by incurring coding overhead, legacy issues, incompatibility issues or simply by introducing bugs. And what once worked perfectly yesterday, or made your work much easier, can turn into a source of pain or frustration today or tomorrow.
Dealing with this trade-off is our responsibility and one that should receive due attention when appropriate. Don’t just include frameworks “because we always use this”, or “because this makes this a one-liner”, or “some people on Reddit are enthousiastic”. There are no shortcuts to a well-architected app and no dependencies should be set in stone.
Being critical of the external code in your own applications is a Good Thing™️. That does not mean that every piece of external code should be scrutinized into oblivion, but you can also not assume that including a dependency once will give you a smooth ride for the future.
TL;DR: Don’t be afraid to depend on the work of others to improve your app, but be vigilant, critical and decisive.