【SwiftUI + SwiftData】MVVMパターンを考えたときに

SwiftUI ベースに MVVM のパターンを考えたとき。

Apple と Orange の子 View があるとして、こういう感じで認識していましたけども MVVM。

1つの View に対して、1つの ViewModel でライフサイクルは同期している。

その View の子の View に対しては親の ViewModel を渡したり、差し込んでいく。

著名な公開されているコードを見てると、こういう形が多いように見えるし、そのほうが書きやすいように思える。

世の中そんな流れですかね。

どうなんですかね。

ModelContext てそんな子に向けてのツールですよね。

どこかなんかいい参考記事ないですかね。

まあ、ざっくりの話なんですけども。


【SwiftUI + SwiftData】List のアイテムの Preview

List や LazyVStack などの、

一つの要素の View の #Preview を

簡単に表示するというだけの件。

よくある List 的な View。

その一つの Item の View。


struct ItemView: View {
  var item: Item

  var body: some View {
    HStack {
      Spacer()
      Text("\(item.i)")
      Spacer()
      Text("\(item.s)")
    }
    .font(.largeTitle)
    .monospaced()
    .background()
  }
}

この一つの View だけを #Preview で簡単に表示したい。

 

🤔 #Preview 用の View

Container のセットはより上位の View でセットするのが定石。

 

🤔 使い方

一つ入れ子でクロージャ。


#Preview {
  PreviewOneModelView { item in
    ItemView(item: item)
  }
  .modelContainer(for: Item.self, inMemory: false)
}

 

🤔 参考

Apple 公式サンプルより簡易化して抜粋しました。


public struct ModelPreview<Model: PersistentModel, Content: View>: View {
    var content: (Model) -> Content
    
    public init(@ViewBuilder content: @escaping (Model) -> Content) {
        self.content = content
    }
    
    public var body: some View {
        ZStack {
            PreviewContentView(content: content)
        }
        .backyardBirdsDataContainer(inMemory: true) // * inMemory
    }
    
    struct PreviewContentView: View {
        var content: (Model) -> Content
        
        @Query private var models: [Model]
        @State private var waitedToShowIssue = false
        
        var body: some View {
            if let model = models.first { // * first
                content(model)
            } else {
                ContentUnavailableView {  // * could not get first item
                    Label {
                        Text(verbatim: "Could not load model for previews")
                    } icon: {
                        Image(systemName: "xmark")
                    }
                }
                .opacity(waitedToShowIssue ? 1 : 0)
                .task {
                    Task {
                        try await Task.sleep(for: .seconds(1))
                        waitedToShowIssue = true
                    }
                }
            }
        }
    }
}

👉 sample-backyard-birds/BackyardBirdsData/General/ModelPreview.swift · apple/sample-backyard-birds hatena-bookmark



【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