I'm using the latest Observation framework with SwiftUI introduced at WWDC 2023. It seems that the new @Environment
property wrapper does not allow to use protocols. Here is what my service implementations looks like:
protocol UserServiceRepresentation {
var user: User { get }
}
// Service implementation
@Observable
final class UserService: UserServiceRepresentable {
private(set) var user: User
func fetchUser() async { ... }
}
// Service mock
@Observable
final class UserServiceMock: UserServiceRepresentable {
let user: User = .mock
}
Previously, it was possible using @EnvironmentObject
to declare a protocol so you can pass your mocked service as your environment object to the view.
struct UserView: View {
// This is possible using EnvironmentObject.
// But it require UserService to conforms to ObservableObject.
@EnvironmentObject private var userService: UserService
var body: some View { ... }
}
#Preview {
UserView()
.environmentObject(UserServiceMock()) // this allows your previews not to rely on production data.
}
Now, with @Environment
, I can't do something like this:
struct UserView: View {
// This is not possible
// Throw compiler error: No exact matches in call to initializer
@Environment(UserService.self) private var userService
var body: some View { ... }
}
I would have been nice to inject my mocked user service like so:
#Preview {
UserView()
.environment(UserServiceMock())
}
Am I missing something or doing it the wrong way? It seems that the new @Environment
property wrapper was not built for this kind of usage. I can't find anything about it on the web. Thanks in advance for your help.
No, the @Environment
initialisers take a concrete type that is Observable
, and there is nothing wrong with keep using EnvironmentObject
and ObservableObject
. If it ain't broke, don't fix it.
If you really like @Observable
for some reason, you can create a custom environment key that stores the user service. The value for the environment key can be an existential protocol type.
struct UserServiceKey: EnvironmentKey {
// you can also set the real user service as the default value
static let defaultValue: any UserServiceRepresentation = UserServiceMock()
}
extension EnvironmentValues {
var userService: any UserServiceRepresentation {
get { self[UserServiceKey.self] }
set { self[UserServiceKey.self] = newValue }
}
}
Then you can call the Environment
initialiser that takes a keypath.
@Environment(\.userService) var service
.environment(\.userService, someService)
Note that if the instance of UserServiceRepresentation
is not Observable
, this will not work. It might be better to require an Observable
conformance.
protocol UserServiceRepresentation: Observable