Search code examples
swiftswiftuiobservation

Using @Bindable with a Observable type in SwiftUI


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?

Example code:

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/


Solution

  • 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.