SwiftUIのListの中に入れたProgressViewが表示されなくなる問題

SwiftUIのListのRowにProgressViewを入れたときに1回目は表示されるのに2回目以降は表示されない不思議な現象にハマったので紹介します。

発生した現象

Listの一番下まで到達したら続きを読み込む処理のために以下のようにProgressView(見やすくするために赤い枠線を付けています)を表示させようとしました。

動画を見ていただくとわかるのですが、スクロールして2回目にProgressViewが表示されるときには枠線は表示されていますがIndicatorが表示されていません。

www.youtube.com

このときのコードは以下になります。

struct ContentView: View {
    @State var items: [ItemType] = (0..<20).map { .item($0) } + [.loading]
    var body: some View {
        List(items) { item in
            switch item {
            case let .item(id):
                Text("ID: \(id)")
            case .loading:
                ProgressView()
                    .frame(maxWidth: .infinity)
                    .border(.red) // 見やすくするため
                    .onAppear {
                        Task {
                            try await Task.sleep(nanoseconds: 10000000000) // load してるっぽくする
                            let newElements:[ItemType] = (items.count-1..<items.count+20).map { .item($0) }
                            items.insert(contentsOf: newElements, at: items.count - 1)
                        }
                    }
            }
        }
    }
}

enum ItemType: Identifiable {
    case item(Int)
    case loading
    var id: String {
        switch self {
        case let .item(id: id):
            return "\(id)"
        case .loading:
            return "loading"
        }
    }
}

原因の推測と対策

原因ははっきりとしたことはわからないのですが、ProgressViewの中身がUIActivityIndicatorViewでデフォルトでhidesWhenStopped = trueになっていてListの機能で再利用されるときに非表示→表示となったときにIndicatorが非表示になったままになったのだとおもいます。

対策としてはProgressViewが再利用されなくなれば良いのでIdentifiableの実装でUUIDのような毎回変化するIDを返してあげればこの問題を回避できます。

enum ItemType: Identifiable {
    case item(Int)
    case loading
    var id: String {
        switch self {
        case let .item(id: id):
            return "\(id)"
        case .loading:
            return "loading_\(UUID().uuidString)" // Viewが再利用されなくなるので表示される
        }
    }
}

動作の様子はこんな感じです。

www.youtube.com

この対策が正しいのかはわかりませんが、ひとまず動きます。