【SwiftData】バックグラウンドで @ModelActor を singleton で使う

UI スレッドをブロックしてるんですよね。

10万レコードのような大きいデータを fetch() すると。

バックグラウンドで大量インサートなどしながら、

UI側でリストなどをスクロールするともっと顕著に分かります。

どうやら、

「インスタンス生成をどこで行うか」

が大事なようです。

singleton の ModelActor 側。


@ModelActor
actor DataSource {
  nonisolated(unsafe) private(set) static var shared: DataSource!

//  static func createInstance(modelContainer: ModelContainer) {
//    print("createInstance()", Thread.current) // main
//    shared = DataSource(modelContainer: modelContainer)
//  }

  static func createInstance(modelContainer: ModelContainer) async {
    print("createInstance()", Thread.current) // not main
    shared = DataSource(modelContainer: modelContainer)
  }

View 側。インスタンス生成はバックグラウンドで。


//    .onAppear { // main
//      DataSource.createInstance(modelContainer: modelContext.container)
//    }
    .task { // not main
      await DataSource.createInstance(modelContainer: modelContext.container)
    }

どうなんすかね。



SwiftData も苦しんでる感じに見えます。

👉 SwiftData does not work on a background Task even inside a custom ModelActor. | Apple Developer Forums hatena-bookmark


【Swift】Apple 公式サンプル にみる @ModelActor を singleton にして ViewModifier にすると便利

 

🤔 @ModelActor の使い方

👉 【SwiftUI】SwiftData でスレッドセーフにバックグラウンドでデータを扱う 🔄 - @ModelActor hatena-bookmark

少しおさらい的にまとめておきたい。

ModelContext は Non-Sendable。ModelContainer は Sendable。

nonisolated func はインスタンスから値のコピーをスナップショット的に切り出す。

 

🤔 方針


- Singleton で使う。

- ViewModifier として利用する View の onAppear でインスタンス生成。

 

🤔 コード

方針に合わせて、

ざっくりイメージとして下書き。



テンプレートとして随時更新予定。

 

🤔 参考 (Apple 公式サンプルコード)

👉 sample-backyard-birds/Multiplatform/Shop/BirdBrain.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark
👉 sample-backyard-birds/BackyardBirdsData/General/BackyardBirdsDataContainer.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark


【Swift】URLSession で Passing argument of non-sendable type '(any URLSessionTaskDelegate)?' outside of main actor-isolated context may introduce data races

今日現在、以下の設定でやっています。


 

🧑🏻‍💻 お天気情報を取得する

無料で公開されている WEB API を使います。

シンプルに取得する。


import SwiftUI

struct TestWeatherView: View {
  private let url = URL(string: "https://wttr.in/?format=3")!

  var body: some View {
    Text(try! String(contentsOf: url))
  }
}

#Preview {
  TestWeatherView()
}

ちょっと窮屈なので砕いていきます。


struct TestWeatherView: View {
  @State private var text = ""
  private let url = URL(string: "https://wttr.in/?format=3")!

  var body: some View {
    Text(text)
      .onAppear {
        text = try! String(contentsOf: url)
      }
  }
}

OK です。

次は、Web クライアントを汎用性のある URLSession に変えます。


struct TestWeatherView: View {
  @State private var text = ""
  private let url = URL(string: "https://wttr.in/?format=3")!

  var body: some View {
    Text(text)
      .task {
        let (data, _) = try! await URLSession.shared.data(from: url)
        text = String(data: data, encoding: .utf8)!
      }
  }
}

ここで、警告がでます。

Passing argument of non-sendable type '(any URLSessionTaskDelegate)?' outside of main actor-isolated context may introduce data races

これは、なんですか。

 

🤔 Passing argument of non-sendable type '(any URLSessionTaskDelegate)?' outside of main actor-isolated context may introduce data races

URLSession のドキュメントを見ておきます。


func data(from url: URL) async throws -> (Data, URLResponse)

👉 data(from:) | Apple Developer Documentation hatena-bookmark

Task 内の以下の部分


let (data, _) = try! await URLSession.shared.data(from: url)

左辺はメインスレッド、

--- 境界 ---

右辺はバックグラウンドスレッド

👉 【Swift】concurrency をマスターするための一つのきっかけ hatena-bookmark

ということで、

いわゆる「アクター境界」を越えているので、

data() の返り値は Sendable でなければなりません。

リファレンスやコードを追いかけてみると、

Data, URLResponse 共に Sendable に準拠しています。

👉 Data | Apple Developer Documentation hatena-bookmark
👉 URLResponse | Apple Developer Documentation hatena-bookmark

あ、Tuple かな ?

などと思いましたがなんか違う。

👉 Pitch: User-defined tuple conformances - Evolution / Pitches - Swift Forums hatena-bookmark

 

🧑🏻‍💻 Actor を使う

ActorSendable に準拠しています。


なので、これでバックグラウンドの通信部分をラップします。


actor WeatherA {

  // OK
  static func getData(url: URL) async -> (Data, URLResponse) {
    try! await URLSession.shared.data(from: url)
  }

  // OK
  nonisolated func getData(url: URL) async -> (Data, URLResponse) {
    try! await URLSession.shared.data(from: url)
  }
}

これで、無警告でOKとなりました。

 

🧑🏻‍💻 extension 化してメインスレッドを避ける

こんな方法でもいけます。


extension URLSession {
  func dataEx(url: URL) async -> (Data, URLResponse) {
    try! await data(from: url)
  }
}

 

🧑🏻‍💻 まとめ

分かれば、なるほど感あるけど、分からなければ全く謎で時間だけ食うので、

そんな誰かと自分用のメモとして。

class でも警告なしでいけるようです。



Actor はデータの非同期操作のために作られてる」感じがする。

外野から使ってみた雰囲気だけだけれども。

 

🧑🏻‍💻 参考