【SwiftUI】ScrollView + LazyVStack vs Large amounts of data

どれくらいの数のアイテムがストレスなく処理できるか。

ユーザー側の操作に問題がでるようであれば、

ページングなどそれなりの処理が必要です。

まず、シンプルに試す。

以下、1ファイルコピペして Canvas で試せます。

あれ、Gist 埋め込みのスタイル変わったか。まあいいか。



雰囲気、問題ないのは、1000個ぐらいですかね、少なめで。

なんせ、@Queryfetch() が1万個くらいになると重い。

これを元に改修していきます。

データ数は10万件にしてやってみます。

 

🤔 大量データのインポート

バックグラウンドで @ModelActor を使います。


@ModelActor
actor ItemService {
  func generate(size: Int) async {
    for i in 0 ..< size {
      modelContext.insert(
        Item(i: i, s: String(format: "%04d", i))
      )

      if ((i + 1) % 1_000 == 0) {
        try? modelContext.save()
        try? await Task.sleep(for: .milliseconds(1))
      }
    }
    try? modelContext.save()
  }
}

View の init().onAppear() で。


ItemListView()
  .onAppear {
    Task { 
      await ItemService(modelContainer: modelContext.container)
        .generate(size: 100_000)
    }

20秒くらいかかりました。

環境に依りますが、5000件/秒ぐらいです。


👉 SwiftData でのバックグラウンドでの大量データインサート #SwiftUI - Qiita hatena-bookmark

 

🤔 最終要素の表示を検出する

1000 個ずつ読み込みます。

1000個目を表示したら次の1000個を読み込みます。

この記述がシンプルで良さげです。


ScrollView {
  LazyVStack {
    ForEach(items) { item in
      Text("\(item.i) | \(item.s)")
        .onAppear {
          if item == items.last { // *
            print("load next!")
          }
        }
    }
  }
}


LazyVStack って単純に追加を繰り返すだけでもストレスなく動くんですね !

ちょっと、引っかかる感じもあるけどタイミング次第か。

 

🤔 データの分割読み込みと追加

データが1000個以上が遅い重いの原因は「読み込み」です。

シンプルなデータクラスでも遅い。

@Query の引数がダイナミックでない」ようなので、

@Observable クラスで対応します。

最終アイテムが表示されたときに、1000個ずつ追加します。

検索文字列のセットには Predicate を、

抽出のオフセットや限度数、並び順は FetchDescriptor を使います。


 

🤔 @Query で Observe しながら fetchCount() を使う

話しがそれますが、こんな方法も試す。



private static var fetchDescriptor: FetchDescriptor<Item> {
  var fd = FetchDescriptor<Item>()
  fd.fetchLimit = 1
  return fd
}

@Query(fetchDescriptor)  private var zo: [Item]

private var count: Int {
  zo.isEmpty ? 0 : try! modelContext.fetchCount(FetchDescriptor<Item>())
}

 

🤔 スレッドの切り替えとスレッド間の受け渡し

いまいち馴染めず使いこなせないのが @MainActor

Strict Concurrency Checking を Complete と最も厳しくしておいて進みます。


てか、自在に Swift バージョンを乗り越えての スレッド自在に使えるやつがどれくらいいるのか ?

そもそも、言語側は、開発側が意図的にスレッドの切り替えをさせない方向で進んるように見えるのだが。



【Swift】最もシンプルに実行時間を計測する

これくらいでいいよなあ。


let start = Date()
try? await Task.sleep(for: .milliseconds(1234))
let end = -start.timeIntervalSinceNow
print(String(format: "%.3f", end))
// 1.246



【SwiftData】@Query の検索条件や並び順を変更する

@Query は Observable で便利です。

引数に、検索条件や並び順を書いておけば、

監視して変更があれば、更新してくれます。


👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、この検索条件や並び順の基準は

ユーザーの意図によって変更の必要があることのほうが多いです。

どうやって、書き換えるか。


struct UserListView: View {
  @Query var users: [User]
  
  init(sort: SortDescriptor<User>) {
    _users = Query(sort: [sort])

👉 How to dynamically change a query's sort order or predicate - a free SwiftData by Example tutorial hatena-bookmark


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String
  @Query private var birds: [Bird]
  private var content: (Bird) -> Content
    
  init(searchText: Binding<String>, @ViewBuilder content: @escaping (Bird) -> Content) {
    _searchText = searchText
    _birds = Query(sort: \.creationDate)
    self.content = content
  }

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchResults.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark

どうやら、今現在としては、


_birds = Query(sort: \.creationDate)

というような

「_ (アンダースコア)」

を使った内部的な Query の書き換えが本筋といったところでしょうか。

Query の引数には、

SortDescriptorFetchDescriptor もあるので、

複雑な条件により抽出も並び替えも可能です。

👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、他の方法ないんですかね。