Here we have registered an instance of AnalyticsProvider for the AnalyticsProviding protocol. This AnalyticsProvider instance is cached by the container and will be returned whenever the container is asked for a dependency matching AnalyticsProviding.
Dependency Resolution
Resolving a dependency is similarly simple:
let analyticsProvider = DependencyContainer.shared.resolve(AnalyticsProviding.self)
This resolves the dependency conforming to AnalyticsProviding that we registered earlier.
As a convenience, a property wrapper is provided to resolve dependencies automatically:
@Dependency private var analyticsProviding: AnalyticsProviding
This property accesses the equivalent of DependencyContainer.shared.resolve(AnalyticsProviding.self).
Optional Dependency Resolution
For dependencies that may not always be registered, an optionalResolve function is provided:
let optionalDependency = DependencyContainer.shared.optionalResolve(Maybe.self)
optionalDependency?.perhaps()
These can also be accessed using a property wrapper:
@OptionalDependency private var maybe: Maybe?
Unregistering Dependencies
When a dependency is registered, an opaque registration token is returned, which can be used later to remove the registration from the container:
let token = DependencyContainer.shared.register(service: AnalyticsProviding.self) {
return AnalyticsProvider()
}
/**
Perform work that depends on the registered dependency
*/
DependencyContainer.shared.remove(token)
Testing
Hopoate is great for unit testing, as it operates using a LIFO system for dependency resolution i.e the most recently registered dependency for a given type is the one returned when dependency resolution is requested. This allows for mock implementations to be registered with the container for the purposes of testing, which can be removed when the test is over. Once the mock dependency is removed, any previously registered dependency for the requested type will be resolved instead.
let mockLogger = MockLogger()
let token = DependencyContainer.shared.register(service: LogProviding.self) {
return mockLogger
}
testedObject.doSomethingThatLogs()
XCTAssertTrue(mockLogger.receivedLogMessage)
DependencyContainer.shared.remove(token)
This process of registering and unregistering a mock dependency can be simplified by using MockContainer from the HopoateTestingHelpers library (SPM users will need to use the separate HopoateTestingHelpersPackage package). The mock container registers a mock object for a given protocol with the dependency container, and unregisters the mock when it is deallocated. Here’s an example:
We have a MockContainer that will register a dependency for the AnalyticsProviding protocol. It will use an instance of MockAnalyticsProvider as the dependency. In our setUp method, we create the container and the mock analytics provider.
Later, in our test function, we execute some code that uses the AnalyticsProviding dependency registered with the dependency container, which in this case is when the user taps a button. Once the button tap has occurred, we can check our mock dependency to see if it has received what we expected, by accessing it from the mock container’s mock property.
Once the test has run, the tearDown function is executed, which sets our mockAnalyticsContainer to nil. This in turn removes the MockAnalyticsProvider from the dependency container, leaving the container in the same state that it was before the test was run.
Dependency Caching
By default, when a service is registered with the dependency container, the container caches the service that is given in the creation closure.
DependencyContainer.shared.register(service: AnalyticsProviding.self) {
return AnalyticsProvider() // <- This object will be returned by all calls to DependencyContainer.shared.resolve(AnalyticsProviding.self)
}
If you do not want this caching behaviour, it can be disabled when registering the dependency:
DependencyContainer.shared.register(service: AnalyticsProviding.self, cacheService: false) {
return AnalyticsProvider() // <- A new instance of AnalyticsProvider will be provided to each call to DependencyContainer.shared.resolve(AnalyticsProviding.self)
}
Installation
Swift Package Manager
Add the following to your package’s dependencies in your package manifest:
Hopoate is a lightweight dependency injection framework for iOS, allowing for simple registration and resolution of application dependencies.
Dependency Registration
Registering a dependency is as simple as calling a single function on Hopoate’s shared container:
Here we have registered an instance of
AnalyticsProvider
for theAnalyticsProviding
protocol. ThisAnalyticsProvider
instance is cached by the container and will be returned whenever the container is asked for a dependency matchingAnalyticsProviding
.Dependency Resolution
Resolving a dependency is similarly simple:
This resolves the dependency conforming to
AnalyticsProviding
that we registered earlier.As a convenience, a property wrapper is provided to resolve dependencies automatically:
This property accesses the equivalent of
DependencyContainer.shared.resolve(AnalyticsProviding.self)
.Optional Dependency Resolution
For dependencies that may not always be registered, an
optionalResolve
function is provided:These can also be accessed using a property wrapper:
Unregistering Dependencies
When a dependency is registered, an opaque registration token is returned, which can be used later to remove the registration from the container:
Testing
Hopoate is great for unit testing, as it operates using a LIFO system for dependency resolution i.e the most recently registered dependency for a given type is the one returned when dependency resolution is requested. This allows for mock implementations to be registered with the container for the purposes of testing, which can be removed when the test is over. Once the mock dependency is removed, any previously registered dependency for the requested type will be resolved instead.
This process of registering and unregistering a mock dependency can be simplified by using
MockContainer
from theHopoateTestingHelpers
library (SPM users will need to use the separate HopoateTestingHelpersPackage package). The mock container registers a mock object for a given protocol with the dependency container, and unregisters the mock when it is deallocated. Here’s an example:We have a
MockContainer
that will register a dependency for theAnalyticsProviding
protocol. It will use an instance ofMockAnalyticsProvider
as the dependency. In oursetUp
method, we create the container and the mock analytics provider.Later, in our test function, we execute some code that uses the
AnalyticsProviding
dependency registered with the dependency container, which in this case is when the user taps a button. Once the button tap has occurred, we can check our mock dependency to see if it has received what we expected, by accessing it from the mock container’smock
property.Once the test has run, the
tearDown
function is executed, which sets ourmockAnalyticsContainer
tonil
. This in turn removes theMockAnalyticsProvider
from the dependency container, leaving the container in the same state that it was before the test was run.Dependency Caching
By default, when a service is registered with the dependency container, the container caches the service that is given in the creation closure.
If you do not want this caching behaviour, it can be disabled when registering the dependency:
Installation
Swift Package Manager
Add the following to your package’s dependencies in your package manifest:
Carthage
Add the following to your
Cartfile
: