Last active
May 12, 2022 06:26
-
-
Save ramzesenok/59dcb0f5a1b639409a4948a89622109c to your computer and use it in GitHub Desktop.
My attempt to build a reusable ViewModel that uses CoreData as its persistence store
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
import Combine | |
import SwiftUI | |
import CoreData | |
class CoreDataViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate { | |
let container: NSPersistentContainer | |
private var controllerUpdates = [NSObject: CurrentValueSubject<[NSFetchRequestResult], Never>]() | |
init(container: NSPersistentContainer) { | |
self.container = container | |
self.container.viewContext.automaticallyMergesChangesFromParent = true | |
} | |
func bind<ValueType: NSFetchRequestResult>( | |
controller: NSFetchedResultsController<ValueType>, | |
to publisher: inout Published<[ValueType]>.Publisher | |
) { | |
controller.delegate = self | |
try? controller.performFetch() | |
let subject = CurrentValueSubject<[NSFetchRequestResult], Never>(controller.fetchedObjects ?? []) | |
self.controllerUpdates[controller] = subject | |
subject | |
.compactMap { $0 as? [ValueType] } | |
.eraseToAnyPublisher() | |
.assign(to: &publisher) | |
} | |
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { | |
self.controllerUpdates[controller]? | |
.send(controller.fetchedObjects ?? []) | |
} | |
func saveContext() { | |
try? self.container.viewContext.save() | |
} | |
} | |
extension CoreDataViewModel { | |
func request<ResultType>( | |
for type: ResultType.Type, | |
sortDescriptors: [NSSortDescriptor] = [], | |
predicate: NSPredicate? = nil | |
) -> NSFetchRequest<ResultType> { | |
let req = NSFetchRequest<ResultType>(entityName: "\(ResultType.self)") | |
req.sortDescriptors = sortDescriptors | |
req.predicate = predicate | |
return req | |
} | |
func controller<ResultType>( | |
request: NSFetchRequest<ResultType>, | |
sectionNameKeyPath: String? = nil, | |
cacheName: String? = nil | |
) -> NSFetchedResultsController<ResultType> { | |
NSFetchedResultsController( | |
fetchRequest: request, | |
managedObjectContext: self.container.viewContext, | |
sectionNameKeyPath: sectionNameKeyPath, | |
cacheName: cacheName | |
) | |
} | |
func controller<ResultType>( | |
type: ResultType.Type, | |
sortDescriptors: [NSSortDescriptor] = [], | |
predicate: NSPredicate? = nil, | |
sectionNameKeyPath: String? = nil, | |
cacheName: String? = nil | |
) -> NSFetchedResultsController<ResultType> { | |
let request = self.request(for: type, sortDescriptors: sortDescriptors, predicate: predicate) | |
return self.controller( | |
request: request, | |
sectionNameKeyPath: sectionNameKeyPath, | |
cacheName: cacheName | |
) | |
} | |
} | |
extension NSManagedObjectContext { | |
func saveChanges(function: String = #function) { | |
if self.hasChanges { | |
do { | |
try self.save() | |
} catch { | |
assertionFailure("\(function) saved with error: \(error.localizedDescription)") | |
} | |
} | |
} | |
} | |
class ViewModel: CoreDataViewModel { | |
@Published var items = [Item]() | |
var cancellables = Set<AnyCancellable>() | |
override init(container: NSPersistentContainer) { | |
super.init(container: container) | |
self.$items | |
.sink(receiveValue: { newValue in | |
print(newValue) | |
}) | |
.store(in: &self.cancellables) | |
self.setupBindings() | |
} | |
func setupBindings() { | |
self.bind( | |
controller: self.controller( | |
type: Item.self, | |
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: false)] | |
), | |
to: &self.$items | |
) | |
} | |
func addItem() { | |
self.container.performBackgroundTask { context in | |
let newItem = Item(context: context) | |
newItem.timestamp = Date() | |
context.saveChanges() | |
} | |
} | |
func deleteItems(offsets: IndexSet) { | |
offsets | |
.map { self.items[$0] } | |
.forEach(self.container.viewContext.delete) | |
self.saveContext() | |
} | |
} | |
struct ContentView: View { | |
@StateObject var viewModel: ViewModel | |
var body: some View { | |
NavigationView { | |
VStack { | |
List { | |
ForEach(self.viewModel.items) { item in | |
NavigationLink { | |
Text("Item at \(item)") | |
} label: { | |
Text("\(item)") | |
} | |
} | |
.onDelete { | |
self.viewModel.deleteItems(offsets: $0) | |
} | |
} | |
.animation(.default, value: viewModel.items) | |
Button(action: self.viewModel.addItem) { | |
Label("Add Item", systemImage: "plus") | |
} | |
} | |
} | |
} | |
} | |
private let itemFormatter: DateFormatter = { | |
let formatter = DateFormatter() | |
formatter.dateStyle = .short | |
formatter.timeStyle = .medium | |
return formatter | |
}() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment