Closed DJBen closed 3 months ago
@DJBen You bring up a good point about root setup for iOS 13+ since we're now dealing with more objects (UIScene
and UISceneDelegate
) where we the developers don't control construction. Correct me if I'm wrong, but I think the main issue to address here is creating guidance on how constructing UIScene
and UISceneDelegate
instances fit into Cleanse.
As a quick aside, the nice thing about Cleanse is that choosing the root object is entirely up to you! 😄 If making the SceneDelegate
the root object for your dependency graph makes more sense cognitively and for your project, then definitely do that. We suggest the AppDelegate
as a potential root because it's the entry point of your application, thus "root" of your application in general. Reasoning behind this decision is so that you can more easily use the objects you bind in your dependency graph inside the app delegate for say handling push notifications, or setting up some data services on launch.
I've been playing around with scene creation and Apple's design makes it difficult to leverage DI (manually or via a framework) since they handle construction of the object and it doesn't look like there is an easy way to get an instance of your UISceneDelegate
to apply property injection (for example, storyboards control construction of view controllers, but expose the UIStoryboard.instantiateViewControllerWithIdentifier(:)
method that we can use to retrieve an instance of the VC and apply property injection on it). One could make a scene their root, but that would it hard to share the object graph across multiple scenes and with the AppDelegate
.
One possible solution that I'm not particularly proud of is to expose a ProjectInjector
instance on your AppDelegate
and have your scene delegate reach out to the AppDelegate
and apply the injections in func scene(_:, willConnectTo:, options:)
. I'd rather not expose a service container similar to Swinject
or other DI frameworks that is just a singleton bag full of objects a UISceneDelegate
instance could reach out to for dependencies. Service containers give DI frameworks a bad name IMO and make them easy to abuse.
Curious about your thoughts on how we might be able to share the object graph between scenes and the AppDelegate
. I really like the sample project you posted, and it might be worth putting it under the Examples/
dir.
I think the main issue to address here is creating guidance on how constructing UIScene and UISceneDelegate instances fit into Cleanse.
Definitely agree.
Because there will be essential initialization logic living inside of AppDelegate
(examples well illustrated in this article), it has merit to make AppDelegate
the root object of the dependency graph to initialize APIs, databases and other things that other application logic may also use later in the app launch.
The UIApplication
singleton exposes a windows
property, which should contain a list of windows created by multiple instances of UIScenes
. Given that each UISceneDelegate
holds a reference to its own window
. The UIApplication
is a singleton that could be accessed anywhere. I propose if we could somehow keep a mirroring copy of window
in AppDelegate
and link it to each window
of the UISceneDelegate
, completing the dependency graph.
After viewing the Apple's presentation given that scenes can be freely created, attached, unattached destroyed, I wonder if each scene's dependencies should be isolated into a Cleanse.Component
that ensures access safely, and also ease the management of destroying the scene because we could just destroy the entire dependency subtree by 0-reference-counting or to through some explicit destroy command to the component.
Let me know what you think. I am still new in dependency injection frameworks so forgive me if I am being too whimsical or being factually wrong.
I wonder if each scene's dependencies should be isolated into a Cleanse.Component that ensures access safely, and also ease the management of destroying the scene because we could just destroy the entire dependency subtree by 0-reference-counting or to through some explicit destroy command to the component.
Yes, scenes are a good example where subcomponents can really shine due to scoping and deinit.
The scenario I'm concerned about is how one can bind a UISceneDelegate
instance into the Cleanse DI graph when using the AppDelegate
as their root to receive dependencies. The scene delegate will likely needs objects bound in the DI graph. For example, in your sample project you inject the main view controller bound in Cleanse to then set as the window's rootViewController
. The scene delegate is also a valid place to spin up services as well so it needs a way to receive these dependencies. However, we don't control construction of the UISceneDelegate
and aren't given an easy way to retrieve an instance upon construction like we can with storyboard based view controllers.
The one way I see around this is by exposing a ComponentFactory<MyMainScene>
in the AppDelegate
that the UISceneDelegate
can reach out to via UIApplication.shared.delegate
(with some extra type casting).
Another option is essentially to add a new operator that will swizzle in a hook for after an object type is initialized, similar to how RxSwift provides sentMessage(:)
to observe on any dynamic selector. It could look something like:
binder
.bind(MyMainScreenDelegate.self)
.observeInit()
.apply { (instance: MyMainScreenDelegate, propertyInjector: PropertyInjector<MyMainScreenDelegate>) in
propertyInjector.injectProperties(into: instance)
}
We could even require specifying a property injector since by using this special operator you are saying that you have no way to control construction of this particular object and therefore must use property injection.
For a change like this we would add a lot of warning documentation around it and a preprocessor macro for those who don't want it included in their binary.
since SceneDelegate
and AppDelegate
are no longer necessary with the latest SwiftUI, is this how to inject it?.
struct AppComponent: Cleanse.RootComponent {
typealias Root = PropertyInjector<AdoptmeApp>
static func configure(binder: Binder<Singleton>) {
binder.include(module: UserData.Module.self)
binder.include(module: User.Module.self)
}
static func configureRoot(binder bind: ReceiptBinder<PropertyInjector<AdoptmeApp>>) -> BindingReceipt<PropertyInjector<AdoptmeApp>> {
bind.propertyInjector { (bind) -> BindingReceipt<PropertyInjector<AdoptmeApp>> in
return bind.to(injector: AdoptmeApp.injectProperties)
}
}
}
@main
class AdoptmeApp: App {
var userData: UserData!
var user: User!
required init() {
let propertyInjector = try? ComponentFactory.of(AppComponent.self).build(())
propertyInjector?.injectProperties(into: self)
precondition(userData != nil)
precondition(user != nil)
}
var body: some Scene {
WindowGroup {
HomeScreen(currentUser: user).environmentObject(userData)
}
}
}
extension AdoptmeApp {
func injectProperties(_ userData: UserData, _ user: User) {
self.userData = userData
self.user = user
}
}
here's the sample: https://github.com/chathil/adoptme-ios
@chathil Yes, that's how I would build your object graph using SwiftUI's @main
annotation. Sample project is great, would you like to add it under the Examples/
folder?
@sebastianv1 sure. i will create the PR ASAP.
Thanks @chathil
Your's example looks great. I have tried to run and saw that it worked properly.
However, I have the question, could you please review it also ?
My concern is how to inject UserData and User object at HomeScreen directly, not passed as property and environment object as in the example ?
@hieunc278 This issue might not be the right place to ask this question? and i don't really understand your question. Do you want to make HomeScreen as the entry point for Cleanse?, or maybe you want to inject both variables via the constructor like this?
HomeScreen(currentUser: user, userData: userData)
i'm not doing any property injection on HomeScreen, only on AdoptmeApp because i don't have access to perform constructor injection. About the environment object that is because i'm using UserData inside child view that's quite deep.
Dear @chathil
Thanks for your feedback.
Sorry if I made you confuse, however, I mean that could we use User and UserData as property injection for HomeScreen as AdoptMeApp ? So we have not to pass UserData via Environment object.
Cleanse is now deprecated. Please read the deprecation notice on the README for next steps. Thank you!
Hi 👋 as iOS app is moving towards multi-window support from iOS 13 an onwards, the example that injects
window
to theAppDelegate
no longer the recommended way. Given that the rising use case of SwiftUI, I made an example that supports SwiftUI and usesSceneDelegate
as the root object of the injection.https://github.com/DJBen/SceneDelegateInjectionExample
I am still unconvinced if
AppDelegate
still should be the root object, but doing so is difficult - the lifecycle ofAppDelegate
andSceneDelegate
seem to be opaque and they do not seem to be dependent on each other. Let me know if I was wrong, but as of today I think making theSceneDelegate
root object that instantiates the window makes more sense.