diff --git a/AlecrimCoreData.podspec b/AlecrimCoreData.podspec index 7b14cf9..a59f03b 100644 --- a/AlecrimCoreData.podspec +++ b/AlecrimCoreData.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "AlecrimCoreData" - s.version = "6.0" + s.version = "6.0.1" s.summary = "A powerful and elegant Core Data framework for Swift." s.homepage = "https://www.alecrim.com/AlecrimCoreData" diff --git a/Source/AlecrimCoreData.xcodeproj/project.pbxproj b/Source/AlecrimCoreData.xcodeproj/project.pbxproj index ec0b171..3a7a6c5 100644 --- a/Source/AlecrimCoreData.xcodeproj/project.pbxproj +++ b/Source/AlecrimCoreData.xcodeproj/project.pbxproj @@ -11,8 +11,10 @@ 1454786620B247F300831016 /* PersistentContainerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1454786520B247F300831016 /* PersistentContainerType.swift */; }; 1454786820B2484900831016 /* PersistentContainerAuxiliarTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1454786720B2484800831016 /* PersistentContainerAuxiliarTypes.swift */; }; 1454786A20B2487300831016 /* CustomPersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1454786920B2487300831016 /* CustomPersistentContainer.swift */; }; + 1462C65F20C8E63A00A7A4E6 /* EntityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1462C65E20C8E63A00A7A4E6 /* EntityObserver.swift */; }; 146B042F208AEAE3002091BF /* FetchRequestController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146B042E208AEAE3002091BF /* FetchRequestController+Extensions.swift */; }; 14B9460020759D0D00A7CFFD /* NSTableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B945FF20759D0D00A7CFFD /* NSTableView+Extensions.swift */; }; + 14C2028F20C8DBAB00821A79 /* ManagedObjectContextType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C2028E20C8DBAB00821A79 /* ManagedObjectContextType.swift */; }; 14CC3374205B28CA00BA682A /* NSArrayController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CC335E205B28CA00BA682A /* NSArrayController+Extensions.swift */; }; 14CC3375205B28CA00BA682A /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CC335F205B28CA00BA682A /* UITableView+Extensions.swift */; }; 14CC3376205B28CA00BA682A /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CC3360205B28CA00BA682A /* UICollectionView+Extensions.swift */; }; @@ -40,8 +42,10 @@ 1454786520B247F300831016 /* PersistentContainerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainerType.swift; sourceTree = ""; }; 1454786720B2484800831016 /* PersistentContainerAuxiliarTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainerAuxiliarTypes.swift; sourceTree = ""; }; 1454786920B2487300831016 /* CustomPersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPersistentContainer.swift; sourceTree = ""; }; + 1462C65E20C8E63A00A7A4E6 /* EntityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityObserver.swift; sourceTree = ""; }; 146B042E208AEAE3002091BF /* FetchRequestController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequestController+Extensions.swift"; sourceTree = ""; }; 14B945FF20759D0D00A7CFFD /* NSTableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Extensions.swift"; sourceTree = ""; }; + 14C2028E20C8DBAB00821A79 /* ManagedObjectContextType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextType.swift; sourceTree = ""; }; 14CC335E205B28CA00BA682A /* NSArrayController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSArrayController+Extensions.swift"; sourceTree = ""; }; 14CC335F205B28CA00BA682A /* UITableView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = ""; }; 14CC3360205B28CA00BA682A /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = ""; }; @@ -112,12 +116,14 @@ 14CC335D205B28CA00BA682A /* Convenience */ = { isa = PBXGroup; children = ( + 1462C65E20C8E63A00A7A4E6 /* EntityObserver.swift */, 146B042E208AEAE3002091BF /* FetchRequestController+Extensions.swift */, - 14CC3360205B28CA00BA682A /* UICollectionView+Extensions.swift */, - 14CC335F205B28CA00BA682A /* UITableView+Extensions.swift */, + 14C2028E20C8DBAB00821A79 /* ManagedObjectContextType.swift */, 14CC335E205B28CA00BA682A /* NSArrayController+Extensions.swift */, 14CC3361205B28CA00BA682A /* NSCollectionView+Extensions.swift */, 14B945FF20759D0D00A7CFFD /* NSTableView+Extensions.swift */, + 14CC3360205B28CA00BA682A /* UICollectionView+Extensions.swift */, + 14CC335F205B28CA00BA682A /* UITableView+Extensions.swift */, ); path = Convenience; sourceTree = ""; @@ -261,6 +267,7 @@ 14CC3374205B28CA00BA682A /* NSArrayController+Extensions.swift in Sources */, 14CC3385205B28CA00BA682A /* FetchedResultsControllerDelegate.swift in Sources */, 14CC337C205B28CA00BA682A /* Expression.swift in Sources */, + 14C2028F20C8DBAB00821A79 /* ManagedObjectContextType.swift in Sources */, 14CC3380205B28CA00BA682A /* Config.swift in Sources */, 14CC3377205B28CA00BA682A /* NSCollectionView+Extensions.swift in Sources */, 1454786820B2484900831016 /* PersistentContainerAuxiliarTypes.swift in Sources */, @@ -268,6 +275,7 @@ 1454786620B247F300831016 /* PersistentContainerType.swift in Sources */, 14CC337B205B28CA00BA682A /* Query.swift in Sources */, 14CC3384205B28CA00BA682A /* FetchedResultsSectionInfo.swift in Sources */, + 1462C65F20C8E63A00A7A4E6 /* EntityObserver.swift in Sources */, 14CC3375205B28CA00BA682A /* UITableView+Extensions.swift in Sources */, 14CC3379205B28CA00BA682A /* ManagedObjectContext.swift in Sources */, 14CC3383205B28CA00BA682A /* FetchRequestController.swift in Sources */, @@ -412,7 +420,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1815; + CURRENT_PROJECT_VERSION = 1837; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -444,7 +452,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1815; + CURRENT_PROJECT_VERSION = 1837; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/Source/AlecrimCoreData/Convenience/EntityObserver.swift b/Source/AlecrimCoreData/Convenience/EntityObserver.swift new file mode 100644 index 0000000..17003b9 --- /dev/null +++ b/Source/AlecrimCoreData/Convenience/EntityObserver.swift @@ -0,0 +1,45 @@ +// +// EntityObserver.swift +// AlecrimCoreData +// +// Created by Vanderlei Martinelli on 07/06/18. +// Copyright © 2018 Alecrim. All rights reserved. +// + +import Foundation + +// MARK: - + +/// A fetch request controller wrapper for one entity only. Can be used as observer for an entity detail presentation, for example. +public final class EntityObserver { + + private let frc: FetchRequestController + + fileprivate init(entity: EntityType, propertyName: String, updateHandler didChangeContentClosure: @escaping () -> Void, context: ManagedObjectContext) { + self.frc = Query(in: context) + .batchSize(0) + .filtered(using: Predicate(format: "SELF == %@", argumentArray: [entity])) + .sorted(by: SortDescriptor(key: propertyName, ascending: true)) + .toFetchRequestController() + + self.frc.didChangeContent(closure: didChangeContentClosure) + } + + deinit { + self.frc.removeAllBindings() + } + +} + +// MARK: - + +extension PersistentContainerType { + public func observer(for entity: EntityType, updateHandler: @escaping () -> Void) -> EntityObserver { + // using any property here is fine, but there must be at least one property + guard let propertyName = entity.entity.properties.first(where: { $0.isTransient == false })?.name else { + fatalError("No property found.") + } + + return EntityObserver(entity: entity, propertyName: propertyName, updateHandler: updateHandler, context: self.viewContext) + } +} diff --git a/Source/AlecrimCoreData/Convenience/ManagedObjectContextType.swift b/Source/AlecrimCoreData/Convenience/ManagedObjectContextType.swift new file mode 100644 index 0000000..bf5b1c9 --- /dev/null +++ b/Source/AlecrimCoreData/Convenience/ManagedObjectContextType.swift @@ -0,0 +1,133 @@ +// +// ManagedObjectContextType.swift +// AlecrimCoreData +// +// Created by Vanderlei Martinelli on 07/06/18. +// Copyright © 2018 Alecrim. All rights reserved. +// + +import Foundation + +// MARK: - + +extension ManagedObjectContext: ManagedObjectContextType {} + +// MARK: - + +public protocol ManagedObjectContextType { + func perform(_ block: @escaping () -> Void) + func performAndWait(_ block: () -> Void) +} + +// MARK: - + +extension ManagedObjectContextType { + + public func async(execute closure: @escaping (Self) throws -> Value, completion: @escaping ((Value?, Error?) -> Void)) { + let context = self + + context.perform { + do { + let value = try closure(context) + completion(value, nil) + } + catch { + completion(nil, error) + } + } + } + + public func async(execute closure: @escaping (Self) -> Value, completion: ((Value) -> Void)? = nil) { + let context = self + + context.perform { + let value = closure(context) + completion?(value) + } + } + + @discardableResult + public func sync(execute closure: (Self) throws -> Value) throws -> Value { + var value: Value? + var outError: Error? + + let context = self + + context.performAndWait { + do { + value = try closure(context) + } + catch { + outError = error + } + } + + if let outError = outError { + throw outError + } + + return value! + } + + @discardableResult + public func sync(execute closure: (Self) -> Value) -> Value { + var value: Value? + + let context = self + + context.performAndWait { + value = closure(context) + } + + return value! + } + +} + +// MARK: - + +extension PersistentContainer { + + public func async(execute closure: @escaping (ManagedObjectContext) throws -> Value, completion: @escaping ((Value?, Error?) -> Void)) { + return self.backgroundContext.async(execute: closure, completion: completion) + } + + public func async(execute closure: @escaping (ManagedObjectContext) -> Value, completion: ((Value) -> Void)? = nil) { + return self.backgroundContext.async(execute: closure, completion: completion) + } + + @discardableResult + public func sync(execute closure: (ManagedObjectContext) throws -> Value) throws -> Value { + return try self.backgroundContext.sync(execute: closure) + } + + @discardableResult + public func sync(execute closure: (ManagedObjectContext) -> Value) -> Value { + return self.backgroundContext.sync(execute: closure) + } + +} + +// MARK: - + +extension CustomPersistentContainer { + + public func async(execute closure: @escaping (Context) throws -> Value, completion: @escaping ((Value?, Error?) -> Void)) { + return self.backgroundContext.async(execute: closure, completion: completion) + } + + public func async(execute closure: @escaping (Context) -> Value, completion: ((Value) -> Void)? = nil) { + return self.backgroundContext.async(execute: closure, completion: completion) + } + + @discardableResult + public func sync(execute closure: (Context) throws -> Value) throws -> Value { + return try self.backgroundContext.sync(execute: closure) + } + + @discardableResult + public func sync(execute closure: (Context) -> Value) -> Value { + return self.backgroundContext.sync(execute: closure) + } + +} diff --git a/Source/AlecrimCoreData/Convenience/NSCollectionView+Extensions.swift b/Source/AlecrimCoreData/Convenience/NSCollectionView+Extensions.swift index d841265..687caff 100644 --- a/Source/AlecrimCoreData/Convenience/NSCollectionView+Extensions.swift +++ b/Source/AlecrimCoreData/Convenience/NSCollectionView+Extensions.swift @@ -126,6 +126,12 @@ extension FetchRequestController { case .move(let oldIndexPath, let newIndexPath): collectionView.moveItem(at: oldIndexPath, to: newIndexPath) + + // workaround to be sure that cells will be refreshed + // note: this only works when using a cell configuration handler + if itemConfigurationHandler != nil { + updatedIndexPaths.append(newIndexPath) + } } } }, completionHandler: { _ in diff --git a/Source/AlecrimCoreData/Convenience/UICollectionView+Extensions.swift b/Source/AlecrimCoreData/Convenience/UICollectionView+Extensions.swift index 6c66c19..1b8c7df 100644 --- a/Source/AlecrimCoreData/Convenience/UICollectionView+Extensions.swift +++ b/Source/AlecrimCoreData/Convenience/UICollectionView+Extensions.swift @@ -122,6 +122,12 @@ extension FetchRequestController { case .move(let oldIndexPath, let newIndexPath): collectionView.moveItem(at: oldIndexPath, to: newIndexPath) + + // workaround to be sure that cells will be refreshed + // note: this only works when using a cell configuration handler + if cellConfigurationHandler != nil { + updatedIndexPaths.append(newIndexPath) + } } } }, completion: { _ in diff --git a/Source/AlecrimCoreData/Core/Persistent Container/CustomPersistentContainer.swift b/Source/AlecrimCoreData/Core/Persistent Container/CustomPersistentContainer.swift index 55ba6b3..1667b58 100644 --- a/Source/AlecrimCoreData/Core/Persistent Container/CustomPersistentContainer.swift +++ b/Source/AlecrimCoreData/Core/Persistent Container/CustomPersistentContainer.swift @@ -12,49 +12,8 @@ import CoreData open class CustomPersistentContainer { // MARK: - - - fileprivate final class HelperPersistentContainer: PersistentContainer { - - private lazy var _viewContext: NSManagedObjectContext = { - let context = Context(concurrencyType: .mainQueueConcurrencyType) - - context.persistentStoreCoordinator = self.persistentStoreCoordinator - - context.automaticallyMergesChangesFromParent = true - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - - return context - }() - - fileprivate override var viewContext: NSManagedObjectContext { return self._viewContext } - - fileprivate override func newBackgroundContext() -> NSManagedObjectContext { - let context = Context(concurrencyType: .privateQueueConcurrencyType) - - context.persistentStoreCoordinator = self.persistentStoreCoordinator - - context.automaticallyMergesChangesFromParent = true - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - - return context - } - - fileprivate override func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { - super.performBackgroundTask { context in - // - context.persistentStoreCoordinator = self.persistentStoreCoordinator - - // - context.automaticallyMergesChangesFromParent = true - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - - // - block(context) - } - } - - } - + + public private(set) lazy var backgroundContext: Context = self.newBackgroundContext() // MARK: - @@ -62,12 +21,17 @@ open class CustomPersistentContainer { // MARK: - - public convenience init() { - try! self.init(storageType: .disk, managedObjectModel: type(of: self).managedObjectModel(), persistentStoreURL: type(of: self).persistentStoreURL(), ubiquitousConfiguration: nil) + /// Use caution when using this initializer. + public init(name: String? = nil) { + self.rawValue = HelperPersistentContainer(name: name) } - - public init(storageType: PersistentContainerStorageType = .disk, managedObjectModel: NSManagedObjectModel, persistentStoreURL: URL, ubiquitousConfiguration: PersistentContainerUbiquitousConfiguration? = nil) throws { - self.rawValue = try HelperPersistentContainer(storageType: storageType, managedObjectModel: managedObjectModel, persistentStoreURL: persistentStoreURL, ubiquitousConfiguration: ubiquitousConfiguration) + + public init(name: String? = nil, managedObjectModel: NSManagedObjectModel, storageType: PersistentContainerStorageType, persistentStoreURL: URL, persistentStoreDescriptionOptions: [String : NSObject]? = nil, ubiquitousConfiguration: PersistentContainerUbiquitousConfiguration? = nil) throws { + self.rawValue = try HelperPersistentContainer(name: name, managedObjectModel: managedObjectModel, storageType: storageType, persistentStoreURL: persistentStoreURL, persistentStoreDescriptionOptions: persistentStoreDescriptionOptions, ubiquitousConfiguration: ubiquitousConfiguration) + } + + public init(name: String, managedObjectModel: NSManagedObjectModel, persistentStoreDescription: NSPersistentStoreDescription, completionHandler: @escaping (NSPersistentContainer, NSPersistentStoreDescription, Error?) -> Void) throws { + self.rawValue = try HelperPersistentContainer(name: name, managedObjectModel: managedObjectModel, persistentStoreDescription: persistentStoreDescription, completionHandler: completionHandler) } // MARK: - @@ -87,3 +51,34 @@ open class CustomPersistentContainer { } } + +// MARK: - + +extension CustomPersistentContainer { + + fileprivate final class HelperPersistentContainer: PersistentContainer { + + private lazy var _viewContext: NSManagedObjectContext = { + let context = Context(concurrencyType: .mainQueueConcurrencyType) + self.configureManagedObjectContext(context) + + return context + }() + + fileprivate override var viewContext: NSManagedObjectContext { return self._viewContext } + + fileprivate override func newBackgroundContext() -> NSManagedObjectContext { + let context = Context(concurrencyType: .privateQueueConcurrencyType) + self.configureManagedObjectContext(context) + + return context + } + + fileprivate override func configureManagedObjectContext(_ context: NSManagedObjectContext) { + context.persistentStoreCoordinator = self.persistentStoreCoordinator + super.configureManagedObjectContext(context) + } + + } + +} diff --git a/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainer.swift b/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainer.swift index f825ccd..e915912 100644 --- a/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainer.swift +++ b/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainer.swift @@ -10,22 +10,27 @@ import Foundation import CoreData @objc(ALCPersistentContainer) -open class PersistentContainer: NSPersistentContainer { +open class PersistentContainer: BasePersistentContainer { // MARK: - - private var didImportUbiquitousContentNotificationObserver: NSObjectProtocol? + public private(set) lazy var backgroundContext: ManagedObjectContext = self.newBackgroundContext() // MARK: - - public convenience init() { - try! self.init(storageType: .disk, managedObjectModel: type(of: self).managedObjectModel(), persistentStoreURL: type(of: self).persistentStoreURL(), ubiquitousConfiguration: nil) + fileprivate var didImportUbiquitousContentNotificationObserver: NSObjectProtocol? + + + // MARK: - + + /// Use caution when using this initializer. + public convenience init(name: String? = nil) { + try! self.init(name: name, managedObjectModel: type(of: self).managedObjectModel(), storageType: .disk, persistentStoreURL: try! type(of: self).persistentStoreURL(), persistentStoreDescriptionOptions: nil, ubiquitousConfiguration: nil) } - public init(storageType: PersistentContainerStorageType, managedObjectModel: NSManagedObjectModel, persistentStoreURL: URL, ubiquitousConfiguration: PersistentContainerUbiquitousConfiguration? = nil) throws { + public init(name: String? = nil, managedObjectModel: NSManagedObjectModel, storageType: PersistentContainerStorageType, persistentStoreURL: URL, persistentStoreDescriptionOptions: [String : NSObject]? = nil, ubiquitousConfiguration: PersistentContainerUbiquitousConfiguration? = nil) throws { // - let name = persistentStoreURL.deletingPathExtension().lastPathComponent - super.init(name: name, managedObjectModel: managedObjectModel) + let name = name ?? persistentStoreURL.deletingPathExtension().lastPathComponent // if storageType == .disk { @@ -39,11 +44,17 @@ open class PersistentContainer: NSPersistentContainer { // let persistentStoreDescription = NSPersistentStoreDescription(url: persistentStoreURL) + // persistentStoreDescription.type = (storageType == .disk ? NSSQLiteStoreType : NSInMemoryStoreType) - persistentStoreDescription.shouldAddStoreAsynchronously = false persistentStoreDescription.shouldInferMappingModelAutomatically = true persistentStoreDescription.shouldMigrateStoreAutomatically = true + // a chance for configuring options (such `NSPersistentHistoryTrackingKey`, for example) + persistentStoreDescriptionOptions?.forEach { + persistentStoreDescription.setOption($0.value, forKey: $0.key) + } + + // deprecated ubiquitous support #if os(macOS) || os(iOS) if let ubiquitousConfiguration = ubiquitousConfiguration { persistentStoreDescription.setOption(ubiquitousConfiguration.containerIdentifier as NSString, forKey: NSPersistentStoreUbiquitousContainerIdentifierKey) @@ -53,67 +64,99 @@ open class PersistentContainer: NSPersistentContainer { #endif // - self.persistentStoreDescriptions = [persistentStoreDescription] - - // this should run synchronously since shouldAddStoreAsynchronously is false - var outError: Swift.Error? - - self.loadPersistentStores { description, error in - if let error = error { - outError = error + try super.init(name: name, managedObjectModel: managedObjectModel, persistentStoreDescription: persistentStoreDescription) { persistentContainer, _, error in + guard error == nil else { + return } // #if os(macOS) || os(iOS) if let _ = ubiquitousConfiguration { - self.didImportUbiquitousContentNotificationObserver = NotificationCenter.default.addObserver(forName: .NSPersistentStoreDidImportUbiquitousContentChanges, object: self.persistentStoreCoordinator, queue: nil) { [weak self] notification in - guard let context = self?.viewContext.parent ?? self?.viewContext else { + (persistentContainer as? PersistentContainer)?.didImportUbiquitousContentNotificationObserver = NotificationCenter.default.addObserver(forName: .NSPersistentStoreDidImportUbiquitousContentChanges, object: persistentContainer.persistentStoreCoordinator, queue: nil) { [weak persistentContainer] notification in + guard let context = persistentContainer?.viewContext.parent ?? persistentContainer?.viewContext else { return } - context.perform { + context.performAndWait { context.mergeChanges(fromContextDidSave: notification) } } } #endif - - // - self.viewContext.automaticallyMergesChangesFromParent = true - self.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - } - - if let outError = outError { - throw outError } } + public override init(name: String, managedObjectModel: NSManagedObjectModel, persistentStoreDescription: NSPersistentStoreDescription, completionHandler: @escaping (NSPersistentContainer, NSPersistentStoreDescription, Error?) -> Void) throws { + try super.init(name: name, managedObjectModel: managedObjectModel, persistentStoreDescription: persistentStoreDescription, completionHandler: completionHandler) + } + deinit { if let didImportUbiquitousContentNotificationObserver = self.didImportUbiquitousContentNotificationObserver { - self.didImportUbiquitousContentNotificationObserver = nil NotificationCenter.default.removeObserver(didImportUbiquitousContentNotificationObserver) + self.didImportUbiquitousContentNotificationObserver = nil + } + } + +} + + +// MARK: - + +open class BasePersistentContainer: NSPersistentContainer { + + // MARK: - + + public init(name: String, managedObjectModel: NSManagedObjectModel, persistentStoreDescription: NSPersistentStoreDescription, completionHandler: @escaping (NSPersistentContainer, NSPersistentStoreDescription, Error?) -> Void) throws { + // + super.init(name: name, managedObjectModel: managedObjectModel) + + // we need to load synchronously in this implementation + persistentStoreDescription.shouldAddStoreAsynchronously = false + self.persistentStoreDescriptions = [persistentStoreDescription] + + // + var outError: Swift.Error? + + self.loadPersistentStores { description, error in + // + if let error = error { + outError = error + } + else { + self.configureManagedObjectContext(self.viewContext) + } + + // + completionHandler(self, description, error) + } + + if let outError = outError { + throw outError } } // MARK: - - + open override func newBackgroundContext() -> NSManagedObjectContext { let context = super.newBackgroundContext() - - context.automaticallyMergesChangesFromParent = true - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - + self.configureManagedObjectContext(context) + return context } - + open override func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { super.performBackgroundTask { context in - // - context.automaticallyMergesChangesFromParent = true - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - - // + self.configureManagedObjectContext(context) block(context) } } + + // MARK: - + + open func configureManagedObjectContext(_ context: NSManagedObjectContext) { + context.automaticallyMergesChangesFromParent = true + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + } + } + diff --git a/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainerType.swift b/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainerType.swift index 5b93b2d..00b83c2 100644 --- a/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainerType.swift +++ b/Source/AlecrimCoreData/Core/Persistent Container/PersistentContainerType.swift @@ -9,11 +9,20 @@ import Foundation import CoreData -public protocol PersistentContainerType: AnyObject {} +// MARK: - + +public protocol PersistentContainerType: AnyObject { + associatedtype ManagedObjectContextType: ManagedObjectContext + + var viewContext: ManagedObjectContextType { get } + var backgroundContext: ManagedObjectContextType { get } +} extension PersistentContainer: PersistentContainerType {} extension CustomPersistentContainer: PersistentContainerType {} +// MARK: - helper static methods + extension PersistentContainerType { public static func managedObjectModel(withName name: String? = nil, in bundle: Bundle? = nil) throws -> NSManagedObjectModel { @@ -43,17 +52,16 @@ extension PersistentContainerType { extension PersistentContainerType { - public static func persistentStoreURL(withName name: String? = nil, in bundle: Bundle? = nil) throws -> URL { + public static func persistentStoreURL(withName name: String? = nil, inPath path: String? = nil) throws -> URL { guard let applicationSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last else { throw PersistentContainerError.applicationSupportDirectoryNotFound } - let bundle = bundle ?? Bundle.main - let bundleLastPathComponent = bundle.bundleURL.deletingPathExtension().lastPathComponent - let name = name ?? bundleLastPathComponent + let name = name ?? Bundle.main.bundleURL.deletingPathExtension().lastPathComponent + let path = path ?? name let persistentStoreURL = applicationSupportURL - .appendingPathComponent(bundleLastPathComponent, isDirectory: true) + .appendingPathComponent(path, isDirectory: true) .appendingPathComponent("CoreData", isDirectory: true) .appendingPathComponent(name, isDirectory: false) .appendingPathExtension("sqlite") @@ -61,19 +69,17 @@ extension PersistentContainerType { return persistentStoreURL } - public static func persistentStoreURL(withName name: String? = nil, forSecurityApplicationGroupIdentifier applicationGroupIdentifier: String, in bundle: Bundle? = nil) throws -> URL { + public static func persistentStoreURL(withName name: String, inPath path: String? = nil, forSecurityApplicationGroupIdentifier applicationGroupIdentifier: String) throws -> URL { guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: applicationGroupIdentifier) else { throw PersistentContainerError.invalidGroupContainerURL } - let bundle = bundle ?? Bundle.main - let bundleLastPathComponent = bundle.bundleURL.deletingPathExtension().lastPathComponent - let name = name ?? bundleLastPathComponent + let path = path ?? name let persistentStoreURL = containerURL .appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Application Support", isDirectory: true) - .appendingPathComponent(bundleLastPathComponent, isDirectory: true) + .appendingPathComponent(path, isDirectory: true) .appendingPathComponent("CoreData", isDirectory: true) .appendingPathComponent(name, isDirectory: false) .appendingPathExtension("sqlite") diff --git a/Source/AlecrimCoreData/Core/Query/FetchRequest.swift b/Source/AlecrimCoreData/Core/Query/FetchRequest.swift index a4bb8d3..5321c4c 100644 --- a/Source/AlecrimCoreData/Core/Query/FetchRequest.swift +++ b/Source/AlecrimCoreData/Core/Query/FetchRequest.swift @@ -25,7 +25,7 @@ public struct FetchRequest: Queryable { public init() { } - internal func toRaw() -> NSFetchRequest { + public func toRaw() -> NSFetchRequest { let entityDescription = Entity.entity() let rawValue = NSFetchRequest(entityName: entityDescription.name!) @@ -41,6 +41,17 @@ public struct FetchRequest: Queryable { return rawValue } + internal func reversed() -> FetchRequest { + guard let existingSortDescriptors = self.sortDescriptors, !existingSortDescriptors.isEmpty else { + return self + } + + var clone = self + clone.sortDescriptors = existingSortDescriptors.map { SortDescriptor(key: $0.key, ascending: !$0.ascending) } + + return clone + } + } // MARK: - @@ -61,7 +72,7 @@ extension FetchRequest { return clone } - public func setBatchSize(_ batchSize: Int) -> FetchRequest { + public func batchSize(_ batchSize: Int) -> FetchRequest { var clone = self clone.batchSize = batchSize diff --git a/Source/AlecrimCoreData/Core/Query/Query.swift b/Source/AlecrimCoreData/Core/Query/Query.swift index f96e71b..86bfbfa 100644 --- a/Source/AlecrimCoreData/Core/Query/Query.swift +++ b/Source/AlecrimCoreData/Core/Query/Query.swift @@ -13,8 +13,8 @@ import CoreData public struct Query { - internal let context: ManagedObjectContext - internal fileprivate(set) var fetchRequest: FetchRequest + public let context: ManagedObjectContext + public fileprivate(set) var fetchRequest: FetchRequest public init(in context: ManagedObjectContext, fetchRequest: FetchRequest = FetchRequest()) { self.context = context @@ -34,10 +34,9 @@ extension Query { } fileprivate func execute(fetchRequest: FetchRequest) -> [Entity] { - return try! self.context.fetch(self.fetchRequest.toRaw()) + return try! self.context.fetch(fetchRequest.toRaw()) } - // public func count() -> Int { @@ -67,6 +66,14 @@ extension Query { public func first() -> Entity? { return self.execute(fetchRequest: self.fetchRequest.prefix(1)).first } + + public func last() -> Entity? { + guard let sortDescriptors = self.fetchRequest.sortDescriptors, !sortDescriptors.isEmpty else { + return self.execute().last // not memory efficient, but will do the job + } + + return self.execute(fetchRequest: self.fetchRequest.reversed().prefix(1)).first + } } @@ -244,9 +251,9 @@ extension Query: Queryable { return clone } - public func setBatchSize(_ batchSize: Int) -> Query { + public func batchSize(_ batchSize: Int) -> Query { var clone = self - clone.fetchRequest = clone.fetchRequest.setBatchSize(batchSize) + clone.fetchRequest = clone.fetchRequest.batchSize(batchSize) return clone } diff --git a/Source/AlecrimCoreData/Core/Query/Queryable.swift b/Source/AlecrimCoreData/Core/Query/Queryable.swift index 692ba48..89ea5f4 100644 --- a/Source/AlecrimCoreData/Core/Query/Queryable.swift +++ b/Source/AlecrimCoreData/Core/Query/Queryable.swift @@ -17,7 +17,7 @@ public protocol Queryable { func dropFirst(_ n: Int) -> Self func prefix(_ maxLength: Int) -> Self - func setBatchSize(_ batchSize: Int) -> Self + func batchSize(_ batchSize: Int) -> Self func filtered(using predicate: Predicate) -> Self diff --git a/Source/AlecrimCoreData/Fetch Request Controller/FetchRequestController.swift b/Source/AlecrimCoreData/Fetch Request Controller/FetchRequestController.swift index 4cea6b8..1dfeb9d 100644 --- a/Source/AlecrimCoreData/Fetch Request Controller/FetchRequestController.swift +++ b/Source/AlecrimCoreData/Fetch Request Controller/FetchRequestController.swift @@ -22,7 +22,11 @@ import CoreData public final class FetchRequestController { + // MARK: - + + public let fetchRequest: FetchRequest public let rawValue: NSFetchedResultsController + internal let rawValueDelegate: FetchedResultsControllerDelegate fileprivate let initialPredicate: Predicate? @@ -47,7 +51,9 @@ public final class FetchRequestController { } public init(fetchRequest: FetchRequest, context: ManagedObjectContext, sectionNameKeyPath: String? = nil, cacheName: String? = nil) { + self.fetchRequest = fetchRequest self.rawValue = NSFetchedResultsController(fetchRequest: fetchRequest.toRaw() as NSFetchRequest, managedObjectContext: context, sectionNameKeyPath: sectionNameKeyPath, cacheName: cacheName) + self.rawValueDelegate = FetchedResultsControllerDelegate() self.initialPredicate = fetchRequest.predicate diff --git a/Source/AlecrimCoreData/Fetch Request Controller/FetchedResultsControllerDelegate.swift b/Source/AlecrimCoreData/Fetch Request Controller/FetchedResultsControllerDelegate.swift index 693ff72..6399183 100644 --- a/Source/AlecrimCoreData/Fetch Request Controller/FetchedResultsControllerDelegate.swift +++ b/Source/AlecrimCoreData/Fetch Request Controller/FetchedResultsControllerDelegate.swift @@ -107,15 +107,11 @@ public func +=(left: ClosuresContainer, right: Closure) { } -// MARK: - - // MARK: - FetchRequestController extensions extension FetchRequestController { public func removeAllBindings() { - self.performFetchIfNeeded() - self.rawValueDelegate.needsReloadDataClosure = nil self.rawValueDelegate.willChangeContentClosuresContainer.removeAll() @@ -137,18 +133,16 @@ extension FetchRequestController { extension FetchRequestController { public func refresh() { - self.performFetchIfNeeded() - - self.rawValueDelegate.needsReloadDataClosure?() + self.rawValueDelegate.willChangeContentClosuresContainer.closures.forEach { $0() } if let cacheName = self.rawValue.cacheName { NSFetchedResultsController.deleteCache(withName: cacheName) } - + self.performFetch() - + self.rawValueDelegate.didChangeContentClosuresContainer.closures.forEach { $0() } } @@ -158,85 +152,64 @@ extension FetchRequestController { @discardableResult internal func needsReloadData(closure: @escaping FetchedResultsControllerDelegate.NeedsReloadDataClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.needsReloadDataClosure = closure return self } } - extension FetchRequestController { @discardableResult public func willChangeContent(closure: @escaping FetchedResultsControllerDelegate.ChangeContentClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.willChangeContentClosuresContainer += closure return self } @discardableResult public func didChangeContent(closure: @escaping FetchedResultsControllerDelegate.ChangeContentClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didChangeContentClosuresContainer += closure return self } @discardableResult public func didInsertSection(closure: @escaping FetchedResultsControllerDelegate.ChangeSectionClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didInsertSectionClosuresContainer += closure return self } @discardableResult public func didDeleteSection(closure: @escaping FetchedResultsControllerDelegate.ChangeSectionClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didDeleteSectionClosuresContainer += closure return self } @discardableResult public func didInsertObject(closure: @escaping FetchedResultsControllerDelegate.ChangeItemClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didInsertObjectClosuresContainer += closure return self } @discardableResult public func didDeleteObject(closure: @escaping FetchedResultsControllerDelegate.ChangeItemClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didDeleteObjectClosuresContainer += closure return self } @discardableResult public func didUpdateObject(closure: @escaping FetchedResultsControllerDelegate.ChangeItemClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didUpdateObjectClosuresContainer += closure return self } @discardableResult public func didMoveObject(closure: @escaping FetchedResultsControllerDelegate.MoveItemClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.didMoveObjectClosuresContainer += closure return self } @discardableResult public func sectionIndexTitle(closure: @escaping FetchedResultsControllerDelegate.SectionIndexTitleClosure) -> Self { - self.performFetchIfNeeded() - self.rawValueDelegate.sectionIndexTitleClosure = closure return self } diff --git a/Source/AlecrimCoreData/Info.plist b/Source/AlecrimCoreData/Info.plist index e2953de..4956a91 100644 --- a/Source/AlecrimCoreData/Info.plist +++ b/Source/AlecrimCoreData/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.0 + 6.0.1 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright