SwiftData Fatal error: failed to find a currently active container 📦

SwiftData Fatal error: failed to find a currently active container

まったく動かない。画面が真っ白。再起動でも同じ。

エラーメッセージ。

SwiftData/ModelContainer.swift:159: Fatal error: failed to find a currently active container for Task
Failed to find any currently loaded container for Task)

コンテナを渡す前に初期化すると問題が解決されるようです。


@main
struct MyApp: App {
  let modelContainer: ModelContainer
    
  init() {
    do {
      modelContainer = try ModelContainer(for: Item.self)
    } catch {
      fatalError("Could not initialize ModelContainer")
    }
  }
    
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .modelContainer(modelContainer)
  }
}

👉 SwiftData Fatal error: failed to find a currently active container | Apple Developer Forums hatena-bookmark

というかんじで、モデルコンテナの初期化位置を最上位にすると直りました。

モデルデータの変更後の整合性の問題でしょうか。

突然動かなくなるのでびっくりします。

 

📦 どこでモデルコンテナを生成するべきか

利用する View につければ、


struct ContentView: View {
  @State private var container = ModelContainer(...)

  var body: some Scene {
    RecipesList()
      .modelContainer(container)
  }
}

それ以下では @Environmentを使って問題なく利用できると思っていましたが。


struct RecipesList: View {
    @Environment(\.modelContext) private var modelContext

The environment’s modelContext property will be assigned a new context associated with this container. All implicit model context operations in this view, such as Query properties, will use the environment’s context.

Environment ののmodelContext プロパティには、このコンテナに関連付けられた新しいコンテキストが割り当てられます。Query プロパティなど、このビューのすべての暗黙のモデルコンテキスト操作は、Environment のコンテキストを使用します。

👉 modelContainer(_:) | Apple Developer Documentation hatena-bookmark

Modifier や Preview などあちこちで 生成していると、利用できる範囲が分かりづらくなるのは確か。



【SwiftUI / SwiftData】Using ViewModifier for setting ModelContainer 🔌

ViewModifiers can be used in nested parent-child Views for each respective #Preview, making it convenient. This also enhances clarity even further.

 

🔌 Using as an extension of View

Create a custom ViewModifier.


struct DogDataContainerViewModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .modelContainer(try! ModelContainer(for: Dog.self))
  }
}

All ViewModifiers will be turned into extensions.


extension View {
  func dogDataContainer() -> some View {
    modifier(DogDataContainerViewModifier())
  }
}

It is used in the implementation of the parent as well as in the #Preview of the child.


struct DogView: View {
  // ...
}

#Preview {
  DogView()
    .dogDataContainer()
}

 

🔌 Initializing or creating data

Additionally, as there are often data initialization or creation tasks, I'll add those.

This will be in the part with View.onAppear().

We'll use ModelContext to manipulate the data.


struct GenerateDataViewModifier: ViewModifier {
  @Environment(\.modelContext) private var modelContext
    
  func body(content: Content) -> some View {
    content.onAppear {
      DataGeneration.generateAllData(modelContext: modelContext)
    }
  }
}

This will also be made into an extension.


extension View {
  func generateData() -> some View {
    modifier(GenerateDataViewModifier())
  }
}

Let's add this to the initial code.


struct DogDataContainerViewModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .generateData()
      .modelContainer(try! ModelContainer(for: Dog.self))
  }
}

 

🔌 Conclusion

I'll summarize it.


struct DogDataContainerViewModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .generateData()
      .modelContainer(try! ModelContainer(for: Dog.self))
  }
}

struct GenerateDataViewModifier: ViewModifier {
  @Environment(\.modelContext) private var modelContext
    
  func body(content: Content) -> some View {
    content.onAppear {
      DataGeneration.generateAllData(modelContext: modelContext)
    }
  }
}

extension View {
  func dogDataContainer() -> some View {
    modifier(DogDataContainerViewModifier())
  }
}

fileprivate extension View {
  func generateData() -> some View {
    modifier(GenerateDataViewModifier())
  }
}

When using, only basic public extensions are used.


struct DogView: View {
  // ...
}

#Preview {
  DogView()
    .dogDataContainer()
}

It can also be used on the implementation side.

For reference, below is Apple's official sample code.

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


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

SwiftData は便利だけども UI スレッドが重くならないのかな。

という考えからの Todo アプリを作成しながらのスレッドまわりの実装修行。

 

🔄 実装イメージ

API にリクエストを投げると画面が自動的に更新される、という形。

時間のかかる処理は UI スレッドに負荷がかからないように他のスレッドで処理する。

SwiftUI のボタン押してのダウンロードリクエストから、WEB API からデータ取得して、SwiftData ストレージ に保存。

SwiftUI 上では SwiftData マクロ @Query で observe しておいて変化検出即時画面更新。

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

そのまま、SwiftData ストレージはローカルキャッシュとなる。

 

🔄 SwiftData はバックグラウンドで使えるのか

WWDC2023 での会話らしいです。

@Duncan: As I understand it, SwiftData model objects are not thread-safe, just like NSManagedObjects. Are there any additional mechanisms to make managing this easier for us than it was in traditional Core Data? e.g., compiler warnings before passing an object out of its context?

@Dave N (Apple): We have provided the ModelActor protocol & the DefaultModelExecutor to make SwiftData work with Swift Concurrency. Use your ModelContainer to initialize a ModelContext in the initializer for your ModelActor conforming actor object and use that context to initialize a DefaultModelExecutor. This will allow you to use that context with async functions on your actor.

@Ben T (Apple): Swift will enforce sendability requirements

@Duncan: SwiftData のモデルオブジェクトは NSManagedObject と同様にスレッドセーフではないと理解しています。従来の Core Data と比べて、オブジェクトを扱う際に管理しやすくするための追加のメカニズムはありますか?例えば、オブジェクトをそのコンテキストから出す前にコンパイラの警告があるでしょうか?

@Dave N(Apple): 私たちは ModelActor プロトコルと DefaultModelExecutor を提供しており、Swift Concurrency と連携するための SwiftData を使用できます。ModelActor に準拠するアクターオブジェクトのイニシャライザで ModelContainer を使用して ModelContext を初期化し、そのコンテキストを使用して DefaultModelExecutor を初期化します。これにより、そのコンテキストをアクターの非同期関数で使用できるようになります。

@Ben T(Apple): Swift は送信可能性の要件を強制します。

👉 Using SwiftData in background? | Apple Developer Forums hatena-bookmark

1年前ぐらいの会話なので、今では使えそうです。

  • ModelContext
  • ModelActor
  • DefaultModelExecutor

が検索のキーワードでしょうか。

 

🔄 @ModelActor

このマクロを expand すると、前述の会話を実装していることがわかります。

class でなく actor に付けるようです。


@ModelActor
actor SampleService {
  nonisolated let modelExecutor: any SwiftData.ModelExecutor
  nonisolated let modelContainer: SwiftData.ModelContainer

  init(modelContainer: SwiftData.ModelContainer) {
    let modelContext = ModelContext(modelContainer)
    self.modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
    self.modelContainer = modelContainer
  }
}

extension SampleService: SwiftData.ModelActor {
}

DefaultSerialModelExecutor() になっているのは、改良して直感的に順番通りに流れるようになったのかな。

ランダムにアドバイスをくれるAPIを利用して実装します。


{
  "slip": { 
    "id": 136, 
    "advice": "Everything matters, but nothing matters that much."
  }
}

👉 Advice Slip JSON API hatena-bookmark

modelContext が非同期関数内で利用できるようになりました。


@ModelActor
actor TodoService {
  private(set) static var shared: TodoService!
  
  static func create(modelContainer: ModelContainer) {
    shared = TodoService(modelContainer: modelContainer)
  }
  
  func download() async {
    do {
      for i in 0 ..< 10 {
        let random = await RandomAdvice.get()
        modelContext.insert(Todo(text: "\(i). \(random.slip.advice)"))
        try modelContext.save()
      }
    } catch {
      if Task.isCancelled {
        print("Task was cancelled.")
      }
    }
  }
}

Task は別ボタンでキャンセルできるように掴んでおく。


Button {
  task = Task {
    await TodoService.shared.download()
  }
} label: {
  Image(systemName: "icloud.and.arrow.down")
}
.onAppear {
  TodoService.create(modelContainer: modelContext.container)
}

 

🔄 まとめ

マクロって GitHub でコードを眺めてるとコードの意味が全くみえない。

しかし、SwiftData のみでここまでできることに驚きました。



恐るべし。

しかし、謎なことが、

👉 【Swift】この ModelActor ってなぜ生きてるの? hatena-bookmark

 

🔄 参考

👉 Context outside of SwiftUI Views | Apple Developer Forums hatena-bookmark
👉 Sendable and @Sendable closures explained with code examples hatena-bookmark
👉 SwiftData background inserts using… | Apple Developer Forums hatena-bookmark
👉 ModelActor Implementation Changes … | Apple Developer Forums hatena-bookmark