Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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:

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)")
      }
    }
  }
}
like image 381
nine stones Avatar asked Mar 25 '20 04:03

nine stones


1 Answers

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.

enter image description here

like image 186
mmklug Avatar answered Dec 31 '22 20:12

mmklug