Skip to content

Instantly share code, notes, and snippets.

@tkhelm
Last active December 27, 2023 08:37
Show Gist options
  • Save tkhelm/7381ae91099979acbb23ce3a47253dd7 to your computer and use it in GitHub Desktop.
Save tkhelm/7381ae91099979acbb23ce3a47253dd7 to your computer and use it in GitHub Desktop.
CoreDataWithCombine
//******* 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