One or multiple packages?

Well…it depends

Juantri Jiménez
New Work Development
4 min readNov 8, 2023

--

Photo by Kelly Sikkema on Unsplash

Swift Package Manager is not just a dependency manager, it is also a tool that allows you to organize your project into modules and create a package structure with which organize your frameworks and dependencies.

If CocoaPods has always been your dependency manager, you should know that all modules and third party frameworks are listed in the Podfile, so any framework can access to any module or third-party framework by the podspec. But, with SPM, we can control this access and create a package structure that “defines” access rules to the shared code.

Normally, a modular project has different kinds of modules depending on their purpose, and we might organize them into the following categories:

  • Dependencies (third-party frameworks)
  • Feature frameworks
  • Services frameworks (APIClient, tracker, formatters, etc)
  • Testing frameworks
  • UI frameworks

Depending on the project you can organize it in a multiple ways, but in this article I will explain the following one:

  • Testing frameworks should just depend on the system tools and maybe in some third-party frameworks (like a snapshot test framework), but they never can depend on features, services or UI code.
  • UI frameworks should depend on our testing frameworks and maybe some third-party frameworks (like animation frameworks), but they never can depend on our core code.
  • Services frameworks can need third-party frameworks but we should try to inject them instead of adding them as dependencies. These frameworks should depend on our testing frameworks, but they can never depend on the feature code.
  • Feature frameworks will need all our frameworks and they can also need some third-party frameworks, but they should always be injected.
  • Third-party frameworks will be added directly to the main target, where we can configure and inject them into the needed frameworks.
Packages graph

Third party frameworks

Let’s create an empty package file for all the third-party frameworks.

First of all is add all the third-party frameworks in the dependencies property. Now we need a target where link all the dependencies so we create one with an empty file to make Xcode compile and resolve the dependencies. Once we have the target, we can create the product and link it into the main target.

We have created a wrapper where all the third-party dependencies will be located. This way, we can control everything: dependency version, whether the dependency should be compiled (maybe depending on the environment), etc.

Dependencies package

But there are also other ways to solve this:

  • You can add the third-party frameworks directly into the main target without a package file.
  • You can create a package file and one product and target per dependency, but the dependencies list of the main target can be huge.

Once the project has access to the dependencies, we can configure and inject them into the frameworks that are needed.

Internal modules

Now it’s time for our frameworks. We will create four packages as we mentioned before: Features, Services, Testing, and UI. Each of them will have a product that contains all the targets in the file, so we add just one product into the dependencies of the main target.

Folder structure and project linking

The main idea is that any feature framework should not be a dependency of any of the rest of our frameworks. So, if we connect the packages as we defined in the packages graph image, we ensure that nothing depends on the feature frameworks but the main target and avoid cyclic dependencies.

Packages definitions

Package modifier

In a package, we can define as many products and targets as we want, but the relation between them is what we should control to avoid unnecessary dependencies.

Swift 5.9 gives us a new access modifier called “package”. It allows access to the code to just the targets defined in the package file.

Conclusion

Organizing the code into modules and these into different packages with a dependency order established by you is a good practice because you can control the dependencies of your modules, favoring DI, testing, and scalability.

--

--

Spanish iOS software engineer. I write about features of Swift and iOS development practices