NavigationSplitViewの"Simultaneous accesses to..."クラッシュの回避方法
iOS 16から登場したNavigationSplitViewとSwift 5.9から登場したmacroの@Observable
の組み合わせでクラッシュが発生することがあります。
この記事ではその回避方法を説明します。
動作環境
- Xcode 15
- iOS 17
クラッシュが起こる原因
List
にはSelectionValue
と呼ばれる選択された保持するパラメーターが定義されています。
このSelectionValue
はHashable
を準拠したインスタンスを指定します。
@MainActor
struct List<SelectionValue, Content> where SelectionValue : Hashable, Content : View
そして、NavigationSplitView
とList
のSelectionValue
、そしてNavigationLink
を組み合わせるとエラーが発生する場合があります。
再現コードとして、文字列を表示複数リスト表示するViewを作りましょう。
let items = ["Item1", "Item2", "Item3", "Item4"]
@Observable
final class FolderViewModel {
var selectedItem: String?
}
items
は単純なStringの配列です。
そしてFolderViewModel
はObservable
のオブジェクトでどのセルが選択されたかを表すselectedItem
プロパティを持っています。
これらを使って、NavigationSplitView
でViewを作成します。
// クラッシュパターン
struct SplitView: View {
@State var viewModel = FolderViewModel()
var body: some View {
NavigationSplitView {
List(items, id: \.self, selection: $viewModel.selectedItem) { item in
NavigationLink(value: item) {
Text(item)
}
}
.navigationTitle("Sidebar")
} detail: {
if let selectedItem = viewModel.selectedItem {
Text(selectedItem)
.navigationTitle(selectedItem)
} else {
Text("Choose an item from the content")
}
}
}
}
NavigationSplitView
の子ViewにList
があり、データとしてitems
を渡しています。
selection
のパラメーターに$viewModel.selectedItem
を渡すことで選択アイテムをFolderViewModel
でハンドリングしています。
NavigationLink
のvalue
イニシャライザーを使ってセルがタップしたら、List
のselection
が更新されるようにします。
NavigationLink(value: item) {
Text(item)
}
NavigationLink
のvalue
イニシャライザーは、NavigationSplitView
内のList
の子Viewに配置される場合、タップされるとvalueに渡した値をList
のselection
の値へ更新します。
Viewの見た目はこのようになります。
このコードを実行し、セルをタップすると次のようなエラーを出力し、アプリがクラッシュしてしまいます。
Simultaneous accesses to 0x6000006e6410, but modification requires exclusive access.
Previous access (a modification) started at SwiftUI`OUTLINED_FUNCTION_11 + 3132 (0x105867044).
エラー発生箇所はmacroで生成されたwithMutation
メソッドで発生していました。
iOS & iPadOS 17 Release Notes
Appleもこのバグは認識しているようで、iOS 17 & iPadOS 17のリリースノートにも記載がありました。
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) (FB12981860)
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.
筆者訳:iOSではNavigationSplitViewの中でListのselection値としてObservableのプロパティを使い、タップジェスチャーでリストが選択されると「Simultaneous accesses to …」のエラーが発生する場合があります。
修正方法。Observableのプロパティについては修正方法が現状ありません。代替方法として、selection値をObservableオブジェクトの外部で保存される別の状態として扱うか、代わりに ObservableObject を使用します。
このエラーが発生した場合、Observable
の利用はあきらめるしか現状なさそうです。
Observable
ではない別な方法で状態を管理するか、ObservableObject
のプロトコルでオブジェクトを管理するのが良いそうです。
AppStorageで回避する
クラッシュを回避するには、Observable
以外で状態を管理する必要があります。
回避方法の一つとしてAppStorage
を利用する方法を考えました。
List
のselect値を@Observable
で管理することをやめて、AppStorage
を利用します。
struct SplitViewWorkaround: View {
// FolderViewModelをAppStorageに変更
@AppStorage public var selectedItem: String?
var body: some View {
NavigationSplitView {
List(items, id: \.self, selection: $selectedItem) { item in
NavigationLink(value: item) {
Text(item)
}
}
.navigationTitle("Sidebar")
} detail: {
if let selectedItem {
Text(selectedItem)
.navigationTitle(selectedItem)
} else {
Text("Choose an item from the content")
}
}
}
}
#Preview {
SplitViewWorkaround(selectedItem: AppStorage("selectedItem"))
}
先ほどクラッシュしたSplitView
との差分は@Observable
を使ったFolderViewModel
をやめてAppStorage
に差し替えただけです。
@State private var viewModel = FolderViewModel()
これでエラーが発生しなくなりました。
エラーが発生しないパターン
ただし、手元で確認したところ、NavigationLink
を使わない場合はクラッシュが発生しませんでした。
下記のコードはView階層がNavigationSplitView
> List
となっていてNavigationLink
がありません。
// OKパターン
struct SplitViewOK: View {
@State private var viewModel = FolderViewModel()
var body: some View {
NavigationSplitView {
List(items, id: \.self, selection: $viewModel.selectedItem) { item in
Text(item)
}
.navigationTitle("Sidebar")
} detail: {
if let selectedItem = viewModel.selectedItem {
Text(selectedItem)
.navigationTitle("selection")
} else {
Text("hello")
.navigationTitle("detail")
}
}
}
}
このコードでもList
のselection
値は更新され、詳細画面が表示されました。
NavigationLink
の内部にバグ原因があるのかもしれません。
まとめ
今回はiOS 17における、@Observable
とNavigationSplitView
の組み合わせによるクラッシュとその回避方法を解説しました。
List
のselection値が@Observable
のオブジェクトでクラッシュするのは意外でした。
アップデートで早く解決することを願います。
参考
- NavigationSplitView
- NavigationLink: init(_:value:)
- List
- https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-17-release-notes#SwiftUI
宣伝
BOOTHより、同人版「Swift Concurrency入門」発売中です。
Swift Concurrencyを網羅的に学べ、さらに既存アプリへの適応方法も解説しています。
日本語で体系的に学べる解説本は他になかなかありません。
1章、2章が立ち読みできるおためし版もありますので、ぜひチェックしてください!