Swift Package Manager (SPM) has limitations when it comes to importing external code into Package.swift
files, making it challenging to manage dependencies in a modularized application. As a result, developers often find themselves duplicating declarations of dependencies or even duplicating version requirements, which can be quite cumbersome.
For example, if you use Swinject in multiple modules, you need to include its declaration in each module, making it even more complicated when you want to update the version. While you could use search and replace to handle this, it would be much better to have a centralized location for common dependencies.
Solution
While there is no built-in feature in SPM to achieve this, because the code in Package.swift
can’t be shared, there is a simple workaround that I recently figured out.
The key insight is that by declaring a Package as a dependency in the
Package.swift
, you can also import all its dependencies.
With this knowledge, you can create a separate package, let’s call it “Core”, where you keep shared models, protocols, and globally shared dependencies such as Dependency Injection (DI) management. Other packages can then depend on the “Core”, effectively sharing its dependencies.
To take it a step further, you can create empty packages that aggregate dependencies into bundles, like “SharedDependencies”, “DebugTools”, “AnalyticsDependencies”, etc.
However, keep in mind that it all depends on your architecture. You need to decide whether it is worth it or not in your specific case.
Sample Code
Below you can see how this solution could be applied in practice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
let package = Package( name: "SharedDependencies", platforms: [.iOS(.v15)], products: [ .library( name: "SharedDependencies", targets: ["SharedDependencies"] ) ], dependencies: [ .package(url: "https://github.com/Swinject/Swinject.git", exact: "2.8.3"), .package(url: "https://github.com/Swinject/SwinjectAutoregistration.git", exact: "2.8.3"), .package(url: "https://github.com/attaswift/BigInt.git", from: "5.3.0") ], targets: [ .target( name: "SharedDependencies", dependencies: [ "Swinject", "SwinjectAutoregistration", "BigInt" ], path: "Sources" ) ] ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let package = Package( name: "BusinessLogic", platforms: [.iOS(.v15)], products: [ .library( name: "BusinessLogic", targets: ["BusinessLogic"] ) ], dependencies: [ .package(path: "../Dependencies/SharedDependencies") ], targets: [ .target( name: "BusinessLogic", dependencies: ["SharedDependencies"], path: "Sources" ) ] ) |
This way, you can access from BusinessLogic
all dependencies declared in SharedDependencies
and you don’t have to duplicate all those declarations. You can simply call import Swinject
or import BigInt
wherever you need it.
This is something similar to sharing dependencies between targets in Podfile
while using CocoaPods:
1 2 3 4 5 6 7 8 9 10 11 |
def dev_tools pod 'SwiftLint' pod 'SwiftFormat/CLI' pod 'SwiftGen' end target 'MyApp' do dev_tools debug_tools ... end |
Full Source Code
You can check out a sample project here: github.com/wojciech-kulik/SPM-Sharing-Dependencies.
Things to Consider
While this approach can simplify dependency management, it’s essential to avoid spreading 3rd party dependencies across the entire project. Instead, it’s better to introduce abstractions.
For instance, if you decide to use Alamofire, limit its usage to a single module and introduce a NetworkingProvider
to handle HTTP requests. Similarly, if you opt for Kingfisher, implement an ImageView
that preconfigures Kingfisher and introduces some parameters if the additional configuration is needed.
The general rule is to limit the import of external dependencies to specific modules, avoiding excessive reliance on shared dependencies. However, some exceptions like Swinject might exist if you decide to use it for the entire DI management in the application. In this case, those “bundles” may become useful.
Works with Local Packages
This solution doesn’t have to be limited only to 3rd party libraries. You can also bundle your local packages into groups for easier management.
Let’s say that in your architecture you create a package per REST service. Most likely all your packages will use the same dependencies. In this case, you could try to apply this solution and create an ApiServiceDependencies
bundle or something similar.