Skip to content

Instantly share code, notes, and snippets.

@ramzesenok
Last active May 12, 2022 06:26
Show Gist options
  • Save ramzesenok/59dcb0f5a1b639409a4948a89622109c to your computer and use it in GitHub Desktop.
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
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