Forked from ramzesenok/CoreDataViewModelArchitecture.swift
Created
December 19, 2021 18:40
-
-
Save codeactual/f8b43da0de4ab4f7013a6547ec211465 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 hidden or 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 context: NSManagedObjectContext | |
| private var controllerUpdates = [NSObject: CurrentValueSubject<[NSFetchRequestResult], Never>]() | |
| init(context: NSManagedObjectContext) { | |
| self.context = context | |
| } | |
| 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.context.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.context, | |
| 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 | |
| ) | |
| } | |
| } | |
| class ViewModel: CoreDataViewModel { | |
| @Published var items = [Item]() | |
| var cancellables = Set<AnyCancellable>() | |
| override init(context: NSManagedObjectContext) { | |
| super.init(context: context) | |
| 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() { | |
| let newItem = Item(context: self.context) | |
| newItem.timestamp = Date() | |
| self.saveContext() | |
| } | |
| func deleteItems(offsets: IndexSet) { | |
| offsets | |
| .map { self.items[$0] } | |
| .forEach(self.context.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