├── .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 | ![](https://github.com/shapehq/SwiftDataDecoupled/blob/main/preview.gif?raw=true) 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 --------------------------------------------------------------------------------