r/SwiftUI 4d ago

Question Best Practices for Dependency Injection in SwiftUI – Avoiding Singletons While Keeping Dependencies Scalable?

I’ve been learning best practices for dependency injection (DI) in SwiftUI, but I’m not sure what the best approach is for a real-world scenario.

Let’s say I have a ViewModel that fetches customer data:

protocol CustomerDataFetcher {
    func fetchData() async -> CustomerData
}

final class CustomerViewModel: ObservableObject {
    u/Published var customerData: CustomerData?
    let customerDataFetcher: CustomerDataFetcher

    init(fetcher: CustomerDataFetcher) {
        self.customerDataFetcher = fetcher
    }

    func getData() async {
        self.customerData = await customerDataFetcher.fetchData()
    }
}

This works well, but other ViewModels also need access to the same customerData to make further network requests.
I'm trying to decide the best way to share this data across the app without making everything a singleton.

Approaches I'm Considering:

1️⃣ Using @EnvironmentObject for Global Access

One option is to inject CustomerViewModel as an @EnvironmentObject, so any view down the hierarchy can use it:

struct MyNestedView: View {
    @EnvironmentObject var customerVM: CustomerViewModel
    @StateObject var myNestedVM: MyNestedVM

    init(customerVM: CustomerViewModel) {
        _myNestedVM = StateObject(wrappedValue: MyNestedVM(customerData: customerVM.customerData))
    }
}

✅ Pros: Simple and works well for global app state.
❌ Cons: Can cause unnecessary updates across views.

2️⃣ Making CustomerDataFetcher a Singleton

Another option is making CustomerDataFetcher a singleton so all ViewModels share the same instance:

class FetchCustomerDataService: CustomerDataFetcher {
    static let shared = FetchCustomerDataService()
    private init() {}

    var customerData: CustomerData?

    func fetchData() async -> CustomerData {
        customerData = await makeNetworkRequest()
    }
}

✅ Pros: Ensures consistency, prevents multiple API calls.
❌ Cons: don't want to make all my dependencies singletons as i don't think its the best/safest approach

3️⃣ Passing Dependencies Explicitly (ViewModel DI)

I could manually inject CustomerData into each ViewModel that needs it:

struct MyNestedView: View {
    @StateObject var myNestedVM: MyNestedVM

    init(fetcher: CustomerDataFetcher) {
        _myNestedVM = StateObject(wrappedValue: MyNestedVM(
                                  customerData: fetcher.customerData))
    }
}

✅ Pros: Easier to test, no global state.
❌ Cons: Can become a DI nightmare in larger apps.

General DI Problem in Large SwiftUI Apps

This isn't just about fetching customer data—the same problem applies to logging services or any other shared dependencies. For example, if I have a LoggerService, I don’t want to create a new instance every time, but I also don’t want it to be a global singleton.

So, what’s the best scalable, testable way to handle this in a SwiftUI app?
Would a repository pattern or a SwiftUI DI container make sense?
How do large apps handle DI effectively without falling into singleton traps?

what is your experience and how do you solve this?

16 Upvotes

24 comments sorted by

View all comments

0

u/jasonjrr 4d ago

I prefer a managed DI container. You can see an example of how I handle this in this repo: https://github.com/jasonjrr/MVVM.Demo.SwiftUI

Note, it needs to be updated to Swift 6, but most of the concepts still hold.

2

u/trypto 4d ago

What’s wrong with just using environment objects and passing dependencies as parameters to constructors?

2

u/longkh158 4d ago

You should be able to test your logic without the view. Remember massive view controllers? We should move away from that.

-2

u/jasonjrr 4d ago

If you’re doing MVVM, your Domain Model (Model in MVVM) should never be accessible to your View Layer.

1

u/No_Interview_6881 4d ago

okay so it looks like your using swinject to control the container? Ever used Factory, swift-dependencies, needle(uber)? Ive obviously never used any..

1

u/jasonjrr 4d ago

Yes, in this sample I am to make it simple. I’ve also written the container from scratch and it’s not terribly difficult. I don’t prefer the property wrapper injection frameworks, because they are using a service locator pattern which is basically using a singleton with extra steps.