DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Why I Started Using Dependency Injection in Python
  • How to Make a Picture-in-Picture Feature in iOS App Using AVFoundation
  • Angular Best Practices For Developing Efficient and Reliable Web Applications
  • HLS Streaming With AVKit and Swift UI for iOS and tvOS

Trending

  • AI's Dilemma: When to Retrain and When to Unlearn?
  • Unlocking Data with Language: Real-World Applications of Text-to-SQL Interfaces
  • How the Go Runtime Preempts Goroutines for Efficient Concurrency
  • How to Practice TDD With Kotlin
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. SwiftData Dependency Injection in SwiftUI Application

SwiftData Dependency Injection in SwiftUI Application

Refactoring the default SwiftData + SwiftUI project example. Removing SwiftData from SwiftUI and replacing Environment with a vanilla Dependency Injection.

By 
Aleksei Pichukov user avatar
Aleksei Pichukov
·
Sep. 27, 23 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
5.4K Views

Join the DZone community and get the full member experience.

Join For Free

Most of the examples Apple provides to demonstrate Dependency Injection in SwiftUI use @Environment. When creating a new project with SwiftData in XCode, you'll notice that the template uses Environment for injecting the modelContext.

Swift
 
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext // <-- 1
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                ...
            }
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        ...
                    }
                }
            }
        }
        ...
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem) // <-- 2
        }
    }

    ...
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true) // <-- 3
}


But what exactly is wrong here? For simple applications, nothing may be amiss, and this approach should function perfectly. However, when working on a rapidly growing application with a large codebase and multiple developers involved, we may encounter some scaling issues. Let's take a closer look at our current setup:

  1. We use the @Environment property wrapper to gain access to the Environment context of the View. Considering Environment as a sort of Dependency Injection (DI) container for the entire View hierarchy, we can place any dependencies at a high level and retrieve them in any other view below (modelContext), making dependency injection significantly simpler in SwiftUI.
  2. We use modelContext, which we just retrieved from Environment, to insert a new item.
  3. We are setting up a modelContainer with the given type available in the scope of our View.

Let's try to challenge this approach:

  • I will start by addressing the main issue that we often see in Apple-provided examples: combining UI and Business Logic in the same function. In our addItem function, we are doing something that our View should not be aware of. We are violating the Single Responsibility Principle by having the View handle both UI and business logic when, ideally, the View should only be responsible for handling user input and notifying another object about it. The specific actions taken by the other object with this information should not be the responsibility of the View.
  • We are working at the implementation level, not at the interface level. The modelContext is an instance of ModelContext, a class and not a protocol, which means we have direct access to the object instance. As such, we can directly manipulate the object's properties and methods without any abstraction or indirection. For example, if we need to retrieve this data from the network or use another data provider in the future, we will need to update our View implementation accordingly.
  • We do not control the lifecycle of the modelContext.
  • We assume here that the data we store is the same data we want to present, which is why we have @Query private var items: [Item] that will automatically trigger a UI update when we add a new item to the list. However, this is not always the case. You may store additional information that you use to prepare a final result for the user, and your UI may not always be an exact reflection of your data schema.

Target Solution

Let's try to think about how we can refactor it in a way that solves all the issues listed above.

Here is our current diagram:

current diagram

And let's take a look at the diagram we want to achieve:

the diagram we want to achieve

Separate Data Layer

First of all, let's separate the data layer from the view and make it a bit more generic. I will add a protocol called DataProvidable and a DataProvider class that will sit behind this protocol. As we all know, naming is one of the most challenging aspects of software development, so perhaps DataProvidable is not the best name, but I've chosen it because it does exactly what its name suggests — provide data.

Swift
 
protocol DataProvidable: AnyObject {
    func getItems() throws -> [Item]
    func set(item: Item) throws
}


Let's also create a separate class for the model that we will store in persistent storage:

Swift
 
@Model
final class ItemModel {

    var timestamp: Date

    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}


The model that we will use in SwiftUI and our ViewModel is the following:

Swift
 
struct Item: Identifiable {
    let id: UUID = UUID()
    let timestamp: Date
}


Now, we have a good foundation to move forward with the solution, and the view and view model are decoupled from the way we are storing and providing data. Since we have a protocol for our data layer, let's implement something behind it:

Swift
 
class DataProvider: DataProvidable {

    private var context: ModelContext

    init(context: ModelContext) {
        self.context = context
    }

    func getItems() throws -> [Item] {
        let items = try context.fetch(FetchDescriptor<ItemModel>())
        return items.map { Item(timestamp: $0.timestamp) }
    }

    func set(item: Item) throws {
        context.insert(
            ItemModel(timestamp: item.timestamp)
        )
        try context.save()
    }
}


We get injected with a ModelContext, and we do not expose it to the outside world. So, the DataProvider only knows how we store the data and where we obtain it from. Another benefit is that it allows us to create a very simple MockDataProvider that can be used in SwiftUI previews:

Swift
 
class MockDataProvider: DataProvidable {

    func getItems() throws -> [Item] {
        return [
            Item(timestamp: Date())
        ]
    }

    func set(item: Item) throws { }
}


View and ViewModel

Since we have our data layer and have mentioned the ViewModel several times, let's finally create it:

Swift
 
@Observable
final class ViewModel {

    var items: [Item] = []
    private let dataProvider: DataProvidable

    init(dataProvider: DataProvidable) {
        self.dataProvider = dataProvider
        do {
            items = try dataProvider.getItems()
        } catch {
            // Error handling
        }
    }

    func onAdd() {
        let item = Item(timestamp: Date())
        do {
            try dataProvider.set(item: item)
            items.append(item)
        } catch {
            // Error handling
        }
    }
}


We do not use the exact DataProvider type here, but the protocol gives us the ability to inject anything that will be behind it and work on the interface level, not the implementation. For example, with such an implementation, we can easily cover this ViewModel with unit tests in a fully isolated environment.

Let's also update our view by utilizing everything we've previously created:

Swift
 
struct ContentView: View {

    private let viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        NavigationSplitView {
            List {
                ...
            }
            .toolbar {
                ToolbarItem {
                    Button(action: viewModel.onAdd) {
                        ...
                    }
                }
            }
        }
        ...
    }
}

#Preview {
    ContentView(
        viewModel: ViewModel(
            dataProvider: MockDataProvider()
        )
    )
}


As you can see, we no longer have a SwiftData dependency in the View. We are also using our MockDataProvider to make the preview work, and we can play with the data there the way we want to present any kind of data in the SwiftUI preview. There is no business logic in the View anymore. All we do is just call the onAdd function from the ViewModel.

How To Use It in the App Now

Currently, our implementation of View takes an instance of ViewModel injected. Our View + ViewModel is now a standalone, isolated component that can be used anywhere in the app. However, for the sake of completeness, let's explore one architecture option that we could use in the application.

First, let's create a Coordinator that will be responsible for providing a View that is currently being presented in the app:

Swift
 
@Observable
final class Coordinator {

    var rootView: AnyView = AnyView(EmptyView())

    private var modelContainer: ModelContainer?

    init() {
        Task { @MainActor in
            setUpView()
        }
    }

    @MainActor
    private func setUpView() {
        guard let modelContainer = try? ModelContainer(for: ItemModel.self) else {
            // Error handling
            return
        }
        self.modelContainer = modelContainer
        rootView = AnyView(
            ContentView(
                viewModel: ViewModel(
                    dataProvider: DataProvider(
                        context: modelContainer.mainContext
                    )
                )
            )
        )
    }
}


To keep things simple, we will also prepare all the dependencies in the coordinator as well. In more complex applications, all the responsibilities can be decoupled into separate components like Router, ViewFactory, etc.

In the setUpView function, we are preparing the ContentView. Now, we need to create an additional high-level View that will serve as a container for our application:

Swift
 
struct AppView: View {

    private var coordinator: Coordinator

    init(coordinator: Coordinator) {
        self.coordinator = coordinator
    }

    var body: some View {
        coordinator.rootView
    }
}

#Preview {
    AppView(coordinator: .init())
}


This view gets Coordinator injected very similarly to the way we implemented it with a View and ViewModel.

The last step of our exercise will be to update the entry point to our application:

Swift
 
@main
struct SwiftDataTestApp: App {

    var body: some Scene {
        WindowGroup {
            AppView(coordinator: .init())
        }
    }
}


Conclusion

We went through a simple refactoring of the default SwiftData example from Apple. Here is a short summary of what we have achieved:

In conclusion, we have refactored the default SwiftData example from Apple to create a more modular and maintainable architecture for our application. Here is a brief overview of what we have accomplished:

  • Removed SwiftData from the View component
  • Separated the View and ViewModel components
  • Created a separate data layer that is hidden by the DataProvidable protocol
  • Added a DataProvider component with SwiftData logic
  • Used a pure dependency injection approach through the init method instead of using Environment

Such refactoring will make our code more flexible, testable, and more aligned with SOLID principles than we had before. Keep in mind that this is just one of many options for structuring our application architecture. If we have a very simple basic app that works alone, the approach suggested by Apple may work for us. However, if we are building an application with multiple developers working on the same codebase, covering it with tests, and planning for possible changes and updates in the future, we may need to consider alternative approaches, and the example presented above is one of the options.

Here, you can find a project: SwiftData and SwiftUI refactoring.

Dependency injection UI Dependency Injection Swift (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Why I Started Using Dependency Injection in Python
  • How to Make a Picture-in-Picture Feature in iOS App Using AVFoundation
  • Angular Best Practices For Developing Efficient and Reliable Web Applications
  • HLS Streaming With AVKit and Swift UI for iOS and tvOS

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!