Because named registrations and resolutions let you change the behavior of the app and determine just which service or value should be resolved for a given type.
Dependency Injection is powerful tool, but named registrations take the entire concept to an entriely different level.
Resolver 1.3 adds a Name
space to Resolver similar to that of Notificiations.Name
. Registering a name lets you use Xcode's autocompletion feature for registrations and to resolve named instances and also ensures that you don't accidentally use "fred" in one place, "Fred" in another, and "Freddy" somewhere else.
You define your own names by extending Resolver.Name
as follows:
extension Resolver.Name {
static let fred = Self("Fred")
static let barney = Self("Barney")
}
Once defined your names can be used in the name
parameter when registering services. Here we define two instances of the same protocol, distinguished by name.
register(name: .fred) { XYZServiceFred() as XYZServiceProtocol }
register(name: .barney) { XYZServiceBarney() as XYZServiceProtocol }
Once defined and registered, names can be used during the resolution process to pick just which version of the service you desire.
let service: XYZServiceProtocol = resolve(name: .fred)
// or
@Injected(name: .barney) var service: XYZServiceProtocol
In addition to services you can also register value types and parameters for later resolution. However, since Resolver registers objects and values based on type inference, the only way to tell one String
from another String
is to name it.
We start once again by defining the names we want to use, in this case appKey
and token
.
extension Resolver.Name {
static let appKey = Self("appKey")
static let token = Self("token")
}
We then register some strings using our .appKey
and token
names.
register(name: .appKey) { "12345" }
register(name: .token) { "123e4567-e89b-12d3-a456-426614174000" }
Which can then be used when we resolve our services. The following code shows how a factory resolves a String parameter named .appKey
, which passes the resulting string value to the XYZSessionService
initialization function.
register { XYZSessionService(key: resolve(name: .appKey)) }
This is a good way to get authentication keys, application keys, and other values to the objects that need them.
We can also use names to control access to mocked data. Consider the following set of registrations.
extension Resolver.Name {
static let data = Self("data")
static let mock = Self("mock")
}
register(name: .data) { XYXService() as XYZServicing }
register(name: .mock) { XYXMockService() as XYZServicing }
register { resolve(name: .name(fromString: Bundle.main.infoDictionary!["mode"] as! String)) as XYZServicing }
Here we've registered the XYZServicing protocol three times: once with the name space .data
, and then again version with the name space .mock
. The third registration, however, has no name.
Instead, it gets a string from the app's info.plist and asks Resolver to resolve an instance with the proper type and with the proper name.
Let's see it in use by a client.
@Injected var service: XYZServicing
The client just asks Resolver for an instance of XYZServicing
.
Behind the scenes, however, and depending upon how the app is compiled and how the "mode" value is set in the app's plist, one build will get actual data, while the other build will get mock data.
And as long as XYXMockService complies with the XYZServicing protocol, the client doesn't care.
Nor should it.
One final note here is that we registered Resolver.Name
instances, but in our factory we converted mode
into a Name
based on the value of the string we pulled from the plist. Just be careful when you're doing this and make sure your passed strings actually match names actually registered in the app.
Finally, consider the next pair of registrations:
extension Resolver.Name {
static let add = Self("add")
static let edit = Self("edit")
}
register(name: .add) { XYZViewModelAdding() as XYZViewModelProtocol }
register(name: .edit) { XYZViewModelEditing() as XYZViewModelProtocol }
Here we're registering two instances of the same protocol, XYZViewModelProtocol
.
But one view model appears to be specific to adding things, while the other's behavior leans more towards editing.
class ViewController: UIViewController, Resolving {
var editMode: Bool = true // set, perhaps, by calling segue
lazy var viewModel: XYZViewModelProtocol = resolver.resolve(name: editMode ? .edit : .add)!
}
Now the view controller gets the proper view model for the job. The lazy var
ensures that the viewModel resolution doesn't occur until after the viewController is instantiated and prepareForSegue
has had a chance to correctly set editMode
.
If you're using Resolver's property wrappers for injection, you can also do the same with @LazyInjected
.
class NamedInjectedViewController: UIViewController {
var editMode: Bool // set, perhaps, by calling segue
@LazyInjected var viewModel: XYZViewModelProtocol
override func viewDidLoad() {
super.viewDidLoad()
$viewModel.name = editMode ? .edit : .add
viewModel.load()
}
}
Again, just make sure you set the property name before using the wrapped viewModel
for the first time.
Name spaces are better than simple string literals. Use them.
That said, you should be aware that Name
supports the ExpressibleByStringLiteral
protocol, which means that you can also use a string literal to register and resolve your instances (e.g. resolve(name: "Fred")
).
String variables, however, are not automatically converted. If you're trying to translate a string variable to a Name
, you either need to initialize it directly Resolver.Name(myString)
, or do as we did in a previous example using the .name(fromString: myString)
syntax.
viewModel = resolver.optional(name: .name(fromString: type))
Be aware that string literal support exists primarily for backwards compatibility with earlier versions of Resolver and that raw string paramaters will probably become deprecated in a future instance of Resolver.
Name spaces are based on a PR concept submitted by Artem K./DesmanLead.