🐞

iOS16.2: .fullscreencover()もしくは.sheetのdismiss()のアニメーションが処理落ちする

2023/01/07に公開

現象

  • 以下のようなある要素のコレクションビューに、モーダルビューから新しい要素を追加する、よくある構造において、モーダルビューをdismiss()した時にdismissのアニメーションが処理落ちしてしまう。
  • Groupでまとめているところは要素の配列が空の時に表示させるビューにも.navigationTitle()や.toolbar()を適用させるため

※実際はもっと複雑なことをしています。

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var store: Store
    @State var flag: Bool = false
    var body: some View {
        NavigationStack {
            Group {
                if store.arr.count == 0 {
                    Text("empty")
                } else {
                    ScrollView {
                        LazyVStack {
                            ForEach(store.arr, id: \.self) { item in
                                Text(item)
                            }
                        }
                    }
                }
            }
            .navigationTitle("Title")
            .toolbar {
                ToolbarItem {
                    Button(action: {
                        self.flag.toggle()
                    }) {
                        Image(systemName: "plus")
                    }
                }
            }
            .fullScreenCover(isPresented: $flag) {
                AddingItemView()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(.gray)
            }
        }
    }
}

struct AddingItemView: View {
    @EnvironmentObject var store: Store
    @Environment(\.dismiss) var dismiss
    @State var text: String = ""
    var body: some View {
        VStack {
            TextField("text", text: $text).padding(.horizontal, 16)
            Button(action: { createItem() }) { Text("add Item").foregroundColor(.black) }

        }
    }
    
    func createItem() {
        store.arr.append(text)
        dismiss()
    }
}

発生状況

  • iOS16.2
  • Xcode14.1以上

試したこと

  • createItem()のなかのUIの更新に関わる箇所を明示的にメインスレッドで走らせるように変更
    • createItem()の不要なメソッドを適切な箇所(ライフサイクルメソッド内等)に移してやる
  • データフローのリファクタリング
  • Viewのリファクタリング
  • その他
    • カメラを使用するアプリケーションなのでカメラのsessionを使用していないときには止めてやる

これらを重点的にやりメモリ使用量はいくらかは下がったのですがそれでも処理落ちは発生していました。ちなみに元々そこまでCPUやメモリを使用していませんでした。

解決方法

結論、Groupの中でstore.arr.countで条件分岐をしていたことが悪さをしていました。

Groupの公式ドキュメントを見てもGroup内で条件分岐をしたコードがあるため条件分岐自体は悪くないのですが、@Publishedな値を使って条件分岐させるとこの現象が起きるのでしょうか。

条件式をstore.arr.isEmptyやarr.contains()にしても同じ現状が置きました。

ちなみに今回はcountが0以上になればScrollViewに切り替わるため、一個要素が追加されれば同じ現象は起きません。

なので単純に条件分岐をやめて他の方法で実装してやれば解決しました。

追記: Groupだと小のViewにも複数.fullScreenCover()のViewが生成されるためということが詳細に判明しました。GroupをそのままZStackに変更してやると解決します。

所感

このバグ?現象を特定するのに2,3日かかりました。各メソッドの処理時間を図ったり、Instrumentsを使って特定してみたりしても解決しなかった時はもう駄目だと思いました。

同じ現象にもし遭遇した方に参考になるように願っています。

(これSwiftUIのバグ?もしそうだったら直ってくれ〜)

Discussion