GlobalDependencies

This is a simple dependency injection framework good for managing global dependencies (app-level singletons basically)
in small and medium-sized apps. It has been used successfully in several small personal projects and a medium sized
commercially released app.
It’s meant to add as little friction as possible to the task of injecting dependencies so early stage apps can still
take on the benefits of dependency injection (better testability, easier maintenance and changes in dependencies)
without slowing down the pace of development.
Adoption
If the instructions here are not clear enough you can also check additional instructions on how to use the dependency
injection system in Dependencies.swift
as well as an example of use in the test target of the framework.
For the sake of this example let’s assume we want to do dependency injection on `URLSession’ so we can mock network
data fetch in tests.
And remember that RINAC (README Is Not a Compiler).
1. Declare a protocol to façade the functionality
All dependencies that we want to integrate in this depdendency injection system ought to hide away behind a protocol.
four our example we’ll declare a RemoteDataFetch' protocol with a similar API as the one we want to façade from
URLSession`
protocol RemoteDataFetch: Dependencies {
func data(from url: URL) async throws -> Data
}
2. Build a default implementation
Build a default implementation to use within the app. Whether this default implementation is complete or not doesn’t
matter for the purposes of this exercise, leave it as an actual mock if unblocking other work is more important than
finalizing that default implementation.
In this case we can just extend URLSession
to adopt the protocol with a bit of extra work.
extension URLSession: RemoteDataFetch {
func data(from url: URL) async throws -> Data {
let (data, response) = try await data(from: url, delegate: nil)
guard response.isValid else {
throw BadResponse
}
return data
}
}
3. Declare a dependency protocol
The dependency will be vended through a property, which will hide its implementation behind the protocol we declared on
step #1. As follows for the example:
protocol RemoteDataFetchDependency {
var remoteDataFetch: any RemoteDataFetch { get }
}
4. Add an extension to GlobalDepdendencies
that implements the new dependency protocol and resolves it.
The accessor for the dependency always takes the same shape. For the case of URLSession
we can just use
URLSession.shared
as the default, for your own types you’ll want to hold onto a private singleton nearby.
extension GlobalDependencies: RemoteDataFetchDependency {
var remoteDataFetch: RemoteDataFetch {
return resolveDependency(forKeyPath: \.remoteDataFetch, defaultImplementation: URLSession.shared)
}
}
5. Add dependency injection to the app components that require it.
There’s several steps to ensuring dependency management is… well managed within an app’s components. Fortunately they
are all very simple to follow.
5.1 Add a private dependencies
property that holds onto injected dependencies.
It’s important to keep dependencies managed by making that property be of the union of all dependency protocol types
instead of GlobalDependency
. That keeps dependencies visible and makes them much more straightforward to manage. I.e.
if a component’s dependency
property is the union of half a dozen protocols you may be taking in too much there.
Let’s assume we’re creating an image cache (probably a dependency itself but we’ll leave that off the example) that
needs both network and local (via protocol LocalDataFetchDependency
) access to get its data. It would look as follows:
class ImageCache {
[...]
private let dependencies: any (RemoteDataFetchDependency & LocalDataFetchDependency)
[...]
}
5.2 Initialize your component with global dependencies.
For the sake of minimalistic adoption it’s best to limit the explicit naming of dependencies to the dependencies
property and just take in GlobalDependencies
at initialization. Alternatively and with a slight increase of friction
you can declare a typealias
with the union of dependencies needed and use that both for the property and any
initializers (not shown in these examples).
For our ImageCache
type it would look as follows:
class ImageCache {
[...]
init(/*any other parameters, */dependencies: GlobalDependencies = .default) {
self.dependencies = dependencies
// Initialize other stuff
[...]
}
[...]
}
5.3 Pass in the dependencies to any newly created components
To make sure that dependency overrides stick, you have to make sure to initialize the dependencies of any component
you create with the ones you have. This just requires using buildGlobal
on any dependencies you’re holding onto
to unwrap back into a GlobalDependencies
value that can fulfill whatever depdendencies the next component has (this
avoids having to wire down dependency overrides through component trees just because some leaf node has different
dependencies than the branches above it).
In our contrived example we’re going to say that our ImageCache
has a
lazy var localResourceManager: LocalResourceManager
that depends on… whatever it wants, really. It would look as
follows:
class ImageCache {
[...]
lazy var localResourceManager: LocalResourceManager = {
LocalResourceManager(/*Other parameters, */dependencies: dependencies.buildGlobal())
}()
[...]
}
6. Mock and override dependencies for tests.
To build tests for components that have dependencies, all you have to do is build a mock implementation of the
dependency-vended protocol and overwrite the dependency with it.
6.1 Build a Mock
Just like any other mock, really. Either build them on the fly or build a configurable one for use in several tests.
Let’s take the latter approach for our example (READMEINAC = README Is Not a Compiler).
class MockRemoteDataFetch: RemoteDataFetch {
var dataFromURLOverride: ((URL) async throws -> Data)?
func data(from url: URL) async throws -> Data {
try await dataFromURLOverride?(url) ?? throw UnexpectedCallError
}
}
6.2 Override the dependency when setting up a test.
If it’s always the same override you can just take care of the matter on the test class’ setUp()
method, otherwise
just run the logic while setting up the individual test proper.
Here is an example for a test on ImageCache
that verifies that the type reacts properly to a network data fetch error
using the mock above.
class ImageCacheTests: XCTestCase {
[...]
func testRemoteError() {
let mockRemoteDataFetch = MockRemoteDataFetch()
mockRemoteDataFetch.dataFromURLOverride = { url in
throw NSError(domain: NSURLErrorDomain, code: URLError.Code.timedOut)
}
let imageCache = ImageCache(dependencies: GlobalDependencies.default.with(override: mockRemoteDataFetch, for: \.remoteDataFetch))
// Then do stuff with that image cache.
[...]
}
[...]
}
Dependency Micropackages
In addition to the base GlobalDependencies package itself, we are building as needed a number of common system framework
façade dependencies that can be quickly and easily adopted to abstract away common hard dependencies and make the
resulting logic far more testable than otherwise.
These are mostly tiny so we call them depdendency micropackages (ok, the jokes write themselves). It’s easy enough to
find those we built ourselves by just looking at the repo list for the account and checking those that are named
*Dependency
.
Since those dependency micropackages are built up as needed by other projects they are usually not going to implement a
complete wrap of the abstracted API (unless all of it was needed at some point). Feel free to fork and expand if needed,
and please bring back a PR with any additions you make that may be useful to others.
Requirements
GlobalDependencies doesn’t have much in the way of dependencies itself so it can work in whatever the tools support.
- Xcode 14.3 or later.
- Swift 5.8 or later.
- iOS 11 or later.
- macOS Catalyst 13 or later (but why?).
- macOS 10.13 or later.
- tvOS 11 or later.
- watchOS 4 or later.
Installation
GlobalDependencies is currently only supported through Swift Package Manager.
If developing your own package, you can add the following lines to your Package.swift
file:
dependencies: [
.package(url: "https://github.com/Gabardone/GlobalDependencies", from: "2.0.0"),
]
To add to an Xcode project, paste https://github.com/Gabardone/GlobalDependencies
into the URL field for a new package
and specify “Up to next major version” starting with the current one.
Contributing
While the GlobalDependencies package itself works “as-is” and is unlikely to get major changes going forward, I know
better than to think it can’t be improved, so suggestions are welcome especially if they come with examples.
The dependency micropackages can always use more work as they are built as needed. If you find yourself using one but
improving it, feel free to submit a PR with the changes. Same if you build a generic one for a system service that
other people could stand to use. I’ll happily point to other people’s dependency packages if anyone ends up creating
any.
Beyond that just take to heart the baseline rules presented in contributing guidelines prior to
submitting a Pull Request.
Thanks, and happy low friction dependency injection!
Developing
Double-click on Package.swift
in the root of the repository to open the project in Xcode. Or open the containing
folder from Xcode (from the command line: open -a Xcode <path to package folder>
works as well).
GlobalDependencies
This is a simple dependency injection framework good for managing global dependencies (app-level singletons basically) in small and medium-sized apps. It has been used successfully in several small personal projects and a medium sized commercially released app.
It’s meant to add as little friction as possible to the task of injecting dependencies so early stage apps can still take on the benefits of dependency injection (better testability, easier maintenance and changes in dependencies) without slowing down the pace of development.
Adoption
If the instructions here are not clear enough you can also check additional instructions on how to use the dependency injection system in
Dependencies.swift
as well as an example of use in the test target of the framework.For the sake of this example let’s assume we want to do dependency injection on `URLSession’ so we can mock network data fetch in tests.
And remember that RINAC (README Is Not a Compiler).
1. Declare a protocol to façade the functionality
All dependencies that we want to integrate in this depdendency injection system ought to hide away behind a protocol. four our example we’ll declare a
RemoteDataFetch' protocol with a similar API as the one we want to façade from
URLSession`2. Build a default implementation
Build a default implementation to use within the app. Whether this default implementation is complete or not doesn’t matter for the purposes of this exercise, leave it as an actual mock if unblocking other work is more important than finalizing that default implementation.
In this case we can just extend
URLSession
to adopt the protocol with a bit of extra work.3. Declare a dependency protocol
The dependency will be vended through a property, which will hide its implementation behind the protocol we declared on step #1. As follows for the example:
4. Add an extension to
GlobalDepdendencies
that implements the new dependency protocol and resolves it.The accessor for the dependency always takes the same shape. For the case of
URLSession
we can just useURLSession.shared
as the default, for your own types you’ll want to hold onto a private singleton nearby.5. Add dependency injection to the app components that require it.
There’s several steps to ensuring dependency management is… well managed within an app’s components. Fortunately they are all very simple to follow.
5.1 Add a private
dependencies
property that holds onto injected dependencies.It’s important to keep dependencies managed by making that property be of the union of all dependency protocol types instead of
GlobalDependency
. That keeps dependencies visible and makes them much more straightforward to manage. I.e. if a component’sdependency
property is the union of half a dozen protocols you may be taking in too much there.Let’s assume we’re creating an image cache (probably a dependency itself but we’ll leave that off the example) that needs both network and local (via
protocol LocalDataFetchDependency
) access to get its data. It would look as follows:5.2 Initialize your component with global dependencies.
For the sake of minimalistic adoption it’s best to limit the explicit naming of dependencies to the
dependencies
property and just take inGlobalDependencies
at initialization. Alternatively and with a slight increase of friction you can declare atypealias
with the union of dependencies needed and use that both for the property and any initializers (not shown in these examples).For our
ImageCache
type it would look as follows:5.3 Pass in the dependencies to any newly created components
To make sure that dependency overrides stick, you have to make sure to initialize the dependencies of any component you create with the ones you have. This just requires using
buildGlobal
on any dependencies you’re holding onto to unwrap back into aGlobalDependencies
value that can fulfill whatever depdendencies the next component has (this avoids having to wire down dependency overrides through component trees just because some leaf node has different dependencies than the branches above it).In our contrived example we’re going to say that our
ImageCache
has alazy var localResourceManager: LocalResourceManager
that depends on… whatever it wants, really. It would look as follows:6. Mock and override dependencies for tests.
To build tests for components that have dependencies, all you have to do is build a mock implementation of the dependency-vended protocol and overwrite the dependency with it.
6.1 Build a Mock
Just like any other mock, really. Either build them on the fly or build a configurable one for use in several tests.
Let’s take the latter approach for our example (READMEINAC = README Is Not a Compiler).
6.2 Override the dependency when setting up a test.
If it’s always the same override you can just take care of the matter on the test class’
setUp()
method, otherwise just run the logic while setting up the individual test proper.Here is an example for a test on
ImageCache
that verifies that the type reacts properly to a network data fetch error using the mock above.Dependency Micropackages
In addition to the base GlobalDependencies package itself, we are building as needed a number of common system framework façade dependencies that can be quickly and easily adopted to abstract away common hard dependencies and make the resulting logic far more testable than otherwise.
These are mostly tiny so we call them depdendency micropackages (ok, the jokes write themselves). It’s easy enough to find those we built ourselves by just looking at the repo list for the account and checking those that are named
*Dependency
.Since those dependency micropackages are built up as needed by other projects they are usually not going to implement a complete wrap of the abstracted API (unless all of it was needed at some point). Feel free to fork and expand if needed, and please bring back a PR with any additions you make that may be useful to others.
Requirements
GlobalDependencies doesn’t have much in the way of dependencies itself so it can work in whatever the tools support.
Tools:
Platforms:
Installation
GlobalDependencies is currently only supported through Swift Package Manager.
If developing your own package, you can add the following lines to your
Package.swift
file:To add to an Xcode project, paste
https://github.com/Gabardone/GlobalDependencies
into the URL field for a new package and specify “Up to next major version” starting with the current one.Contributing
While the GlobalDependencies package itself works “as-is” and is unlikely to get major changes going forward, I know better than to think it can’t be improved, so suggestions are welcome especially if they come with examples.
The dependency micropackages can always use more work as they are built as needed. If you find yourself using one but improving it, feel free to submit a PR with the changes. Same if you build a generic one for a system service that other people could stand to use. I’ll happily point to other people’s dependency packages if anyone ends up creating any.
Beyond that just take to heart the baseline rules presented in contributing guidelines prior to submitting a Pull Request.
Thanks, and happy low friction dependency injection!
Developing
Double-click on
Package.swift
in the root of the repository to open the project in Xcode. Or open the containing folder from Xcode (from the command line:open -a Xcode <path to package folder>
works as well).