【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


Hilt で KSP の依存関係の設定 (build.gradle.kts)

kapt から KSP に移行しようとしてハマる。

 

🚀 Dagger + KSP

今回は使わなったが動く。


plugins {
  id("org.jetbrains.kotlin.android") version "1.9.0"
  id("com.google.devtools.ksp") version "1.9.0-1.0.12"
}

dependencies {
  ksp("com.google.dagger:dagger-compiler:2.48") // Dagger compiler
  ksp("com.google.dagger:hilt-compiler:2.48")   // Hilt compiler
}

👉 Dagger KSP hatena-bookmark

 

🚀 Hilt + kapt


// build.gradle.kts (Project)

plugins {
  id("com.google.dagger.hilt.android") version "2.44" apply false
}


// build.gradle.kts (Module)

plugins {
  kotlin("kapt")
  id("com.google.dagger.hilt.android")
}

dependencies {
  implementation("com.google.dagger:hilt-android:2.44")
  kapt("com.google.dagger:hilt-android-compiler:2.44")
}

kapt {
  correctErrorTypes = true
}

👉 Hilt を使用した依存関係の注入  |  Android デベロッパー  |  Android Developers hatena-bookmark

 

🚀 Hilt + KSP


// build.gradle.kts (Project)

plugins {
  id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false
  id("com.google.dagger.hilt.android") version "2.44" apply false
}


// build.gradle.kts (Module)

plugins {
  id("com.google.devtools.ksp")
  id("com.google.dagger.hilt.android")
}

dependencies {
  implementation("com.google.dagger:hilt-android:2.44")
  ksp("com.google.dagger:hilt-android-compiler:2.44")
}

👉 kapt から KSP に移行する  |  Android デベロッパー  |  Android Developers hatena-bookmark

👉 Revisions · Hilt + kapt → KSP hatena-bookmark

 

🚀 まとめ

Hilt で公式リファレンスを見ながら、kapt → KSP と順番に変化させていけばスムーズに対応できたのに、Dagger KSP ページを見ながら進んだのがハマった原因。

Version Catalog を使っていくことになりそうなので、抜粋しておく。