In Xcode 12 beta 2 using SwiftUI I want my ComplicationController to make a FetchRequest in order to update my complications but I am having trouble injecting the persistent store into the environment.
I should note this is a pure SwiftUI App in watchOS where the app entry point is the #main struct. There is no ExtensionDelegate or HostingController.
For the watchOS app itself this is how I'm setting up the PersistentContainer:
struct RunPlanner: App {
#Environment(\.scenePhase) private var scenePhase
#StateObject private var persistentStore = PersistentStore.shared
#ObservedObject var selection = TabSelection()
var body: some Scene {
WindowGroup {
TabView(selection: $selection.currentTab) {
WatchAddRunView(tabSelection: selection)
.tag(0)
ContentView()
.tag(1)
}
.environment(\.managedObjectContext, persistentStore.context)
.animation(.easeIn)
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("\(#function) REPORTS - App change of scenePhase to ACTIVE")
case .inactive:
print("\(#function) REPORTS - App change of scenePhase to INACTIVE")
case .background:
print("\(#function) REPORTS - App change of scenePhase to BACKGROUND")
savePersistentStore()
default:
print("\(#function) REPORTS - App change of scenePhase Default")
}
}
}
func savePersistentStore() {
persistentStore.saveContext()
}
}
This works for the app itself to save values to CoreData however my ComplicationController is not seeing the NSPersistentStoreContainer and I'm not sure how to inject it.
My current attempt within my ComplicationController class is this:
class ComplicationController: NSObject, CLKComplicationDataSource {
#Environment(\.managedObjectContext) var moc
let request = NSFetchRequest<RunEvents>(entityName: "RunEvents")
....complication code...
}
func getSavedRunName() -> String {
var activeName = "Run Roster"
do {
let savedRuns = try moc.fetch(request)
savedRuns.forEach({
if $0.isActive {
guard let fetchedName = $0.name else { return }
activeName = fetchedName
}
})
} catch {
print("Error in Fetch Request")
}
return activeName
}
However the getSavedRunName method will cause the app to crash on execution with the debugger saying "reason: '+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'RunEvents''"
I've searched and fumbled around for various solutions with no positive results. Any insight here is very much appreciated.
-Dan
The CLKComplicationDataSource is a separate separate executable from the WKExtensionDelegate, so you need to set up your CoreData stack in each. Additionally, you use an App Group to share the same Core Data files. I use an extension like this from all my targets.
extension NSPersistentContainer {
static func appContainer() -> NSPersistentContainer {
let container = NSPersistentContainer(name: AppConstants.databaseName)
let storeURL = URL.storeURL(for: AppConstants.appGroupName, databaseName: AppConstants.databaseName)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}
}
Note that there would be additional work TBD to sync changes made in extensions back into the app's contexts. My complications (and widgets) are read-only, and the OS runs them on demand, so they initialize fresh and up-to-date.
When syncing across devices, I use CloudCore
Related
I am converting existing app from NSPersistentContainer to NSPersistentCloudKitContainer
AppDelegate code:
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name:"GridModel")
// enable history tracking and remote notifications
guard let publicStoreDescription = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): failed to retrieve a persistent store description.")
}
// public
let publicStoreUrl = publicStoreDescription.url!.deletingLastPathComponent().appendingPathComponent("GridModel-public.sqlite")
publicStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
publicStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
let containerIdentifier = publicStoreDescription.cloudKitContainerOptions!.containerIdentifier
let publicStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
if #available(iOS 14.0, *) {
publicStoreOptions.databaseScope = CKDatabaseScope.public
} else {
// Fallback on earlier versions???
}
publicStoreDescription.cloudKitContainerOptions = publicStoreOptions
print(containerIdentifier)
container.loadPersistentStores(completionHandler: { (loadedStoreDescription, error) in
if let loadError = error as NSError? {
fatalError("###\(#function): Failed to load persistent stores:\(loadError)")
} else if let cloudKitContainerOptions = loadedStoreDescription.cloudKitContainerOptions {
if #available(iOS 14.0, *) {
if .public == loadedStoreDescription.cloudKitContainerOptions?.databaseScope {
self._publicPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
} else if .private == loadedStoreDescription.cloudKitContainerOptions?.databaseScope {
self._privatePersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
} else if .shared == cloudKitContainerOptions.databaseScope {
self._sharedPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
}
} else {
// Fallback on earlier versions
}
} /*else if appDelegate.testingEnabled {
if loadedStoreDescription.url!.lastPathComponent.hasSuffix("private.sqlite") {
self._privatePersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
} else if loadedStoreDescription.url!.lastPathComponent.hasSuffix("shared.sqlite") {
self._sharedPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
}
}*/
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// Pin the viewContext to the current generation token, and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
#if DEBUG
do {
// Use the container to initialize the development schema.
try container.initializeCloudKitSchema(options: [])
} catch {
// Handle any errors.
fatalError("###\(#function): failed to load persistent stores: \(error)")
}
#endif
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(self,
selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
return container
}()
The very first time I run, Creates all the corresponding CD CKRecords in CloudKit public database _defaultZone. All values are copied from Core Data to CloudKit.
When the app is actively running I deleted a Core Data record and added a new record it did not delete it from Cloud Kit. Also when I add a new record in Core Data it did not export and add to Cloud Kit. When the app is actively running any changes I make are not exported and updated in Cloud Kit. What changes should I make to the code?
But when I run the app again then all the previous changes that I made when the app was active are updated. So looks like it does not perform live updates until the app is rerun again.
I get another error on the Debug Console "Custom zones are not allowed in public DB". I am not writing to custom DB. I wonder if this is preventing active updates?
oreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _requestAbortedNotInitialized:](1983): <NSCloudKitMirroringDelegate: 0x2824d16c0> - Never successfully initialized and cannot execute request '<NSCloudKitMirroringExportRequest: 0x280a84780> FDB61E8D-658F-4C77-8615-EF07BAB72BAD' due to error: <CKError 0x28119ba20: "Partial Failure" (2/1011); "Failed to modify some records"; uuid = 6C66A151-DB59-4A1B-B3BD-02CFB9A4139C; container ID = "iCloud.com.greenendpoint.nr2r"; partial errors: {
B-63B7-45D2-BFE0-599972FB6FD7:(com.apple.coredata.cloudkit.zone:defaultOwner) = <CKError 0x2810e44e0: "Server Rejected Request" (15/2027); server message = "Custom zones are not allowed in public DB"; op = 23AFDAC55F70DC8D; uuid = 6C66A151-DB59-4A1B-B3BD-02CFB9A4139C>
Please help!
It worked. I had to delete my app on the test device and restart again. Looks like it recreated the data store and synced with Cloud Kit.
My app is meant to have a bunch of workouts in core data, each with a relationship to many exercises. A view should display the data in each workout (name, description etc.) and then iterate and display each exercise belonging to that workout.
Adding exercises and displaying them works fine. If an exercise is deleted, however it:
deletes from coredata no worries
the information seems to delete from iterableExercises
however, the Text line does not disappear. it goes from, for example "Squat, Description" to simply " , "
If I close the app entirely and reopen, then the " , " lines do completely disappear.
The problem code:
if let iterableExercises = workout.exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) {exercise in
Text("\(exercise.name ?? ""), \(exercise.desc ?? "")")
}
}
I've got the entity relationship set as ordered, but I've also tried unordered with .allObjects instead of .array. This clearly isn't the problem as it's the array iterableExercises that's not correctly being reset?
EDIT: to reproduce, here's all the code you need and some screenshots of the CoreData model.
import SwiftUI
import CoreData
class ViewModel: ObservableObject {
let container: NSPersistentCloudKitContainer
#Published var savedWorkouts: [WorkoutEntity] = []
#Published var savedExercises: [ExerciseEntity] = []
// MARK: INIT
init() {
container = NSPersistentCloudKitContainer(name: "mre")
container.loadPersistentStores { description, error in
if let error = error {
print("Error loading CoreData: \(error)")
}
}
fetchWorkoutEntities()
fetchExerciseEntities()
}
// MARK: FETCHERS
func fetchWorkoutEntities() {
let request = NSFetchRequest<WorkoutEntity>(entityName: "WorkoutEntity")
do {
savedWorkouts = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching WorkoutEntity: \(error)")
}
}
func fetchExerciseEntities() {
let request = NSFetchRequest<ExerciseEntity>(entityName: "ExerciseEntity")
do {
savedExercises = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching ExerciseEntity: \(error)")
}
}
// MARK: SAVE
func saveData() {
do {
try container.viewContext.save()
fetchWorkoutEntities()
fetchExerciseEntities()
} catch let error {
print("Error saving: \(error)")
}
}
// MARK: ADDERS
func addWorkout(name: String) {
let _ = WorkoutEntity(context: container.viewContext)
saveData()
}
func addExerciseToWorkout(workout: WorkoutEntity, name: String) {
let newExercise = ExerciseEntity(context: container.viewContext)
newExercise.name = name
workout.addToExercises(newExercise)
saveData()
}
// MARK: DELETERS
func deleteWorkout(workout: WorkoutEntity) {
container.viewContext.delete(workout)
saveData()
}
func deleteExercise(exercise: ExerciseEntity) {
container.viewContext.delete(exercise)
saveData()
}
// MARK: TODO: UPDATERS
}
struct ContentView: View {
#StateObject var data = ViewModel()
var body: some View {
VStack {
Button {
data.addWorkout(name: "workout")
data.addExerciseToWorkout(workout: data.savedWorkouts[0], name: "[exercisename]")
} label: {
Text("Click ONCE to add workout to work with")
}
Spacer()
if let iterableExercises = data.savedWorkouts[0].exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) { exercise in
Button {
data.deleteExercise(exercise: exercise)
} label: {
Text("Click to delete \(exercise.name ?? "") AFTER DELETING IF THIS STILL SHOWS BUT DOESN'T SHOW THE EXERCISE NAME THEN IT'S BROKEN")
}
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
screenshots of model
I’m not sure if this is the ONLY solution as #malhal gave quite an extensive and seemingly useful response.
But I came across a much easier and immediate fix, within my original solution. The inverse relationships must be specified. Doing this resolved all issues.
We don't use view model objects in SwiftUI. You need to learn the View struct and property wrappers which gives the consistency and efficiency of value types with the benefits of reference types. The property wrapper for core data is #FetchRequest which invalidates the View when the results change. It's also a DynamicProperty (which is how it gets the context from the environment) that you can use it directly without the property wrapper syntax which allows you to use a param in a predicate, in your case to do fetch the one-to-many relation, e.g.
struct WorkoutView: View {
private var fetchRequest: FetchRequest<Exercise>
private var exercices: FetchedResults<Exercise> {
fetchRequest.wrappedValue
}
init(workout: Workout) {
let sortAscending = true
let sortDescriptors = [SortDescriptor(\Exercise.timestamp, order: sortAscending ? .forward : .reverse)]
fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: NSPredicate(format: "workout = %#", workout), animation: .default)
}
var body: some View {
List(exercises) { exercise in
ExerciseView(exercise: exercise)
}
}
}
For creating the NSPersistentContainer check out the Xcode App template with Core Data checked. Looks like this:
#main
struct TestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
The reason it is not an #StateObject is we don't want to invalidate this body when it changes and we need it to be init for previewing which is a different singleton.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
... see template
That other code in your view model class can be moved to NSManagedObject and NSManagedObjectContext extensions. Use the Editor menu to generate the NSManagedObject extension for the model, the files need tidying up though and make sure use extension is selected for the entity.
I am importing a large amount of data and persisting it via CoreData, setting constraints to ensure no data is duplicated (e.g. if it is imported multiple times). I do this via batch insert and use the merge policy NSMergeByPropertyStoreTrumpMergePolicy because any existing data in the store should not be overwritten upon import.
Later, the user can trigger some actions that require another batch insert, but with the merge policy NSMergeByPropertyObjectTrumpMergePolicy because now I want any changed attributes to have the new values persisted to the store.
This is all just as I need, except... based on my experimentation, there is no way to effectively CHANGE the merge policy once you have set it.
Once we make an insert using NSMergeByPropertyStoreTrumpMergePolicy, even if we now update context.mergePolicy to NSMergeByPropertyObjectTrumpMergePolicy, this will have no effect. Core Data will continue to generate SQL for the prior merge policy instead. Restarting the app and running the two steps in the opposite order verifies the same behavior -- whichever merge policy you specify first will become "stuck" in Core Data's behavior, even if you later update context.mergePolicy again in your code.
In fact, I found that even if I set two different merge policies on two different contexts (e.g. viewContext and backgroundContext) -- the behavior still gets "stuck". Whichever merge policy you use first will become the merge policy Core Data is using to generate SQL requests, even for the other context!
Can anybody verify if they have run into this and thoughts on workarounds?
I have found one thing -- which is to go all the way up the stack and create a second PersistenceController (and with it, NSPersistentContainer), using one for each merge policy. This works, but seems like overkill / not as intended by Apple?
Here is a simple SwiftUI-based project for testing.
You just need a CoreData model with an entity named Item, and two String attributes: col1 and col2. Set col1 as a constraint on Item.
And I recommend adding the launch argument to your scheme -com.apple.CoreData.SQLDebug 1 so you can see the SQL that Core Data is generating. More info on that here: https://useyourloaf.com/blog/debugging-core-data/
import SwiftUI
import CoreData
class Model: ObservableObject {
var dataArray = [[String:Any]]()
}
struct ContentView: View {
#State var model: Model = Model()
var body: some View {
VStack {
Button("Insert Data - PropertyStoreTrump") {
insertData()
}
Button("Insert More Data - PropertyObjectTrump") {
insertMoreData()
}
Spacer()
Button("Fetch Data") {
fetchAllData()
}
Button("Clear Data") {
clearAllData()
}
}
}
// Insert data using NSMergeByPropertyStoreTrumpMergePolicy
private func insertData() {
model.dataArray = [[String:Any]]()
model.dataArray.append(["col1":"1", "col2":"1"])
model.dataArray.append(["col1":"2", "col2":"1"])
model.dataArray.append(["col1":"3", "col2":"1"])
batchInsert(context: PersistenceController.shared.container.viewContext, mergePolicy: NSMergeByPropertyStoreTrumpMergePolicy)
}
// Insert data using NSMergeByPropertyObjectTrumpMergePolicy
private func insertMoreData() {
model.dataArray = [[String:Any]]()
model.dataArray.append(["col1":"2", "col2":"0"])
model.dataArray.append(["col1":"3", "col2":"0"])
model.dataArray.append(["col1":"4", "col2":"0"])
batchInsert(context: PersistenceController.shared.container.newBackgroundContext(), mergePolicy: NSMergeByPropertyObjectTrumpMergePolicy)
}
private func batchInsert(context: NSManagedObjectContext, mergePolicy: AnyObject) {
context.mergePolicy = mergePolicy
context.perform {
let insertRequest = NSBatchInsertRequest(entity: Item.entity(), objects: model.dataArray)
do {
let result = try context.execute(insertRequest)
} catch {
print("batch insert error: \(error)")
}
}
}
private func fetchAllData() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Item")
do {
let results = try PersistenceController.shared.container.viewContext.fetch(fetchRequest) as! [Item]
for result in results {
print(" \(result.col1 ?? "") \(result.col2 ?? "")")
}
} catch {
print("error with fetch: \(error)")
}
}
private func clearAllData() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Item")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try PersistenceController.shared.container.viewContext.execute(batchDeleteRequest)
} catch {
print("error with delete request: \(error)")
}
}
}
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataStackOverflow")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
The workaround I found is to add this line to PersistenceController
static let shared2 = PersistenceController()
then use PersistenceController.shared.container... for one merge policy
and PersistenceController.shared2.container... for the other
I get this error when trying to get Xcode working with Core Data. Any help would be very appreciated.
Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x6000008a8820>
After commenting out all the other environments so that the persistence envinronment is only used in the top level, I still have the same error.
Here is the Apps Top-Level Swift File:
import SwiftUI
import CoreData
#main
struct Draw_DailyApp: App {
let persistenceController = PersistenceController.shared
#Environment(\.scenePhase) var scenePhase
#FetchRequest(entity: Drawing.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Drawing.img, ascending: true)])
var orders: FetchedResults<Drawing>
#State var showOrderSheet = false
// Creating a global environment and placing it into the environment
#ObservedObject var searchObjectController: SearchObjectController = SearchObjectController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(self.searchObjectController)
}
.onChange(of: scenePhase) { _ in
persistenceController.save()
}
}
}
and here is the file where persistent controller is used (Core Data xcdatamodeld has one Binary Data attribute called img)
Persistence.Swift
import CoreData
import UIKit
struct PersistenceController {
static let shared = PersistenceController()
func save() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Show some error here
}
}
}
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Drawing(context: viewContext)
newItem.img = Data()
newItem.date = "02081999"
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "DrawDaily")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
PS: I didn't start this project with Core Data, I added it in later
Your #FetchRequest doesn't have a NSManagedObjectContext upon init. You are creating it with this line
let persistenceController = PersistenceController.shared
Your #FetchRequest has to move down into the ContentView for it to work.
This line has to be called
.environment(\.managedObjectContext, persistenceController.container.viewContext)
when initializing the View that contains the #FetchRequest
Preview canvas is is crashing but in simulator everything working fine. I assuming it related to #ObservedObject and #Fetchrequest...
tried solution for here Previewing ContentView with CoreData
doesn't work
import SwiftUI
import CoreData
struct TemplateEditor: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: GlobalPlaceholders.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \GlobalPlaceholders.category, ascending: false),
]
) var placeholders: FetchedResults<GlobalPlaceholders>
#ObservedObject var documentTemplate: Templates
#State private var documentTemplateDraft = DocumentTemplateDraft()
#Binding var editing: Bool
var body: some View {
VStack(){
HStack(){
cancelButton
Spacer()
saveButton
}.padding()
addButton
ForEach(placeholders) {placeholder in
Text(placeholder.name)
}
TextField("Title", text: $documentTemplateDraft.title)
TextField("Body", text: $documentTemplateDraft.body)
.padding()
.frame(width: 100, height:400)
Spacer()
}
...
}
struct TemplateEditor_Previews: PreviewProvider {
static var previews: some View {
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Templates")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Templates.created, ascending: false)]
let documentTemplate = try! managedObjectContext.fetch(request).first as! Templates
return TemplateEditor(documentTemplate: documentTemplate, editing: .constant(true)).environment(\.managedObjectContext, managedObjectContext).environmentObject(documentTemplate)
}
}
Expected to generate preview
I'm not sure if your try line will work if there is no data.
let documentTemplate = try! managedObjectContext.fetch(request).first as! Templates
To get mine to work I created a test Item to use. Like this:
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Test data
let newEvent = Event.init(context: context)
newEvent.timestamp = Date()
return DetailView(event: newEvent).environment(\.managedObjectContext, context)
}
}
I've also noticed that I needed the .environment(.managedObjectContext, context) code in an earlier tabView that hosted the CoreData views or the preview would fail.
This answer seems to work in my recent project by replacing the default ContentView_Previews struct, though others are questioning whether it pulls persistent data. Credit goes to #ShadowDES - in the Master/Detail template project in Xcode Beta 7
I'm able to CRUD anything using Canvas (XCode Version 11.3 (11C29)) and it seems to run flawlessly.
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
#endif
What works for me:
I create all of my sample data in the preview property of my persistence controller, building off of the template generated by Xcode when starting a project with the following settings: Interface - SwiftUI, Lifecycle - SwiftUI App, Use Core Data, Host in CloudKit. I have posted the template here:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// ** Prepare all sample data for previews here ** //
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// handle error for production
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "SwiftUISwiftAppCoreDataCloudKit")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// handle error for production
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
In my preview, I inject the persistence controller into the preview environment and for my view argument I use the registeredObjects.first(where:) method on the preview viewContext to pull the first object of the desired type:
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(item: PersistenceController.preview.container.viewContext.registeredObjects.first(where: { $0 is Item }) as! Item)
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Edited 11/15/21
Persistence
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
Seed().prepareData(for: viewContext)
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "SwiftUISwiftAppCoreDataCloudKit")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// handle error for production
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
struct Seed {
func prepareData(for viewContext: NSManagedObjectContext) {
// ** Prepare all sample data for previews here ** //
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// handle error for production
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
Item Preview
struct ItemView_Previews: PreviewProvider {
static let persistence = PersistenceController.preview
static var item: Item = {
let context = persistence.container.viewContext
let item = Item(context: context)
item.timestamp = Date()
return item
}()
static var previews: some View {
ItemView(item: item)
.environment(\.managedObjectContext, persistence.container.viewContext)
}
}
So, if you put some code in an onAppear handler in the preview, it will run on boot. It even live updates as you type!
struct TemplateEditor_Previews: PreviewProvider {
static var previews: some View {
TemplateEditor().environment(\.managedObjectContext, AppDelegate.viewContext).onAppear {
let entity = GlobalPlaceholders(context: AppDelegate.viewContext)
entity.name = "abc123"
// Or create more, if you need more example data
try! AppDelegate.viewContext.save()
}
}
}
Note that I've wrapped up my viewContext in a static method on AppDelegate to make access a tiny bit less verbose and easier to remember:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
static var persistentContainer: NSPersistentContainer {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer
}
static var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
Works for SwiftUI 2 app using the App template
I also had the previews crash and none of the other solutions were suitable or worked for me.
What I did was rather than the following:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
.environment(
\.managedObjectContext,
CoreDataManager.context
)
}
}
I fixed it with:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = CoreDataManager.context
/* Optional sample data can be inserted here */
return ContentView()
.environment(
\.managedObjectContext,
context
)
}
}
Where CoreDataManager is:
enum CoreDataManager {
static var context: NSManagedObjectContext {
persistentContainer.viewContext
}
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyContainerName")
container.loadPersistentStores { description, error in
guard let error = error else { return }
fatalError("Core Data error: '\(error.localizedDescription)'.")
}
return container
}()
}
Not exactly sure why this helped, but now it works perfectly. Additionally you can add sample data to this context where I have marked with a comment.
This is my solution.
I don't want use CoreData in view. I want MVVM style.
So you need to mock Core data for display in Canvas view.
This is an example :
// View
struct MyView: View {
#ObservedObject var viewModel: PreviewViewModel
}
// View Model
final class MyViewModel: ObservableObject {
#Published var repository: RepositoryProtocol // CoreData
}
// Repository
protocol RepositoryProtocol { }
class Repository: RepositoryProtocol { ... }
class MockRepository: RepositoryProtocol { ... } // Create a Mock
// Init of your view
// If Canvas use mock
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
repository = MockRepository()
// else App use Repository
} else {
repository = Repository.shared
}
let viewModel = MyViewModel(repository:repository)
MyViewModel(viewModel: viewModel)
This worked for me. In the AppDelegate create a different preview context and fill it with objects.
lazy var persistentContainerPreview: NSPersistentContainer = {
let persistentContainer = NSPersistentContainer(name: "MyModel")
persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
}
})
let didCreateSampleData = UserDefaults.standard.bool(forKey: "didCreateSampleData")
if !didCreateSampleData {
let context = persistentContainer.viewContext
let recipe = Recipe(context: context)
recipe.title = "Soup 2"
recipe.difficultyName = "NOT TOO TRICKY"
recipe.difficultyValue = 1
recipe.heroImage = "dsfsdf"
recipe.ingredients = "meat"
recipe.method = "sdcsdsd"
recipe.published = Date()
recipe.recipeId = 1
recipe.servings = 4
recipe.tags = "sdfs"
recipe.totalTime = 100
recipe.totalTimeFormatted = "Less than 2 hours"
try! context.save()
}
return persistentContainer
}()
Then in your preview.
struct RecipeView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainerPreview.viewContext
let recipe = try! context.fetch(Recipe.fetchRequest()).first as! Recipe
RecipeView(recipe: recipe).environment(\.managedObjectContext, context)
}
}
One option is to NOT use Core Data in previews. This is helpful enough to see the UI of what I'm building but I'll still need to use Simulator to test the functionality.
#if !DEBUG
// Core Data related code e.g. #FetchRequest
#endif
What was suggested in Previewing ContentView with CoreData worked for me, Xcode Version 11.0 (11A419c) Mac OS 10.15 Beta (19A558d). My crash logs showed an index error,
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty NSArray'
because there was no data there, so I had to handle this unique "preview" case and that got things working.
It crashes because it was instructed so in the PersistenceController:
struct PersistenceController {
...
static var preview: PersistenceController = {
...
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
...
}
So the actual reason can be seen in the crash report. Actually, XCode 12.4 shows a warning about checking the crash report; however, the report is too verbose for a newbie like me from web development. Thus it took me a while to find out the problem, so I hope this would save some time for others.
...and the problem in my case was a required attribute was not set while populating the core data model for previews.
The thing is you need to find out which line cause the crash.
Since the canvas doesn't show the detailed error, using OSLog and Console.app to debug would be a possible solution.
For example:
import os.log
struct YourView_Previews: PreviewProvider {
static var previews: some View {
os_log("[DEBUG]-\(#function)---1--")
let moc = PersistenceController.preview.container.viewContext
os_log("[DEBUG]-\(#function)---2--")
let item = Item.previewData(context: moc)
os_log("[DEBUG]-\(#function)---3--")
return YourView(item: item, now: Date())
.environment(\.managedObjectContext, moc)
}
}
Remember to use filter to better catch the debug message from Console.
After finding out which line cause the crash, you can further look into the line and continue the process until you find the culprit.
(In my case, I forgot to add UUID to the preview data which causing the canvas to crash.)