A very simple CoreData app: All code provided below.
A NavigationView containing
Each linked ListView (Message/Post) has
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With