How to use a picker on CoreData relationships in SwiftUI - core-data

G'day everyone,
I'm trying to work out how CoreData relationships can work with UI elements like pickers.
At the moment I have a 3 view app (based on the Xcode boilerplate code) which displays a list of parent entities, which have children which have children. I want a picker to select which grandchild a child entity should refer to.
At the moment I have two funny side effects:
When I run the app as a preview (so there is pre-populated data... this sample code will break without the data in place),
the selected grandchild in the picker is the grandchild of the first
child, irrespective of which child you're dropped into in the first
view.
When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
When I select a child and "save" that, the value in the child summary does not change, until I click another child at which point the value changes before the transition to the modal view.
I am clearly missing something in my understanding of the sequence of events when presenting modals in SwiftUI... can any what shed any light on what I've done wrong?
Here's a video to make this more clear:
https://github.com/andrewjdavison/Test31/blob/main/Test31%20-%20first%20click%20issue.mov?raw=true
Git repository of the sample is https://github.com/andrewjdavison/Test31.git, but in summary:
Data Model:
View Source:
import SwiftUI
import CoreData
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#Binding var licence: Licence
#Binding var showModal: Bool
#State var selectedElement: Element
#FetchRequest private var elements: FetchedResults<Element>
init(currentLicence: Binding<Licence>, showModal: Binding<Bool>, context: NSManagedObjectContext) {
self._licence = currentLicence
self._showModal = showModal
let fetchRequest: NSFetchRequest<Element> = Element.fetchRequest()
fetchRequest.sortDescriptors = []
self._elements = FetchRequest(fetchRequest: fetchRequest)
_selectedElement = State(initialValue: currentLicence.wrappedValue.licenced!)
}
func save() {
licence.licenced = selectedElement
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $selectedElement, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
}
}
Text("Selected: \(selectedElement.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
struct RegisterView : View {
#Environment(\.managedObjectContext) private var viewContext
#State var showModal: Bool = false
var currentRegister: Register
#State var currentLicence: Licence
init(currentRegister: Register) {
currentLicence = Array(currentRegister.licencedUsers! as! Set<Licence>)[0]
self.currentRegister = currentRegister
}
var body: some View {
VStack {
List {
ForEach (Array(currentRegister.licencedUsers! as! Set<Licence>), id: \.self) { licence in
Button(action: {currentLicence = licence; showModal = true}) {
HStack {
Text("\(licence.leasee!) : ")
Text("\(licence.licenced!.desc!)")
}
}
}
}
}
.sheet(isPresented: $showModal) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Register.id, ascending: true)],
animation: .default)
private var registers: FetchedResults<Register>
var body: some View {
NavigationView {
List {
ForEach(registers) { register in
NavigationLink(destination: RegisterView(currentRegister: register)) {
Text("Register id \(register.id!)")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
[1]: https://i.stack.imgur.com/AfaNb.png

I didn't really understand this
• selected grandchild in the picker is the grandchild of the first child, irrespective of which child you're dropped into in the first view.
• When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
Could you attach a video that represents a problem?
But I can give you a solution to the preview problem and the second one.
Preview
If you use preview with Core Data, you need to use a viewContextcreated with MockData and pass it to your View. Here I provide a generic code, that can be modified for each of your views:
In your Persistance struct (CoreData Manager) declare a variable preview with your preview Items:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// Here you create your Mock Data
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Make sure it has inMemory: Bool in its init, as it is responsible for separating real viewContext and previewContext:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
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)")
}
})
}
Create Mock Item from your viewContext and pass it to preview:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItems = try! context.fetch(request)
YourView(item: fetchedItems)
}
}
If you use #FetchRequest and #FetchedResults it makes it easier, as they will do creating and fetching objects for you. Just implement a preview like this:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
YourView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Here is Persistence struct created by Xcode at the moment of the project initialization:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
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)")
}
})
}
}
Second problem
Core Data objects are built with classes, so their type is a reference. When you change a property is a class it doesn't notifiy the view struct to redraw with a new value. (exception is classes, that are created to notify about changes.)
You need to explicitly tell your RegisterView struct to redraw itself after you dismiss your LicenceView. You can do it by creating one more variable in your RegisterView - #State var id = UUID(). Then attach an .id(id) modifier at the end of your VStack
VStack {
//your code
}.id(id)
Finally, create a function viewDismissed which will change the id property in your struct:
func viewDismissed() {
id = UUID()
}
Now, attach this function to your sheet with an optional parameter onDismiss
.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}

OK. Huge vote of thanks to Lorem for getting me to the answer. Thanks too for Roma, but it does turn out that his solution, whilst it worked to resolve one of my key problems, does introduce inefficiencies - and didn't resolve the second one.
If others are hitting the same issue I'll leave the Github repo up, but the crux of it all was that #State shouldn't be used when you're sharing CoreData objects around. #ObservedObject is the way to go here.
So the resolution to the problems I encountered were:
Use #ObservedObject instead of #State for passing around the CoreData objects
Make sure that the picker has a tag defined. The documentation I head read implied that this gets generated automatically if you use ".self" as the id for the objects in ForEach, but it seems this is not always reliable. so adding ".tag(element as Element?)" to my picker helped here.
Note: It needed to be an optional type because CoreData makes all the attribute types optional.
Those two alone fixed the problems.
The revised "LicenceView" struct is here, but the whole solution is in the repo.
Cheers!
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var licence: Licence
#Binding var showModal: Bool
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Element.desc, ascending: true)],
animation: .default)
private var elements: FetchedResults<Element>
func save() {
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $licence.licenced, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
.tag(element as Element?)
}
}
Text("Selected: \(licence.licenced!.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}

Related

SwiftUI ForEach force UI update when updating the contents of a core data relationship

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.

Saving Item in CoreData Initiates Navigation

In my ContentView I have a FetchRequest<Project>. I navigate to ProjectView using a NavigationLink. From ProjectView I navigate to AddItemView using another NavigationLink. In AddItemView when I add an Item to the Project and call container.viewContext.save() the AddItemView automatically dismisses back to the ContentView.
My guess is that saving to CoreData updates the FetchRequest<Project> list which in turn updates the views, but I am not sure.
How can I save a new Item to the Project in CoreData and only navigate back to ProjectView and not ContentView?
To reproduce:
Create new Single View App and check Core Data and Host in CloudKit
In the .xcdatamodel delete the default Entity and replace it with an Entity called Project which has attributes date: Date and title: String and an Entity called Item which has an attribute name: String. Give the Project a relationship called items (to type Item) and choose “to many” on the right. Give the Item a relationship called project that is the inverse of items.
Replace the code in Persistence.swift with this:
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataBug")
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)")
}
})
}
}
Copy ContentView
// ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
let projects: FetchRequest<Project>
init() {
projects = FetchRequest<Project>(entity: Project.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Project.date, ascending: false)
])
}
var body: some View {
NavigationView {
List {
ForEach(projects.wrappedValue) { project in
NavigationLink(destination: ProjectView(project: project)) {
Text(project.title ?? "Title")
}
}
}
.navigationTitle("Projects")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
Button {
withAnimation {
let project = Project(context: moc)
let now = Date()
project.date = now
project.title = now.description
try? moc.save()
}
} label: {
Label("Add Project", systemImage: "plus")
}
}
}
}
}
}
Create ProjectView.swift and copy this:
// ProjectView.swift
import SwiftUI
struct ProjectView: View {
#ObservedObject var project: Project
var items: [Item] {
project.items?.allObjects as? [Item] ?? []
}
var body: some View {
List {
ForEach(items) { item in
Text(item.name ?? "")
}
}
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
NavigationLink(destination: AddItemView(project: project)) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
Create AddItemView.swift and copy this:
import SwiftUI
// AddItemView.swift
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var moc
let project: Project
#State private var selectedName: String = ""
var body: some View {
TextField("Type name here", text: $selectedName)
.navigationTitle("Add Item")
.navigationBarItems(trailing: Button("Add") {
let ingestion = Item(context: moc)
ingestion.project = project
ingestion.name = selectedName
try? moc.save()
presentationMode.wrappedValue.dismiss()
})
}
}
Run the app. Click the plus on the top right. Click the project that just slid in. In the ProjectView click the plus on the top right again. Type a name in the TextField and click add on the top right. When the AddItemView is dismissed it probably went back to ContentView. If not add another item to the project.

Passing Core Data FetchedResults<T> for previews in SwiftUI

I have a parent view which does a #FetchRequest and passes the FetchedResults<T> to a child view. Everything works, and the child view is able to parse through the FetchedResults. However, I can't figure out how to set up the data so that the child's Preview struct will work. What's the proper way to set up some constant data in Preview struct so that I can instantiate the child view and pass in FetchedResults<T>?
As FetchedResults<T> is a RandomAccessCollection and swift array also is a RandomAccessCollection, here is possible solution.
Update: verified with Xcode 13.3 / iOS 15.4
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Person.entity(), sortDescriptors: [])
var result: FetchedResults<Person>
var body: some View {
VStack(alignment: .leading) {
Text("Persons").font(.title)
PersonsView(results: result) // FetchedResults<Person> is a collection
}
}
}
// generalize PersonsView to depend just on collection
struct PersonsView<Results:RandomAccessCollection>: View where Results.Element == Person {
let results: Results
var body: some View {
ForEach(results, id: \.self) { person in
Text("Name: \(person.name ?? "<unknown>")")
}
}
}
// Tested with Xcode 11.4 / iOS 13.4
// DOES NOT WORK ANYMORE !!
// struct ChildView_Previews: PreviewProvider {
// static var previews: some View {
// PersonsView(results: [Person()]) // << use regular array //to test
// }
}
Update: fixed & tested part for Xcode 12 / iSO 14 (due to crash of above PreviewProvider)
It appears entity now should be read & specified explicitly:
struct ChildView_Previews: PreviewProvider {
static let entity = NSManagedObjectModel.mergedModel(from: nil)?.entitiesByName["Person"]
static var previews: some View {
let person = Person(entity: entity!, insertInto: nil)
person.name = "Test Name"
return PersonsView(results: [person])
}
}
Use the preview PersistenceController within your pre-generated PersistenceController struct (inside the "Persistence.swift" file).
so if you pass an item from a Core Data "Item"-entity:
struct ContentView: View {
...
private var items: FetchedResults<Item>
..
ForEach(items) { item in
DetailView(item: item)
}
..
In the Detail-View go like this:
struct DetailView: View {
var item: FetchedResults<Item>.Element
var body: some View {
Text("Items text = \(item.text ?? "")")
}
}
struct Detail_Previews: PreviewProvider {
static var previews: some View {
let viewContext = PersistenceController.preview.container.viewContext
let previewItem = Item(context: viewContext)
previewItem.text = "Text4preview"
return Detail(item: previewItem)
}
}

How to update #FetchRequest, when a related Entity changes in SwiftUI?

In a SwiftUI View i have a List based on #FetchRequest showing data of a Primary entity and the via relationship connected Secondary entity.
The View and its List is updated correctly, when I add a new Primary entity with a new related secondary entity.
The problem is, when I update the connected Secondary item in a detail view, the database gets updated, but the changes are not reflected in the Primary List.
Obviously, the #FetchRequest does not get triggered by the changes in another View.
When I add a new item in the primary view thereafter, the previously changed item gets finally updated.
As a workaround, i additionally update an attribute of the Primary entity in the detail view and the changes propagate correctly to the Primary View.
My question is:
How can I force an update on all related #FetchRequests in SwiftUI Core Data?
Especially, when I have no direct access to the related entities/#Fetchrequests?
import SwiftUI
extension Primary: Identifiable {}
// Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
entity: Primary.entity(),
sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
var primary: Primary
#State private var newSecondaryName = ""
var body: some View {
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
primary.secondary?.secondaryName = newSecondaryName
// TODO: ❌ workaround to trigger update on primary #FetchRequest
primary.managedObjectContext.refresh(primary, mergeChanges: true)
// primary.primaryName = primary.primaryName
try? primary.managedObjectContext?.save()
presentationMode.wrappedValue.dismiss()
}
}
I also struggled with this and found a very nice and clean solution:
You have to wrap the row in a separate view and use #ObservedObject in that row view on the entity.
Here's my code:
WineList:
struct WineList: View {
#FetchRequest(entity: Wine.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Wine.name, ascending: true)
]
) var wines: FetchedResults<Wine>
var body: some View {
List(wines, id: \.id) { wine in
NavigationLink(destination: WineDetail(wine: wine)) {
WineRow(wine: wine)
}
}
.navigationBarTitle("Wines")
}
}
WineRow:
struct WineRow: View {
#ObservedObject var wine: Wine // !! #ObserveObject is the key!!!
var body: some View {
HStack {
Text(wine.name ?? "")
Spacer()
}
}
}
You need a Publisher which would generate event about changes in context and some state variable in primary view to force view rebuild on receive event from that publisher.
Important: state variable must be used in view builder code, otherwise rendering engine would not know that something changed.
Here is simple modification of affected part of your code, that gives behaviour that you need.
#State private var refreshing = false
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
// below use of .refreshing is just as demo,
// it can be use for anything
Text("\(primary.primaryName ?? "nil")" + (self.refreshing ? "" : ""))
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
// here is the listener for published context event
.onReceive(self.didSave) { _ in
self.refreshing.toggle()
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
An alternative method: using a Publisher and List.id():
struct ContentView: View {
/*
#FetchRequest...
*/
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) //the publisher
#State private var refreshID = UUID()
var body: some View {
List {
...
}
.id(refreshID)
.onReceive(self.didSave) { _ in //the listener
self.refreshID = UUID()
print("generated a new UUID")
}
}
}
Every time you call save() of NSManagedObjects in a context, it genertates a new UUID for the List view, and it forces the List view to refresh.
To fix that you have to add #ObservedObject to var primary: Primary in SecondaryView to work List properly. Primary belong to NSManagedObject class, which already conforms to #ObservableObject protocol. This way the changes in instances of Primary are observed.
import SwiftUI
extension Primary: Identifiable {}
// Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
entity: Primary.entity(),
sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var primary: Primary
#State private var newSecondaryName = ""
var body: some View {
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
primary.secondary?.secondaryName = newSecondaryName
try? primary.managedObjectContext?.save()
presentationMode.wrappedValue.dismiss()
}
}
I tried to touch the primary object in the detail view like this:
// TODO: ❌ workaround to trigger update on primary #FetchRequest
if let primary = secondary.primary {
secondary.managedObjectContext?.refresh(primary, mergeChanges: true)
}
Then the primary list will update. But the detail view has to know about the parent object. This will work, but this is probably not the SwiftUI or Combine way...
Edit:
Based on the above workaround, I modified my project with a global save(managedObject:) function. This will touch all related Entities, thus updating all relevant #FetchRequest's.
import SwiftUI
import CoreData
extension Primary: Identifiable {}
// MARK: - Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \Primary.primaryName, ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
print("body PrimaryListView"); return
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(secondary: primary.secondary!)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")")
.font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// MARK: - Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
var secondary: Secondary
#State private var newSecondaryName = ""
var body: some View {
print("SecondaryView: \(secondary.secondaryName ?? "")"); return
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.secondary.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
secondary.secondaryName = newSecondaryName
// save Secondary and touch Primary
(UIApplication.shared.delegate as! AppDelegate).save(managedObject: secondary)
presentationMode.wrappedValue.dismiss()
}
}
extension AppDelegate {
/// save and touch related objects
func save(managedObject: NSManagedObject) {
let context = persistentContainer.viewContext
// if this object has an impact on related objects, touch these related objects
if let secondary = managedObject as? Secondary,
let primary = secondary.primary {
context.refresh(primary, mergeChanges: true)
print("Primary touched: \(primary.primaryName ?? "no name")")
}
saveContext()
}
}
If you are here, i don't find the reason why your view isn't updating, i think this will help you:
Always use the #ObservedObject when you declare a core data type.
If you are using MVVM, wrap the view model also with #ObservedObject, and in the VM create the core data type with #Published.
This is an example of creating a VM with #ObservedObject, so when core data receives the update, the instance of the view model recreate itself, and the view is updated.
class ProductTitleValueViewModel: BaseViewModel, ObservableObject {
// MARK: - Properties
#Published var product: Product
var colorSet: [Color]
var currency: Currency
// MARK: - Init
init(product: Product, colorSet: [Color], currency: Currency) {
self.product = product
self.colorSet = colorSet
self.currency = currency
}
}
struct ProductTitleValueView: View {
#ObservedObject var viewModel: ProductTitleValueViewModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
HStack {
Circle()
.fill(
LinearGradient(colors: viewModel.colorSet, startPoint: .leading, endPoint: .trailing)
)
.opacity(0.6)
.frame(width: 20, height: 20)
Text(viewModel.product.wrappedName)
.font(.callout.bold())
.foregroundColor(ThemeColor.lightGray)
}
Text(viewModel.product.balance.toCurrency(with: viewModel.currency))
.font(.callout.bold())
.padding(.leading, 28)
}
}
}
If you follow this 2 simple things, you are not going to have problem with core date updating your views.

SwiftUI Preview canvas and Core Data

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.)

Resources