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】NavigationSplitView と TabView の切り替え判定 🔀

マルチプラットフォームアプリで、

Mac/iPad と iPhone/AppleTV でグループ分けしてそれなりに切り替える。


if prefersTabNavigation { // *
//if UIDevice.current.userInterfaceIdiom == .phone { // NG on macOS
//if UserInterface.prefersTabNavigation {
  TabView(selection: $screen) {
    ForEach(Screen.allCases) { screen in
      screen.destination
        .tag(screen as Screen?)
        .tabItem { screen.label }
    }
  }
} else {
  NavigationSplitView { 
    SidebarList(screen: $screen)
  } detail: {
    DetailColumn(screen: screen)
  }
}

判定の条件 (Bool) をどう書くか。

 

🔀 Apple 公式サンプル

WWDC23 でアナウンスの新機能ですか。

👉 Unleash the UIKit trait system - WWDC23 - Videos - Apple Developer hatena-bookmark

便利な SwiftUI Environment に UIKit UITrait をブリッジするという UITraitBridgedEnvironmentKey を使った方法。


// Create custom Environment
struct PrefersTabNavigationEnvironmentKey: EnvironmentKey {
  static var defaultValue: Bool = false
}

extension EnvironmentValues {
  var prefersTabNavigation: Bool {
    get { self[PrefersTabNavigationEnvironmentKey.self] }
    set { self[PrefersTabNavigationEnvironmentKey.self] = newValue }
  }
}

// Bridge UITrait read only
#if os(iOS)
extension PrefersTabNavigationEnvironmentKey: UITraitBridgedEnvironmentKey {
  static func read(from traitCollection: UITraitCollection) -> Bool {
    return traitCollection.userInterfaceIdiom == .phone || traitCollection.userInterfaceIdiom == .tv
  }

  static func write(to mutableTraits: inout UIMutableTraits, value: Bool) {
    // Do not write
  }
}
#endif

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


@Environment(\.prefersTabNavigation) private var prefersTabNavigation

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

高級な感じで相互読み書き可能な状態にできるのはいいけども。

 

🔀 その他

なんとなく試しながら書いてみました。

Constants な感じで。書き方はいくつかあるようです。

enum を使う。意見はいろいろありそう。


enum UserInterface { // as namespace
  static var prefersTabNavigation: Bool {
    #if os(iOS) //canImport(UIKit)
    let idiom = UIDevice.current.userInterfaceIdiom
    return idiom == .phone || idiom == .tv
    #else
    return false
    #endif
  }
}

フツーに struct で書く。


struct UserInterface {
  static var prefersTabNavigation: Bool {
    #if os(iOS) //canImport(UIKit)
    let idiom = UIDevice.current.userInterfaceIdiom
    return idiom == .phone || idiom == .tv
    #else
    return false
    #endif
  }
}

👉 `static let` in enum vs struct? - Using Swift - Swift Forums hatena-bookmark

呼び出し側。


if UserInterface.prefersTabNavigation {

 

🔀 まとめ


#if os(iOS)

ての、なんとなく

なるべくは使いたくないです。

Apple 公式サンプルは、新機能を強引に Read Only でサンプルコードに利用した感じに見えるけどもー。




【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


【Swift】この ModelActor ってなぜ生きてるの?

公式サンプルコードを見ながら作りました。



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

🔄 謎

サンプルコードに従った流れで実装しました。

ModelActor の内部 に static な自己インスタンスを持って、


@ModelActor
actor BirdBrain {
    private(set) static var shared: BirdBrain!
    
    static func createSharedInstance(modelContext: ModelContext) {
        shared = BirdBrain(modelContainer: modelContext.container)
    }
    
    func process(transaction verificationResult: VerificationResult<Transaction>) async {
    }
    
    func status(for statuses: [Product.SubscriptionInfo.Status], ids: PassIdentifiers) -> PassStatus {
    }
    
    func checkForUnfinishedTransactions() async {
    }
    
    func observeTransactionUpdates() {
    }

View の onAppear() でインスタンス生成。ModelActor 内の関数は Task 内で呼ぶ。


struct BackyardBirdsShopViewModifier: ViewModifier {
    @Environment(\.modelContext) private var modelContext
    
    func body(content: Content) -> some View {
        ZStack {
            content
        }
        .subscriptionPassStatusTask()
        .onAppear {
            BirdBrain.createSharedInstance(modelContext: modelContext)
        }
        .task {
            await BirdBrain.shared.observeTransactionUpdates()
            await BirdBrain.shared.checkForUnfinishedTransactions()
        }

👉 sample-backyard-birds/Multiplatform/Shop/BirdBrain.swift at 832515e4abb9224c1970e40a3bd9b82900019187 · apple/sample-backyard-birds hatena-bookmark
👉 sample-backyard-birds/Multiplatform/Shop/BackyardBirdsShopViewModifier.swift at main · apple/sample-backyard-birds hatena-bookmark

というかんじで、

  • ModelActor の内部 に static な自己インスタンスを持つ。
  • View の onAppear() でインスタンス生成。
  • ModelActor 内の関数は Task 内で呼ぶ。

このきまりで実装すると動くっちゃあ動くけど。

しかし、この ModelActor ってなぜ生きてるの?

いつ死ぬの?

死んでるように見えるけど、なぜ関数が呼べるのですか。

View と一緒に死ぬ?

(→ つづく)


【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