From d5114fc4bcc19316b3316f86f4caec0eaf7ec1ee Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 8 Oct 2019 21:09:34 +0900 Subject: [PATCH] WIP --- CoreStore.xcodeproj/project.pbxproj | 40 ++ .../SwiftUI Demo/SwiftUIView.swift | 67 +- Sources/DiffableDataSource.TableView.swift | 107 ++++ Sources/DiffableDataSource.swift | 29 + Sources/DynamicObject.swift | 13 +- Sources/EnvironmentKeys.swift | 4 +- ...ls.CoreStoreFetchedResultsController.swift | 5 + ...Internals.DiffableDataSourceSnapshot.swift | 606 ++++++++++++++++++ ...als.FallbackDiffableDataUIDispatcher.swift | 157 +++++ ...edDiffableDataSourceSnapshotDelegate.swift | 65 +- Sources/ListSnapshot.swift | 186 +++++- Sources/LiveList.swift | 51 +- Sources/LiveQuery.swift | 288 +++++---- 13 files changed, 1393 insertions(+), 225 deletions(-) create mode 100644 Sources/DiffableDataSource.TableView.swift create mode 100644 Sources/DiffableDataSource.swift create mode 100644 Sources/Internals.DiffableDataSourceSnapshot.swift create mode 100644 Sources/Internals.FallbackDiffableDataUIDispatcher.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index de467fc..c96128e 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -536,6 +536,22 @@ B5AEFAB61C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; B5AEFAB71C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; B5AEFAB81C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; + B5BF7FAD234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */; }; + B5BF7FAE234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */; }; + B5BF7FAF234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */; }; + B5BF7FB0234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */; }; + B5BF7FB2234C97910070E741 /* DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB1234C97910070E741 /* DiffableDataSource.swift */; }; + B5BF7FB3234C97910070E741 /* DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB1234C97910070E741 /* DiffableDataSource.swift */; }; + B5BF7FB4234C97910070E741 /* DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB1234C97910070E741 /* DiffableDataSource.swift */; }; + B5BF7FB5234C97910070E741 /* DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB1234C97910070E741 /* DiffableDataSource.swift */; }; + B5BF7FB7234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB6234C97CE0070E741 /* DiffableDataSource.TableView.swift */; }; + B5BF7FB8234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB6234C97CE0070E741 /* DiffableDataSource.TableView.swift */; }; + B5BF7FB9234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB6234C97CE0070E741 /* DiffableDataSource.TableView.swift */; }; + B5BF7FBA234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FB6234C97CE0070E741 /* DiffableDataSource.TableView.swift */; }; + B5BF7FBC234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FBB234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift */; }; + B5BF7FBD234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FBB234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift */; }; + B5BF7FBE234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FBB234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift */; }; + B5BF7FBF234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FBB234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift */; }; B5C976E31C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */; }; B5C976E41C6C9F9A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */; }; B5C976E51C6C9F9B00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */; }; @@ -942,6 +958,10 @@ B5AD60CD1C90141E00F2B2E8 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreBridge.swift; sourceTree = ""; }; B5BDC9271C2024F2008147CD /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = SOURCE_ROOT; }; + B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.DiffableDataSourceSnapshot.swift; sourceTree = ""; }; + B5BF7FB1234C97910070E741 /* DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableDataSource.swift; sourceTree = ""; }; + B5BF7FB6234C97CE0070E741 /* DiffableDataSource.TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableDataSource.TableView.swift; sourceTree = ""; }; + B5BF7FBB234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.FallbackDiffableDataUIDispatcher.swift; sourceTree = ""; }; B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UnsafeDataTransaction+Observing.swift"; sourceTree = ""; }; B5C976E61C6E3A5900B1AF90 /* Internals.CoreStoreFetchedResultsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Internals.CoreStoreFetchedResultsController.swift; sourceTree = ""; }; B5CA2B071F7E5ACA004B1936 /* WhereClauseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhereClauseType.swift; sourceTree = ""; }; @@ -1513,6 +1533,8 @@ B5D8CA752346E7590055D7D1 /* DataStack+DataSources.swift */, B5F335572348D75D00FD649F /* LiveResult.swift */, B5E294DC2349F8E7003E5956 /* SnapshotResult.swift */, + B5BF7FB1234C97910070E741 /* DiffableDataSource.swift */, + B5BF7FB6234C97CE0070E741 /* DiffableDataSource.TableView.swift */, ); name = DataSources; sourceTree = ""; @@ -1643,6 +1665,8 @@ B5C976E61C6E3A5900B1AF90 /* Internals.CoreStoreFetchedResultsController.swift */, B5474D142227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift */, B51260921E9B28F100402229 /* Internals.EntityIdentifier.swift */, + B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */, + B5BF7FBB234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift */, B54A6A541BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift */, B501322F2346B76E00FC238B /* Internals.FetchedDiffableDataSourceSnapshotDelegate.swift */, B5FAD6AB1B51285300714891 /* Internals.MigrationManager.swift */, @@ -2018,6 +2042,7 @@ B56007141B3F6C2800A9A8F9 /* SectionBy.swift in Sources */, B5DE522B230BD7CC00A22534 /* Internals.swift in Sources */, B5E84F371AFF85470064E85B /* NSManagedObjectContext+Transaction.swift in Sources */, + B5BF7FB7234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */, B5ECDC1D1CA81A2100C7F112 /* CSDataStack+Querying.swift in Sources */, B5C976E31C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */, B53FBA121CAB63CB00F0D40A /* Progress+ObjectiveC.swift in Sources */, @@ -2051,6 +2076,7 @@ B51260931E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, B5DAFB482203D9F8003FCCD0 /* Where.Expression.swift in Sources */, B5FE4DA21C8481E100FA6A91 /* StorageInterface.swift in Sources */, + B5BF7FAD234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */, B53FB9FE1CAB2D2F00F0D40A /* CSMigrationResult.swift in Sources */, B5DBE2D21C991B3E00B5CEFA /* CSDataStack.swift in Sources */, B50392F91C478FF3009900CA /* NSManagedObject+Transaction.swift in Sources */, @@ -2104,6 +2130,7 @@ B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */, B56923E81EB827F5007C4DC9 /* InferredSchemaMappingProvider.swift in Sources */, B53B275F1EE3B92E00E9B352 /* CoreStoreManagedObject.swift in Sources */, + B5BF7FBC234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */, B5215CA41FA47DFD00139E3A /* FetchChainBuilder.swift in Sources */, B5D33A011E96012400C880DE /* Relationship.swift in Sources */, B5E84EE81AFF84610064E85B /* CoreStoreLogger.swift in Sources */, @@ -2123,6 +2150,7 @@ B5E84EF81AFF846E0064E85B /* CoreStore+Transaction.swift in Sources */, B5E84F301AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift in Sources */, B5831B7A1F34ACBA00A9F647 /* Transformable.swift in Sources */, + B5BF7FB2234C97910070E741 /* DiffableDataSource.swift in Sources */, B546F9691C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, B549F65E1E569C7400FBAB2D /* QueryableAttributeType.swift in Sources */, B5E84F211AFF84860064E85B /* CoreStore+Observing.swift in Sources */, @@ -2203,6 +2231,7 @@ 82BA18B21C4BBD3900A0916E /* ImportableObject.swift in Sources */, 82BA18AE1C4BBD3100A0916E /* DataStack+Transaction.swift in Sources */, 82BA18AB1C4BBD3100A0916E /* AsynchronousDataTransaction.swift in Sources */, + B5BF7FBD234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */, B5D339D91E9489AB00C880DE /* CoreStoreObject.swift in Sources */, 82BA18CE1C4BBD7100A0916E /* Internals.FetchedResultsControllerDelegate.swift in Sources */, B56923FB1EB82956007C4DC9 /* CSXcodeDataModelSchema.swift in Sources */, @@ -2230,6 +2259,7 @@ B53FBA141CAB63CB00F0D40A /* Progress+ObjectiveC.swift in Sources */, B5831B761F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, B5E5FA4F22D162F400330931 /* ObjectSnapshot.swift in Sources */, + B5BF7FB3234C97910070E741 /* DiffableDataSource.swift in Sources */, B5E1B5AA1CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B5D339F21E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, B5E1B59F1CAA2568007FD580 /* CSDataStack+Observing.swift in Sources */, @@ -2326,6 +2356,7 @@ 82BA18B51C4BBD3F00A0916E /* BaseDataTransaction+Querying.swift in Sources */, B5F335592348D76300FD649F /* LiveResult.swift in Sources */, B501FDDF1CA8D05000BE22EF /* CSSectionBy.swift in Sources */, + B5BF7FAE234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */, B538BA781D15B3E30003A766 /* CoreStoreBridge.m in Sources */, B51260801E97A18000402229 /* CoreStoreObject+Convenience.swift in Sources */, 82BA18D31C4BBD7100A0916E /* NSManagedObjectContext+CoreStore.swift in Sources */, @@ -2357,6 +2388,7 @@ B5ECDC0D1CA8161B00C7F112 /* CSGroupBy.swift in Sources */, B5D339E81E9493A500C880DE /* Entity.swift in Sources */, 82BA18CC1C4BBD6400A0916E /* Progress+Convenience.swift in Sources */, + B5BF7FB8234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */, 82BA18C01C4BBD5300A0916E /* DataStack+Observing.swift in Sources */, 82BA18A61C4BBD2900A0916E /* DefaultLogger.swift in Sources */, ); @@ -2411,6 +2443,7 @@ B546F9761C9C553300D5AC55 /* SetupResult.swift in Sources */, B53FBA161CAB63CB00F0D40A /* Progress+ObjectiveC.swift in Sources */, B5ECDC271CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, + B5BF7FBF234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */, B5D339DB1E9489AB00C880DE /* CoreStoreObject.swift in Sources */, B52DD1951BE1F92500949AFE /* CoreStoreError.swift in Sources */, B56923FD1EB82956007C4DC9 /* CSXcodeDataModelSchema.swift in Sources */, @@ -2438,6 +2471,7 @@ B5831B781F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, B52DD1981BE1F92500949AFE /* CoreStore+Setup.swift in Sources */, B5E5FA5122D162F400330931 /* ObjectSnapshot.swift in Sources */, + B5BF7FB5234C97910070E741 /* DiffableDataSource.swift in Sources */, B5D339F41E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, B5220E241D13085E009BC71E /* NSFetchedResultsController+Convenience.swift in Sources */, B559CD471CAA8B6300E4D58B /* CSSetupResult.swift in Sources */, @@ -2534,6 +2568,7 @@ B5220E251D13088E009BC71E /* ListObserver.swift in Sources */, B5F3355B2348D76500FD649F /* LiveResult.swift in Sources */, B538BA7A1D15B3E30003A766 /* CoreStoreBridge.m in Sources */, + B5BF7FB0234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */, B51260821E97A18000402229 /* CoreStoreObject+Convenience.swift in Sources */, B52DD1A01BE1F92C00949AFE /* UnsafeDataTransaction.swift in Sources */, B5ECDC331CA81CDC00C7F112 /* CSCoreStore+Transaction.swift in Sources */, @@ -2565,6 +2600,7 @@ B598514B1C90289F00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B5D339EA1E9493A500C880DE /* Entity.swift in Sources */, B52DD1AA1BE1F93500949AFE /* TypeErasedClauses.swift in Sources */, + B5BF7FBA234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */, B53FBA021CAB2D2F00F0D40A /* CSMigrationResult.swift in Sources */, B51FE5AF1CD4D00300E54258 /* CoreStore+CustomDebugStringConvertible.swift in Sources */, ); @@ -2619,6 +2655,7 @@ B56321AD1BD6521C006C9394 /* Internals.MigrationManager.swift in Sources */, B563219D1BD65216006C9394 /* DataStack+Observing.swift in Sources */, B56321961BD65216006C9394 /* From.swift in Sources */, + B5BF7FBE234C99190070E741 /* Internals.FallbackDiffableDataUIDispatcher.swift in Sources */, B5D339DA1E9489AB00C880DE /* CoreStoreObject.swift in Sources */, B5ECDC021CA80CBA00C7F112 /* CSWhere.swift in Sources */, B56923FC1EB82956007C4DC9 /* CSXcodeDataModelSchema.swift in Sources */, @@ -2646,6 +2683,7 @@ B5E1B5AB1CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B5831B771F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, B5E5FA5022D162F400330931 /* ObjectSnapshot.swift in Sources */, + B5BF7FB4234C97910070E741 /* DiffableDataSource.swift in Sources */, B5D339F31E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, B5E1B5A01CAA2568007FD580 /* CSDataStack+Observing.swift in Sources */, B5ECDC261CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, @@ -2742,6 +2780,7 @@ B56321921BD65216006C9394 /* BaseDataTransaction+Querying.swift in Sources */, B5F3355A2348D76500FD649F /* LiveResult.swift in Sources */, B501FDE01CA8D05000BE22EF /* CSSectionBy.swift in Sources */, + B5BF7FAF234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */, B538BA791D15B3E30003A766 /* CoreStoreBridge.m in Sources */, B51260811E97A18000402229 /* CoreStoreObject+Convenience.swift in Sources */, B56321B11BD6521C006C9394 /* NSManagedObjectContext+CoreStore.swift in Sources */, @@ -2773,6 +2812,7 @@ B5ECDC0E1CA8161B00C7F112 /* CSGroupBy.swift in Sources */, B5D339E91E9493A500C880DE /* Entity.swift in Sources */, B56321A41BD65216006C9394 /* CoreStore+Migration.swift in Sources */, + B5BF7FB9234C97CE0070E741 /* DiffableDataSource.TableView.swift in Sources */, B56321A01BD65216006C9394 /* ObjectObserver.swift in Sources */, B56321951BD65216006C9394 /* TypeErasedClauses.swift in Sources */, ); diff --git a/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift b/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift index bd12f94..107ead4 100644 --- a/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift +++ b/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift @@ -26,27 +26,32 @@ struct SwiftUIView: View { var body: some View { NavigationView { List { - ForEach(palettes.snapshot, id: \.self) { palette in - NavigationLink( - destination: DetailView(palette: palette), - label: { - HStack { - Color(palette.color) - .frame(width: 30, height: 30, alignment: .leading) - Text(palette.colorText) - } + ForEach(palettes.snapshot.sectionIdentifiers, id: \.self) { (sectionID: String) in + Section(header: Text(sectionID)) { + ForEach(self.palettes.snapshot[section: sectionID], id: \.self) { palette in + NavigationLink( + destination: DetailView(palette: palette), + label: { + HStack { + Color(palette.color) + .cornerRadius(5) + .frame(width: 30, height: 30, alignment: .leading) + Text(palette.colorText) + } + } + ) } - ) - } - .onDelete { indices in - let palettes = self.palettes.snapshot[indices] - self.dataStack.perform( - asynchronous: { transaction in + .onDelete { itemIndices in + let objectsToDelete = self.palettes.snapshot[section: sectionID, itemIndices: itemIndices] + self.dataStack.perform( + asynchronous: { transaction in - transaction.delete(palettes) - }, - completion: { _ in } - ) + transaction.delete(objectsToDelete) + }, + completion: { _ in } + ) + } + } } } .navigationBarTitle(Text("SwiftUI")) @@ -90,16 +95,36 @@ struct SwiftUIView: View { @available(iOS 13.0.0, *) struct DetailView: View { + + @Environment(\.dataStack) + var dataStack: DataStack @ObservedObject var palette: Palette + @State var hue: Float = 0 + @State var saturation: Float = 0 + @State var brightness: Float = 0 + + init(palette: Palette) { + + self.palette = palette + self.hue = Float(palette.hue.value) + self.saturation = palette.saturation.value + self.brightness = palette.brightness.value + } + var body: some View { ZStack { Color(palette.color) .cornerRadius(20) .padding(20) - Text(palette.colorText) - .navigationBarTitle(Text("Color")) + VStack { + Text(palette.colorText) + .navigationBarTitle(Text("Color")) + Slider(value: $hue, in: 0.0 ... 359.0 as ClosedRange) + Slider(value: $saturation, in: 0.0 ... 1.0 as ClosedRange) + Slider(value: $brightness, in: 0.0 ... 0.1 as ClosedRange) + } } } } diff --git a/Sources/DiffableDataSource.TableView.swift b/Sources/DiffableDataSource.TableView.swift new file mode 100644 index 0000000..2ff6cd9 --- /dev/null +++ b/Sources/DiffableDataSource.TableView.swift @@ -0,0 +1,107 @@ +// +// DiffableDataSource.TableView.swift +// CoreStore +// +// Copyright © 2018 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#if canImport(UIKit) + +import UIKit +import CoreData + + +//// MARK: - DiffableDataSource +// +//extension DiffableDataSource { +// +// // MARK: - TableView +// +// public open class TableView: NSObject, UITableViewDataSource { +// +// // MARK: Public +// +// public typealias ObjectType = D +// +// public var defaultRowAnimation: UITableView.RowAnimation = .automatic +// +// public init(tableView: UITableView, cellProvider: @escaping (UITableView, IndexPath, ObjectType) -> UITableViewCell?) { +// +// self.tableView = tableView +// self.cellProvider = cellProvider +// +// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) { +// +// self.rawDataSource = UITableViewDiffableDataSource( +// tableView: tableView, +// cellProvider: { (tableView, indexPath, managedObjectID) -> UITableViewCell? in +// +// cellProvider( +// } +// ) +// } +// else { +// +// self.rawDataSource = nil +// } +// +// super.init() +// +// tableView.dataSource = self +// } +// +// public func apply(_ snapshot: ListSnapshot, animatingDifferences: Bool = true) { +// +// let dataSource = UITableViewDiffableDataSource. +// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) { +// +// self.rawDataSource! as! UITableViewDiffableDataSource +// +// } +// else { +// +// } +// core.apply( +// snapshot, +// view: tableView, +// animatingDifferences: animatingDifferences, +// performUpdates: { tableView, changeset, setSections in +// tableView.reload(using: changeset, with: self.defaultRowAnimation, setData: setSections) +// }) +// } +// +// +// // MARK: Private +// +// private weak var tableView: UITableView? +// private let cellProvider: (UITableView, IndexPath, ObjectType) -> UITableViewCell? +// private let rawDataSource: Any? +// +// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) +// private var diffableDataSource: UITableViewDiffableDataSource { +// +// return self.rawDataSource! as! UITableViewDiffableDataSource +// } +// } +//} + + +#endif diff --git a/Sources/DiffableDataSource.swift b/Sources/DiffableDataSource.swift new file mode 100644 index 0000000..f4bcc45 --- /dev/null +++ b/Sources/DiffableDataSource.swift @@ -0,0 +1,29 @@ +// +// File.swift +// CoreStore +// +// Copyright © 2018 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + + +// MARK: - DiffableDataSource + +public enum DiffableDataSource {} diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index 6e84757..0cf760d 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -33,7 +33,12 @@ import CoreData All CoreStore's utilities are designed around `DynamicObject` instances. `NSManagedObject` and `CoreStoreObject` instances all conform to `DynamicObject`. */ public protocol DynamicObject: AnyObject { - + + /** + The object ID for this instance + */ + typealias ObjectID = NSManagedObjectID + /** Used internally by CoreStore. Do not call directly. */ @@ -52,7 +57,7 @@ public protocol DynamicObject: AnyObject { /** Used internally by CoreStore. Do not call directly. */ - func cs_id() -> NSManagedObjectID + func cs_id() -> ObjectID /** Used internally by CoreStore. Do not call directly. @@ -103,7 +108,7 @@ extension NSManagedObject: DynamicObject { return object.isKind(of: self) } - public func cs_id() -> NSManagedObjectID { + public func cs_id() -> ObjectID { return self.objectID } @@ -166,7 +171,7 @@ extension CoreStoreObject { return (self as AnyClass).isSubclass(of: type as AnyClass) } - public func cs_id() -> NSManagedObjectID { + public func cs_id() -> ObjectID { return self.rawObject!.objectID } diff --git a/Sources/EnvironmentKeys.swift b/Sources/EnvironmentKeys.swift index 8370585..2472e96 100644 --- a/Sources/EnvironmentKeys.swift +++ b/Sources/EnvironmentKeys.swift @@ -29,9 +29,9 @@ public struct DataStackEnvironmentKey: EnvironmentKey { @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) extension EnvironmentValues { - + // MARK: Public - + public var dataStack: DataStack { get { return self[DataStackEnvironmentKey.self] diff --git a/Sources/Internals.CoreStoreFetchedResultsController.swift b/Sources/Internals.CoreStoreFetchedResultsController.swift index 4db2239..e27bf45 100644 --- a/Sources/Internals.CoreStoreFetchedResultsController.swift +++ b/Sources/Internals.CoreStoreFetchedResultsController.swift @@ -82,6 +82,11 @@ extension Internals { try self.reapplyAffectedStores(self.typedFetchRequest, self.managedObjectContext) try self.performFetch() + + if case let delegate as FetchedDiffableDataSourceSnapshotDelegate = self.delegate { + + delegate.initialFetch() + } } @nonobjc diff --git a/Sources/Internals.DiffableDataSourceSnapshot.swift b/Sources/Internals.DiffableDataSourceSnapshot.swift new file mode 100644 index 0000000..65f8057 --- /dev/null +++ b/Sources/Internals.DiffableDataSourceSnapshot.swift @@ -0,0 +1,606 @@ +// +// Internals.DiffableDataSourceSnapshot.swift +// CoreStore +// +// Copyright © 2018 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#if canImport(UIKit) || canImport(AppKit) + +import CoreData + +#if canImport(UIKit) +import UIKit + +#elseif canImport(AppKit) +import AppKit + +#endif + + +// MARK: - Internals + +extension Internals { + + // MARK: Internal + + internal typealias DiffableDataSourceSnapshot = _Internal_DiffableDataSourceSnapshot + + + // MARK: - FallbackDiffableDataSourceSnapshot + + // Implementation based on https://github.com/ra1028/DiffableDataSources + internal struct FallbackDiffableDataSourceSnapshot: DiffableDataSourceSnapshot { + + // MARK: Internal + + init(sections: [NSFetchedResultsSectionInfo]) { + + self.structure = .init(sections: sections) + } + + // MARK: DiffableDataSourceSnapshot + + init() { + + self.structure = .init() + } + + var numberOfItems: Int { + + return self.itemIdentifiers.count + } + + var numberOfSections: Int { + + return self.sectionIdentifiers.count + } + + var sectionIdentifiers: [NSString] { + + return self.structure.allSectionIDs + } + + var itemIdentifiers: [NSManagedObjectID] { + + self.structure.allItemIDs + } + + func numberOfItems(inSection identifier: NSString) -> Int { + + return self.itemIdentifiers(inSection: identifier).count + } + + func itemIdentifiers(inSection identifier: NSString) -> [NSManagedObjectID] { + + return self.structure.items(in: identifier) + } + + func sectionIdentifier(containingItem identifier: NSManagedObjectID) -> NSString? { + + return self.structure.section(containing: identifier) + } + + func indexOfItem(_ identifier: NSManagedObjectID) -> Int? { + + return self.itemIdentifiers.firstIndex(of: identifier) + } + + func indexOfSection(_ identifier: NSString) -> Int? { + + return self.sectionIdentifiers.firstIndex(of: identifier) + } + + mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: NSString?) { + + self.structure.append(itemIDs: identifiers, to: sectionIdentifier) + } + + mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID) { + + self.structure.insert(itemIDs: identifiers, before: beforeIdentifier) + } + + mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID) { + + self.structure.insert(itemIDs: identifiers, after: afterIdentifier) + } + + mutating func deleteItems(_ identifiers: [NSManagedObjectID]) { + + self.structure.remove(itemIDs: identifiers) + } + + mutating func deleteAllItems() { + + self.structure.removeAllItems() + } + + mutating func moveItem(_ identifier: NSManagedObjectID, beforeItem toIdentifier: NSManagedObjectID) { + + self.structure.move(itemID: identifier, before: toIdentifier) + } + + mutating func moveItem(_ identifier: NSManagedObjectID, afterItem toIdentifier: NSManagedObjectID) { + + self.structure.move(itemID: identifier, after: toIdentifier) + } + + mutating func reloadItems(_ identifiers: [NSManagedObjectID]) { + + self.structure.update(itemIDs: identifiers) + } + + mutating func appendSections(_ identifiers: [NSString]) { + + self.structure.append(sectionIDs: identifiers) + } + + mutating func insertSections(_ identifiers: [NSString], beforeSection toIdentifier: NSString) { + + self.structure.insert(sectionIDs: identifiers, before: toIdentifier) + } + + mutating func insertSections(_ identifiers: [NSString], afterSection toIdentifier: NSString) { + + self.structure.insert(sectionIDs: identifiers, after: toIdentifier) + } + + mutating func deleteSections(_ identifiers: [NSString]) { + + self.structure.remove(sectionIDs: identifiers) + } + + mutating func moveSection(_ identifier: NSString, beforeSection toIdentifier: NSString) { + + self.structure.move(sectionID: identifier, before: toIdentifier) + } + + mutating func moveSection(_ identifier: NSString, afterSection toIdentifier: NSString) { + + self.structure.move(sectionID: identifier, after: toIdentifier) + } + + mutating func reloadSections(_ identifiers: [NSString]) { + + self.structure.update(sectionIDs: identifiers) + } + + + // MARK: Private + + private var structure: BackingStructure + + + // MARK: - BackingStructure + + internal struct BackingStructure { + + // MARK: Internal + + var sections: [Section] + + init() { + + self.sections = [] + } + + init(sections: [NSFetchedResultsSectionInfo]) { + + self.sections = sections.map { + + Section( + id: $0.name as NSString, + items: $0.objects? + .compactMap({ ($0 as? NSManagedObject)?.objectID }) + .map(Item.init(id:)) ?? [], + isReloaded: false + ) + } + } + + var allSectionIDs: [NSString] { + + return self.sections.map({ $0.id }) + } + + var allItemIDs: [NSManagedObjectID] { + + return self.sections.lazy.flatMap({ $0.elements }).map({ $0.id }) + } + + func items(in sectionID: NSString) -> [NSManagedObjectID] { + + guard let sectionIndex = self.sectionIndex(of: sectionID) else { + + Internals.abort("Section \"\(sectionID)\" does not exist") + } + return self.sections[sectionIndex].elements.map({ $0.id }) + } + + func section(containing itemID: NSManagedObjectID) -> NSString? { + + return self.itemPositionMap()[itemID]?.section.id + } + + mutating func append(itemIDs: [NSManagedObjectID], to sectionID: NSString?) { + + let index: Array
.Index + if let sectionID = sectionID { + + guard let sectionIndex = self.sectionIndex(of: sectionID) else { + + Internals.abort("Section \"\(sectionID)\" does not exist") + } + index = sectionIndex + } + else { + + let section = self.sections + guard !section.isEmpty else { + + Internals.abort("No sections exist") + } + index = section.index(before: section.endIndex) + } + + let items = itemIDs.lazy.map(Item.init) + self.sections[index].elements.append(contentsOf: items) + } + + mutating func insert(itemIDs: [NSManagedObjectID], before beforeItemID: NSManagedObjectID) { + + guard let itemPosition = self.itemPositionMap()[beforeItemID] else { + + Internals.abort("Item \(beforeItemID) does not exist") + } + let items = itemIDs.lazy.map(Item.init) + self.sections[itemPosition.sectionIndex].elements + .insert(contentsOf: items, at: itemPosition.itemRelativeIndex) + } + + mutating func insert(itemIDs: [NSManagedObjectID], after afterItemID: NSManagedObjectID) { + + guard let itemPosition = self.itemPositionMap()[afterItemID] else { + + Internals.abort("Item \(afterItemID) does not exist") + } + let itemIndex = self.sections[itemPosition.sectionIndex].elements + .index(after: itemPosition.itemRelativeIndex) + let items = itemIDs.lazy.map(Item.init) + self.sections[itemPosition.sectionIndex].elements + .insert(contentsOf: items, at: itemIndex) + } + + mutating func remove(itemIDs: [NSManagedObjectID]) { + + let itemPositionMap = self.itemPositionMap() + var removeIndexSetMap: [Int: IndexSet] = [:] + + for itemID in itemIDs { + + guard let itemPosition = itemPositionMap[itemID] else { + + continue + } + removeIndexSetMap[itemPosition.sectionIndex, default: []] + .insert(itemPosition.itemRelativeIndex) + } + for (sectionIndex, removeIndexSet) in removeIndexSetMap { + + for range in removeIndexSet.rangeView.reversed() { + + self.sections[sectionIndex].elements.removeSubrange(range) + } + } + } + + mutating func removeAllItems() { + + for sectionIndex in self.sections.indices { + + self.sections[sectionIndex].elements.removeAll() + } + } + + mutating func move(itemID: NSManagedObjectID, before beforeItemID: NSManagedObjectID) { + + guard let removed = self.remove(itemID: itemID) else { + + Internals.abort("Item \(itemID) does not exist") + } + guard let itemPosition = self.itemPositionMap()[beforeItemID] else { + + Internals.abort("Item \(beforeItemID) does not exist") + } + self.sections[itemPosition.sectionIndex].elements + .insert(removed, at: itemPosition.itemRelativeIndex) + } + + mutating func move(itemID: NSManagedObjectID, after afterItemID: NSManagedObjectID) { + + guard let removed = self.remove(itemID: itemID) else { + + Internals.abort("Item \(itemID) does not exist") + } + guard let itemPosition = self.itemPositionMap()[afterItemID] else { + + Internals.abort("Item \(afterItemID) does not exist") + } + let itemIndex = self.sections[itemPosition.sectionIndex].elements + .index(after: itemPosition.itemRelativeIndex) + self.sections[itemPosition.sectionIndex].elements + .insert(removed, at: itemIndex) + } + + mutating func update(itemIDs: [NSManagedObjectID]) { + + let itemPositionMap = self.itemPositionMap() + for itemID in itemIDs { + + guard let itemPosition = itemPositionMap[itemID] else { + + Internals.abort("Item \(itemID) does not exist") + } + self.sections[itemPosition.sectionIndex].elements[itemPosition.itemRelativeIndex].isReloaded = true + } + } + + mutating func append(sectionIDs: [NSString]) { + + let newSections = sectionIDs.lazy.map(Section.init) + self.sections.append(contentsOf: newSections) + } + + mutating func insert(sectionIDs: [NSString], before beforeSectionID: NSString) { + + guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else { + + Internals.abort("Section \"\(beforeSectionID)\" does not exist") + } + let newSections = sectionIDs.lazy.map(Section.init) + self.sections.insert(contentsOf: newSections, at: sectionIndex) + } + + mutating func insert(sectionIDs: [NSString], after afterSectionID: NSString) { + + guard let beforeIndex = self.sectionIndex(of: afterSectionID) else { + + Internals.abort("Section \"\(afterSectionID)\" does not exist") + } + let sectionIndex = self.sections.index(after: beforeIndex) + let newSections = sectionIDs.lazy.map(Section.init) + self.sections.insert(contentsOf: newSections, at: sectionIndex) + } + + mutating func remove(sectionIDs: [NSString]) { + + for sectionID in sectionIDs { + + self.remove(sectionID: sectionID) + } + } + + mutating func move(sectionID: NSString, before beforeSectionID: NSString) { + + guard let removed = self.remove(sectionID: sectionID) else { + + Internals.abort("Section \"\(sectionID)\" does not exist") + } + guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else { + + Internals.abort("Section \"\(beforeSectionID)\" does not exist") + } + self.sections.insert(removed, at: sectionIndex) + } + + mutating func move(sectionID: NSString, after afterSectionID: NSString) { + + guard let removed = self.remove(sectionID: sectionID) else { + + Internals.abort("Section \"\(sectionID)\" does not exist") + } + guard let beforeIndex = self.sectionIndex(of: afterSectionID) else { + + Internals.abort("Section \"\(afterSectionID)\" does not exist") + } + let sectionIndex = self.sections.index(after: beforeIndex) + self.sections.insert(removed, at: sectionIndex) + } + + mutating func update(sectionIDs: [NSString]) { + + for sectionID in sectionIDs { + + guard let sectionIndex = self.sectionIndex(of: sectionID) else { + + continue + } + self.sections[sectionIndex].isReloaded = true + } + } + + + // MARK: Private + + private func sectionIndex(of sectionID: NSString) -> Array
.Index? { + + return self.sections.firstIndex(where: { $0.id == sectionID }) + } + + @discardableResult + private mutating func remove(itemID: NSManagedObjectID) -> Item? { + + guard let itemPosition = self.itemPositionMap()[itemID] else { + + return nil + } + return self.sections[itemPosition.sectionIndex].elements + .remove(at: itemPosition.itemRelativeIndex) + } + + @discardableResult + private mutating func remove(sectionID: NSString) -> Section? { + + guard let sectionIndex = self.sectionIndex(of: sectionID) else { + + return nil + } + return self.sections.remove(at: sectionIndex) + } + + private func itemPositionMap() -> [NSManagedObjectID: ItemPosition] { + + return self.sections.enumerated().reduce(into: [:]) { result, section in + + for (itemRelativeIndex, item) in section.element.elements.enumerated() { + + result[item.id] = ItemPosition( + item: item, + itemRelativeIndex: itemRelativeIndex, + section: section.element, + sectionIndex: section.offset + ) + } + } + } + + + // MARK: - Item + + internal struct Item: Identifiable, Equatable { + + var isReloaded: Bool + + init(id: NSManagedObjectID, isReloaded: Bool) { + + self.id = id + self.isReloaded = isReloaded + } + + init(id: NSManagedObjectID) { + + self.init(id: id, isReloaded: false) + } + + func isContentEqual(to source: Item) -> Bool { + + return !self.isReloaded && self.id == source.id + } + + // MARK: Identifiable + + var id: NSManagedObjectID + } + + + // MARK: - Section + + internal struct Section: Identifiable, Equatable { + + var elements: [Item] = [] + var isReloaded: Bool + + init(id: NSString, items: [Item], isReloaded: Bool) { + self.id = id + self.elements = items + self.isReloaded = isReloaded + } + + init(id: NSString) { + + self.init(id: id, items: [], isReloaded: false) + } + + init(source: Section, elements: S) where S.Element == Item { + + self.init(id: source.id, items: Array(elements), isReloaded: source.isReloaded) + } + + func isContentEqual(to source: Section) -> Bool { + + return !self.isReloaded && self.id == source.id + } + + // MARK: Identifiable + + var id: NSString + } + + + // MARK: - ItemPosition + + fileprivate struct ItemPosition { + + var item: Item + var itemRelativeIndex: Int + var section: Section + var sectionIndex: Int + } + } + } +} + + +// MARK: - NSDiffableDataSourceSnapshot: Internals.DiffableDataSourceSnapshot + +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) +extension NSDiffableDataSourceSnapshot: Internals.DiffableDataSourceSnapshot where SectionIdentifierType == NSString, ItemIdentifierType == NSManagedObjectID {} + + +// MARK: - Internals.DiffableDataSourceSnapshot + +internal protocol _Internal_DiffableDataSourceSnapshot { + + init() + + var numberOfItems: Int { get } + var numberOfSections: Int { get } + var sectionIdentifiers: [NSString] { get } + var itemIdentifiers: [NSManagedObjectID] { get } + + func numberOfItems(inSection identifier: NSString) -> Int + func itemIdentifiers(inSection identifier: NSString) -> [NSManagedObjectID] + func sectionIdentifier(containingItem identifier: NSManagedObjectID) -> NSString? + func indexOfItem(_ identifier: NSManagedObjectID) -> Int? + func indexOfSection(_ identifier: NSString) -> Int? + + mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: NSString?) + mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID) + mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID) + mutating func deleteItems(_ identifiers: [NSManagedObjectID]) + mutating func deleteAllItems() + mutating func moveItem(_ identifier: NSManagedObjectID, beforeItem toIdentifier: NSManagedObjectID) + mutating func moveItem(_ identifier: NSManagedObjectID, afterItem toIdentifier: NSManagedObjectID) + mutating func reloadItems(_ identifiers: [NSManagedObjectID]) + mutating func appendSections(_ identifiers: [NSString]) + mutating func insertSections(_ identifiers: [NSString], beforeSection toIdentifier: NSString) + mutating func insertSections(_ identifiers: [NSString], afterSection toIdentifier: NSString) + mutating func deleteSections(_ identifiers: [NSString]) + mutating func moveSection(_ identifier: NSString, beforeSection toIdentifier: NSString) + mutating func moveSection(_ identifier: NSString, afterSection toIdentifier: NSString) + mutating func reloadSections(_ identifiers: [NSString]) +} + + + + +#endif diff --git a/Sources/Internals.FallbackDiffableDataUIDispatcher.swift b/Sources/Internals.FallbackDiffableDataUIDispatcher.swift new file mode 100644 index 0000000..f091916 --- /dev/null +++ b/Sources/Internals.FallbackDiffableDataUIDispatcher.swift @@ -0,0 +1,157 @@ +// +// Internals.FallbackDiffableDataUIDispatcher.swift +// CoreStore +// +// Copyright © 2018 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#if canImport(UIKit) || canImport(AppKit) + +import CoreData + +#if canImport(QuartzCore) +import QuartzCore + +#endif + + +// MARK: - Internals + +extension Internals { + + // MARK: Internal + +// // Implementation based on https://github.com/ra1028/DiffableDataSources +// internal final class FallbackDiffableDataUIDispatcher { +// +// // MARK: Internal +// +// internal func apply() { +// +// } +// +// func snapshot() -> DiffableDataSourceSnapshot { +// var snapshot = DiffableDataSourceSnapshot() +// snapshot.structure.sections = currentSnapshot.structure.sections +// return snapshot +// } +// +// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { +// guard 0.. ItemIdentifierType { +// guard let itemIdentifier = itemIdentifier(for: indexPath) else { +// universalError("Item not found at the specified index path(\(indexPath)).") +// } +// +// return itemIdentifier +// } +// +// func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { +// let indexPathMap: [ItemIdentifierType: IndexPath] = sections.enumerated() +// .reduce(into: [:]) { result, section in +// for (itemIndex, item) in section.element.elements.enumerated() { +// result[item.differenceIdentifier] = IndexPath( +// item: itemIndex, +// section: section.offset +// ) +// } +// } +// return indexPathMap[itemIdentifier] +// } +// +// func numberOfSections() -> Int { +// return sections.count +// } +// +// func numberOfItems(inSection section: Int) -> Int { +// return sections[section].elements.count +// } +// +// +// +// // MARK: Private +// +// private let dispatcher: MainThreadSerialDispatcher = .init() +// +// private var currentSnapshot: Internals.DiffableDataSourceSnapshot = FallbackDiffableDataSourceSnapshot() +// private var sections: [FallbackDiffableDataSourceSnapshot.BackingStructure.Section] = [] +// +// +// // MARK: - MainThreadSerialDispatcher +// +// fileprivate final class MainThreadSerialDispatcher { +// +// // MARK: FilePrivate +// +// fileprivate init() { +// +// self.executingCount.initialize(to: 0) +// } +// +// deinit { +// +// self.executingCount.deinitialize(count: 1) +// self.executingCount.deallocate() +// } +// +// fileprivate func dispatch(_ action: @escaping () -> Void) { +// +// let count = OSAtomicIncrement32(self.executingCount) +// if Thread.isMainThread && count == 1 { +// +// action() +// OSAtomicDecrement32(executingCount) +// } +// else { +// +// DispatchQueue.main.async { [weak self] in +// +// guard let self = self else { +// +// return +// } +// action() +// OSAtomicDecrement32(self.executingCount) +// } +// } +// } +// +// +// // MARK: Private +// +// private let executingCount: UnsafeMutablePointer = .allocate(capacity: 1) +// } +// } +} + +#endif diff --git a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift index 4489085..92f8f92 100644 --- a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift +++ b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift @@ -23,8 +23,6 @@ // SOFTWARE. // -#if canImport(UIKit) || canImport(AppKit) - import Foundation import CoreData @@ -41,10 +39,9 @@ import AppKit // MARK: - FetchedDiffableDataSourceSnapshot -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) internal protocol FetchedDiffableDataSourceSnapshotHandler: AnyObject { - func controller(_ controller: NSFetchedResultsController, didChangContentWith snapshot: NSDiffableDataSourceSnapshotReference) + func controller(_ controller: NSFetchedResultsController, didChangContentWith snapshot: Internals.DiffableDataSourceSnapshot) } @@ -54,7 +51,6 @@ extension Internals { // MARK: - FetchedDiffableDataSourceSnapshotDelegate - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) internal final class FetchedDiffableDataSourceSnapshotDelegate: NSObject, NSFetchedResultsControllerDelegate { // MARK: Internal @@ -77,18 +73,71 @@ extension Internals { self.fetchedResultsController?.delegate = nil } + internal func initialFetch() { + + #if canImport(UIKit) || canImport(AppKit) + + if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) { + + return + } + + #endif + + guard let fetchedResultsController = self.fetchedResultsController else { + + return + } + self.controllerDidChangeContent(fetchedResultsController.dynamicCast()) + } + // MARK: NSFetchedResultsControllerDelegate + #if canImport(UIKit) || canImport(AppKit) + + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) @objc dynamic func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { self.handler?.controller( controller, - didChangContentWith: snapshot + didChangContentWith: snapshot as NSDiffableDataSourceSnapshot ) } + + #endif + + @objc + dynamic func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + + self.reloadedIDs = [] + } + + @objc + dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + + self.handler?.controller( + controller, + didChangContentWith: FallbackDiffableDataSourceSnapshot( + sections: controller.sections ?? [] + ) + ) + self.reloadedIDs = [] + } + +// @objc +// dynamic func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { +// +// +//// [1].difference(from: <#T##BidirectionalCollection#>) +// let managedObject = anObject as! NSManagedObject +// self.reloadedIDs.insert(managedObject.objectID) +// } + + + // MARK: Private + + private var reloadedIDs: Set = [] } } - -#endif diff --git a/Sources/ListSnapshot.swift b/Sources/ListSnapshot.swift index 4ce0717..dba82f5 100644 --- a/Sources/ListSnapshot.swift +++ b/Sources/ListSnapshot.swift @@ -23,8 +23,6 @@ // SOFTWARE. // -#if canImport(UIKit) || canImport(AppKit) - import CoreData #if canImport(UIKit) @@ -38,21 +36,170 @@ import AppKit // MARK: - LiveList -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) public struct ListSnapshot: SnapshotResult, RandomAccessCollection, Hashable { // MARK: Public - - public subscript(indices: S) -> [ObjectType] where S.Element == Index { - + + public typealias SectionID = String + public typealias ItemID = D.ObjectID + + public subscript(indices indices: S) -> [ObjectType] where S.Element == Index { + let context = self.context! - let objectIDs = self.snapshotStruct.itemIdentifiers + let objectIDs = self.diffableSnapshot.itemIdentifiers return indices.map { position in - + let objectID = objectIDs[position] return context.fetchExisting(objectID)! } } + + public subscript(section sectionID: SectionID) -> [ObjectType] { + + let context = self.context! + let objectIDs = self.itemIdentifiers(inSection: sectionID) + return objectIDs.map { + + return context.fetchExisting($0)! + } + } + + public subscript(section sectionID: SectionID, itemIndices itemIndices: S) -> [ObjectType] where S.Element == Int { + + let context = self.context! + let objectIDs = self.itemIdentifiers(inSection: sectionID) + return itemIndices.map { position in + + let objectID = objectIDs[position] + return context.fetchExisting(objectID)! + } + } + + public var numberOfItems: Int { + + return self.diffableSnapshot.numberOfItems + } + + public var numberOfSections: Int { + + return self.diffableSnapshot.numberOfSections + } + + public var sectionIdentifiers: [String] { + + return self.diffableSnapshot.sectionIdentifiers as [String] + } + + public var itemIdentifiers: [ItemID] { + + return self.diffableSnapshot.itemIdentifiers as [ItemID] + } + + public func numberOfItems(inSection identifier: SectionID) -> Int { + + return self.diffableSnapshot.numberOfItems(inSection: identifier as NSString) + } + + public func itemIdentifiers(inSection identifier: SectionID) -> [ItemID] { + + return self.diffableSnapshot.itemIdentifiers(inSection: identifier as NSString) + } + + public func itemIdentifiers(inSection identifier: SectionID, atIndices indices: IndexSet) -> [ItemID] { + + let itemIDs = self.itemIdentifiers(inSection: identifier) + return indices.map({ itemIDs[$0] }) + } + + public func sectionIdentifier(containingItem identifier: ItemID) -> SectionID? { + + return self.diffableSnapshot.sectionIdentifier(containingItem: identifier) as SectionID? + } + + public func indexOfItem(_ identifier: ItemID) -> Index? { + + return self.diffableSnapshot.indexOfItem(identifier) + } + + public func indexOfSection(_ identifier: SectionID) -> Int? { + + return self.diffableSnapshot.indexOfSection(identifier as NSString) + } + + public mutating func appendItems(_ identifiers: [ItemID], toSection sectionIdentifier: SectionID? = nil) { + + self.diffableSnapshot.appendItems(identifiers, toSection: sectionIdentifier as NSString?) + } + + public mutating func insertItems(_ identifiers: [ItemID], beforeItem beforeIdentifier: ItemID) { + + self.diffableSnapshot.insertItems(identifiers, beforeItem: beforeIdentifier) + } + + public mutating func insertItems(_ identifiers: [ItemID], afterItem afterIdentifier: ItemID) { + + self.diffableSnapshot.insertItems(identifiers, afterItem: afterIdentifier) + } + + public mutating func deleteItems(_ identifiers: [ItemID]) { + + self.diffableSnapshot.deleteItems(identifiers) + } + + public mutating func deleteAllItems() { + + self.diffableSnapshot.deleteAllItems() + } + + public mutating func moveItem(_ identifier: ItemID, beforeItem toIdentifier: ItemID) { + + self.diffableSnapshot.moveItem(identifier, beforeItem: toIdentifier) + } + + public mutating func moveItem(_ identifier: ItemID, afterItem toIdentifier: ItemID) { + + self.diffableSnapshot.moveItem(identifier, afterItem: toIdentifier) + } + + public mutating func reloadItems(_ identifiers: [ItemID]) { + + self.diffableSnapshot.reloadItems(identifiers) + } + + public mutating func appendSections(_ identifiers: [SectionID]) { + + self.diffableSnapshot.appendSections(identifiers as [NSString]) + } + + public mutating func insertSections(_ identifiers: [SectionID], beforeSection toIdentifier: SectionID) { + + self.diffableSnapshot.insertSections(identifiers as [NSString], beforeSection: toIdentifier as NSString) + } + + public mutating func insertSections(_ identifiers: [SectionID], afterSection toIdentifier: SectionID) { + + self.diffableSnapshot.insertSections(identifiers as [NSString], afterSection: toIdentifier as NSString) + } + + public mutating func deleteSections(_ identifiers: [SectionID]) { + + self.diffableSnapshot.deleteSections(identifiers as [NSString]) + } + + public mutating func moveSection(_ identifier: SectionID, beforeSection toIdentifier: SectionID) { + + self.diffableSnapshot.moveSection(identifier as NSString, beforeSection: toIdentifier as NSString) + } + + public mutating func moveSection(_ identifier: SectionID, afterSection toIdentifier: SectionID) { + + self.diffableSnapshot.moveSection(identifier as NSString, afterSection: toIdentifier as NSString) + } + + public mutating func reloadSections(_ identifiers: [SectionID]) { + + self.diffableSnapshot.reloadSections(identifiers as [NSString]) + } // MARK: SnapshotResult @@ -64,18 +211,18 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec public var startIndex: Index { - return 0 + return self.diffableSnapshot.itemIdentifiers.startIndex } public var endIndex: Index { - return self.snapshotStruct.numberOfItems + return self.diffableSnapshot.itemIdentifiers.endIndex } public subscript(position: Index) -> ObjectType { let context = self.context! - let objectID = self.snapshotStruct.itemIdentifiers[position] + let objectID = self.diffableSnapshot.itemIdentifiers[position] return context.fetchExisting(objectID)! } @@ -106,16 +253,14 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec // MARK: Internal internal init() { - - self.snapshotReference = .init() - self.snapshotStruct = self.snapshotReference as NSDiffableDataSourceSnapshot + + self.diffableSnapshot = Internals.FallbackDiffableDataSourceSnapshot() self.context = nil } - internal init(snapshotReference: NSDiffableDataSourceSnapshotReference, context: NSManagedObjectContext) { - - self.snapshotReference = snapshotReference - self.snapshotStruct = snapshotReference as NSDiffableDataSourceSnapshot + internal init(diffableSnapshot: Internals.DiffableDataSourceSnapshot, context: NSManagedObjectContext) { + + self.diffableSnapshot = diffableSnapshot self.context = context } @@ -123,9 +268,6 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec // MARK: Private private let id: UUID = .init() - private let snapshotReference: NSDiffableDataSourceSnapshotReference - private let snapshotStruct: NSDiffableDataSourceSnapshot private let context: NSManagedObjectContext? + private var diffableSnapshot: Internals.DiffableDataSourceSnapshot } - -#endif diff --git a/Sources/LiveList.swift b/Sources/LiveList.swift index b87765e..e6a0cf8 100644 --- a/Sources/LiveList.swift +++ b/Sources/LiveList.swift @@ -23,18 +23,8 @@ // SOFTWARE. // -#if canImport(UIKit) || canImport(AppKit) - import CoreData -#if canImport(UIKit) -import UIKit - -#elseif canImport(AppKit) -import AppKit - -#endif - #if canImport(Combine) import Combine @@ -48,7 +38,6 @@ import SwiftUI // MARK: - LiveList -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) public final class LiveList { // MARK: Public @@ -57,23 +46,22 @@ public final class LiveList { willSet { - #if canImport(Combine) - - let oldValue = self.snapshot - guard newValue != oldValue else { + guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) else { return } + #if canImport(Combine) + #if canImport(SwiftUI) withAnimation { self.objectWillChange.send() } - + #else self.objectWillChange.send() - + #endif - + #endif } } @@ -182,13 +170,20 @@ public final class LiveList { applyFetchClauses: applyFetchClauses ) - #if canImport(Combine) - self.rawObjectWillChange = ObservableObjectPublisher() + if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) { - #else - self.rawObjectWillChange = nil + #if canImport(Combine) + self.rawObjectWillChange = ObservableObjectPublisher() - #endif + #else + self.rawObjectWillChange = nil + + #endif + } + else { + + self.rawObjectWillChange = nil + } // if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer { @@ -278,14 +273,16 @@ public final class LiveList { // MARK: - LiveList: FetchedDiffableDataSourceSnapshotHandler -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) extension LiveList: FetchedDiffableDataSourceSnapshotHandler { // MARK: FetchedDiffableDataSourceSnapshotHandler - internal func controller(_ controller: NSFetchedResultsController, didChangContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + internal func controller(_ controller: NSFetchedResultsController, didChangContentWith snapshot: Internals.DiffableDataSourceSnapshot) { - self.snapshot = .init(snapshotReference: snapshot, context: controller.managedObjectContext) + self.snapshot = .init( + diffableSnapshot: snapshot, + context: controller.managedObjectContext + ) } } @@ -307,5 +304,3 @@ extension LiveList: LiveResult { } #endif - -#endif diff --git a/Sources/LiveQuery.swift b/Sources/LiveQuery.swift index 5ecdd67..c270229 100644 --- a/Sources/LiveQuery.swift +++ b/Sources/LiveQuery.swift @@ -23,143 +23,151 @@ // SOFTWARE. // -#if canImport(SwiftUI) && canImport(Combine) - -import CoreData -import Combine -import SwiftUI - - -#warning("TODO: autoupdating doesn't work yet") -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) -@propertyWrapper -public struct LiveQuery: DynamicProperty { - - // MARK: Public - - @Environment(\.dataStack) - public var dataStack: DataStack - - public typealias ObjectType = Result.ObjectType - - - // MARK: @propertyWrapper - - public fileprivate(set) var wrappedValue: Result { - - get { - - return self.nonMutatingWrappedValue.wrappedValue - } - set { - - self.nonMutatingWrappedValue = LazyNonmutating { newValue } - } - } - - public var projectedValue: Result { - - return self.wrappedValue - } - - - // MARK: DynamicProperty - - public mutating func update() { - - SwiftUI.withAnimation { - - let dataStack = self.dataStack - if self.set(dataStack: dataStack) { - - return - } - self.wrappedValue = self.newWrappedValue(dataStack) - } - } - - - // MARK: FilePrivate - - fileprivate let newWrappedValue: (DataStack) -> Result - - fileprivate init(newWrappedValue: @escaping (DataStack) -> Result) { - - self.newWrappedValue = newWrappedValue - } - - - // MARK: Private - - private var nonMutatingWrappedValue: LazyNonmutating = .init { fatalError() } - - private var currentDataStack: DataStack? - - private mutating func set(dataStack: DataStack) -> Bool { - - guard self.currentDataStack != dataStack else { - - return false - } - self.currentDataStack = dataStack - - let newWrappedValue = self.newWrappedValue - self.nonMutatingWrappedValue = LazyNonmutating { - - newWrappedValue(dataStack) - } - return true - } - - - // MARK: - LazyNonmutating - - fileprivate final class LazyNonmutating { - - // MARK: FilePrivate - - lazy var wrappedValue: Value = self.initializer() - - init(_ initializer: @escaping () -> Value) { - - self.initializer = initializer - } - - - // MARK: Private - - private var initializer: () -> Value - } -} - - -#if canImport(UIKit) || canImport(AppKit) - -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) -extension LiveQuery { - - public init(liveList: LiveList) where Result == LiveList { - - self.init( - newWrappedValue: { _ in liveList } - ) - } - - public init(_ clauseChain: FetchChainBuilder) where Result == LiveList { - - self.init( - newWrappedValue: { $0.liveList(clauseChain) } - ) - } - - public init(_ clauseChain: SectionMonitorChainBuilder) where Result == LiveList { - - self.init( - newWrappedValue: { $0.liveList(clauseChain) } - ) - } -} - -#endif - -#endif +//#if canImport(SwiftUI) && canImport(Combine) +// +//import CoreData +//import Combine +//import SwiftUI +// +// +//#warning("TODO: autoupdating doesn't work yet") +//@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) +//@propertyWrapper +//public struct LiveQuery: DynamicProperty { +// +// // MARK: Public +// +// @Environment(\.dataStack) +// public var dataStack: DataStack +// +// public typealias ObjectType = Result.ObjectType +// +// +// // MARK: @propertyWrapper +// +// public var wrappedValue: Result { +// +// get { +// +// return self.backingWrapper!.wrappedValue +// } +// set { +// +// self.backingWrapper!.wrappedValue = newValue +// } +// } +// +// public var projectedValue: ObservedObject.Wrapper { +// +// return self.backingWrapper!.projectedValue +// } +// +// +// // MARK: DynamicProperty +// +// public mutating func update() { +// +// SwiftUI.withAnimation { +// +// let dataStack = self.dataStack +// +// if self.backingWrapper == nil { +// +// self.backingWrapper = ObservedObject(wrappedValue: self.newLiveResult(dataStack)) +// } +// else { +// +// self.backingWrapper!.wrappedValue = self.newLiveResult(dataStack) +// } +// self.backingWrapper!.update() +// } +// } +// +// +// // MARK: FilePrivate +// +// fileprivate let newLiveResult: (DataStack) -> Result +// +// fileprivate init(newLiveResult: @escaping (DataStack) -> Result) { +// +// self.newLiveResult = newLiveResult +// } +// +// +// // MARK: Private +// +//// private var nonMutatingWrappedValue: LazyNonmutating = .init { fatalError() } +// +// private var currentDataStack: DataStack? +// private var backingWrapper: ObservedObject? +// +// private mutating func set(dataStack: DataStack) -> Bool { +// +// guard self.currentDataStack != dataStack else { +// +// return false +// } +// self.currentDataStack = dataStack +// if self.backingWrapper == nil { +// +// self.backingWrapper = ObservedObject(wrappedValue: self.newLiveResult(dataStack)) +// } +// else { +// +// self.backingWrapper?.wrappedValue = self.newLiveResult(dataStack) +// } +// return true +// } +// +// +// // MARK: - LazyNonmutating +// +// fileprivate final class LazyNonmutating { +// +// // MARK: FilePrivate +// +// lazy var wrappedValue: Value = self.initializer() +// +// init(_ initializer: @escaping () -> Value) { +// +// self.initializer = initializer +// } +// +// +// // MARK: Private +// +// private var initializer: () -> Value +// } +//} +// +// +//#if canImport(UIKit) || canImport(AppKit) +// +//@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) +//extension LiveQuery { +// +// public init(liveList: LiveList) where Result == LiveList { +// +// self.init( +// newLiveResult: { _ in liveList } +// ) +// } +// +// public init(_ clauseChain: FetchChainBuilder) where Result == LiveList { +// +// self.init( +// newLiveResult: { $0.liveList(clauseChain) } +// ) +// } +// +// public init(_ clauseChain: SectionMonitorChainBuilder) where Result == LiveList { +// +// self.init( +// newLiveResult: { $0.liveList(clauseChain) } +// ) +// } +//} +// +//#endif +// +//#endif