├── .gitignore
├── DB
├── .swiftpm
│ └── xcode
│ │ ├── package.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── simonbs.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ │ └── xcuserdata
│ │ └── simonbs.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── Package.swift
└── Sources
│ ├── DB
│ ├── Entry.swift
│ └── EntryRepository.swift
│ └── DBSwiftData
│ ├── Internal
│ └── FetchedResultsController.swift
│ ├── SwiftDataDB.swift
│ ├── SwiftDataEntry.swift
│ └── SwiftDataEntryRepository.swift
├── LICENSE
├── README.md
├── SwiftDataDecoupled.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcuserdata
│ └── simonbs.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── SwiftDataDecoupled
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
└── Source
│ ├── EntryDetail
│ ├── DismissButton.swift
│ ├── EntryDetailHeader.swift
│ └── EntryDetailView.swift
│ ├── EntryList
│ ├── AddButton.swift
│ ├── EntryListView.swift
│ └── EntryRow.swift
│ ├── ExampleApp.swift
│ ├── Misc
│ ├── CardContainer.swift
│ └── SheetPresentation.swift
│ └── Previews
│ ├── PreviewEntry.swift
│ └── PreviewEntryRepository.swift
└── preview.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build
3 | .swiftpm
4 | xcuserdata
5 |
--------------------------------------------------------------------------------
/DB/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DB/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DB/.swiftpm/xcode/package.xcworkspace/xcuserdata/simonbs.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shapehq/SwiftDataDecoupled/97512aaaf99234b9c0099325df2834b589bcae3d/DB/.swiftpm/xcode/package.xcworkspace/xcuserdata/simonbs.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/DB/.swiftpm/xcode/xcuserdata/simonbs.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | DB-Package.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | DB.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 | DBSwiftData.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 2
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | DB
26 |
27 | primary
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/DB/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "DB",
8 | platforms: [.iOS(.v17)],
9 | products: [
10 | .library(name: "DB", targets: [
11 | "DB"
12 | ]),
13 | .library(name: "DBSwiftData", targets: [
14 | "DBSwiftData"
15 | ])
16 | ],
17 | targets: [
18 | .target(name: "DB"),
19 | .target(name: "DBSwiftData", dependencies: [
20 | "DB"
21 | ])
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/DB/Sources/DB/Entry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol Entry: AnyObject, Identifiable, Equatable, Observable {
4 | var date: Date { get }
5 | var isEnabled: Bool { get set }
6 | }
7 |
--------------------------------------------------------------------------------
/DB/Sources/DB/EntryRepository.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol EntryRepository: AnyObject, Observable {
4 | associatedtype EntryType: Entry
5 | var models: [EntryType] { get }
6 | func addEntry()
7 | func deleteEntry(_ entry: EntryType)
8 | func fetchModels() throws
9 | }
10 |
--------------------------------------------------------------------------------
/DB/Sources/DBSwiftData/Internal/FetchedResultsController.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import Foundation
3 | import SwiftData
4 |
5 | @Observable
6 | final class FetchedResultsController {
7 | private(set) var models: [T] = []
8 |
9 | private let modelContext: ModelContext
10 | private let predicate: Predicate?
11 | private let sortDescriptors: [SortDescriptor]
12 |
13 | init(
14 | modelContext: ModelContext,
15 | predicate: Predicate? = nil,
16 | sortDescriptors: [SortDescriptor] = []
17 | ) {
18 | self.modelContext = modelContext
19 | self.predicate = predicate
20 | self.sortDescriptors = sortDescriptors
21 | setupNotification()
22 | }
23 |
24 | func fetch() throws {
25 | let fetchDesciptor = FetchDescriptor(predicate: predicate, sortBy: sortDescriptors)
26 | models = try modelContext.fetch(fetchDesciptor)
27 | }
28 |
29 | private func setupNotification() {
30 | // Ideally we'd use the ModelContext.didSave notification but this doesn't seem to be sent.
31 | // Last tested with iOS 17 beta 8 on September 4th, 2023.
32 | NotificationCenter.default.addObserver(
33 | self,
34 | selector: #selector(didSave),
35 | name: .NSPersistentStoreRemoteChange,
36 | object: nil
37 | )
38 | }
39 |
40 | @objc private func didSave(_ notification: Notification) {
41 | do {
42 | try fetch()
43 | } catch {}
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/DB/Sources/DBSwiftData/SwiftDataDB.swift:
--------------------------------------------------------------------------------
1 | import SwiftData
2 |
3 | public struct SwiftDataDB {
4 | public let modelContainer: ModelContainer
5 |
6 | public init(isStoredInMemoryOnly: Bool) {
7 | let modelConfiguration = ModelConfiguration(
8 | isStoredInMemoryOnly: isStoredInMemoryOnly
9 | )
10 | modelContainer = try! ModelContainer(
11 | for: SwiftDataEntry.self,
12 | configurations: modelConfiguration
13 | )
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/DB/Sources/DBSwiftData/SwiftDataEntry.swift:
--------------------------------------------------------------------------------
1 | import DB
2 | import Foundation
3 | import SwiftData
4 |
5 | @Model
6 | public final class SwiftDataEntry: Entry {
7 | public let date: Date
8 | public var isEnabled = false
9 |
10 | public init() {
11 | self.date = Date()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/DB/Sources/DBSwiftData/SwiftDataEntryRepository.swift:
--------------------------------------------------------------------------------
1 | import CoreData
2 | import DB
3 | import SwiftData
4 |
5 | @Observable
6 | public final class SwiftDataEntryRepository: EntryRepository {
7 | public var models: [SwiftDataEntry] {
8 | fetchedResultsController.models
9 | }
10 |
11 | private let modelContext: ModelContext
12 | private let fetchedResultsController: FetchedResultsController
13 |
14 | public init(modelContext: ModelContext) {
15 | self.modelContext = modelContext
16 | self.fetchedResultsController = FetchedResultsController(
17 | modelContext: modelContext,
18 | sortDescriptors: [SortDescriptor(\.date, order: .reverse)]
19 | )
20 | }
21 |
22 | public func addEntry() {
23 | let entry = SwiftDataEntry()
24 | modelContext.insert(entry)
25 | }
26 |
27 | public func deleteEntry(_ entry: SwiftDataEntry) {
28 | modelContext.delete(entry)
29 | }
30 |
31 | public func fetchModels() throws {
32 | try fetchedResultsController.fetch()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Shape ApS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftDataDecoupled
2 | Example project showing how the data and view layers can be decoupled when using SwiftData for persistence.
3 |
4 | 
5 |
6 | ## Table of Contents
7 |
8 | - [✨ Motivation](#-motivation)
9 | - [🧪 Solution](#-solution)
10 | - [🤔 Drawbacks](#-drawbacks)
11 |
12 | ## ✨ Motivation
13 |
14 | During WWDC23 Apple announced [SwiftData](https://developer.apple.com/documentation/swiftdata), a framework for quickly adding persistence to iOS apps. SwiftData builds on top of [Core Data](https://developer.apple.com/documentation/coredata/) but moves schema definition to plain Swift files. Consider the following model which defines a model that can be persisted using SwiftData.
15 |
16 | ```swift
17 | @Model
18 | final class EntryModel {
19 | let date: Date
20 | var isEnabled = false
21 |
22 | public init() {
23 | date = Date()
24 | }
25 | }
26 | ```
27 |
28 | 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.
29 |
30 | Apple's suggested way of using SwiftData in SwiftUI is using the [@Query](https://developer.apple.com/documentation/swiftdata/query) property wrapper and passing a [ModelContext](https://developer.apple.com/documentation/SwiftData/ModelContext) to the view using the [modelContext](https://developer.apple.com/documentation/SwiftUI/EnvironmentValues/modelContext) environment value.
31 |
32 | ```swift
33 | struct EntryListView: View {
34 | @Environment(\.modelContext) private var modelContext
35 | @Query private var models: [EntryModel]
36 |
37 | var body: some View {
38 | List {
39 | ForEach(models) { model in
40 | Text(entry.date, style: \.date)
41 | }
42 | }
43 | }
44 | }
45 | ```
46 |
47 | 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.
48 |
49 | ## 🧪 Solution
50 |
51 | 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](https://github.com/shapehq/SwiftDataDecoupled/tree/main/DB) which contains the following two targets.
52 |
53 | |Target|Description
54 | |-|-|
55 | |DB|The interface for our database.|
56 | |DBSwiftData|Concrete implementations of the interfaces in defined the DB target. These implementations use SwiftData for persisting data.|
57 |
58 | 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.
59 |
60 | The DB target contains [EntryRepository](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DB/EntryRepository.swift), a repository containing objects that conform to [Entry](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DB/Entry.swift). Types implementing the Entry and EntryRepository protocols must conform to the [Observable](https://developer.apple.com/documentation/observation/observable) protocol in order for changes to be reflected in SwiftUI Views.
61 |
62 | Notice that our EntryRepository protocol contains a property named `models`.
63 |
64 | ```swift
65 | public protocol EntryRepository: AnyObject, Observable {
66 | associatedtype EntryType: Entry
67 | var models: [EntryType] { get }
68 | func addEntry()
69 | func deleteEntry(_ entry: EntryType)
70 | func fetchModels() throws
71 | }
72 | ```
73 |
74 | 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.
75 |
76 | The DBSwiftData contains implementations that conform to these protocols, namely [SwiftDataEntry](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DBSwiftData/SwiftDataEntry.swift) and [SwiftDataEntryRepository](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DBSwiftData/SwiftDataEntryRepository.swift). These implementations persist models using SwiftData.
77 |
78 | An important detail is that our DBSwiftData target introduces [FetchedResultsController](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DBSwiftData/Internal/FetchedResultsController.swift), a naive implementation of Core Data's [NSFetchedResultsController](https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller) which re-fetches models whenever the data in the store changes. Our [SwiftDataEntryRepository](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DBSwiftData/SwiftDataEntryRepository.swift) uses an instance of FetchedResultsController to back the `models` property.
79 |
80 | 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.
81 |
82 | ```swift
83 | struct EntryListView: View {
84 | let entryRepository: EntryRepositoryType
85 |
86 | var body: some View {
87 | List {
88 | ForEach(entryRepository.models) { model in
89 | Text(entry.date, style: \.date)
90 | }
91 | }
92 | .onAppear {
93 | do {
94 | try entryRepository.fetchModels()
95 | } catch {}
96 | }
97 | }
98 | }
99 | ```
100 |
101 | 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](https://developer.apple.com/documentation/swiftdata/modelcontainer) and exposing it.
102 |
103 | ```swift
104 | @main
105 | struct ExampleApp: App {
106 | private let db: SwiftDataDB
107 |
108 | init() {
109 | db = SwiftDataDB(isStoredInMemoryOnly: false)
110 | }
111 |
112 | var body: some Scene {
113 | WindowGroup {
114 | EntryListView(
115 | entryRepository: SwiftDataEntryRepository(
116 | modelContext: db.modelContainer.mainContext
117 | )
118 | )
119 | }
120 | }
121 | }
122 | ```
123 |
124 | With this we have removed our view's dependency on SwiftData entirely 🙌
125 |
126 | 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.
127 |
128 | ## 🤔 Drawbacks
129 |
130 | Our implementation of [FetchedResultsController](https://github.com/shapehq/SwiftDataDecoupled/blob/main/DB/Sources/DBSwiftData/Internal/FetchedResultsController.swift) 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).
131 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 60;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 7270076C2AA5C963001072B5 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7270076B2AA5C963001072B5 /* ExampleApp.swift */; };
11 | 72C8395F2AA5A83B00A5F949 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72C8395E2AA5A83B00A5F949 /* Assets.xcassets */; };
12 | 72C839622AA5A83B00A5F949 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72C839612AA5A83B00A5F949 /* Preview Assets.xcassets */; };
13 | 72C839902AA5B55100A5F949 /* EntryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C839812AA5B55100A5F949 /* EntryRow.swift */; };
14 | 72C839912AA5B55100A5F949 /* EntryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C839822AA5B55100A5F949 /* EntryListView.swift */; };
15 | 72C839922AA5B55100A5F949 /* EntryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C839842AA5B55100A5F949 /* EntryDetailView.swift */; };
16 | 72C839942AA5B55100A5F949 /* PreviewEntryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C839872AA5B55100A5F949 /* PreviewEntryRepository.swift */; };
17 | 72C839982AA5B55100A5F949 /* SheetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C8398D2AA5B55100A5F949 /* SheetPresentation.swift */; };
18 | 72C839992AA5B55100A5F949 /* AddButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C8398E2AA5B55100A5F949 /* AddButton.swift */; };
19 | 72C8399A2AA5B55100A5F949 /* DismissButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C8398F2AA5B55100A5F949 /* DismissButton.swift */; };
20 | 72C8399E2AA5BD9000A5F949 /* EntryDetailHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C8399D2AA5BD9000A5F949 /* EntryDetailHeader.swift */; };
21 | 72C839A22AA5BE3100A5F949 /* CardContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C839A12AA5BE3100A5F949 /* CardContainer.swift */; };
22 | 72C839A82AA5BF1300A5F949 /* PreviewEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C839A72AA5BF1300A5F949 /* PreviewEntry.swift */; };
23 | 72C839B02AA5C23D00A5F949 /* DB in Frameworks */ = {isa = PBXBuildFile; productRef = 72C839AF2AA5C23D00A5F949 /* DB */; };
24 | 72C839B22AA5C23D00A5F949 /* DBSwiftData in Frameworks */ = {isa = PBXBuildFile; productRef = 72C839B12AA5C23D00A5F949 /* DBSwiftData */; };
25 | /* End PBXBuildFile section */
26 |
27 | /* Begin PBXFileReference section */
28 | 7270076B2AA5C963001072B5 /* ExampleApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
29 | 72C839572AA5A83A00A5F949 /* SwiftDataDecoupled.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftDataDecoupled.app; sourceTree = BUILT_PRODUCTS_DIR; };
30 | 72C8395E2AA5A83B00A5F949 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
31 | 72C839612AA5A83B00A5F949 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
32 | 72C839812AA5B55100A5F949 /* EntryRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntryRow.swift; sourceTree = ""; };
33 | 72C839822AA5B55100A5F949 /* EntryListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntryListView.swift; sourceTree = ""; };
34 | 72C839842AA5B55100A5F949 /* EntryDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntryDetailView.swift; sourceTree = ""; };
35 | 72C839872AA5B55100A5F949 /* PreviewEntryRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewEntryRepository.swift; sourceTree = ""; };
36 | 72C8398D2AA5B55100A5F949 /* SheetPresentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetPresentation.swift; sourceTree = ""; };
37 | 72C8398E2AA5B55100A5F949 /* AddButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddButton.swift; sourceTree = ""; };
38 | 72C8398F2AA5B55100A5F949 /* DismissButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissButton.swift; sourceTree = ""; };
39 | 72C8399D2AA5BD9000A5F949 /* EntryDetailHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDetailHeader.swift; sourceTree = ""; };
40 | 72C839A12AA5BE3100A5F949 /* CardContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardContainer.swift; sourceTree = ""; };
41 | 72C839A72AA5BF1300A5F949 /* PreviewEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewEntry.swift; sourceTree = ""; };
42 | /* End PBXFileReference section */
43 |
44 | /* Begin PBXFrameworksBuildPhase section */
45 | 72C839542AA5A83A00A5F949 /* Frameworks */ = {
46 | isa = PBXFrameworksBuildPhase;
47 | buildActionMask = 2147483647;
48 | files = (
49 | 72C839B22AA5C23D00A5F949 /* DBSwiftData in Frameworks */,
50 | 72C839B02AA5C23D00A5F949 /* DB in Frameworks */,
51 | );
52 | runOnlyForDeploymentPostprocessing = 0;
53 | };
54 | /* End PBXFrameworksBuildPhase section */
55 |
56 | /* Begin PBXGroup section */
57 | 72C8394E2AA5A83A00A5F949 = {
58 | isa = PBXGroup;
59 | children = (
60 | 72C839592AA5A83A00A5F949 /* SwiftDataDecoupled */,
61 | 72C839582AA5A83A00A5F949 /* Products */,
62 | );
63 | sourceTree = "";
64 | };
65 | 72C839582AA5A83A00A5F949 /* Products */ = {
66 | isa = PBXGroup;
67 | children = (
68 | 72C839572AA5A83A00A5F949 /* SwiftDataDecoupled.app */,
69 | );
70 | name = Products;
71 | sourceTree = "";
72 | };
73 | 72C839592AA5A83A00A5F949 /* SwiftDataDecoupled */ = {
74 | isa = PBXGroup;
75 | children = (
76 | 72C8395E2AA5A83B00A5F949 /* Assets.xcassets */,
77 | 72C839602AA5A83B00A5F949 /* Preview Content */,
78 | 72C8397F2AA5B55100A5F949 /* Source */,
79 | );
80 | path = SwiftDataDecoupled;
81 | sourceTree = "";
82 | };
83 | 72C839602AA5A83B00A5F949 /* Preview Content */ = {
84 | isa = PBXGroup;
85 | children = (
86 | 72C839612AA5A83B00A5F949 /* Preview Assets.xcassets */,
87 | );
88 | path = "Preview Content";
89 | sourceTree = "";
90 | };
91 | 72C8397F2AA5B55100A5F949 /* Source */ = {
92 | isa = PBXGroup;
93 | children = (
94 | 7270076B2AA5C963001072B5 /* ExampleApp.swift */,
95 | 72C839832AA5B55100A5F949 /* EntryDetail */,
96 | 72C839802AA5B55100A5F949 /* EntryList */,
97 | 72C839A32AA5BE5400A5F949 /* Misc */,
98 | 72C839862AA5B55100A5F949 /* Previews */,
99 | );
100 | path = Source;
101 | sourceTree = "";
102 | };
103 | 72C839802AA5B55100A5F949 /* EntryList */ = {
104 | isa = PBXGroup;
105 | children = (
106 | 72C8398E2AA5B55100A5F949 /* AddButton.swift */,
107 | 72C839812AA5B55100A5F949 /* EntryRow.swift */,
108 | 72C839822AA5B55100A5F949 /* EntryListView.swift */,
109 | );
110 | path = EntryList;
111 | sourceTree = "";
112 | };
113 | 72C839832AA5B55100A5F949 /* EntryDetail */ = {
114 | isa = PBXGroup;
115 | children = (
116 | 72C8398F2AA5B55100A5F949 /* DismissButton.swift */,
117 | 72C839842AA5B55100A5F949 /* EntryDetailView.swift */,
118 | 72C8399D2AA5BD9000A5F949 /* EntryDetailHeader.swift */,
119 | );
120 | path = EntryDetail;
121 | sourceTree = "";
122 | };
123 | 72C839862AA5B55100A5F949 /* Previews */ = {
124 | isa = PBXGroup;
125 | children = (
126 | 72C839872AA5B55100A5F949 /* PreviewEntryRepository.swift */,
127 | 72C839A72AA5BF1300A5F949 /* PreviewEntry.swift */,
128 | );
129 | path = Previews;
130 | sourceTree = "";
131 | };
132 | 72C839A32AA5BE5400A5F949 /* Misc */ = {
133 | isa = PBXGroup;
134 | children = (
135 | 72C8398D2AA5B55100A5F949 /* SheetPresentation.swift */,
136 | 72C839A12AA5BE3100A5F949 /* CardContainer.swift */,
137 | );
138 | path = Misc;
139 | sourceTree = "";
140 | };
141 | /* End PBXGroup section */
142 |
143 | /* Begin PBXNativeTarget section */
144 | 72C839562AA5A83A00A5F949 /* SwiftDataDecoupled */ = {
145 | isa = PBXNativeTarget;
146 | buildConfigurationList = 72C839652AA5A83B00A5F949 /* Build configuration list for PBXNativeTarget "SwiftDataDecoupled" */;
147 | buildPhases = (
148 | 72C839532AA5A83A00A5F949 /* Sources */,
149 | 72C839542AA5A83A00A5F949 /* Frameworks */,
150 | 72C839552AA5A83A00A5F949 /* Resources */,
151 | );
152 | buildRules = (
153 | );
154 | dependencies = (
155 | );
156 | name = SwiftDataDecoupled;
157 | packageProductDependencies = (
158 | 72C839AF2AA5C23D00A5F949 /* DB */,
159 | 72C839B12AA5C23D00A5F949 /* DBSwiftData */,
160 | );
161 | productName = SwiftDataDecoupled;
162 | productReference = 72C839572AA5A83A00A5F949 /* SwiftDataDecoupled.app */;
163 | productType = "com.apple.product-type.application";
164 | };
165 | /* End PBXNativeTarget section */
166 |
167 | /* Begin PBXProject section */
168 | 72C8394F2AA5A83A00A5F949 /* Project object */ = {
169 | isa = PBXProject;
170 | attributes = {
171 | BuildIndependentTargetsInParallel = 1;
172 | LastSwiftUpdateCheck = 1500;
173 | LastUpgradeCheck = 1500;
174 | TargetAttributes = {
175 | 72C839562AA5A83A00A5F949 = {
176 | CreatedOnToolsVersion = 15.0;
177 | };
178 | };
179 | };
180 | buildConfigurationList = 72C839522AA5A83A00A5F949 /* Build configuration list for PBXProject "SwiftDataDecoupled" */;
181 | compatibilityVersion = "Xcode 14.0";
182 | developmentRegion = en;
183 | hasScannedForEncodings = 0;
184 | knownRegions = (
185 | en,
186 | Base,
187 | );
188 | mainGroup = 72C8394E2AA5A83A00A5F949;
189 | packageReferences = (
190 | 72C839AE2AA5C23D00A5F949 /* XCLocalSwiftPackageReference "DB" */,
191 | );
192 | productRefGroup = 72C839582AA5A83A00A5F949 /* Products */;
193 | projectDirPath = "";
194 | projectRoot = "";
195 | targets = (
196 | 72C839562AA5A83A00A5F949 /* SwiftDataDecoupled */,
197 | );
198 | };
199 | /* End PBXProject section */
200 |
201 | /* Begin PBXResourcesBuildPhase section */
202 | 72C839552AA5A83A00A5F949 /* Resources */ = {
203 | isa = PBXResourcesBuildPhase;
204 | buildActionMask = 2147483647;
205 | files = (
206 | 72C839622AA5A83B00A5F949 /* Preview Assets.xcassets in Resources */,
207 | 72C8395F2AA5A83B00A5F949 /* Assets.xcassets in Resources */,
208 | );
209 | runOnlyForDeploymentPostprocessing = 0;
210 | };
211 | /* End PBXResourcesBuildPhase section */
212 |
213 | /* Begin PBXSourcesBuildPhase section */
214 | 72C839532AA5A83A00A5F949 /* Sources */ = {
215 | isa = PBXSourcesBuildPhase;
216 | buildActionMask = 2147483647;
217 | files = (
218 | 72C8399A2AA5B55100A5F949 /* DismissButton.swift in Sources */,
219 | 72C839A22AA5BE3100A5F949 /* CardContainer.swift in Sources */,
220 | 72C8399E2AA5BD9000A5F949 /* EntryDetailHeader.swift in Sources */,
221 | 72C839982AA5B55100A5F949 /* SheetPresentation.swift in Sources */,
222 | 72C839992AA5B55100A5F949 /* AddButton.swift in Sources */,
223 | 72C839A82AA5BF1300A5F949 /* PreviewEntry.swift in Sources */,
224 | 7270076C2AA5C963001072B5 /* ExampleApp.swift in Sources */,
225 | 72C839922AA5B55100A5F949 /* EntryDetailView.swift in Sources */,
226 | 72C839942AA5B55100A5F949 /* PreviewEntryRepository.swift in Sources */,
227 | 72C839902AA5B55100A5F949 /* EntryRow.swift in Sources */,
228 | 72C839912AA5B55100A5F949 /* EntryListView.swift in Sources */,
229 | );
230 | runOnlyForDeploymentPostprocessing = 0;
231 | };
232 | /* End PBXSourcesBuildPhase section */
233 |
234 | /* Begin XCBuildConfiguration section */
235 | 72C839632AA5A83B00A5F949 /* Debug */ = {
236 | isa = XCBuildConfiguration;
237 | buildSettings = {
238 | ALWAYS_SEARCH_USER_PATHS = NO;
239 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
240 | CLANG_ANALYZER_NONNULL = YES;
241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
243 | CLANG_ENABLE_MODULES = YES;
244 | CLANG_ENABLE_OBJC_ARC = YES;
245 | CLANG_ENABLE_OBJC_WEAK = YES;
246 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
247 | CLANG_WARN_BOOL_CONVERSION = YES;
248 | CLANG_WARN_COMMA = YES;
249 | CLANG_WARN_CONSTANT_CONVERSION = YES;
250 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
251 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
252 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
253 | CLANG_WARN_EMPTY_BODY = YES;
254 | CLANG_WARN_ENUM_CONVERSION = YES;
255 | CLANG_WARN_INFINITE_RECURSION = YES;
256 | CLANG_WARN_INT_CONVERSION = YES;
257 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
258 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
259 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
261 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
262 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
263 | CLANG_WARN_STRICT_PROTOTYPES = YES;
264 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
265 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
266 | CLANG_WARN_UNREACHABLE_CODE = YES;
267 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
268 | COPY_PHASE_STRIP = NO;
269 | DEBUG_INFORMATION_FORMAT = dwarf;
270 | ENABLE_STRICT_OBJC_MSGSEND = YES;
271 | ENABLE_TESTABILITY = YES;
272 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
273 | GCC_C_LANGUAGE_STANDARD = gnu17;
274 | GCC_DYNAMIC_NO_PIC = NO;
275 | GCC_NO_COMMON_BLOCKS = YES;
276 | GCC_OPTIMIZATION_LEVEL = 0;
277 | GCC_PREPROCESSOR_DEFINITIONS = (
278 | "DEBUG=1",
279 | "$(inherited)",
280 | );
281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
283 | GCC_WARN_UNDECLARED_SELECTOR = YES;
284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
285 | GCC_WARN_UNUSED_FUNCTION = YES;
286 | GCC_WARN_UNUSED_VARIABLE = YES;
287 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
288 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
289 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
290 | MTL_FAST_MATH = YES;
291 | ONLY_ACTIVE_ARCH = YES;
292 | SDKROOT = iphoneos;
293 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
294 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
295 | };
296 | name = Debug;
297 | };
298 | 72C839642AA5A83B00A5F949 /* Release */ = {
299 | isa = XCBuildConfiguration;
300 | buildSettings = {
301 | ALWAYS_SEARCH_USER_PATHS = NO;
302 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
303 | CLANG_ANALYZER_NONNULL = YES;
304 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
306 | CLANG_ENABLE_MODULES = YES;
307 | CLANG_ENABLE_OBJC_ARC = YES;
308 | CLANG_ENABLE_OBJC_WEAK = YES;
309 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
310 | CLANG_WARN_BOOL_CONVERSION = YES;
311 | CLANG_WARN_COMMA = YES;
312 | CLANG_WARN_CONSTANT_CONVERSION = YES;
313 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
315 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
316 | CLANG_WARN_EMPTY_BODY = YES;
317 | CLANG_WARN_ENUM_CONVERSION = YES;
318 | CLANG_WARN_INFINITE_RECURSION = YES;
319 | CLANG_WARN_INT_CONVERSION = YES;
320 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
321 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
324 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
326 | CLANG_WARN_STRICT_PROTOTYPES = YES;
327 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
328 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
329 | CLANG_WARN_UNREACHABLE_CODE = YES;
330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
331 | COPY_PHASE_STRIP = NO;
332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
333 | ENABLE_NS_ASSERTIONS = NO;
334 | ENABLE_STRICT_OBJC_MSGSEND = YES;
335 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
336 | GCC_C_LANGUAGE_STANDARD = gnu17;
337 | GCC_NO_COMMON_BLOCKS = YES;
338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
340 | GCC_WARN_UNDECLARED_SELECTOR = YES;
341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
342 | GCC_WARN_UNUSED_FUNCTION = YES;
343 | GCC_WARN_UNUSED_VARIABLE = YES;
344 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
345 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
346 | MTL_ENABLE_DEBUG_INFO = NO;
347 | MTL_FAST_MATH = YES;
348 | SDKROOT = iphoneos;
349 | SWIFT_COMPILATION_MODE = wholemodule;
350 | VALIDATE_PRODUCT = YES;
351 | };
352 | name = Release;
353 | };
354 | 72C839662AA5A83B00A5F949 /* Debug */ = {
355 | isa = XCBuildConfiguration;
356 | buildSettings = {
357 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
358 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
359 | CODE_SIGN_STYLE = Automatic;
360 | CURRENT_PROJECT_VERSION = 1;
361 | DEVELOPMENT_ASSET_PATHS = "\"SwiftDataDecoupled/Preview Content\"";
362 | DEVELOPMENT_TEAM = 8NQFWJHC63;
363 | ENABLE_PREVIEWS = YES;
364 | GENERATE_INFOPLIST_FILE = YES;
365 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
366 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
367 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
368 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
369 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
370 | LD_RUNPATH_SEARCH_PATHS = (
371 | "$(inherited)",
372 | "@executable_path/Frameworks",
373 | );
374 | MARKETING_VERSION = 1.0;
375 | PRODUCT_BUNDLE_IDENTIFIER = dk.simonbs.SwiftDataDecoupled;
376 | PRODUCT_NAME = "$(TARGET_NAME)";
377 | SWIFT_EMIT_LOC_STRINGS = YES;
378 | SWIFT_VERSION = 5.0;
379 | TARGETED_DEVICE_FAMILY = "1,2";
380 | };
381 | name = Debug;
382 | };
383 | 72C839672AA5A83B00A5F949 /* Release */ = {
384 | isa = XCBuildConfiguration;
385 | buildSettings = {
386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
387 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
388 | CODE_SIGN_STYLE = Automatic;
389 | CURRENT_PROJECT_VERSION = 1;
390 | DEVELOPMENT_ASSET_PATHS = "\"SwiftDataDecoupled/Preview Content\"";
391 | DEVELOPMENT_TEAM = 8NQFWJHC63;
392 | ENABLE_PREVIEWS = YES;
393 | GENERATE_INFOPLIST_FILE = YES;
394 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
395 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
396 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
397 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
398 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
399 | LD_RUNPATH_SEARCH_PATHS = (
400 | "$(inherited)",
401 | "@executable_path/Frameworks",
402 | );
403 | MARKETING_VERSION = 1.0;
404 | PRODUCT_BUNDLE_IDENTIFIER = dk.simonbs.SwiftDataDecoupled;
405 | PRODUCT_NAME = "$(TARGET_NAME)";
406 | SWIFT_EMIT_LOC_STRINGS = YES;
407 | SWIFT_VERSION = 5.0;
408 | TARGETED_DEVICE_FAMILY = "1,2";
409 | };
410 | name = Release;
411 | };
412 | /* End XCBuildConfiguration section */
413 |
414 | /* Begin XCConfigurationList section */
415 | 72C839522AA5A83A00A5F949 /* Build configuration list for PBXProject "SwiftDataDecoupled" */ = {
416 | isa = XCConfigurationList;
417 | buildConfigurations = (
418 | 72C839632AA5A83B00A5F949 /* Debug */,
419 | 72C839642AA5A83B00A5F949 /* Release */,
420 | );
421 | defaultConfigurationIsVisible = 0;
422 | defaultConfigurationName = Release;
423 | };
424 | 72C839652AA5A83B00A5F949 /* Build configuration list for PBXNativeTarget "SwiftDataDecoupled" */ = {
425 | isa = XCConfigurationList;
426 | buildConfigurations = (
427 | 72C839662AA5A83B00A5F949 /* Debug */,
428 | 72C839672AA5A83B00A5F949 /* Release */,
429 | );
430 | defaultConfigurationIsVisible = 0;
431 | defaultConfigurationName = Release;
432 | };
433 | /* End XCConfigurationList section */
434 |
435 | /* Begin XCLocalSwiftPackageReference section */
436 | 72C839AE2AA5C23D00A5F949 /* XCLocalSwiftPackageReference "DB" */ = {
437 | isa = XCLocalSwiftPackageReference;
438 | relativePath = DB;
439 | };
440 | /* End XCLocalSwiftPackageReference section */
441 |
442 | /* Begin XCSwiftPackageProductDependency section */
443 | 72C839AF2AA5C23D00A5F949 /* DB */ = {
444 | isa = XCSwiftPackageProductDependency;
445 | productName = DB;
446 | };
447 | 72C839B12AA5C23D00A5F949 /* DBSwiftData */ = {
448 | isa = XCSwiftPackageProductDependency;
449 | productName = DBSwiftData;
450 | };
451 | /* End XCSwiftPackageProductDependency section */
452 | };
453 | rootObject = 72C8394F2AA5A83A00A5F949 /* Project object */;
454 | }
455 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled.xcodeproj/xcuserdata/simonbs.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwiftDataDecoupled.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/EntryDetail/DismissButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DismissButton: View {
4 | @Environment(\.dismiss) private var dismiss
5 |
6 | var body: some View {
7 | Button {
8 | dismiss()
9 | } label: {
10 | Image(systemName: "xmark.circle.fill")
11 | .padding()
12 | .font(.title2)
13 | }
14 | }
15 | }
16 |
17 | #Preview(traits: .sizeThatFitsLayout) {
18 | DismissButton()
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/EntryDetail/EntryDetailHeader.swift:
--------------------------------------------------------------------------------
1 | import DB
2 | import SwiftUI
3 |
4 | struct EntryDetailHeader: View {
5 | let entry: EntryType
6 |
7 | private let dateFormatter: DateFormatter = {
8 | let dateFormatter = DateFormatter()
9 | dateFormatter.dateStyle = .none
10 | dateFormatter.timeStyle = .medium
11 | return dateFormatter
12 | }()
13 |
14 | var body: some View {
15 | ZStack {
16 | HStack {
17 | DismissButton()
18 | .tint(.secondary)
19 | Spacer()
20 | }
21 | Text(dateFormatter.string(from: entry.date))
22 | .font(.headline)
23 | .padding([.leading, .trailing])
24 | }
25 | .padding(.top)
26 | }
27 | }
28 |
29 | #Preview {
30 | EntryDetailHeader(
31 | entry: PreviewEntry(date: Date(), isEnabled: false)
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/EntryDetail/EntryDetailView.swift:
--------------------------------------------------------------------------------
1 | import DB
2 | import SwiftUI
3 |
4 | struct EntryDetailView: View {
5 | @Bindable var entry: EntryType
6 |
7 | var body: some View {
8 | CardContainer {
9 | VStack(spacing: 0) {
10 | EntryDetailHeader(entry: entry)
11 | Spacer()
12 | HStack {
13 | Spacer()
14 | Toggle(isOn: $entry.isEnabled) {
15 | Text("Enabled")
16 | }
17 | .fixedSize(horizontal: true, vertical: true)
18 | Spacer()
19 | }
20 | .padding([.leading, .trailing])
21 | Spacer()
22 | }
23 | }
24 | }
25 | }
26 |
27 | #Preview {
28 | EntryDetailView(
29 | entry: PreviewEntry(date: Date(), isEnabled: false)
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/EntryList/AddButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AddButton: View {
4 | let onSelect: () -> Void
5 |
6 | var body: some View {
7 | Button {
8 | onSelect()
9 | } label: {
10 | Image(systemName: "plus")
11 | }
12 | }
13 | }
14 |
15 | #Preview {
16 | AddButton(onSelect: {})
17 | }
18 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/EntryList/EntryListView.swift:
--------------------------------------------------------------------------------
1 | import DB
2 | import SwiftUI
3 |
4 | struct EntryListView: View {
5 | let entryRepository: EntryRepositoryType
6 |
7 | @State private var selectedEntry: EntryRepositoryType.EntryType?
8 |
9 | var body: some View {
10 | NavigationView {
11 | List {
12 | ForEach(entryRepository.models) { entry in
13 | EntryRow(entry: entry) {
14 | selectedEntry = entry
15 | } onDelete: {
16 | entryRepository.deleteEntry(entry)
17 | }
18 | }
19 | }
20 | .animation(.default, value: entryRepository.models)
21 | .navigationTitle("Entries")
22 | .toolbar {
23 | ToolbarItem(placement: .primaryAction) {
24 | AddButton {
25 | entryRepository.addEntry()
26 | }
27 | }
28 | }
29 | .sheetPresentation(presentedItem: $selectedEntry) { entry in
30 | EntryDetailView(entry: entry)
31 | }
32 | .onAppear {
33 | do {
34 | try entryRepository.fetchModels()
35 | } catch {}
36 | }
37 | }
38 | }
39 | }
40 |
41 | #Preview {
42 | EntryListView(
43 | entryRepository: PreviewEntryRepository(models: [
44 | PreviewEntry(
45 | date: Date().addingTimeInterval(-3600),
46 | isEnabled: false
47 | ),
48 | PreviewEntry(
49 | date: Date().addingTimeInterval(-9000),
50 | isEnabled: true
51 | ),
52 | PreviewEntry(
53 | date: Date().addingTimeInterval(-12 * 3600),
54 | isEnabled: true
55 | )
56 | ])
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/EntryList/EntryRow.swift:
--------------------------------------------------------------------------------
1 | import DB
2 | import SwiftUI
3 |
4 | struct EntryRow: View {
5 | let entry: EntryType
6 | let onSelect: () -> Void
7 | let onDelete: () -> Void
8 |
9 | private let dateFormatter: DateFormatter = {
10 | let dateFormatter = DateFormatter()
11 | dateFormatter.dateStyle = .none
12 | dateFormatter.timeStyle = .medium
13 | return dateFormatter
14 | }()
15 |
16 | var body: some View {
17 | Button {
18 | onSelect()
19 | } label: {
20 | HStack {
21 | Text(dateFormatter.string(from: entry.date))
22 | if entry.isEnabled {
23 | Spacer()
24 | Image(systemName: "checkmark")
25 | }
26 | }
27 | }
28 | .tint(.primary)
29 | .swipeActions {
30 | Button(role: .destructive) {
31 | onDelete()
32 | } label: {
33 | Image(systemName: "trash")
34 | }
35 | }
36 | }
37 | }
38 |
39 | #Preview {
40 | EntryRow(
41 | entry: PreviewEntry(date: Date(), isEnabled: false),
42 | onSelect: {},
43 | onDelete: {}
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | import DBSwiftData
2 | import SwiftUI
3 |
4 | @main
5 | struct ExampleApp: App {
6 | private let db: SwiftDataDB
7 |
8 | init() {
9 | db = SwiftDataDB(isStoredInMemoryOnly: false)
10 | }
11 |
12 | var body: some Scene {
13 | WindowGroup {
14 | EntryListView(
15 | entryRepository: SwiftDataEntryRepository(
16 | modelContext: db.modelContainer.mainContext
17 | )
18 | )
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/Misc/CardContainer.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CardContainer: View {
4 | private let content: () -> Content
5 |
6 | init(@ViewBuilder content: @escaping () -> Content) {
7 | self.content = content
8 | }
9 |
10 | var body: some View {
11 | ZStack(alignment: .topLeading) {
12 | Color.clear
13 | .background(.regularMaterial)
14 | .edgesIgnoringSafeArea(.all)
15 | content()
16 | }
17 | }
18 | }
19 |
20 | #Preview(traits: .sizeThatFitsLayout) {
21 | CardContainer {
22 | Text("Hello world!")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/Misc/SheetPresentation.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | func sheetPresentation- (
5 | presentedItem: Binding
- ,
6 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()],
7 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil,
8 | prefersGrabberVisible: Bool = true,
9 | prefersScrollingExpandsWhenScrolledToEdge: Bool = false,
10 | preferredCornerRadius: CGFloat = 20,
11 | @ViewBuilder sheetView: @escaping (Item) -> SheetView,
12 | onDismiss: (() -> Void)? = nil
13 | ) -> some View {
14 | modifier(
15 | SheetPresentationViewModifier(
16 | presentedItem: presentedItem,
17 | detents: detents,
18 | largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier,
19 | prefersGrabberVisible: prefersGrabberVisible,
20 | prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge,
21 | preferredCornerRadius: preferredCornerRadius,
22 | sheetView: sheetView,
23 | onDismiss: onDismiss
24 | )
25 | )
26 | }
27 | }
28 |
29 | private struct SheetPresentationViewModifier
- : ViewModifier {
30 | private let presentedItem: Binding
-
31 | private let detents: [UISheetPresentationController.Detent]
32 | private let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
33 | private let prefersGrabberVisible: Bool
34 | private let prefersScrollingExpandsWhenScrolledToEdge: Bool
35 | private let preferredCornerRadius: CGFloat
36 | private let sheetView: (Item) -> SheetView
37 | private let onDismiss: (() -> Void)?
38 |
39 | init(
40 | presentedItem: Binding
- ,
41 | detents: [UISheetPresentationController.Detent],
42 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?,
43 | prefersGrabberVisible: Bool,
44 | prefersScrollingExpandsWhenScrolledToEdge: Bool,
45 | preferredCornerRadius: CGFloat,
46 | @ViewBuilder sheetView: @escaping (Item) -> SheetView,
47 | onDismiss: (() -> Void)? = nil
48 | ) {
49 | self.presentedItem = presentedItem
50 | self.detents = detents
51 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
52 | self.prefersGrabberVisible = prefersGrabberVisible
53 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
54 | self.preferredCornerRadius = preferredCornerRadius
55 | self.sheetView = sheetView
56 | self.onDismiss = onDismiss
57 | }
58 |
59 | func body(content: Content) -> some View {
60 | content.background(
61 | SheetPresentationController(
62 | presentedItem: presentedItem,
63 | detents: detents,
64 | largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier,
65 | prefersGrabberVisible: prefersGrabberVisible,
66 | prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge,
67 | preferredCornerRadius: preferredCornerRadius,
68 | sheetView: sheetView,
69 | onDismiss: onDismiss
70 | )
71 | )
72 | }
73 | }
74 |
75 | private struct SheetPresentationController
- : UIViewControllerRepresentable {
76 | private let viewController = UIViewController()
77 |
78 | @Binding private var presentedItem: Item?
79 | private let detents: [UISheetPresentationController.Detent]
80 | private let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
81 | private let prefersGrabberVisible: Bool
82 | private let prefersScrollingExpandsWhenScrolledToEdge: Bool
83 | private let preferredCornerRadius: CGFloat
84 | private let sheetView: (Item) -> SheetView
85 | private var onDismiss: (() -> Void)?
86 |
87 | init(
88 | presentedItem: Binding
- ,
89 | detents: [UISheetPresentationController.Detent],
90 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?,
91 | prefersGrabberVisible: Bool,
92 | prefersScrollingExpandsWhenScrolledToEdge: Bool,
93 | preferredCornerRadius: CGFloat,
94 | sheetView: @escaping (Item) -> SheetView,
95 | onDismiss: (() -> Void)? = nil
96 | ) {
97 | self._presentedItem = presentedItem
98 | self.detents = detents
99 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
100 | self.prefersGrabberVisible = prefersGrabberVisible
101 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
102 | self.preferredCornerRadius = preferredCornerRadius
103 | self.sheetView = sheetView
104 | self.onDismiss = onDismiss
105 | }
106 |
107 | func makeCoordinator() -> Coordinator {
108 | Coordinator(parent: self)
109 | }
110 |
111 | func makeUIViewController(context: Context) -> UIViewController {
112 | viewController.view.backgroundColor = .clear
113 | return viewController
114 | }
115 |
116 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
117 | guard let presentedItem else {
118 | uiViewController.dismiss(animated: true)
119 | return
120 | }
121 | if uiViewController.presentedViewController == nil {
122 | let sheetViewController = SheetHostingViewController(
123 | detents: detents,
124 | largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier,
125 | prefersGrabberVisible: prefersGrabberVisible,
126 | prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge,
127 | preferredCornerRadius: preferredCornerRadius,
128 | rootView: sheetView(presentedItem)
129 | )
130 | sheetViewController.sheetPresentationController?.delegate = context.coordinator
131 | sheetViewController.delegate = context.coordinator
132 | uiViewController.present(sheetViewController, animated: true)
133 | } else if let hostingController = uiViewController.presentedViewController as? UIHostingController {
134 | hostingController.rootView = sheetView(presentedItem)
135 | }
136 | }
137 |
138 | final class Coordinator: NSObject, UISheetPresentationControllerDelegate, SheetHostingViewControllerDelegate {
139 | var parent: SheetPresentationController
140 |
141 | init(parent: SheetPresentationController) {
142 | self.parent = parent
143 | }
144 |
145 | func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
146 | guard parent.presentedItem != nil else {
147 | return
148 | }
149 | parent.presentedItem = nil
150 | if let onDismiss = parent.onDismiss {
151 | onDismiss()
152 | }
153 | }
154 |
155 | func sheetViewControllerDidDisappear(
156 | _ sheetViewController: SheetHostingViewController
157 | ) {
158 | parent.presentedItem = nil
159 | if let onDismiss = parent.onDismiss {
160 | onDismiss()
161 | }
162 | }
163 | }
164 | }
165 |
166 | private protocol SheetHostingViewControllerDelegate: AnyObject {
167 | func sheetViewControllerDidDisappear(
168 | _ sheetViewController: SheetHostingViewController
169 | )
170 | }
171 |
172 | private class SheetHostingViewController: UIHostingController {
173 | weak var delegate: SheetHostingViewControllerDelegate?
174 |
175 | private let detents: [UISheetPresentationController.Detent]
176 | private let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
177 | private let prefersGrabberVisible: Bool
178 | private let prefersScrollingExpandsWhenScrolledToEdge: Bool
179 | private let preferredCornerRadius: CGFloat
180 |
181 | init(
182 | detents: [UISheetPresentationController.Detent],
183 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?,
184 | prefersGrabberVisible: Bool,
185 | prefersScrollingExpandsWhenScrolledToEdge: Bool,
186 | preferredCornerRadius: CGFloat,
187 | rootView: Content
188 | ) {
189 | self.detents = detents
190 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
191 | self.prefersGrabberVisible = prefersGrabberVisible
192 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
193 | self.preferredCornerRadius = preferredCornerRadius
194 | super.init(rootView: rootView)
195 | }
196 |
197 | required dynamic init?(coder aDecoder: NSCoder) {
198 | fatalError("init(coder:) has not been implemented")
199 | }
200 |
201 | override func viewDidLoad() {
202 | super.viewDidLoad()
203 | view.backgroundColor = .clear
204 | if let sheetPresentationController {
205 | sheetPresentationController.detents = detents
206 | sheetPresentationController.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
207 | sheetPresentationController.prefersGrabberVisible = prefersGrabberVisible
208 | sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
209 | sheetPresentationController.preferredCornerRadius = preferredCornerRadius
210 | }
211 | }
212 |
213 | override func viewDidDisappear(_ animated: Bool) {
214 | super.viewDidDisappear(animated)
215 | if isBeingDismissed {
216 | delegate?.sheetViewControllerDidDisappear(self)
217 | }
218 | }
219 | }
220 |
221 | private struct SheetPresentationPreviewView : View {
222 | @State private var presentedItem: String?
223 |
224 | var body: some View {
225 | VStack {
226 | Button {
227 | if presentedItem == nil {
228 | presentedItem = "Hello world!"
229 | } else {
230 | presentedItem = nil
231 | }
232 | } label: {
233 | if presentedItem != nil {
234 | Text("Dismiss Sheet")
235 | } else {
236 | Text("Present Sheet")
237 | }
238 | }
239 | }
240 | .sheetPresentation(presentedItem: $presentedItem) { presentedItem in
241 | ZStack {
242 | Color.white
243 | Text(presentedItem)
244 | }
245 | .edgesIgnoringSafeArea(.all)
246 | }
247 | }
248 | }
249 |
250 | #Preview {
251 | SheetPresentationPreviewView()
252 | }
253 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/Previews/PreviewEntry.swift:
--------------------------------------------------------------------------------
1 | import DB
2 | import Foundation
3 |
4 | final class PreviewEntry: Entry {
5 | let date: Date
6 | var isEnabled: Bool
7 |
8 | init(date: Date, isEnabled: Bool) {
9 | self.date = date
10 | self.isEnabled = isEnabled
11 | }
12 |
13 | static func == (lhs: PreviewEntry, rhs: PreviewEntry) -> Bool {
14 | lhs.date == rhs.date && lhs.isEnabled == rhs.isEnabled
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/SwiftDataDecoupled/Source/Previews/PreviewEntryRepository.swift:
--------------------------------------------------------------------------------
1 | import DB
2 |
3 | final class PreviewEntryRepository: EntryRepository {
4 | let models: [PreviewEntry]
5 |
6 | init(models: [PreviewEntry] = []) {
7 | self.models = models
8 | }
9 |
10 | func addEntry() {}
11 |
12 | func deleteEntry(_ entry: PreviewEntry) {}
13 |
14 | func fetchModels() throws {}
15 | }
16 |
--------------------------------------------------------------------------------
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shapehq/SwiftDataDecoupled/97512aaaf99234b9c0099325df2834b589bcae3d/preview.gif
--------------------------------------------------------------------------------