2 min read

Containers

Last Updated - Platform 25.0 - SDK 20.0

Containers are the easiest way to inject custom code. The shared container provides an easy way replace or decorate the dependencies of SDK features by injecting factory closures.

For bespoke features, the shared container can be extended with custom dependencies to provide a consistent dependency injection interface.

Containers are intended for global use. To replace a dependency of a single instance of a feature you should use that features initialiser or builder directly.

Supported Types

Data and domain layer objects:

  • Container.shared.mappers.{mapper}
  • Container.shared.repositories.{repository}
  • Container.shared.services.{service}

Presentation layer objects:

  • Container.shared.coordinators.{coordinator}
  • Container.shared.dataSources.{viewDataSource} for table, collection, and stack view data sources
  • Container.shared.mappers.{viewDataMapper}
  • Container.shared.views.{view}

Special objects:

  • Container.shared.navigator for global navigation handling (defaults to NavigationHelper)
  • Container.shared.networkSession for the global network session
  • Container.shared.resourceResolver for global resource resolution

Scopes

Containers support two scopes:

  • Factory scope for Factory dependencies which will always return a new instance.
  • Singleton scope for SingletonFactory dependencies which will always return the same instance.

The SingletonFactory supports both scope types so you can inject a closure to always return new instances into them if needed. Singleton scope is the default for all SDK dependencies except for views and data sources.

Injection

To inject a custom factory for a SingletonFactory dependency you can use any of the following. Replace will act the same as .single for SingletonFactory dependencies.

Container.shared.mappers.someMapper = .single { CustomMapper() }
Container.shared.mappers.someMapper = .factory { CustomMapper() }
Container.shared.mappers.someMapper.replace { CustomMapper() }

To inject a custom factory for a Factory dependency you can use either of the following. Replace will act the same as .factory for Factory dependencies.

Container.shared.views.someView = .factory { CustomView() }
Container.shared.views.someView.replace { CustomView() }

Decoration

If you need to add behaviour to a dependency without changing much else you can use the decorator pattern. Use .decorate to modify the object using its interface or decorate it in your own object.

Container.shared.mappers.someMapper.decorate { CustomMapper(decorating: $0) }
Container.shared.mappers.someMapper.decorate(CustomMapper.init)
Container.shared.dataSources.someDataSource.decorate {
var dataSource = $0
dataSource.itemSize = CGFloat(width: 400, height: 200)
return dataSource
}

Usage

To retrieve an instance of an object here are a few examples. Views have access to a convenient static variable container without needing Container.shared.

let mapper = Container.shared.mappers.someMapper()
class CustomMapper: SomeMapper {
init(subMapper: SubMapper = Container.shared.mappers.subMapper()) { ... }
}
class CustomView: UIView {
lazy var anotherView = container.views.anotherView()
init(someView: SomeView = container.views.someView()) { ... }
}

To retrieve the default Poq implementation of an object use Container.poq.mappers.someMapper(). It is unlikely that you will need to use this.

Custom Dependencies

Dependencies can be injected directly into containers without using the strongly typed variables. But this can be easily error prone if you make mistakes with the key.

Container.shared.inject(key: "Something", object)
Container.shared.inject(object)

Dependencies can be resolved in the same way using the following or similar convenience functions.

Container.shared.resolve(Something.self, key: "Something")
Container.shared.resolve(Something.self)

However, for custom shared features, to get the best out of containers you can use extensions and namespaces. To create custom strongly typed dependencies use the following.

public extension Container.Mappers {
var someMapper: SingletonFactory<SomeMapper> {
get { resolve { PoqPriceViewDataMapper() } }
set { inject(newValue) }
}
}

Remember for Views or DataSources you must use Factory instead of SingletonFactory. Using single instances for views is bound to cause a crash due to sharing the same view.

To create custom namespaces use the following.

public extension Container {
enum NamespaceCustom {}
typealias Custom = Namespace<NamespaceCustom>
var custom: Custom { namespace() }
}
// The above will allow the following.
Container.shared.custom.something()