【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 で Singleton

前回、Apple サンプルコードを見ながら、生存期間が謎だったのですが。

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

シンプルなクラスにするとこういうことですね。


class Cat {
  private(set) static var shared: Cat!

  private init() {}

  static func createInstance() {
    shared = Cat()
  }

  func show() {
    print("ねこ")
  }
}

Cat.createInstance()
Cat.shared.show() // ねこ

一度インスタンスを生成すると、

全スコープから参照可能な状態で、アプリが死ぬまで生きてるんですね。


👉 Singleton pattern - Wikipedia hatena-bookmark

どう書いてますか Singleton。

少し Xcode PlayGround で試してみます。

ざっくりこのようなイメージでいたけども。


class Monkey {
  private(set) static var instance: Monkey?

  private init() {}

  static func createInstance() -> Monkey {
    if (instance == nil) {
      instance = Monkey()
    }
    return instance!
  }

  func show() {
    print("さる")
  }
}

var monkey: Monkey? = Monkey.createInstance()
monkey?.show() // "さる"
monkey = nil
monkey?.show() // not show

公式ドキュメントを見ると以下のような感じになってます。

👉 Managing a Shared Resource Using a Singleton | Apple Developer Documentation hatena-bookmark


class Dog {
  static let shared = Dog()

  func show() {
    print("いぬ")
  }
}

Dog.shared.show() // いぬ


class Kiji {
  static let shared: Kiji = {
    let instance = Kiji()
    return instance
  }()

  func show() {
    print("きじ")
  }
}

Kiji.shared.show() // きじ

var shared: Kiji? = Kiji.shared
shared?.show() // きじ
shared = nil
shared?.show() // not show

本当に死んでるのか?

Swift では、結構嫌がられてますよね Singleton。

使いすぎて収集つかずに太っちゃうやつですかね。

👉 Automatic Reference Counting | Documentation hatena-bookmark
👉 Kotlin で書きたい「正しいシングルトン(Singleton)」 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 と一緒に死ぬ?

(→ つづく)