Originally asked on Swift Forums: https://forums.swift.org/t/using-bindable-with-a-observable-type/70993
I'm using SwiftUI environments in my app to hold a preferences object which is an @Observable object
But I want to be able to inject different instances of the preferences object for previews vs the production code so I've abstracted my production object in to a Preferences
protocol and updated my Environment key's type to:
protocol Preferences { }
@Observable
final class MyPreferencesObject: Preferences { }
@Observable
final class MyPreviewsObject: Preferences { }
// Environment key
private struct PreferencesKey: EnvironmentKey {
static let defaultValue : Preferences & Observable = MyPreferencesObject()
}
extension EnvironmentValues {
var preferences: Preferences & Observable {
get { self[PreferencesKey.self] }
set { self[PreferencesKey.self] = newValue }
}
}
The compiler is happy with this until I go to use @Bindable
in my code where the compiler explodes with a generic error,
eg:
@Environment(\.preferences) private var preferences
// ... code
@Bindable var preferences = preferences
If I change the environment object back to a conforming type eg:
@Observable
final class MyPreferencesObject() { }
private struct PreferencesKey: EnvironmentKey {
static let defaultValue : MyPreferencesObject = MyPreferencesObject()
}
extension EnvironmentValues {
var preferences: MyPreferencesObject {
get { self[PreferencesKey.self] }
set { self[PreferencesKey.self] = newValue }
}
}
Then @Bindable
is happy again and things compile.
Specifically the compiler errors with:
Failed to produce diagnostic for expression; please submit a bug report (Swift.org - Contributing) On the parent function the @Bindable is inside of
and with a
Command SwiftCompile failed with a nonzero exit code In the app target.
Is this a known issue/limitation? Or am I missing something here?
import Observation
import SwiftUI
struct ContentView: View {
@Environment(SomeProtocol.self) private var someThing
var body: some View {
@Bindable var thing = someThing
VStack {
TextField("Name", text: $thing.name)
}
.padding()
}
}
#Preview {
ContentView()
.environment(Actual(name: ""))
}
protocol SomeProtocol: AnyObject, Observable {
var name: String { get set }
}
@Observable
class Actual: SomeProtocol {
var name: String
init(name: String) {
self.name = name
}
}
Example project here : https://github.com/adammcarter/observable-example/
SwiftUI requires concrete types to do its job.
A protocol
does not actually conform to itself it just outlines the requirements.
SwiftUI cannot interpret the concrete type of an Environment
at runtime.
You can create your own solution using Dependency Injection.