- SwiftUI solves dependency injection in an ingenious, extensible way.
- The EnvironmentValues struct can resolve services from arbitrary, even user defined EnvironmentKeys.
- Furthermore, SwiftUI allows you to modify the environment at any point in the view tree. This means you can have different services injected into a subtree of Views.
- So you can go nuts with configurations, should you need to, which you probably won’t.
- So, what's a good service?
- A good service can do anything.
- By default, it stays in its domain and does what you'd expect.
- But when developing or testing, you may want it to do all kinds of crazy things.
- So it should have access to the whole environment, to be able to do anything.
@dynamicMemberLookup protocol Service: EnvironmentKey where Value == Self {
init()
var environment: EnvironmentValues { get set }
typealias Endpoint<Action> = (EnvironmentValues) -> Action
associatedtype Endpoints
var endpoints: Endpoints { get }
}
extension Service {
static var defaultValue: Self { .init() }
}
extension Service {
subscript<Action>(dynamicMember keyPath: KeyPath<Endpoints, Endpoint<Action>>) -> Action {
endpoints[keyPath: keyPath](environment)
}
}
extension EnvironmentValues {
subscript<S: Service>(service keyPath: KeyPath<S, S>) -> S {
get {
var instance = self[S.self]
instance.environment = self
return instance
}
set {
self[S.self] = newValue
}
}
}
extension Environment where Value: Service {
init() {
self.init(\.[service: \Value.self])
}
}
- Wow, thats both stupid and stupid complicated 👍, but how is it to use?
struct Google: Service {
var environment: EnvironmentValues = .init()
var endpoints = (
open: { environment in
{ query in
environment.openURL(url(for: query))
}
} as Endpoint<(String) -> Void>,
fetch: { environment in
{ query in
//we're using `shared` session here, but we might as well retrieve one from the environment
let session = URLSession.shared
let (data, response) = try await session.data(from: url(for: query))
return data
}
} as Endpoint<(String) async throws -> Data>
)
private static func url(for query: String) -> URL {
var components = URLComponents(string: "https://www.google.com")!
components.queryItems = [URLQueryItem(name: "q", value: query)]
return components.url!
}
}
struct GoogleView: View {
@Environment() private var google: Google
@State var query: String = ""
@State var fetchedData: Result<Data, Error>?
@State var currentRequest: Task<(), Never>?
var body: some View {
VStack {
HStack {
TextField("Search", text: $query)
Button {
currentRequest?.cancel()
currentRequest = Task {
fetchedData = await Result {
try await google.fetch(query)
}
}
} label: {
Text("Fetch")
}
Button {
google.open(query)
} label: {
Text("Open")
}
}
}
Text("Result: " + String(describing: fetchedData))
}
}
// helper
extension Result where Failure == Error {
init(catching body: () async throws -> Success) async {
do {
self = .success(try await body())
} catch {
self = .failure(error)
}
}
}
struct GoogleView_Previews: PreviewProvider {
static var previews: some View {
// default config
GoogleView()
// fetch from google but open in duckduckgo
GoogleView()
.environment(
\.[service: \Google.self].endpoints.open,
{ environment in
{ query in
var components = URLComponents(string: "https://www.duckduckgo.com")!
components.queryItems = [URLQueryItem(name: "q", value: query)]
environment.openURL(components.url!)
}
}
)
}
}