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 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
}
}
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 (or should?) I replace MVVM in this CoreData SwiftUI app with a Nested managedObjectContext?
I have opted to avoid a viewModel in this case after discovering:
moc.refresh(contact, mergeChanges: false)
Apple docs: https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506224-refresh
So you can toss aside the ContactViewModel
, keep the ContentView
as is and use the following:
Contact Profile
The enum
havae been made an extension to the ContactProfile
view.
import SwiftUI
import CoreData
struct ContactProfile: View {
@Environment(\.managedObjectContext) private var moc
@ObservedObject var contact: Contact
@State private var isEditing = false
@State private var errorAlertIsPresented = false
@State private var errorAlertTitle = ""
var body: some View {
VStack {
if !isEditing {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
.font(.largeTitle)
.padding(.top)
Spacer()
} else {
Form{
TextField("First Name", text: $contact.firstName ?? "")
TextField("First Name", text: $contact.lastName ?? "")
}
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(isEditing ? true : false)
.navigationBarItems(leading:
Button (action: {
/// This is the key change:
moc.refresh(contact, mergeChanges: false)
withAnimation {
self.isEditing = false
}
}, label: {
Text(isEditing ? "Cancel" : "")
}),
trailing:
Button (action: {
if isEditing { saveContact() }
withAnimation {
if !errorAlertIsPresented {
self.isEditing.toggle()
}
}
}, label: {
Text(!isEditing ? "Edit" : "Done")
})
)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) }) }
private func saveContact() {
do {
if contact.firstName!.isEmpty {
throw ValidationError.missingFirstName
}
if contact.lastName!.isEmpty {
throw ValidationError.missingLastName
}
try moc.save()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
extension ContactProfile {
enum ValidationError: LocalizedError {
case missingFirstName
case missingLastName
var errorDescription: String? {
switch self {
case .missingFirstName:
return "Please enter a first name for this contact."
case .missingLastName:
return "Please enter a last name for this contact."
}
}
}
}
This also requires the code below that can be found at this link:
SwiftUI Optional TextField
import SwiftUI
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
Related Topics
Apple Push Notification Limitation
Avspeechsynthesizer in Background Mode
React Native Xcode Project Product Archive Fails with Duplicate Symbols for Architecture Arm64
Changing Uitableview's Section Header/Footer Title Without Reloading the Whole Table View
How Detect Swipe Gesture Direction
The Sandbox Is Not in Sync with the Podfile.Lock-Ios
Get Name of Airplay Device Using Avplayer
How to Send Email Using Mfmailcomposeviewcontroller in Simulator
Cannot Assign to Property in Protocol - Swift Compiler Error
Main.Async VS Main.Sync() VS Global().Async in Swift3 Gcd
How to Get the Front Camera in Swift
Posting Photos to Facebook Fan Page with iOS Sdk
Can't Load Uiviewcontroller Xib File in Storyboard in Swift
Uitextview Is Not Scrolled to Top When Loaded
Constant Movement in Spritekit
How to Convert Uicolor Value to a Named Color String