Environments Vs DI Framework-Based Injections In SwiftUI

Environments Vs DI Framework-Based Injections In SwiftUI

By Balraj Verma, Author (iOS Question Bank)

Dependencies can be a crucial part of an application, making their management quite challenging. Teams often debate how to manage dependencies in SwiftUI, especially when it comes to environment injections versus initializer injections (using third-party frameworks). While SwiftUI provides an elegant, practical, and most importantly, simple way to manage environment injections with a low learning curve, it’s important to consider whether using DI framework-based injections is appropriate for your specific use cases.

DI is future-you saying thank you to present-you.

We will go over some important use cases that might be very useful if someone needs to decide whether to use the DI framework or the system-provided injection techniques (i.e., environments) in order to accomplish their objectives. Let’s get started.

However, before we go, we shall comprehend a few crucial use scenarios where they must be given with extreme caution. I’m sharing a few use cases that I’ve observed, but if you have any that you may have experienced, please leave a comment so that everyone who is reading can benefit from this case study.

DI via Frameworks

  • DI could be a fantastic tool for injecting services between modules in a multi-module design where each module functions as a separate package and can be loosely connected.
  • Where modules are required to inject any global objects, including network managers, database managers, and objects from the Common Utilities class. When third-party frameworks offer scopes for resolved services, such as app, shared, or module scopes, it can be highly beneficial to comprehend the scope and adjust your dependencies accordingly.
  • It could be quite beneficial to use a third-party framework to mock up your functionality because you don’t have to worry about a lot of things yourself—just register and resolve your dependencies/services.

What challenges could arise when using them for everything?

  • If dependencies are not filled at compilation time, like dagger for JVM does, it could be quite dangerous. Run-time surprises are possible. Additionally, tracking runtime dependencies can occasionally be quite challenging. Remember that if it generates code during compilation, it will function.

Shall SwiftUI views be resolved through a DI system or framework?

SwiftUI views are heavily used for navigation and UI-related responsibilities such as presenting alerts, sheets, and flows. This raises an important architectural question: should views themselves be resolved via a dependency injection (DI) system?

Another related concern is modularization. If you have a use case where a view needs to be presented from another module, what is the better approach?

  • Using a DI container to resolve views, or
  • Implementing a custom view resolver or registry?

Personally, I’m not a big fan of resolving views through DI containers. While DI frameworks are excellent for managing business logic dependencies, pushing everything — including views — into a DI system can introduce hidden implementations. This often leads to confusion, especially for new developers trying to understand where a view is coming from or how it is constructed.

Instead of registering views in a general-purpose DI framework, I prefer having a dedicated view registry or view resolver specifically designed for view composition and navigation. This keeps view creation explicit and easier to reason about while still allowing flexibility across modules.

This approach also helps avoid turning the DI framework into a god object—responsible for resolving every dependency in the system. By clearly separating concerns (business dependencies vs. UI composition), the architecture remains more readable, maintainable, and developer-friendly.

Let’s talk about SwifUI’s Environments.

When we discuss environments, it’s crucial to have a clear understanding of their intended use. Here are some common use cases and potential issues that can arise when using them without proper guidance.

  • If environments are injected in the middle of views, they will remain in memory until the entire stack or the root view of that stack is deallocated
  • If we are using Sheets/Fullcover modals, it is better to use anything using environments so that the injected objects will be freed if they are dismissed and the application will have smaller memory footprints
  • I typically opt for DI-based injections between my stack-based push and pop operations. Additionally, whenever I present sheets, I employ environments, even for functional objects. And, yes, for Views I don’t use DI frameworks; I prefer my own view registry service or resolve them via protocols.

What problems may we now observe when utilizing environments?

  • Environment objects are not automatically inherited by sheets, alerts, and other modals. They need to be manually injected.
  • See the figure below for Invisible Coupling:
RootView
 └── TabView
     └── NavigationStack
         └── ListView
             └── DetailView
                 └── SubDetailView
                     └── ComponentView
  • If ComponentView needs an environment object, you must inject it at RootView even if nothing in between uses it. This creates invisible coupling across your entire app.

  • Using within a multiple-window-based app, where each window may or may not have the same environment. Changes to one window affect all others, which may or may not be beneficial.

Best practices for environments

  • Use environment for truly global state only—theme, locale, accessibility settings etc.
  • Use initializers for business logic dependencies—services, view models, Network data source, and repositories layers

Always pass environment objects to sheets/modals explicitly

  • Document environment requirements—At a minimum, add comments describing what a view expects from its environment. This helps everyone understand why certain design decisions were made and ensures that anyone modifying the view, model, or class is aware of these assumptions and constraints.

Key Takeaways

  • DI containers and SwiftUI’s environment are competing systems—they both want to manage the lifecycles.
  • Choose the right tool for the job: Use SwiftUI’s environment for view-level dependencies and limit DI containers to functional layers where they excel.
  • Silent failures are expensive: Misusing either system can cause production bugs that don’t crash—they just produce wrong results, making them harder to detect and fix.

The harsh truth: If you’re fighting to make a DI container work in SwiftUI, you’re fighting the framework itself. SwiftUI was designed around explicit dependency passing and the environment system, not service locator patterns.

Back to blog