Last active
December 27, 2023 08:37
-
-
Save tkhelm/7381ae91099979acbb23ce3a47253dd7 to your computer and use it in GitHub Desktop.
CoreDataWithCombine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//******* STORAGE MODEL **************** | |
import CoreData | |
import Combine | |
class URLMetadataStorage: NSObject, ObservableObject { | |
static let shared = URLMetadataStorage() | |
let parentContext = PersistenceController.shared.container.viewContext | |
//Create a child context to hold temp copy during 'add' or or 'edit', prior to user saving | |
//If the user doesn't save, the child context is de-allocated automatically. | |
lazy var childContext: NSManagedObjectContext = { | |
let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) | |
managedObjectContext.parent = parentContext | |
return managedObjectContext | |
}() | |
//This is what Combine publishes to the view model | |
var allItems = CurrentValueSubject<[URLMetadata], Never>([]) | |
private let itemFetchController: NSFetchedResultsController<URLMetadata> | |
private override init() { | |
//When class is first invoked, init pulls all data from the Core Data entity and | |
// publishes it through Combine for the view model to observe as 'allItems' above | |
parentContext.automaticallyMergesChangesFromParent = true | |
let itemRequest = URLMetadata.createItemFetchRequest() //see extension below | |
let itemSort = NSSortDescriptor(key: "timestamp", ascending: false) //modify depending on data model | |
itemRequest.sortDescriptors = [itemSort] | |
itemFetchController = NSFetchedResultsController( | |
fetchRequest: itemRequest, | |
managedObjectContext: parentContext, | |
sectionNameKeyPath: nil, | |
cacheName: nil | |
) | |
super.init() | |
itemFetchController.delegate = self | |
do { | |
try self.itemFetchController.performFetch() | |
self.allItems.value = self.itemFetchController.fetchedObjects ?? [] | |
} catch { | |
NSLog("Error: could not fetch objects") | |
} | |
} | |
} | |
//MARK: - CRUD functions specifically for Core Data | |
extension URLMetadataStorage { | |
func create() -> URLMetadata { | |
let newItem = URLMetadata(context: childContext) | |
newItem.id = UUID() | |
newItem.timestamp = Date() | |
return newItem | |
} | |
func add(_ itemToAdd: URLMetadata) { | |
//with Core Data child context, no need to do anything other than save the context | |
saveContext() | |
} | |
func update(_ draft: URLMetadata) { | |
guard let itemToUpdate = childContext.object(with: draft.objectID) as? URLMetadata else { | |
fatalError("Error: itemToUpdate not found in Core Data") | |
} | |
itemToUpdate.timestamp = draft.timestamp | |
itemToUpdate.title = draft.title | |
itemToUpdate.url = draft.url | |
itemToUpdate.image = draft.image | |
saveContext() | |
} | |
func delete(_ id: UUID) { | |
//get the object that is being updated | |
var fetchedItems: [URLMetadata] = [] | |
let request: NSFetchRequest<NSFetchRequestResult> = URLMetadata.fetchRequest() | |
request.predicate = NSPredicate(format: "id == %@", id as CVarArg) | |
do { fetchedItems = try self.parentContext.fetch(request) as! [URLMetadata] } | |
catch { print("Failed to fetch: \(error)") } | |
guard fetchedItems.count == 1 else { | |
print("Item to update not found in Core Data") | |
return | |
} | |
let itemToDelete = fetchedItems.first! | |
parentContext.delete(itemToDelete) | |
saveContext() | |
} | |
func findByID(_ uuid: UUID) -> URLMetadata? { | |
var results: [URLMetadata]? | |
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "URLMetadata") | |
request.predicate = NSPredicate(format: "id == %@", uuid as CVarArg) | |
do { | |
results = try parentContext.fetch(request) as? [URLMetadata] | |
} catch let error as NSError { | |
print("Could not fetch ImageElement. \(error), \(error.userInfo)") | |
} | |
guard results != nil, !results!.isEmpty else { | |
print("Fetch results are missing") | |
return nil | |
} | |
guard results!.count == 1 else { | |
print("Error: Found \(results!.count) ImageElaments with the same UUID") | |
return nil | |
} | |
return results!.first | |
} | |
func saveContext() { | |
do {try self.childContext.save() | |
try self.parentContext.save() } | |
catch let error as NSError { | |
print("Could not save. \(error), \(error.userInfo)") | |
} | |
} | |
} | |
// Change fetchRequest() because it has an annoying flaw right now: it uses the same name as a | |
//different method that comes from NSManagedObject, and Xcode can’t tell which one you mean. | |
//Rename it to createFetchRequest() to avoid the ambiguity. | |
//See https://www.hackingwithswift.com/read/38/4/creating-an-nsmanagedobject-subclass-with-xcode | |
extension URLMetadata { | |
@nonobjc public class func createItemFetchRequest() -> NSFetchRequest<URLMetadata> { | |
return NSFetchRequest<URLMetadata>(entityName: "URLMetadata") | |
} | |
} | |
extension URLMetadataStorage: NSFetchedResultsControllerDelegate { | |
//This updates the published variable above whenever the underlying database changes | |
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { | |
if let items = controller.fetchedObjects as? [URLMetadata] { | |
self.allItems.value = items | |
} | |
} | |
} | |
//Note: Modify these based on the characteristics of the data model to support sorting and equating | |
extension URLMetadata: Comparable { | |
public static func < (lhs: URLMetadata, rhs: URLMetadata) -> Bool { | |
return lhs.timestamp! <= rhs.timestamp! | |
} | |
public static func > (lhs: URLMetadata, rhs: URLMetadata) -> Bool { | |
return lhs.timestamp! >= rhs.timestamp! | |
} | |
public static func == (lhs: URLMetadata, rhs: URLMetadata) -> Bool { | |
return | |
lhs.id == rhs.id && | |
lhs.timestamp == rhs.timestamp && | |
lhs.title == rhs.title && | |
lhs.url == rhs.url && | |
lhs.image == rhs.image | |
} | |
} | |
//******* VIEW MODEL **************** | |
import SwiftUI | |
import Combine | |
class URLMetadataViewModel: ObservableObject { | |
@Published private(set) var allURLMetadata: [URLMetadata] = [] | |
//Keep allURLMetadata up to date when Core Data changes | |
private var cancellable: AnyCancellable? | |
init(metadataPublisher: AnyPublisher<[URLMetadata], Never> = URLMetadataStorage.shared.allItems.eraseToAnyPublisher() ) { | |
cancellable = metadataPublisher.sink { metadata in | |
self.allURLMetadata = metadata | |
} | |
} | |
func create() -> URLMetadata { //Create empty record for population | |
return URLMetadataStorage.shared.create() | |
} | |
func add(itemToAdd: URLMetadata) { //save a new populated record | |
URLMetadataStorage.shared.add(itemToAdd) | |
} | |
func update(itemToUpdate: URLMetadata) { //update an existing record | |
URLMetadataStorage.shared.update(itemToUpdate) | |
} | |
func delete(id: UUID) { //delete an existing record | |
URLMetadataStorage.shared.delete(id) | |
} | |
func findByID(id: UUID) -> URLMetadata? { | |
URLMetadataStorage.shared.findByID(id) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment