Here, I need to make a request using a GRPC client that shall be passed into the APIClient upon initialization.
How can I do so and access that value in my staticliveValue field?
Expected Result:
struct APIClient {
var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
}
extension APIClient: DependencyKey {
static let liveValue = Self(
sendEmailOTP: { request in
// try! await Task.sleep(for: .seconds(1))
let result = grpcClient.makeAPICall(....)
return SendEmailOTPResponse(
success: nil,
error: .invalidEmail
)
}
)
}
And the usage of this API client in a reducer is as follows
@Reducer
struct MyFeature {
@Dependency(\.apiClient) var apiClient: APIClient
...
}
You could introduce a @Dependency(\.grpcClient) and access it there. If the GRPCClient requires initial configuration you can use prepareDependencies at the entry point of your app:
@main
struct MyApp: App {
init() {
prepareDependencies {
$0.grpcClient = GRPCClient(/* ... */)
}
}
var body: some Scene { /* ... */ }
}
If you don't want layers of dependencies here, your APIClient could only conform to TestDependencyKey and you would do the same prep you need to do in liveValue but at the app entry point:
@main
struct MyApp: App {
init() {
prepareDependencies {
// Custom initializer that uses the client to set up each endpoint
$0.apiClient = APIClient(grpcClient: grpcClient)
}
}
var body: some Scene { /* ... */ }
}
While that shoud work,
but I won't be able to access the client inside the static let liveValue field.
Any suggestions on how can I do so?
That's the end goal.
In the tutorial, such as the basic Counter app, they have this NumberFactClient
struct NumberFactClient {
var fetch: (Int) async throws -> String
}
extension NumberFactClient: DependencyKey {
// A value used when your feature is run in simulators and devices,
// and it’s the place where it is appropriate to make live network requests
static let liveValue = Self(
fetch: { number in
let (data, _) = try await URLSession.shared.data(
from: URL(string: "http://numbersapi.com/\(number)")!
)
return String(decoding: data, as: UTF8.self)
}
)
}
// This is what allows for the syntax @Dependency(\.numberFact) in the reducer.
extension DependencyValues {
var numberFact: NumberFactClient {
get { self[NumberFactClient.self] }
set { self[NumberFactClient.self] = newValue }
}
}
Here, they're using URLSession.shared.data so it's easy... but I want to inject the GRPC client as opposed to accessing it via singleton
Could you capture the client inside of the APIClient closures to maintain that strong reference and then use that for the rest of the app?
struct APIClient {
var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
// other initializers go here...
/// Initialize the API Client backed by the GRPCClient.
init(grpcClient: GRPCClient) {
self.init(sendEmailOTP: { request in
let result = grpcClient.makeAPICall(....)
return SendEmailOTPResponse(
success: nil,
error: .invalidEmail
})
}
}
Edit: just realized this was pretty much Stephen's suggestion, but that's what the implementation could look like.
From my understanding the intent of the Dependencies package was that all dependencies had some default implementation (the "live" value of the UUIDGenerator for example was just UUID()) but they introduced prepareDependencies to allow you to globally introduce dependencies into your app that don't have a "default" implementation but need some initial configuration. The only quirk now is that you still have to define the liveValue of your DependencyKey.
In my app my default liveValue will, by default, assert a preconditionFailure but I don't know if that's the most recommended pattern to use. In the SharingGRDB framework the pointfree guys use an in-memory database but report what is going on to the developer. Just some ideas of what you could use as a default for your APIClient before it's initialized.
lxalfonso hopefully this would be my last question. I've went through the resoruces but I really need a single answer to my question. Perhaps stephencelis can also answer this.
So how should I be defining my APIClient?
This is my original APIClient
import ComposableArchitecture
import Foundation
import GRPC
struct APIClient {
var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
var loginWithEmailOTP: (LoginWithEmailOTPRequest) async -> LoginWithEmailOTPResponse
}
extension APIClient: DependencyKey {
static let liveValue = Self(
sendEmailOTP: { request in
return SendEmailOTPResponse.invalid() // errorMessage: "APIClient's sendOTP must be defined in the constructor"
},
loginWithEmailOTP: { request in
return LoginWithEmailOTPResponse.empty()
}
)
}
extension DependencyValues {
var apiClient: APIClient {
get { self[APIClient.self] }
set { self[APIClient.self] = newValue }
}
}
As per the suggestions (from my understanding), I should define my functions inside the init?
struct APIClient {
let grpcClient: MyGRPCManager
var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
var loginWithEmailOTP: (LoginWithEmailOTPRequest) async -> LoginWithEmailOTPResponse
init(grpcClient: MyGRPCManager) {
self.grpcClient = grpcClient
self.sendEmailOTP = { request in
let result = grpcClient.process(request)
// process it...
return SendEmailOTPResponse(...)
}
self. loginWithEmailOTP = { request in ..... }
}
}
extension APIClient: DependencyKey {
static let liveValue = Self(grpcClient: MyGRPCManager.mock())
}
And then the usage would be
@main
struct MyApp: App {
init() {
prepareDependencies {
$0.apiClient = APIClient(grpcClient: GRPCClient(/* ... */))
}
}
var body: some Scene { /* ... */ }
}
You usually don't want to define dependencies in terms of other dependencies. That is, APIClient shouldn't capture a MyGRPCManager directly. Instead, the live instance should grab it as a dependency.
static var liveValue: Self {
@Dependency(\.grpcClient) var grpcClient
return Self {
// Use client and other network calls.
} loginWithEmailOTP: {
//...
}
}
You can also call the dependency inside each closure, not sure if it matters.
Then you would prepare them at app launch.
@main
struct MyApp: App {
init() {
prepareDependencies {
$0.grpcClient = GRPCClient...
$0.apiClient = APIClient...
}
}
var body: some Scene { /* ... */ }
}