SwiftUI + Core Data: list doesn't refresh changes (no relationship) - core-data

Recently I have integrated Core Data to my SwiftUI App.
Just to simplify, I have two views:
TimerListView: fetch data from Core Data and show the list
TimerDetailView: show Timer details and update data
struct TimerListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Timer.id, ascending: true)],
animation: .default)
private var timers: FetchedResults<Timer>
var body: some View {
NavigationView{
List {
ForEach(timers) { timer in
NavigationLink(destination: TimerDetailView(timer: timer)) {
TimerCardView(timer: timer)
}
}
}
}
[...]
}
}
Where
struct TimerDetailView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var timer: Timer
Outcomes
Data is correctly updated in Core Data (if I close and reopen the App data is correctly updated)
When from TimerDetailView I turn back to TimerListView (after an update) the list doesn't show the updated data

SOLVED: all child classes view have to define Timer attribute using #ObservedObject wrapper.
In previous code I replaced, into TimerCardView(timer: timer):
from var timer: Timer
to #ObservedObject var timer: Timer
And now WORKS!

Related

List not updating after inserting new value from another target SwiftUI

I'm trying to setup a action extension for my app. The action extension lets you save text to the main app thought a shared CoreData container.
The problem is that when I save text from the extension and return to the main app the updated data is not automatically loaded. The data added thought the extension does show up when I restart the app.
struct RecentsView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [], predicate: nil, animation: .default)
private var masterClips: FetchedResults<Clip>
var body: some View {
NavigationView {
List {
ForEach(masterClips, id: \.self) { item in
Text(item.text!)
}
}
}
}
}

#Binding - passing FetchedResults, single Entities, to SubViews in Core Data + SwiftUI

I just want to pass a binding to an Core Data - Entity to a SwiftUI SubView screen inside a ForEach Loop with a binding, so I have access to edit properties of the Entity, I can save my context and get automatic updated views..
how can I achieve something like this:
ContentView {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity:ToDo.entity(), sortDescriptors: [])
private var toDoItems: FetchedResults<ToDo>
... the following part is what I need:
ForEach(toDoItems) { (item:ToDo) in
NavigationLink(
destination: MyEditView($item),
...
}
}
You need to set an #ObservedObject var item: ToDo. Your CoreData entity is a class that conforms to ObservableObject and will force a view update when any property is changed in it.
struct NextView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var item: ToDo
var body: some View {
Button(action: {
item.name = "New value"
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
print("SAVED CONTEXT")
} catch let error {
print("Error: SAVING CONTEXT \(error), \(error.localizedDescription)")
}
}, label: {
Text("\(item.name)")
})
}
}
You can now make any change you want and save the context when needed

Isolated managed object context for FetchRequest in SwiftUI

In a SwiftUI view, by default FetchRequest fetches from the managedObjectContext environment value. If instead the view wants to fetch into an isolated context, for example to make discardable edits without polluting the context of other views, how can it change the context that FetchRequest uses?
One option is to wrap the view in an outer view that creates the isolated context and then calls the wrapped view using it:
var body: some View {
WrappedView().environment(\.managedObjectContext, isolatedContext)
}
This is tedious, however. You have to create two views and pass all the wrapped views' arguments through the wrapper. Is there a better way to tell FetchRequest which context to use?
If you use the standard PersistentController that Apple gives as a startup you could try using
.environment(\.managedObjectContext, privateContext)
Your View would need this property to make it work. #State shouldn't be necessary since the changes are being done in the background by other means such as notifications.
let privateContext = PersistenceController.shared.container.newBackgroundContext()
Invoking newBackgroundContext() method causes the persistent container to create and return a new NSManagedObjectContext with the concurrencyType set to NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType. This new context will be associated with the NSPersistentStoreCoordinator directly and is set to consume NSManagedObjectContextDidSave broadcasts automatically.
Then to test it out using most of the sample code from Apple.
struct SampleSharedCloudKitApp: App {
let privateContext = PersistenceController.shared.container.newBackgroundContext()
var body: some Scene {
WindowGroup {
VStack{
Text(privateContext.description) //Added this to match with ContentView
ContentView()
.environment(\.managedObjectContext, privateContext)
//Once you pass the privateContext here everything below it will have the privateContext
//You don't need to connect it with #FetchRequest by any other means
}
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
List {
Text((items.first!.managedObjectContext!.concurrencyType == NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType).description) //This shows true
Text(items.first!.managedObjectContext!.description)// This description matches the parent view
Text(viewContext.description)// This description matches the parent view
Also, something to note is that you have to set
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
In order for the main context to show the changes done after saving the privateContext. I put it in the PersistenceController init right after the loadPersistentStores closure.

How do you update a TabView in SwiftUI when the CoreData is modified?

I am trying to modify the underlying CoreData for a TabView in SwiftUI and have that reflected when a user creates or deletes data, but the view never gets "redrawn" Here is what I have so far:
struct AddUserView: View {
#State var showingAddNewUser = false
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Users.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \User.fName, ascending: true)
]
) var fetchedUsers: FetchedResults<Users>
var body: some View {
TabView{
if(fetchedUsers.count > 0){
ForEach(fetchedUsers) { user in
UserProfileView(showingAddNewUser: showingAddNewUser, user: user, managedObjectContext: _managedObjectContext)
}
AddUserView(showingAddNewuser: false)
} else{
AddUserView(showingAddNewuser: false)
}
}.frame(width: UIScreen.main.bounds.width, height: 400)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
For the UserProfileView I have:
struct UserProfileView: View {
#State var showingAddNewUser = false
#ObservedObject var user: Users
#Environment(\.managedObjectContext) var managedObjectContext
var body: some View {
the body of my user profile view.. Essentially some Text views to display user info along with a delete button
after editing the context is saved here
private func saveContext() {
do {
try managedObjectContext.save()
} catch {
print("Error saving managed object context: \(error)")
}
}
When I use the delete button the user is deleted but the TabView in the AddUserView is not redrawn/updated. How do I recall the "fetch" request to update the data set?

Does the #FetchRequest property wrapper automatically notify when changes happen?

If I'm using an NSPersistentCloudKitContainer, I see in the sqlite file that changes are automatically synced. I can also make changes in the CloudKit Dashboard, restart the app and see the changes. If I use #FetchRequest to load some items, should the items array automatically update?
// Fetch some items
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
// Show them in a List
List(items) { item in
VStack {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
}
Yes, if you set automaticallyMergesChangesFromParent = true which is not in the default code provided by Apple when you check Use CoreData and CloudKit.
let container = NSPersistentCloudKitContainer(name: "MyApp")
container.viewContext.automaticallyMergesChangesFromParent = true

Resources