iOS 13.4 CoreData SwiftUI app crashes with "EXC_BREAKPOINT (code=1, subcode=0x1f3751f08)" on device - core-data

A very simple CoreData app: All code provided below.
Start up with CoreData template single view app.
2 entities with a string attribute each: Message(title) and Post(name)
A NavigationView containing
NavigationLink to a list of messages
NavigationLink to a list of posts
Each linked ListView (Message/Post) has
a button to add an item to the list
a button to remove all items from the list
Now, when you run this app on a simulator (any iOS 13.x version) all runs as expected from the description above.
But on a DEVICE running iOS 13.4
Tap "Messages"
Creating/deleting messages works fine, SwiftUi view updates immediately.
Tap "back"
Tap "Messages" again. While still creating/deleting messages works fine: The debugger now shows a warning: "Context in environment is not connected to a persistent store coordinator: NSManagedObjectContext: 0x280ed72c0
Tap "Posts"
==> App crashes with EXC_BREAKPOINT (code=1, subcode=0x1f3751f08)
You can start the process with Posts first, too. Then the same crash occurs on the messages list view.
I strongly believe this is an iOS 13.4 bug because similar code ran fine on Xcode 11.3 / iOS 13.3.
Does anyone know a fix or workaround for this?
Here is a link to the full project: Full Xcode Project
The ContentView:
import SwiftUI
import CoreData
struct MessageList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Message.entity(), sortDescriptors: [])
var messages: FetchedResults<Message>
var body: some View {
List() {
ForEach(messages, id: \.self) { message in
Text(message.title ?? "?")
}
}
.navigationBarItems(trailing:
HStack(spacing: 16) {
Button(action: deleteMessages) {
Image(systemName: "text.badge.minus")
}
Button(action: addMessage) {
Image(systemName: "plus.app")
}
}
)
}
func addMessage() {
let m = Message(context: moc)
m.title = "Message: \(Date())"
try! moc.save()
}
func deleteMessages() {
messages.forEach {
moc.delete($0)
}
}
}
struct PostList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Post.entity(), sortDescriptors: [])
var posts: FetchedResults<Post>
var body: some View {
List {
ForEach(0..<posts.count, id: \.self) { post in
Text(self.posts[post].name ?? "?")
}
}
.navigationBarItems(trailing:
HStack(spacing: 16) {
Button(action: deletePosts) {
Image(systemName: "text.badge.minus")
}
Button(action: addPost) {
Image(systemName: "plus.app")
}
}
)
}
func addPost() {
let p = Post(context: moc)
p.name = "Post \(UUID().uuidString)"
try! moc.save()
}
func deletePosts() {
posts.forEach {
moc.delete($0)
}
try! moc.save()
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
NavigationView {
VStack(alignment: .leading){
NavigationLink(destination: MessageList()) {
Text("Messages")
}.padding()
NavigationLink(destination: PostList()) {
Text("Posts")
}.padding()
Spacer()
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, moc)
}
}
Screenshot of the Model:
The SceneDelegate (unaltered from template, provided for completeness):
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {}
func sceneDidBecomeActive(_ scene: UIScene) {}
func sceneWillResignActive(_ scene: UIScene) {}
func sceneWillEnterForeground(_ scene: UIScene) {}
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
The AppDelegate (unaltered from template, provided for completeness):
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Coredata134")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

Update iOS 14.0 (beta 1):
This issue seems to have been resolved on iOS 14.
I also believe this is a bug.
You can workaround for now by setting the environment variable again within the NavigationLinks in ContentView:
NavigationLink(destination: MessageList().environment(\.managedObjectContext, moc)) {
Text("Messages")
}.padding()
NavigationLink(destination: PostList().environment(\.managedObjectContext, moc)) {
Text("Posts")
}.padding()
EDIT:
Just noticed that this workaround has at least one serious negative side effect: in case the #FetchRequest in the destination View uses a sortDescriptor and the destination View itself contains a NavigationLink, (e.g. to a DetailView), then modifying an attribute contained in the sortDescriptor in the DetailView will cause the DetailView to be popped and pushed again as soon as the new attribute value leads to a new sort order.
To demonstrate this:
a) add a new attribute of type Integer 16 named "value" to the Message entity in the Core Data model.
b) update func addMessage() as follows:
func addMessage() {
let m = Message(context: moc)
m.title = "Message: \(Date())"
m.value = 0
try! moc.save()
}
c) add the following struct to ContentView.swift
struct MessageDetailList: View {
#ObservedObject var message: Message
var body: some View {
Button(action: {
self.message.value += 1
}) {
Text("\(message.title ?? "?"): value = \(message.value)")
}
}
}
d) Update the ForEach in struct MessageList as follows:
ForEach(messages, id: \.self) { message in
NavigationLink(destination: MessageDetailList(message: message).environment(\.managedObjectContext, self.moc)) {
Text("\(message.title ?? "?"): value = \(message.value)")
}
}
e) replace #FetchRequest in MessageList with:
#FetchRequest(entity: Message.entity(), sortDescriptors: [NSSortDescriptor(key: "value", ascending: false)])
Run the code and tap on "Messages". Create three messages, then tap on the third one. In the DetailView, tap on the Button. This will increase the value attribute of this message to 1 and thus resort the fetch results on MessageList, which will trigger a pop and push again of the detail list.

Related

SwiftUI CoreData master detail questions

I'm trying to create a master detail relationship with CoreData. I have a settings tab that is used to select the master (it's global and not done very often by the user). There is another tab that shows the detail entries for the current master.
The master has one field, name, a string and the details array. The detail has one field, name, a string. I'm using UUID().uuidString to populate the names for the example.
The problem I'm having is that when I select the detail tab, it shows the details for the current master. If I add details (click the + button) they do not appear until I change the master (settings -> select master). If I edit the details and delete some, the list entries go away but when I finish editing, they immediately come back. I can switch masters and then go back to the edited master and the data looks correct (I have to change the activeMaster published property).
I'm thinking that the published property isn't forcing the update to the details view because swift doesn't see the master variable change. I may also not be adding or deleting the details correctly.
How is adding details to a master typically done (here master is one to many details)
How is deleting details from a master typically done?
Is the data no showing up due to the published property not "publishing" Any ideas on how to better do this?
Thanks.
Code is below.
Here's the global application data:
import Foundation
import CoreData
import SwiftUI
class ApplicationData: ObservableObject
{
let container: NSPersistentContainer
#Published var activeMaster: Master?
init(preview: Bool = false)
{
container = NSPersistentContainer(name: "MasterDetail")
if (preview)
{
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)")
}
})
}
}
Just persistence and a single optional active master. The application data is created in the application code and set as an environment object:
import SwiftUI
#main
struct MasterDetailApp: App
{
#StateObject var appData = ApplicationData()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appData)
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
}
The tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView()
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
The detail tab allows the user to add details and to edit the list:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
List
{
ForEach(appData.activeMaster?.wrappedDetail ?? [])
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
The settings view just has a navigation link to a view to select the master and an add button to add masters:
import Foundation
import SwiftUI
struct SettingsView: View
{
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
Form
{
Section(header: Text("Masters"))
{
NavigationLink(destination: SelectMastersView(selectedMaster: $appData.activeMaster), label:
{
Text(appData.activeMaster?.name ?? "Select Master")
})
Button
{
let master = Master(context: viewContext)
master.name = UUID().uuidString
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
}
}
}
}
}
The view for selecting the master just has a fetch request to get all masters and assign the selected one to the global app data published property:
import Foundation
import SwiftUI
struct SelectMastersView: View
{
#Environment(\.dismiss) var dismiss
#FetchRequest(entity: Master.entity(), sortDescriptors: [], animation: .default)
var masters: FetchedResults<Master>
#Binding var selectedMaster: Master?
var body: some View
{
List
{
ForEach(masters)
{ master in
Text(master.name ?? "None")
.onTapGesture
{
selectedMaster = master
dismiss()
}
}
}
.navigationBarTitle("Masters")
}
}
Edited to add extension to Master I forgot to post.
import Foundation
extension Master
{
var wrappedDetail: [Detail]
{
detail?.allObjects as! [Detail]
}
}
I finally figured it out this morning. I think putting the example code together last night helped quite a bit.
I got it work by creating fetch request in the detail view and passing the master into the view in init().
Here's the updated code for the tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView(master: appData.activeMaster)
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
and the updated detail view:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
#FetchRequest(entity: Detail.entity(), sortDescriptors: [])
var details: FetchedResults<Detail>
let master: Master?
init(master: Master?)
{
self.master = master
if master != nil
{
let predicate = NSPredicate(format: "%K == %#", #keyPath(Detail.master), master ?? NSNull())
_details = FetchRequest(sortDescriptors: [], predicate: predicate)
}
}
#ViewBuilder
var body: some View
{
NavigationView
{
List
{
if master != nil
{
ForEach(details)
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton().disabled(master == nil || details.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
This is not as clean as I'd like. I had to move to a view build for the list. I'd like to be able to create an empty fetch request so I don't have to use a view builder.

onAppear is causing problem with the preview but no error is shown

self learning beginner here.
When I remove .onAppear{add()}, the preview works fine. I tried to attach it to other the body view, the Vstack but it causes another error. I read/watched several tutorials but nothing like this is mentioned....
Any help is appreciated
struct ListView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
#FetchRequest(sortDescriptors: []) var positives: FetchedResults<PositiveEntity>
var body: some View {
VStack {
Text("+")
.onAppear{add()}
.onTapGesture (count: 2){
do {
increment(targets.first!) //I also sense that doing "!" is not good. But it's the only way I can keep it from causing error "Cannot convert value of type 'FetchedResults' to expected argument type 'X'"
try moc.save()
} catch {
print("error")
}
}
}
}
func increment(_ item: TargetEntity) {
item.countnum += 1
save()
}
func add() {
let countnum = TargetEntity(context: moc)
countnum.countnum = 0
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}
EDIT 20220509:
As advised by #Yrb (great thanks), the error is likely caused by the lack of a proper set up of preview var in the persistence file. I post the relevant code here for visiblity.
Data Controller file
import CoreData
import Foundation
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "CounterLateApr")
init () {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
preview code in a view
struct ListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
ListView()
}
}
}
[AppName].app file
import SwiftUI
#main
struct CounterLateAprApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}

How to use a picker on CoreData relationships in SwiftUI

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")
}
}
}
}

after delete of item in SwiftUI List (backed by Core Data) getting a "Thread 1: EXC_BAD_INSTRUCTION" error? (code attached)

After completing a delete of a row in a SwiftUI List I am getting a "Thread 1: EXC_BAD_INSTRUCTION" error. It seems the Core Data delete works as after I restart that data has been removed. So maybe something to do with SwiftUI trying to update it's view after the Core Data delete is performed????
Code:
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: GCItem.entity(), sortDescriptors: []) var gcItems: FetchedResults<GCItem>
private func addItem(title:String) {
let newItem = GCItem(context: context)
newItem.id = UUID()
newItem.title = title
do {
try context.save()
} catch let e as NSError {
fatalError("Unresolved error \(e), \(e.userInfo)")
}
}
private func deleteItem(at offsets:IndexSet) {
self.context.perform {
// Delete Item
for index in offsets {
let item = self.gcItems[index]
self.context.delete(item)
}
// Persist
do {
try self.context.save()
} catch let e as NSError {
// TODO: How to undelete list???
print("ERROR : Can not save GCItem items: \(e.description)")
}
}
}
var body: some View {
NavigationView {
VStack {
List() {
ForEach(gcItems) { gcItem in
HStack {
Text("test")
}
}
.onDelete(perform: self.deleteItem)
}
Button(action: { self.addItem(title: "Testing 123") }) {
Text("ADD ITEM")
}
}
}
}
}
#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
Screen Shot
The fix was NOT to use attributes in Core Data named "id". So I changed this to "myId" and then things worked fine.

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.

Resources