GithubHelp home page GithubHelp logo

shapehq / swiftdatadecoupled Goto Github PK

View Code? Open in Web Editor NEW
54.0 12.0 0.0 1.38 MB

Example project showing how the data and view layers can be decoupled when using SwiftData for persistence.

License: MIT License

Swift 100.00%
ios swift swiftdata swiftdata-example swiftui swiftui-example swiftui-learning

swiftdatadecoupled's Introduction

SwiftDataDecoupled

Example project showing how the data and view layers can be decoupled when using SwiftData for persistence.

Table of Contents

โœจ Motivation

During WWDC23 Apple announced SwiftData, a framework for quickly adding persistence to iOS apps. SwiftData builds on top of Core Data but moves schema definition to plain Swift files. Consider the following model which defines a model that can be persisted using SwiftData.

@Model
final class EntryModel {
    let date: Date
    var isEnabled = false

    public init() {
        date = Date()
    }
}

Not only does this type specify the Swift model but it also specifies the schema of the underlying Core Data store. This is execellent and makes data persistence much simpler.

Apple's suggested way of using SwiftData in SwiftUI is using the @Query property wrapper and passing a ModelContext to the view using the modelContext environment value.

struct EntryListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var models: [EntryModel]

    var body: some View {
        List {
            ForEach(models) { model in
                Text(entry.date, style: \.date)
            }
        }
    }
}

The downside of this is that it our views know about SwiftData, and as such, our views become tightly coupled to a specific database. We want to ensure our view layer is loosely coupled to our data layer.

๐Ÿงช Solution

To achieve loose coupling between our SwiftUI view and the underlying SwiftData store, we utilize Dependency Injection to inject our data store through constructors. We add a local Swift package named DB which contains the following two targets.

Target Description
DB The interface for our database.
DBSwiftData Concrete implementations of the interfaces in defined the DB target. These implementations use SwiftData for persisting data.

In our sample app we store entries with a date and a flag specifying whether this entry is enabled or not. There is no underlying meaning behind these entries. They are meant for learning purposes only. In other applications these entries would be domain specific, e.g. you may store a booking, a favorited track, or a movie.

The DB target contains EntryRepository, a repository containing objects that conform to Entry. Types implementing the Entry and EntryRepository protocols must conform to the Observable protocol in order for changes to be reflected in SwiftUI Views.

Notice that our EntryRepository protocol contains a property named models.

public protocol EntryRepository: AnyObject, Observable {
    associatedtype EntryType: Entry
    var models: [EntryType] { get }
    func addEntry()
    func deleteEntry(_ entry: EntryType)
    func fetchModels() throws
}

Because types implementing the EntryRepository protocol conform to Observable, changes to the models property will cause SwiftUI views to update. With this our SwiftUI views no longer need to rely on the @Query property wrapper.

The DBSwiftData contains implementations that conform to these protocols, namely SwiftDataEntry and SwiftDataEntryRepository. These implementations persist models using SwiftData.

An important detail is that our DBSwiftData target introduces FetchedResultsController, a naive implementation of Core Data's NSFetchedResultsController which re-fetches models whenever the data in the store changes. Our SwiftDataEntryRepository uses an instance of FetchedResultsController to back the models property.

Continuing our example from earlier, we can now adjust EntryListView to be constructed with a type conforming to EntryRepository and use that to fetch models.

struct EntryListView<EntryRepositoryType: EntryRepository>: View {
    let entryRepository: EntryRepositoryType

    var body: some View {
        List {
            ForEach(entryRepository.models) { model in
                Text(entry.date, style: \.date)
            }
        }
        .onAppear {
            do {
                try entryRepository.fetchModels()
            } catch {}
        }
    }
}

Lastly, we'll need to inject an implementation of EntryRepository into our view. We do this using Dependency Injection by passing the repository to the view through its constructor. Our DBSwiftData target exposes a SwiftDataDB type that configures a the SwiftData stack, effectively creating an instance of ModelContainer and exposing it.

@main
struct ExampleApp: App {
    private let db: SwiftDataDB

    init() {
        db = SwiftDataDB(isStoredInMemoryOnly: false)
    }

    var body: some Scene {
        WindowGroup {
            EntryListView(
                entryRepository: SwiftDataEntryRepository(
                    modelContext: db.modelContainer.mainContext
                )
            )
        }
    }
}

With this we have removed our view's dependency on SwiftData entirely ๐Ÿ™Œ

The benefit of decoupling our view and data layers like this is that we now have a codebase where it is straightforward to replace the SwiftData persistence with types that persist in a different database, should we ever want to do so.

๐Ÿค” Drawbacks

Our implementation of FetchedResultsController is naive but plays a key part in decoupling SwiftData from the view. Ideally we would like Apple to implement and expose a SwiftData-equivalent of Core Data's NSFetchedResultsController (FB13114301).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.