hmlongco / Factory

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

Issue with factory registrations in preview #148

Closed ekohlwey closed 1 year ago

ekohlwey commented 1 year ago

Hi there, I'm working on adding an example using Factory to the bazel rules_swift_package_manager project. I'm having an issue where factory doesn't seem to process the registrations I've added in the preview. I believe my usage is consistent with the docs. I've created a branch and will link relevant sections below. The dependency version is 2.2.0.

The preview itself see patch is using the same Container.shared.fooService.register { LocalFooServiceProxy(..) } type syntax as is described in the docs.

The Container extension see patch also seems to be according to docs.

Finally the resolution seems appropriate as well see patch.

I've tried adding some debug print messages to the code for my example as well as Factory and was able to help me debug whats happening. The print statements were added to Registrations.swift#register() and Registrations.swift#resolve(), to show the contents of the registration map while calling register() and resolve(). The logs are below:

["fooService<Client_Client.FooServiceProxy>": Factory.TypedFactory<(), Client_Client.FooServiceProxy>(factory: (Function))]
[:]
["fooService<Client_Client.FooServiceProxy>": Factory.TypedFactory<(), Client_Client.FooServiceProxy>(factory: (Function))]
[:]
Getting foo text
Getting remote foo
Getting foo text
Getting remote foo

The logs show that actually during the call to register(), the registration map is indeed populated. But by the time resolve() is called, the registration map has somehow been cleared. The remaining lines in the log simply show that indeed the default implementation is being invoked instead of the intended (mocked) implementation during preview.

I'm unsure how the registrations map is getting cleared and have been unable to make progress on debugging the issue.

Any guidance you have is very appreciated!

hmlongco commented 1 year ago

A couple of things. First ContentView's content property should probably be a StateObject, not State, since Content() is an observable object.

Second you're doing....

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    Group{
      let _ = Container.shared.fooService.register { LocalFooServiceProxy(fooName: "bar", blockReturn: false) }
      ContentView(content: Content())
      let _ = Container.shared.fooService.register { LocalFooServiceProxy(fooName: "bar", blockReturn: true) }
      ContentView(content: Content())
    }
  }
}

While the doc shows...

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            let _ = Container.shared.myService.register { MockServiceN(4) }
            let vm1 = ContentViewModel()
            ContentView(viewModel: vm1)

            let _ = Container.shared.myService.register { MockServiceN(8) }
            let vm2 = ContentViewModel()
            ContentView(viewModel: vm2)
        }
    }
}

Creating vm1 on a separate line is important since the passed value is thunked. Do it as shown in the first example, and ContentView() won't be evaluated until later when the view is rendered.

ekohlwey commented 1 year ago

I'm still experiencing the issue with the following setup:

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    Group{
      let _ = Container.shared.fooService.onPreview { LocalFooServiceProxy(fooName: "bar", blockReturn: false) }
      let content1 = Content()
      ContentView(content: content1)
      let _ = Container.shared.fooService.onPreview { LocalFooServiceProxy(fooName: "bar", blockReturn: true) }
      let content2 = Content()
      ContentView(content: content2)
    }
  }
}

I also tried setting up the app with a similar injection scenario, with the same outcome.

@main
struct TestApp: App {
    var body: some Scene {
      let _ = Container.shared.fooService.register{ LocalFooServiceProxy(fooName: "baz", blockReturn: false)}
      let content = Content()
      WindowGroup {
        ContentView(content:content)
      }
    }
}

When I tried to step through this, I found that the compiler had optimized a lot of the calls out and that the debugger was somewhat uninformative about what was happening.

I do see the Factory library itself has tests that I think are equivalent code as well, so I'm quite confused about whats happening and really do appreciate your patience looking into this.

One idea I'm wondering about is that the first access to the Container variable happens inside a Task. If the compiler is lazily evaluating these (what I think you mean by Thunking above?), perhaps this is a shared memory access issue (like it needs a volatile or a map that uses atomic access)? Do you think that might be the source of the issue? In Containter.swift from my patch, eg.:

class Content: ObservableObject {
  @Published var text:String?
  let fooServiceProxy = Container.shared.fooService()
  var valueUpdater: Any?
  init() {
    Task {
      print("Getting foo text")
      text = await self.fooServiceProxy.getFoo()
      print("Got foo text")
    }
    valueUpdater = $text.sink { value in
      guard let value else { return }
      Task {
        await self.fooServiceProxy.setFoo(foo:value)
      }
    }
  }
}

Thanks so much for your input on this!

hmlongco commented 1 year ago

I verified the approach mentioned above in the FactoryDemo app's content view. (from develop)

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            let _ = Container.shared.myServiceType.onPreview { MockServiceN(44) }
            let model1 = ContentViewModel()
            ContentView(model: model1)
            let _ = Container.shared.myServiceType.onPreview { MockServiceN(88) }
            let model2 = ContentViewModel()
            ContentView(model: model2)
        }
    }
}

If you wanted to build a minimal standard Xcode project that demonstrates the problem I could download and look at it, but I'm not sure the would help with your build system.

Other than that I'm not quite sure there's a lot more I can do with this, since Factory seems to be working as designed.

One thing I might mention, however, is that when you run tests or previews Xcode has to run part of your app. As such you need to make sure that other processes aren't interfering.