A Great App Is the Sum of Its Frameworks
Over the years, all iOS apps I’ve been working on ended up – in one way or another – as monoliths. Until iOS 8 it was also impossible to have dynamicly-linked frameworks in your app. But the winds have changed, and Apple’s extension architecture embraces dynamic frameworks, even relies on them to share logic between an app and its extension(s).
Even then, you might just end up with an app target and a framework target, which contains most of your business logic (maybe it’s also called „core“, „foundation“, or „base“). In the end, this is also just another monolith.
This is bad on so many levels: As your app grows, your team might grow too and these monoliths will cause team-specific problems like merge conflicts from hell (Xcode project files and interface builder documents are not known for their easy mergability) and a search-based project structure, where files are nested so deep inside a group/folder structure, that you can only really work with them if you know them by name and also the keyboard shortcut for quick open (cmd+shift+o).
So, now you ended up with one of these monoliths, your team has grown, and you are a living direction sign for them, always pointing to existing classes or features that are barely known and buried deep inside the codebase. Is there a way out of this?
The Framework Approach
The way out of this is seldomly a complete rewrite, as the rewrite may as well end up in the same situation as the original app, just a year later. I made the experience that refactoring and a change of the development mindset is oftentimes the right solution for these cases:
Analyzing and refactoring your codebase can help you find code pieces that can be clustered. These feature clusters are perfect candidates for custom frameworks.
Apple probably does the same with its apps. If they need a new feature in an app, they sometimes build it in a way which makes it reusable in other apps as well: as part of a framework. Great examples are the Contacts and ContactsUI frameworks. They encapsulate everything you need to work with contacts and address book data, even view controllers. So Mail can create and pick contacts using the same view controllers as the Contacts app itself.
The separation of UI and non-UI components helps with testability because you can cover your core business logic excessivly with unit tests and use UI tests for view controllers that present these core features to the user.
You don’t even have to re-use code in other apps or extensions to benefit from having frameworks.
When you isolate functionality that fits into a framework, you will automatically find opportunities for improvements: e.g. extracting the code out of a massive view controller, or replacing unexpected dependencies to other parts of your application with dependency injection. Overall you will likely find more ways of improving the code because of the focus on the feature itself in the isolated environment of the framework. Improved testability – for example – is always on top of my list after creating a feature framework from existing code.
A framework also represents a good opportunity for a team member to step up and take the responsibility maintaining it. This is not only great for the framework quality but for your team as well.
While this approach is great for the maintainability of your code and probably the productivity of your team, this also has some downsides.
One of these is Xcode itself. If you choose to develop your framework in a separate project file, you may run into strange linker problems when you have many intertwined dependencies between frameworks.
If you use Swift, you also have to check that your app doesn’t ship duplicated Swift Core Libraries, which would increase the app bundle size dramatically.
Another problem is the increased app launch time. This issue was pretty bad before iOS 9.3 and caused massive slowdowns dependening on the number of frameworks you embedded. Since then I actually do not worry much about app launch time increases anymore.
And finally there is the problem of increased compile times depending on your framework’s dependency graph. Xcode might not be able to parallelize your app builds anymore, so non-incremental build times can increase very much.
There are additional tools that you can consider to manage your frameworks. Carthage and CocoaPods are popular package managers that allow you to decouple your framework sources from your app project. E.g. Carthage can fetch your (internal or external) framework dependencies from GitHub and build them in a separate folder before you even open your project file. These pre-built frameworks are then used just like any other system framework.
But beware, this would mean that changes in the feature framework require changes to multiple repositories which takes a lot more time than updating a framework that lives in the same project tree as your app.
Even Apple sees the benefit in package managers and built the Swift Package Manager to distribute libraries with little project setup. This tool is still new and limited to macOS libraries and doesn’t offer the benefits of full-fledged frameworks.
Despite some downsides, I can really recommend breaking up apps into frameworks or libraries. If your app pulls in frameworks and is only a small shell around these, it enables you to work with your code in interesting new ways:
- building an example app for your framework to run UI tests (with static app data)
- recombining your app’s features into a new app for a different user focus
- a database viewer app vs. a database editor app
- building the next generation of your app based on your core frameworks in parallel to your existing app
It’s worth a try!