hmlongco / Factory

A new approach to Container-Based Dependency Injection for Swift and SwiftUI.
MIT License
1.71k stars 107 forks source link

Support @Observable macro #120

Closed emadhegab closed 7 months ago

emadhegab commented 1 year ago

in a model where i use @Observable macro ,

@Observable
class SplashScreenViewModel {
    var presented: Bool = false

    @Injected(\.loginService) private var loginService: LoginServicing

i get 2 errors at the Injected vars

@Observable requires property 'loginService' to have an initial value (from macro 'Observable') Property wrapper cannot be applied to a computed property

hmlongco commented 1 year ago

This is an issue with @Observable.

"In the current implementation, the @Observable macro requires that all stored properties have a default value. This helps observable types rely on definitive initialization, use the implicitly generated initializers, and define additional initializers in extensions."

You should, however, be able to get past the issue by doing something like...

    @ObservationIgnored @Injected(\.loginService) private var loginService: LoginServicing

See: https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro

Also keep in mind that if you use @Observable your application can only run on iOS 17 and higher.

joeljfischer commented 8 months ago

I did figure this out, even before seeing this, but I'm seeing a new issue that I wasn't having with ObservableObject. My SwiftUI previews where I inject a preview object like:

struct WeatherAgeFooterView_Previews: PreviewProvider {
    static var previews: some View {
        let _ = Container.shared.weatherManager.register { WeatherManagerPreview() }

        WeatherAgeFooterView()
    }
}

is no longer working. In all my previews, I now get errors because they are running the Impl objects in my ViewModels instead of the preview objects I'm registering. This doesn't happen if I switch back to ObservableObject (which I really don't want to do because of the performance benefits to @Observable).

EDIT: I'd also be interested in a documentation section on how to get Factory injection like the above working with #Preview...if that's even possible.

hmlongco commented 8 months ago

I think your problem is elsewhere, as the following works...

import Factory
#if canImport(Observation)
import Observation
#endif
import SwiftUI

@available(iOS 17, *)
protocol ObservationServiceType: AnyObject {
    var name: String { get set }
}

@available(iOS 17, *)
@Observable
class ObservationService: ObservationServiceType {
    var name: String = "ObservationService"
}

@available(iOS 17, *)
@Observable
class MockObservationService: ObservationServiceType {
    var name: String = "MockObservationService"
}

@available(iOS 17, *)
extension Container {
    var observableService: Factory<ObservationServiceType> {
        self { ObservationService() }
    }
}

@available(iOS 17, *)
struct ObservableView: View {
    @Injected(\.observableService) var observableService
    var body: some View {
        HStack {
            Text(observableService.name)
            Spacer()
            Button("Mutate") {
                observableService.name += " *"
            }
        }
        .padding()
    }
}

@available(iOS 17, *)
#Preview {
    let _ = Container.shared.observableService.register { MockObservationService() }
    return ObservableView()
}
joeljfischer commented 8 months ago

Okay, I tested this myself, and you're correct, it works. I have no idea why my previews are now using the default Impl versions and not preview versions of my services, but it doesn't appear to be what I thought it was.

cneuwirt commented 6 months ago

Are there any plans to support @Bindable with @Injected wrapper so bindings are available for use in Publisher Combinators

Initial comment