How to Organize Your Swift Packages
If you want proper code organization for your iOS or macOS app in 2023, consider Swift packages: reusable components of Swift, Objective-C, Objective-C++, C, or C++ code. Using Swift Package Manager, you can modularize your code very easily, and everything is also integrated into Xcode.
I won't be explaining Swift packages, how to create a package, or how to link it because you can find this information pretty much everywhere, including in the official documentation.
Instead, let's talk about how to actually separate your code into packages, what packages you'll have to create, and how to organize them. In this post, I will present the package architecture we have at Dashlane and explain why it completely fits our needs.
It’s important to remember that the architecture I’m illustrating is just a suggestion. Everyone is free to adapt and improve it per their project’s requirements and needs. Every team and project is different, and what works for one may not work for others, even though it's a pretty generic solution. It’s always a good idea to evaluate your options and make the decision that works best for your specific use case.
Package architecture at Dashlane
This is a chart of the current package architecture we use at Dashlane.
Let's dive into the different layers of that architecture and the relationships between them.
"I see Foundation packages on the first layer."
In this layer, you’ll be able to define packages containing code and types that are at the very base level of your application. Here, you’ll likely put things that are used everywhere in the application, like some business-related types, common extensions, and database-related tools as models.
Foundation packages can be imported from any other package that is not at the Foundation level.
For example, we have our DashlaneAPI package that contains the autogenerated API models, but also a UIDelight package that contains a lot of view extensions or view components used everywhere.
“I understand that Foundation packages are like the most common atomic pieces of the app. Why is the next layer called Core packages?”
Core packages are able to import Foundation packages. Those packages contain logic and backend code related to something specific in the application, and they are independent from each other.
For example, we have CoreSession, which contains the logic and code related to our users’ sessions and login data. CoreSession is able to import DashlaneAPI.
We also have CoreLocalization, which contains common localized strings used in multiple parts of the application. This package uses Swiftgen to auto-generate strings by importing a Swift plugin.
“Core packages contain backend and logic, which means the UI will go to Feature packages, right?”
These packages are the higher layer of our architecture. Each package in this layer represents a feature or a group of related features in the application. They encapsulate features and also contain UI code (all views and viewmodels belong here). They can also act as a bridge for multiple Core packages to be used for one specific purpose in a Feature package.
Feature packages can import any Foundation package and also any Core package. Of course, a Feature package cannot import another Feature package.
For example, we have our whole login flow contained in a LoginKit package.
“There are 3 different layers of packages. Is that all?”
We also have other packages that don’t fit our architecture, such as Swift plugins (SwiftGen or Sourcery, for example) and custom tooling. When working with packages, we can't have build phases like application targets. That’s why we use Swift plugins instead to run scripts at build-time for your packages.
“What are the relationships between the layers?”
As illustrated in the chart above, there are a few rules we must follow when developing in packages.
- A Feature package can import everything except other Feature packages
- A Core package can only import Foundation packages
- A Foundation package can't import other packages
- Swift plugins can be imported/used by any other package
“Do you have a naming convention for packages?”
We decided to name every Core package "CoreSomething" and every Feature package "SomethingKit." This way, it's easy for developers to know if they are able to use that package or not and what kind of API they would expect to see in it.
“What would a dependency graph from the feature layer to the Foundation layer look like?”
Below is a visual representation of the dependency graph for our LoginKit feature.
As you can see, even when you have a lot of packages, the graph is still readable, and we can clearly see the layers. I voluntarily excluded Swift plugins and external packages from this graph since they are not considered in the architecture.
“What are the benefits of using Swift packages?”
There are a few reasons why using Swift packages is beneficial.
First, it clearly improves the code architecture. By creating packages, we separate our app into different parts, and all of these parts can have their own unit tests, UI tests, integration tests, and more. That means we have a better code structure and can test our code separately by package—and the execution time will be reduced. Using this kind of architecture also pushes developers to add more injection to packages and have a proper separation of concerns. In addition, it makes it easier for testing because it allows developers to build mocks.
Apple recommends using Swift packages, which is yet another reason to follow this best practice. There is also another major benefit: a Swift package can be easily imported into another project or app. At Dashlane, we have a few different applications in our Apple development environment: The password manager iOS app, the Authenticator app, and our macOS app. We also have the autofill extension on both iOS and Safari on Mac. By using Swift packages, we’re able to write code that is not just well separated but also easily reusable across all our apps. Without these packages, we would have to write a lot of boilerplate and duplicate code across our different apps.
“Swift packages are interesting, but what are the benefits of this specific package architecture?”
When starting to work with packages, you can easily end up creating cyclic dependencies (even if Xcode will tell you if you make an obvious mistake in the package definition). At some point, you may also question where to put the code you want to export in a package because it could fit in multiple places.
Before we set up this package architecture, we created a "CommonPackage" that contained a lot of code used everywhere that we couldn’t easily put somewhere else because it was too dependent on other features/parts of the app. With time, this package has grown and contained a lot of code. It eventually became the new tote, just like the one we were trying to get rid of at the beginning when creating Swift packages. And believe me, once this spaghetti code is there, it will take a lot of work to clean it—much more work than doing it the proper way from the beginning.
By adopting a clear architecture like this one (or similar) when developing, you will be forced to put your code into packages. Sometimes, when your code doesn't fit into only one package, that means the code has to be split. It’s a good practice to start thinking about the package architecture at the beginning of your project or the moment when you adopt Swift packages to avoid all those issues I mentioned earlier.
You might ask yourself why we use this 3-layer stack dependent on each other. We could have chosen a more flattened architecture with totally independent packages. If you come from the dependency injection world, you might think that a flattened architecture is better. A flattened architecture could be better separated and independent, but it would require us to define multiple protocols and duplicated types in each package in order to communicate between packages.
We have chosen to use this three-layer architecture because it allows us to stay flexible but still have a clear separation of concern. This three-layer architecture is a pragmatic choice that avoids a lot of boilerplate code that we would have to deal with by using a flattened architecture, and it is similar to operating systems or language libraries that are stacked on low-level libraries.
Also, we’re able to maintain only one repository containing all the packages by defining them as local Swift packages. Having only one repository is useful and really helps to maintain and evolve the codebase easily. Merge requests have never been so easy to review.
“I get it. Using a package looks cool. Now, tell me about the dark side.”
It’s very helpful and can be relatively easy to divide your app into packages, but some cases can get you into trouble. Here is a short list of complex situations we’ve met during our journey into Swift packages:
- Handle links between different packages of the same layer, and avoid cyclic dependencies between packages
- Choose where to develop a feature that is shared across several modules
- Handle assets and code-generation tools in packages (Swiftgen/Sourcery)
- (And the most difficult one:) Handle exportation into packages of an existing massive app that already has many dependencies
I will address those issues in future posts where I will be able to go a bit deeper.
“What is your overall opinion of Swift packages?”
My feedback on Swift packages is pretty clear. They are useful, they allow developers to have a decent architecture, and it's a lot easier to develop on this project (with multiple apps) since we started to organize it into packages.
However, even though Swift has many advantages, creating packages and exporting code into it can lead to some complicated situations, and you have to be careful. By adopting a clear architecture at the beginning, you’ll be able to avoid such situations.
Certainly, the sooner you start to modularize your application, the better. It’s more difficult to try to modularize an existing app, and it gets even harder if it’s a big app. So when building a new iOS app, think about modularization from the beginning—you’ll thank yourself later.
“Do you recommend a Github repository illustrating this package architecture?” If after reading this you want to take a look more deeply at an example, you can find the code of our open-sourced Apple apps at Dashlane on this repository: https://github.com/Dashlane/apple-apps.
To learn more from Dashlane’s expert engineers, check out our other blogs.
Sign up to receive news and updates about Dashlane
Thanks! You're subscribed. Be on the lookout for updates straight to your inbox.