Core Data with SwiftUI MVVM Feedback
For a cutting edge way to wrap the NSFetchedResultsController
in SwiftUI compatible code you might want to take a look at AsyncStream.
However, @FetchRequest
currently is implemented as a DynamicProperty
so if you did that too it would allow access the managed object context from the @Environment
in the update
func which is called on the DynamicProperty
before body
is called on the View
. You can use an @StateObject
internally as the FRC delegate.
Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View
data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.
And FYI, when using Combine's ObservableObject
we don't sink
the pipeline or use cancellables
. Instead, assign
the output of the pipeline to an @Published
. However, if you aren't using CombineLatest
, then perhaps reconsider if you should really be using Combine at all.
How to pass/get Core Data context in SwiftUI MVVM ViewModel?
Here is your scenario
let contentView = MainView(context: context) // << inject
.environment(\.managedObjectContext, context)
struct MainView: View {
@Environment(\.managedObjectContext) var context
@ObservedObject private var viewModel: ViewModel // << declare
init(context: NSManagedObjectContext) {
self.viewModel = ViewModel(context: context) // initialize
}
}
Accessing Core Data Stack in MVVM application
You can use Core Data Singleton class
import CoreData
class CoreDataStack {
static let shared = CoreDataStack()
private init() {}
var managedObjectContext: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
var workingContext: NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.managedObjectContext
return context
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyStuff")
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
RaiseError.raise()
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext() {
self.managedObjectContext.performAndWait {
if self.managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
appPrint("Main context saved")
} catch {
appPrint(error)
RaiseError.raise()
}
}
}
}
func saveWorkingContext(context: NSManagedObjectContext) {
do {
try context.save()
appPrint("Working context saved")
saveContext()
} catch (let error) {
appPrint(error)
RaiseError.raise()
}
}
}
Core Data is not thread safe. If you write something on manageObject and don't want to save that, but some other thread save the context, then the changes that you don't want to persist will also persist.
So to avoid this situation always create working context - which is private.
When you press save, then first private context get saved and after that you save main context.
In MVVM you should have DataLayer through which your ViewModel interact with Core Data singleton class.
How to preview Core Data objects in Canvas when using a CoreDataViewModel
The point of a preview is to use the ordinary view with some given data and not to duplicate the code from the actual view. Apart from that you seem to have much in place.
I would inject the Core Data manager class into the view model to start with:
class CoreDataViewModel: ObservableObject{
let manager: CoreDataManager
@Published var cars: [Car] = []
init(coreDataManager: CoreDataManager = .instance){
self.manager = coreDataManager
getCars()
}
Then I would simplify the preview to
struct CarsView_Previews: PreviewProvider {
static var previews: some View {
CarsView(coreDataViewModel: CoreDataViewModel(coreDataManager: .preview))
}
}
You are already creating one Car object in the preview
so why not do that in a loop, here I have moved it into a separate function that can be called from inside the preview
declaration or separately
#if DEBUG
extension CoreDataManager {
func createMockCars() {
for i in 1...5 {
let car = Car(context: self.context)
car.make = "Make \(i)"
car.model = "Model \(i)"
}
try! self.context.save()
}
}
#endif
As mentioned in one of the comments you should consider renaming your view model to something to do with cars like CarsViewModel or CarListViewModel.
Related Topics
How to Upload Images from The Browser to Amazon S3 Using Vapor and Leaf
Swift 3: Convert a String to an Array
Firebase Swift 3 Get List of Child in a Array
Xcode 9.3 Watchkit Crash on Wkinterfacebutton Tap
List Inside Scrollview Is Not Displayed on Watchos
Swift: Convert Byte Array into Ciimage
Bar Button Item Tint Color Not Working
Swift: Check Which Value in Nsarray Is Closest to Another Given Value
In Swift, Dynamic Height for UItextview in UIcollectionview
Using Scenekit for Hittesting Not Returning a Hit with Scnnode
Swift 1.2 Assigning Let After Initialization
Drawing at Cocoa with Swift Creates an Error
Aws Cognito Credentialsprovider.Login Always Shows Nil (Swift)
Force Refresh on Another Viewcontroller Component with Swift
Using Cfarraygetvalueatindex in Swift with Unsafepointer (Aupreset)
Adding Items to The Dock Menu from My View Controller in My Cocoa App
Problem with Frameworks in Command Line Tool
Swift Flatmap on Array with Elements Are Optional Has Different Behavior