iOS 17b6: Simultaneous accesses to ..., but modification requires exclusive access crash using Observation and SwiftUI

Since iOS/iPadOS beta 6, I get a crash just by simply selecting an item in the sidebar of a navigation view. The selected item is part of an app mode, with conforms to the Observation protocol:

import SwiftUI
import Observation

@main
struct MREAApp: App {
    @State private var appModel = AppModel.shared
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appModel)
        }
    }
}

@Observable class AppModel {
    static var shared = AppModel()
    
    var selectedFolder: Folder? = nil
}

struct Folder: Hashable, Identifiable {
    let id = UUID()
    let name: String
}

struct ContentView: View {
    @Environment(AppModel.self) private var appModel
    
    var folders = [Folder(name: "Recents"), Folder(name: "Deleted"), Folder(name: "Custom")]
    
    var body: some View {
        NavigationSplitView {
            SidebarView(appModel: appModel, folders: folders)
        } detail: {
            if let folder = appModel.selectedFolder {
                Text(folder.name)
            }
            else {
                Text("No selection")
            }
        }
    }
}


struct SidebarView: View {
    @Bindable var appModel: AppModel
    
    var folders: [Folder]
    
    var body: some View {
        List(selection: $appModel.selectedFolder) {
            ForEach(folders, id: \.self) { folder in
                NavigationLink(value: folder) {
                    Text(folder.name)
                }
            }
        }
    }
}

To reproduce the bug, just tap on one of the item.

Oddly enough, this works fine in the Simulator.

macOS 14 beta 5 is not affected either.

Apple folks: FB12981860

Just curious:

  1. Why do you need a shared static property on AppModel?
  2. Since Folder conforms to identifiable in SidebarView could you just use ForEach(folders) { folder in?

Note: I don't have an iOS 17 device so couldn't test it on real device.

Could you make the above 2 changes mentioned and test it? Just trying to understand what causes the crash?

Why do you need a shared static property on AppModel?

This is obviously just a demo project to demonstrate the issue. In my real project, that property has to be accessible from other parts of the app

Since Folder conforms to identifiable in SidebarView could you just use ForEach(folders) { folder in?

Doesn't make a difference.

The crash output looks like this:

@ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()

internal nonisolated func access<Member>(
    keyPath: KeyPath<AppModel , Member>
) {
  _$observationRegistrar.access(self, keyPath: keyPath)
}

internal nonisolated func withMutation<Member, MutationResult>(
  keyPath: KeyPath<AppModel , Member>,
  _ mutation: () throws -> MutationResult
) rethrows -> MutationResult {
  try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) --> Thread 1: Simultaneous accesses to 0x281bcc760, but modification requires exclusive access
}

@ObservationIgnored private var _selectedFolder: Folder? = nil

SwiftData seems to be broken in iOS 17b6. My SwiftData app no longer opens, it crashes on launch with a SwiftData-related "Symbol not found" error.

The reason it works in the simulator is the simulator is still iOS 17b5, so the code is compatible.

I was really hoping there would be a new Xcode (or new iOS) beta today to fix these issues.

Given below is my understanding (I could be wrong).

It looks like a concurrency issue.

@Observable does ensure properties are accessed on the main thread, so it is thread safe.

However by accessing a static property containing the AppModel, the AppModel might not longer guarantee thread safety.

Based on the crash it seems like it is caused by simultaneous access (possibly from different threads).

Debug

  • Turn on Swift Strict Concurrency to complete to spot any warnings thrown by the compiler.

Solution

  • If you want to pass around the model use @Environment across views and access them across views
  • If you have different models and need to talk to each other find ways to isolate and protect access using actors.

Also, this seems to be specific to the list selection in the sidebar in NavigationSplitView as I'm using the same app model elsewhere in my project and everything works fine. I did have the same crash in another part of my project where I use a similar UI, which was also using the app model. In that particular case, I could remove the property from the app model, which solved the issue.

I'm having the same issue with NavigationSplitView in Beta 7. I made a post here with a very basic reproducible sample: https://developer.apple.com/forums/thread/736237.

From the iOS & iPadOS 17 Beta 8 Release Notes:

SwiftUI

Known Issues

  • On iOS, using an Observable object’s property as a selection value of a List inside NavigationSplitView may cause a “Simultaneous accesses to …” error when a list selection is made via tap gesture. (113978783)

    Workaround: There is no current workaround for Observable properties. Alternatives include factoring out the selection value into separate state stored outside the object, or using ObservableObject instead.

Your best bet is to wait for the next beta (or RC) and try again with that. I would only use the workaround if you need a fix right now.

Still an issue with Beta 8. Perhaps beta 9 (if there's an update this week), RC or even 17.1 😬

Not fixed in RC unfortunately.

Not fixed in RC unfortunately.

Indeed. The fix for this missed the iOS 17.0 train )-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Was fine on macOS but now borked in the latest RC.

I have faced same issue. Only can repeat on real device. I have also used static property in view.

Please retest this on the just-announced iOS 17.1 beta seed.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

As of Xcode 15.0.1, this is still happening.

Interesting, I have found that if you show a NavigationStack in your first view, push some contents into it, and then you push Switch to another element in your Sidebar, it won't crash, but your pushed views will stay there, covering the other selection's content. You can pop back and it won't crash.

struct SidebarEntryPointView: View {
    @Binding var appNavigation: AppNavigation
    
    var body: some View {
        NavigationSplitView(columnVisibility: $appNavigation.navigationStyleColumnVisibility) {
            List(selection: $appNavigation.rootView) {
                NavigationLink(value: appNavigation.rootView) {
                    Label {
                        Text("Anime")
                    } icon: {
                        Image(systemName: "tv")
                    }
                }
                .tag(RootView.animeList)
                NavigationLink(value: appNavigation.rootView) {
                    Label {
                        Text("Manga")
                    } icon: {
                        Image(systemName: "book")
                    }
                }
                .tag(RootView.mangaList)
            }
        } detail: {
            switch appNavigation.rootView {
            case .animeList:
                NavigationStack(path: $appNavigation.animeListNavigationPath) {
                    Button("Click Me") {
                        appNavigation.animeListNavigationPath.append(NavigationElement.character(characterId: 34))
                        appNavigation.animeListNavigationPath.append(NavigationElement.staff(staffId: 33))
                        print(appNavigation.animeListNavigationPath)
                        //$firstTabNavigation.navigationPath += [.character(characterId: 23)]
                        //$firstTabNavigation.navigationPath.append(.staff(staffId: 23))
                    }
                    .navigationDestination(for: NavigationElement.self) { screen in
                        switch screen {
                        case .character(let characterId):
                            DummyCharacterView(id: characterId)
                        case.staff(let staffId):
                            DummyStaffView(id: staffId)
                        default:
                            Text("wat")
                        }
                    }
                }
            default: Text("No hay")
            }
        }
    }
}

In this example, I have two sidebar elements, Anime and Manga. The app launches in the Anime selected state, and it has a button that pushes two views into the NavigationStack. If you push the button and then Switch the selection to Manga, it works (though you need to pop back to see the "no hay" view).

So this is probably caused by the NavigationStack somehow? Still something I hope to see addressed in Xcode 15.1.

No workarounds? I'm facing this with RealmSwift on the main thread in iOS 17. I don't understand how two calls on main thread are able to be "simultaneous"

iOS 17b6: Simultaneous accesses to ..., but modification requires exclusive access crash using Observation and SwiftUI
 
 
Q