(WIP) SwiftUI working demo for LiveList<D>

This commit is contained in:
John Estropia
2019-10-04 19:12:32 +09:00
parent c5a996d5ed
commit 953c9723a8
12 changed files with 1422 additions and 347 deletions

View File

@@ -0,0 +1,29 @@
//
// CoreStoreObject+DataSources.swift
// CoreStore iOS
//
// Created by John Estropia on 2019/10/04.
// Copyright © 2019 John Rommel Estropia. All rights reserved.
//
#if canImport(UIKit) || canImport(AppKit)
#if canImport(Combine)
import Combine
// MARK: - LiveList: ObservableObject
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
extension CoreStoreObject: ObservableObject {
// MARK: ObservableObject
public var objectWillChange: ObservableObjectPublisher {
return self.cs_toRaw().objectWillChange
}
}
#endif
#endif

View File

@@ -0,0 +1,171 @@
//
// DataStack+DataSources.swift
// CoreStore iOS
//
// 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 Foundation
import CoreData
// MARK: - DataStack
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
extension DataStack {
public func liveList<D>(_ from: From<D>, _ fetchClauses: FetchClause...) -> LiveList<D> {
return self.liveList(from, fetchClauses)
}
public func liveList<D>(_ from: From<D>, _ fetchClauses: [FetchClause]) -> LiveList<D> {
return LiveList(
dataStack: self,
from: from,
sectionBy: nil,
applyFetchClauses: { fetchRequest in
fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) }
Internals.assert(
fetchRequest.sortDescriptors?.isEmpty == false,
"An \(Internals.typeName(LiveList<D>.self)) requires a sort information. Specify from a \(Internals.typeName(OrderBy<D>.self)) clause or any custom \(Internals.typeName(FetchClause.self)) that provides a sort descriptor."
)
}
)
}
public func liveList<B: FetchChainableBuilderType>(_ clauseChain: B) -> LiveList<B.ObjectType> {
return self.liveList(
clauseChain.from,
clauseChain.fetchClauses
)
}
public func liveList<D>(createAsynchronously: @escaping (LiveList<D>) -> Void, _ from: From<D>, _ fetchClauses: FetchClause...) {
self.liveList(
createAsynchronously: createAsynchronously,
from, fetchClauses
)
}
public func liveList<D>(createAsynchronously: @escaping (LiveList<D>) -> Void, _ from: From<D>, _ fetchClauses: [FetchClause]) {
_ = LiveList(
dataStack: self,
from: from,
sectionBy: nil,
applyFetchClauses: { fetchRequest in
fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) }
Internals.assert(
fetchRequest.sortDescriptors?.isEmpty == false,
"An \(Internals.typeName(LiveList<D>.self)) requires a sort information. Specify from a \(Internals.typeName(OrderBy<D>.self)) clause or any custom \(Internals.typeName(FetchClause.self)) that provides a sort descriptor."
)
},
createAsynchronously: createAsynchronously
)
}
public func liveList<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) -> LiveList<D> {
return self.liveList(
from,
sectionBy,
fetchClauses
)
}
public func liveList<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: [FetchClause]) -> LiveList<D> {
return LiveList(
dataStack: self,
from: from,
sectionBy: sectionBy,
applyFetchClauses: { fetchRequest in
fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) }
Internals.assert(
fetchRequest.sortDescriptors?.isEmpty == false,
"An \(Internals.typeName(LiveList<D>.self)) requires a sort information. Specify from a \(Internals.typeName(OrderBy<D>.self)) clause or any custom \(Internals.typeName(FetchClause.self)) that provides a sort descriptor."
)
}
)
}
public func liveList<B: SectionMonitorBuilderType>(_ clauseChain: B) -> LiveList<B.ObjectType> {
return self.liveList(
clauseChain.from,
clauseChain.sectionBy,
clauseChain.fetchClauses
)
}
public func liveList<D>(createAsynchronously: @escaping (LiveList<D>) -> Void, _ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) {
self.liveList(
createAsynchronously: createAsynchronously,
from,
sectionBy,
fetchClauses
)
}
public func liveList<D>(createAsynchronously: @escaping (LiveList<D>) -> Void, _ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: [FetchClause]) {
_ = LiveList(
dataStack: self,
from: from,
sectionBy: sectionBy,
applyFetchClauses: { fetchRequest in
fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) }
Internals.assert(
fetchRequest.sortDescriptors?.isEmpty == false,
"An \(Internals.typeName(LiveList<D>.self)) requires a sort information. Specify from a \(Internals.typeName(OrderBy<D>.self)) clause or any custom \(Internals.typeName(FetchClause.self)) that provides a sort descriptor."
)
},
createAsynchronously: createAsynchronously
)
}
public func liveList<B: SectionMonitorBuilderType>(createAsynchronously: @escaping (LiveList<B.ObjectType>) -> Void, _ clauseChain: B) {
self.liveList(
createAsynchronously: createAsynchronously,
clauseChain.from,
clauseChain.sectionBy,
clauseChain.fetchClauses
)
}
}
#endif

View File

@@ -102,7 +102,10 @@ extension DataStack {
*/
public func monitorList<B: FetchChainableBuilderType>(_ clauseChain: B) -> ListMonitor<B.ObjectType> {
return self.monitorList(clauseChain.from, clauseChain.fetchClauses)
return self.monitorList(
clauseChain.from,
clauseChain.fetchClauses
)
}
/**
@@ -114,7 +117,11 @@ extension DataStack {
*/
public func monitorList<D>(createAsynchronously: @escaping (ListMonitor<D>) -> Void, _ from: From<D>, _ fetchClauses: FetchClause...) {
self.monitorList(createAsynchronously: createAsynchronously, from, fetchClauses)
self.monitorList(
createAsynchronously: createAsynchronously,
from,
fetchClauses
)
}
/**
@@ -182,7 +189,11 @@ extension DataStack {
*/
public func monitorSectionedList<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) -> ListMonitor<D> {
return self.monitorSectionedList(from, sectionBy, fetchClauses)
return self.monitorSectionedList(
from,
sectionBy,
fetchClauses
)
}
/**
@@ -248,7 +259,12 @@ extension DataStack {
*/
public func monitorSectionedList<D>(createAsynchronously: @escaping (ListMonitor<D>) -> Void, _ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) {
self.monitorSectionedList(createAsynchronously: createAsynchronously, from, sectionBy, fetchClauses)
self.monitorSectionedList(
createAsynchronously: createAsynchronously,
from,
sectionBy,
fetchClauses
)
}
/**

View File

@@ -1,276 +0,0 @@
//
// Internals.FetchedResultsControllerDelegate.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.
//
import Foundation
import CoreData
// MARK: - FetchedResultsControllerHandler
@available(macOS 10.12, *)
internal protocol FetchedResultsControllerHandler: AnyObject {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeObject anObject: Any, atIndexPath indexPath: IndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String?) -> String?
}
// MARK: - Internal
extension Internals {
// MARK: - FetchedResultsControllerDelegate
@available(macOS 10.12, *)
internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: Internal
@nonobjc
internal var enabled = true
@nonobjc
internal let taskGroup = DispatchGroup()
@nonobjc
internal weak var handler: FetchedResultsControllerHandler?
@nonobjc
internal weak var fetchedResultsController: Internals.CoreStoreFetchedResultsController? {
didSet {
oldValue?.delegate = nil
self.fetchedResultsController?.delegate = self
}
}
deinit {
self.fetchedResultsController?.delegate = nil
}
// MARK: NSFetchedResultsControllerDelegate
@available(iOS 13.0, *)
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshot) {
}
@objc
dynamic func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.taskGroup.enter()
guard self.enabled else {
return
}
if #available(iOS 10.3, tvOS 10.3, watchOS 3.2, macOS 10.13, *) {}
else {
self.deletedSections = []
self.insertedSections = []
}
self.handler?.controllerWillChangeContent(controller)
}
@objc
dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
defer {
self.taskGroup.leave()
}
guard self.enabled else {
return
}
self.handler?.controllerDidChangeContent(controller)
}
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
guard self.enabled else {
return
}
if #available(iOS 10.3, tvOS 10.3, watchOS 3.2, macOS 10.13, *) {
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: indexPath,
forChangeType: type,
newIndexPath: newIndexPath
)
return
}
guard var actualType = NSFetchedResultsChangeType(rawValue: type.rawValue) else {
// This fix is for a bug where iOS passes 0 for NSFetchedResultsChangeType, but this is not a valid enum case.
// Swift will then always execute the first case of the switch causing strange behaviour.
// https://forums.developer.apple.com/thread/12184#31850
return
}
// This whole dance is a workaround for a nasty bug introduced in XCode 7 targeted at iOS 8 devices
// http://stackoverflow.com/questions/31383760/ios-9-attempt-to-delete-and-reload-the-same-index-path/31384014#31384014
// https://forums.developer.apple.com/message/9998#9998
// https://forums.developer.apple.com/message/31849#31849
if case .update = actualType,
indexPath != nil,
newIndexPath != nil {
actualType = .move
}
switch actualType {
case .update:
guard let section = indexPath?[0] else {
return
}
if self.deletedSections.contains(section)
|| self.insertedSections.contains(section) {
return
}
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else {
return
}
guard indexPath == newIndexPath else {
break
}
if self.insertedSections.contains(indexPath[0]) {
// Observers that handle the .Move change are advised to delete then reinsert the object instead of just moving. This is especially true when indexPath and newIndexPath are equal. For example, calling tableView.moveRowAtIndexPath(_:toIndexPath) when both indexPaths are the same will crash the tableView.
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: indexPath,
forChangeType: .move,
newIndexPath: newIndexPath
)
return
}
if self.deletedSections.contains(indexPath[0]) {
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: nil,
forChangeType: .insert,
newIndexPath: indexPath
)
return
}
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: indexPath,
forChangeType: .update,
newIndexPath: nil
)
return
default:
break
}
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: indexPath,
forChangeType: actualType,
newIndexPath: newIndexPath
)
}
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
guard self.enabled else {
return
}
if #available(iOS 10.3, tvOS 10.3, watchOS 3.2, macOS 10.13, *) {}
else {
switch type {
case .delete: self.deletedSections.insert(sectionIndex)
case .insert: self.insertedSections.insert(sectionIndex)
default: break
}
}
self.handler?.controller(
controller,
didChangeSection: sectionInfo,
atIndex: sectionIndex,
forChangeType: type
)
}
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String) -> String? {
return self.handler?.controller(
controller,
sectionIndexTitleForSectionName: sectionName
)
}
// MARK: Private
@nonobjc
private var deletedSections = Set<Int>()
@nonobjc
private var insertedSections = Set<Int>()
}
}

View File

@@ -0,0 +1,94 @@
//
// Internals.FetchedDiffableDataSourceSnapshotDelegate.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 Foundation
import CoreData
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
// MARK: - FetchedDiffableDataSourceSnapshot
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
internal protocol FetchedDiffableDataSourceSnapshotHandler: AnyObject {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangContentWith snapshot: NSDiffableDataSourceSnapshotReference)
}
// MARK: - Internal
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
@nonobjc
internal weak var handler: FetchedDiffableDataSourceSnapshotHandler?
@nonobjc
internal weak var fetchedResultsController: Internals.CoreStoreFetchedResultsController? {
didSet {
oldValue?.delegate = nil
self.fetchedResultsController?.delegate = self
}
}
deinit {
self.fetchedResultsController?.delegate = nil
}
// MARK: NSFetchedResultsControllerDelegate
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
self.handler?.controller(
controller,
didChangContentWith: snapshot
)
}
}
}
#endif

View File

@@ -948,7 +948,7 @@ public final class ListMonitor<D: DynamicObject>: Hashable {
return
}
let (newFetchedResultsController, newFetchedResultsControllerDelegate) = ListMonitor.recreateFetchedResultsController(
let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController(
context: self.fetchedResultsController.managedObjectContext,
from: self.from,
sectionBy: self.sectionBy,
@@ -1083,7 +1083,7 @@ public final class ListMonitor<D: DynamicObject>: Hashable {
self.isSectioned = (sectionBy != nil)
self.from = from
self.sectionBy = sectionBy
(self.fetchedResultsController, self.fetchedResultsControllerDelegate) = ListMonitor.recreateFetchedResultsController(
(self.fetchedResultsController, self.fetchedResultsControllerDelegate) = Self.recreateFetchedResultsController(
context: context,
from: from,
sectionBy: sectionBy,

View File

@@ -23,16 +23,28 @@
// SOFTWARE.
//
#if canImport(Combine)
#if canImport(UIKit) || canImport(AppKit)
import Combine
import CoreData
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
#if canImport(SwiftUI)
import SwiftUI
#endif
// MARK: - LiveList
@available(iOS 13.0, *)
public class LiveList<D: DynamicObject>: Hashable, ObservableObject {
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
public final class LiveList<D: DynamicObject>: Hashable {
// MARK: Public (Accessors)
@@ -41,38 +53,272 @@ public class LiveList<D: DynamicObject>: Hashable, ObservableObject {
*/
public typealias ObjectType = D
public var snapshot: Snapshot = []
// MARK: ObservableObject
public var objectWillChange: ObservableObjectPublisher {
return self.cs_toRaw().objectWillChange
public fileprivate(set) var snapshot: Snapshot = .empty {
didSet {
#if canImport(Combine)
let newValue = self.snapshot
guard newValue != oldValue else {
return
}
#if canImport(SwiftUI)
withAnimation {
self.objectWillChange.send()
}
#else
self.objectWillChange.send()
#endif
#endif
}
}
// MARK: Equatable
public static func == (_ lhs: LiveList<D>, _ rhs: LiveList<D>) -> Bool {
return lhs === rhs
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
// MARK: Internal
internal convenience init(dataStack: DataStack, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void) {
self.init(
context: dataStack.mainContext,
from: from,
sectionBy: sectionBy,
applyFetchClauses: applyFetchClauses,
createAsynchronously: nil
)
}
internal convenience init(dataStack: DataStack, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, createAsynchronously: @escaping (LiveList<ObjectType>) -> Void) {
self.init(
context: dataStack.mainContext,
from: from,
sectionBy: sectionBy,
applyFetchClauses: applyFetchClauses,
createAsynchronously: createAsynchronously
)
}
internal convenience init(unsafeTransaction: UnsafeDataTransaction, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void) {
self.init(
context: unsafeTransaction.context,
from: from,
sectionBy: sectionBy,
applyFetchClauses: applyFetchClauses,
createAsynchronously: nil
)
}
internal convenience init(unsafeTransaction: UnsafeDataTransaction, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, createAsynchronously: @escaping (LiveList<ObjectType>) -> Void) {
self.init(
context: unsafeTransaction.context,
from: from,
sectionBy: sectionBy,
applyFetchClauses: applyFetchClauses,
createAsynchronously: createAsynchronously
)
}
// MARK: FilePrivate
fileprivate let rawObjectWillChange: Any?
// MARK: Private
private let observer: Internals.FetchedResultsControllerDelegate
private var fetchedResultsController: Internals.CoreStoreFetchedResultsController
private var fetchedResultsControllerDelegate: Internals.FetchedDiffableDataSourceSnapshotDelegate
private var applyFetchClauses: (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void
private var observerForWillChangePersistentStore: Internals.NotificationObserver!
private var observerForDidChangePersistentStore: Internals.NotificationObserver!
private let from: From<ObjectType>
private let sectionBy: SectionBy<ObjectType>?
private static func recreateFetchedResultsController(context: NSManagedObjectContext, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void) -> (controller: Internals.CoreStoreFetchedResultsController, delegate: Internals.FetchedDiffableDataSourceSnapshotDelegate) {
let fetchRequest = Internals.CoreStoreFetchRequest<NSManagedObject>()
fetchRequest.fetchLimit = 0
fetchRequest.resultType = .managedObjectResultType
fetchRequest.includesPendingChanges = false
fetchRequest.shouldRefreshRefetchedObjects = true
let fetchedResultsController = Internals.CoreStoreFetchedResultsController(
context: context,
fetchRequest: fetchRequest,
from: from,
sectionBy: sectionBy,
applyFetchClauses: applyFetchClauses
)
let fetchedResultsControllerDelegate = Internals.FetchedDiffableDataSourceSnapshotDelegate()
fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController
return (fetchedResultsController, fetchedResultsControllerDelegate)
}
private init(context: NSManagedObjectContext, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, createAsynchronously: ((LiveList<ObjectType>) -> Void)?) {
self.from = from
self.sectionBy = sectionBy
(self.fetchedResultsController, self.fetchedResultsControllerDelegate) = Self.recreateFetchedResultsController(
context: context,
from: from,
sectionBy: sectionBy,
applyFetchClauses: applyFetchClauses
)
#if canImport(Combine)
self.rawObjectWillChange = ObservableObjectPublisher()
#else
self.rawObjectWillChange = nil
#endif
// if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer {
//
// self.sectionIndexTransformer = sectionIndexTransformer
// }
// else {
//
// self.sectionIndexTransformer = { $0 }
// }
self.applyFetchClauses = applyFetchClauses
self.fetchedResultsControllerDelegate.handler = self
guard let coordinator = context.parentStack?.coordinator else {
return
}
self.observerForWillChangePersistentStore = Internals.NotificationObserver(
notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresWillChange,
object: coordinator,
queue: OperationQueue.main,
closure: { [weak self] (note) -> Void in
guard let `self` = self else {
return
}
// self.isPersistentStoreChanging = true
//
// guard let removedStores = (note.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore]).flatMap(Set.init),
// !Set(self.fetchedResultsController.typedFetchRequest.safeAffectedStores() ?? []).intersection(removedStores).isEmpty else {
//
// return
// }
// self.refetch(self.applyFetchClauses)
}
)
self.observerForDidChangePersistentStore = Internals.NotificationObserver(
notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresDidChange,
object: coordinator,
queue: OperationQueue.main,
closure: { [weak self] (note) -> Void in
guard let `self` = self else {
return
}
// if !self.isPendingRefetch {
//
// let previousStores = Set(self.fetchedResultsController.typedFetchRequest.safeAffectedStores() ?? [])
// let currentStores = previousStores
// .subtracting(note.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore] ?? [])
// .union(note.userInfo?[NSAddedPersistentStoresKey] as? [NSPersistentStore] ?? [])
//
// if previousStores != currentStores {
//
// self.refetch(self.applyFetchClauses)
// }
// }
//
// self.isPersistentStoreChanging = false
}
)
if let createAsynchronously = createAsynchronously {
// transactionQueue.async {
//
// try! self.fetchedResultsController.performFetchFromSpecifiedStores()
// self.taskGroup.notify(queue: .main) {
//
// createAsynchronously(self)
// }
// }
}
else {
try! self.fetchedResultsController.performFetchFromSpecifiedStores()
}
}
// MARK: - Snapshot
public struct Snapshot: RandomAccessCollection {
public struct Snapshot: RandomAccessCollection, Hashable {
public subscript(indices: IndexSet) -> [ObjectType] {
let context = self.context!
let objectIDs = self.snapshotStruct.itemIdentifiers
return indices.map { position in
let objectID = objectIDs[position]
return context.fetchExisting(objectID)!
}
}
// MARK: RandomAccessCollection
public var startIndex: Index {
return 0
}
public var endIndex: Index {
return self.snapshotStruct.numberOfItems
}
public subscript(position: Index) -> ObjectType {
let context = self.context!
let objectID = self.snapshotStruct.itemIdentifiers[position]
return context.fetchExisting(objectID)!
}
@@ -95,36 +341,85 @@ public class LiveList<D: DynamicObject>: Hashable, ObservableObject {
// /// supplying `IndexingIterator` as its associated `Iterator`
// /// type.
// public typealias Iterator = IndexingIterator<FetchedResults<Result>>
private let diffableSource: NSDiffable
}
}
extension ListMonitor: ObservableObject {
public var objectWillChange: ObservableObjectPublisher {
// MARK: Equatable
public static func == (_ lhs: Snapshot, _ rhs: Snapshot) -> Bool {
return lhs.snapshotReference == rhs.snapshotReference
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(self.snapshotReference)
}
// MARK: Internal
internal static var empty: Snapshot {
return .init()
}
internal init(snapshotReference: NSDiffableDataSourceSnapshotReference, context: NSManagedObjectContext) {
self.snapshotReference = snapshotReference
self.snapshotStruct = snapshotReference as NSDiffableDataSourceSnapshot<NSString, NSManagedObjectID>
self.context = context
}
// MARK: Private
return withUnsafePointer(to: &Static.objectWillChange) {
self.userInfo[
$0,
lazyInit: ObservableObjectPublisher.init
] as! ObservableObjectPublisher
private let snapshotReference: NSDiffableDataSourceSnapshotReference
private let snapshotStruct: NSDiffableDataSourceSnapshot<NSString, NSManagedObjectID>
private let context: NSManagedObjectContext?
private init() {
self.snapshotReference = .init()
self.snapshotStruct = self.snapshotReference as NSDiffableDataSourceSnapshot<NSString, NSManagedObjectID>
self.context = nil
}
}
}
@available(iOS 13.0, *)
extension CoreStoreObject: ObservableObject {
public var objectWillChange: ObservableObjectPublisher {
return self.cs_toRaw().objectWillChange
// 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<NSFetchRequestResult>, didChangContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
self.snapshot = .init(snapshotReference: snapshot, context: controller.managedObjectContext)
}
}
fileprivate enum Static {
static var objectWillChange: Void?
#if canImport(Combine)
import Combine
// MARK: - LiveList: ObservableObject
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
extension LiveList: ObservableObject {
// MARK: ObservableObject
public var objectWillChange: ObservableObjectPublisher {
return self.rawObjectWillChange! as! ObservableObjectPublisher
}
}
#endif
#endif