hmlongco / Factory

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

Question: How do I achieve scene scoped dependencies #182

Closed notapplicableio closed 2 months ago

notapplicableio commented 10 months ago

My app supports scenes and I would like to resolve dependencies based on scope.

Can anyone recommend how to do this?

I think I could achieve this using a custom container. Or maybe a parameter.

doozMen commented 10 months ago

You cannot as it is a singleton pattern and not tree based. SwiftUI and scenes work tree based so you would need a tree based structure not one as factory is that relies on a singleton as this will work in strange ways.

The current workaround I found is to use the context feature on Factory to have different registrations per scene. Then pass the scene to the environment.

public struct SceneKey: EnvironmentKey {
  public static let defaultValue: String = "mainScene"
}

extension EnvironmentValues {
  public var scene: String {
    get { self[SceneKey.self] }
    set { self[SceneKey.self] = newValue }
  }
}

extension Container {
  var someValue: Factory<String> {
    self { "no scene value" }
      .context(.arg("mainScene")) { "main scene value" }
      .context(.arg("scene2")) { "scene 2 value" }
  }
}

struct ContentView: View {
  @Environment(\.scene) var scene

  var body: some SwiftUI.View {
    View(scene)
  }

 struct View: SwiftUI.View {
   let value: String

   init(_ scene: String) {
     FactoryContext.setArg(scene, forKey: "your.app.scene")
     value = Container.shared.someValue()
   }

   var body: some SwiftUI.View {
     Text("on scene \(value)")
   }
 }
}
doozMen commented 10 months ago

But If I am really honest too myself I think factory which relies on a singleton to resolve the values is in its current state fighting with the way SwiftUI passes values via the tree of the views to its children. The problem is also specific to SwiftUI as it requires the environment to be looked for in the tree of views, so if you need a value on your viewModel it is not possible with a view based structure.

If you would take inspiration from what Kotlin does there they use a concept CompositionLocal that can be used on any object. It works similarly to how @Environment works but on any object. Would be cool if Factory could support something like that for SwiftUI. But I have no idea if that is achievable without acces to SwiftUI code base...

hmlongco commented 9 months ago

I'd have to think about this, but the key to the issue would probably focus around some mechanism that allows Container.shared to return a different container for each scene.

doozMen commented 9 months ago

I'd have to think about this, but the key to the issue would probably focus around some mechanism that allows Container.shared to return a different container for each scene.

That would be the ideal scenario yes. Would be awesome if this could happen. Maybe you could start a discussion to start thinking about this scenario?

Would you need more use cases? Anyway how can we help with this?

hmlongco commented 8 months ago

One comment on this is that, by and large, one should be injecting services and not data.

doozMen commented 8 months ago

@hmlongco the question of @notapplicableio is valid! How to deal with tree based architectures. You seam to also have the answer build into your library. You can create a ManagedContainer that does not rely on the singleton approach. This container in SwiftUI you can inject per scene.

Your remark however about the data and services is shortsighted and unrelated. It relates to my earlier questions. It is unrespectfull and does not help your library any further. Your constant neglect to respond properly to the issue is simply uncreative.

To answer the question about using Factory with a tree I came up with the following way to use Factory as is leveraging ManagedContainer. Just keep in mind that I recommend not to use Factory and just stick to SwiftUI own injection system with environment. But if you like Factory here is how you can acheive scene based dependency injection with Services. @notapplicableio

import SwiftUI

@main
struct DemoSingleThemeApp: App {
  let c1 = CustomContainer()
  let c2 = CustomContainer()

  init() {
    c1.registrations.service.register { Service(response: "c1 response") }
    c2.registrations.service.register { Service(response: "c2 response") }
  }

  var body: some Scene {
    WindowGroup {
      VStack {
        ContentView()
          .environment(\.container, c1)
        ContentView()
          .environment(\.container, c2)
      }
      .environment(\.theme, TC.shared.theme())
      .environment(\.subTheme, TC.shared.subTheme())
    }
  }
}

The container setup contains a little trick to avoid the callAsFunction requirement via dynamic member lookup but for the rest it is a normal Factory container that you inject into the tree top down, making it possible to have different container instances per scene.

To make this work you need to

  1. Create EnvironmentValues key and default values
  2. inject them into the swiftUI tree from the application
  3. overwrite the environment down the tree or scene

First the EnvironmentValues setup

struct CustomContainerKey: EnvironmentKey {
  static let defaultValue: CustomContainer = CustomContainer()
}

extension EnvironmentValues {
  var container: CustomContainer {
    get { self[CustomContainerKey.self] }
    set { self[CustomContainerKey.self] = newValue }
  }
}

The custom container with syntactic sugar for @dynamicMemberLookup to avoid the unwrapped type to be Factory<CustomContainer> but just CustomContainer

@dynamicMemberLookup
struct CustomContainer {
  final class Registrations: ManagedContainer {
    let manager = ContainerManager()
    var service: Factory<Service> { self { Service(response: "default response") } }
  }

  let registrations = Registrations()

  subscript(dynamicMember member: KeyPath<Registrations, Factory<Service>>) -> Service {
    self.registrations[keyPath: member]()
  }
}

Tip: If you are using UIKit consider looking into UIEnvironment to inject into a viewController tree https://github.com/nonameplum/UIEnvironment

To conclude I found it very frustrating and at moments rude to negotiate with you @hmlongco and we cannot construct a business relation if you cannot respond to me or to other people more creative and respectful. Considering their perspective and knowledge. I will stop my sponsorship and wish you all the best! (unfortunately I took a yearly obligation so enjoy it until august)

hmlongco commented 8 months ago

@doozMen Sorry you feel that way. The comment was largely an aside I made while working on the project this last weekend and wasn't meant to be disrespectful in any way.

That said, Factory is an open source project that's provided to the Swift community gratis, and one created after I already had Resolver available. It's a project on which I've already spend a great deal of time coding, debugging, testing, and documenting. And then improving again. And again.

But it isn't my day job, and any work I do on the project is on my own personal time.

Feature requests, bugs, and other issues are problematic as those typically require a even greater amount of time and effort, and what time I've had to work on Factory recently has been spent more on the issues around frameworks and Sendable conformance.

So while I try to answer questions as best I can, those answers have to be fitted into my schedule and, quite frankly, into my life. Fortunately other people have jumped in from time to time to answer questions and that's been appreciated as well.

That includes proposing solutions like the one above.

Speaking of which, I've considered using the environment before, but that solution only works as long as you're using Factory (or ANY container, for that matter) in a MV pattern.

It tends to break down in an MVVM scenario as environment variables are only available during body evaluation and as such there isn't really a good way to pass the container up into the VM when needed. Especially if you're creating StateObjects or view models during view initialization.

So while it's a solution, I'm not sure it's a workable solution for everyone who uses SwiftUI.

I'm also not sure that you're not going to run into another environment issue as I suspect that it's not propagated exactly how you expect.

At any rate, sorry to see you go. Your support was appreciated, regardless of what you might think.

doozMen commented 8 months ago

I do not think anything. I just have a proposal to work with you on this subject. Suggested a discussion on the subject. I think you did a grate job. I just do not like you simply downgrading my and other input in this way. I make simple suggestions and you seam to question my knowledge. Downgrade what I say, again now when you imply MVVM, which I did not implement in the example above.

It is not fun that you again seam to suggest that environment does not work. I do not know why you say this as the code above works with observable state included.

So instead of putting difficult words around other solutions maybe ask for a demo project. Maybe we have a different style and that is not a match but this is not the way I hoped this would go. I would have hoped that we could discuss approaches with more people in the swift community and I thought you had a nice position with factory to adres that need.

Please also remove me from the readme now, do not wait until august. Thanks for your enormous effort. I know it is not your day job. I did not ask for your input or implementation. I asked for a discussion on how to handle tree based structures like jetpack compose and swiftui with Factory. Which I still find a valid question.

Downgrading my code examples to mvvm or other unrelated things just does not add value to the debate.

Good luck regardless and thanks for all your open source work and super nice blog posts that I have enjoyed and still will enjoy.

notapplicableio commented 8 months ago

@doozMen You may not realise this but the tone and wording of your responses above come across as angry and reactive. I don't think this was necessary or appropriate for this forum or this thread. Your responses have made me feel quite uncomfortable.

doozMen commented 8 months ago

@notapplicableio @hmlongco now it feels all against me. I just tried to make a point and clearly failed. We can just agree to disagree then. I do agree this forum is not the right place to resolve or for this kind of discussions. So my apologies for that.