【Swift】Widget から SwiftData にアクセスする
初めに
今回は WidgetKit の Widget で表示するデータを SwiftData から取得する方法についてまとめてみたいと思います。最終的には、データ更新をすぐに Widget に反映させられるような実装を行います。
記事の対象者
- Swift 学習者
- Widget で SwiftData のデータを活用したい方
目的
今回は上記の通り、 Widget で SwiftData のデータを読み取って表示することを目的とします。
簡単に手順をまとめつつサンプルを作ってみたいと思います。
今回作成するサンプルは以下のリポジトリで公開しています。
よろしければ適宜ご参照ください。
実装
実装は以下の手順で進めていきます。
- iOS プロジェクトの作成
- Widget Extension の追加
- App Groups の追加
- コード修正
1. iOS プロジェクトの作成
まずは iOS プロジェクトを作成していきます。
File > New > Project ...
で iOS の App を選択します。
筆者の手元ではアプリの名前は「WidgetKit+SwiftData」としておきます。
また、今回は SwiftData を使用するので、 Storage
の項目は SwiftData
としておきます。
これでプロジェクトの作成は完了です。
2. Widget Extension の追加
次に Widget Extension の追加を行います。
File > New > Target ...
で追加するターゲットを選択します。
今回は以下の画像のように Widget Extension
を追加します。
筆者の手元では名前は「CustomWidget」としておきます。
Widget を追加できたら Widget Extension の scheme を Activate しておきます。
これで Widget Extension の追加は完了です。
3. App Groups の追加
次に App Groups の追加を行います。
WidgetKit+SwiftData の Signing & Capabilities
を開いて、「+Capability」を押下します。
そこで「App Groups」を選択することで App Groups を追加することができます。
この App Groups の追加をアプリ側(WidgetKit+SwiftData)と Widget側(CustomWidgetExtension)の Target の両方で行います。
App Groups を両方で追加できたら紐付けを行います。
以下のようにアプリ側で新たに App Groups を作成してチェックします。
アプリの方が完了したら Widget の方も同じ App Groups にチェックを入れます。
これでアプリと Widget でデータ共有するための紐付けが完了します。
4. コード修正
最後にコード修正を行います。
コード修正は以下の流れで進めていきます。
- アプリ側の実装
- Widget 側の実装
1. アプリ側の実装
まずは Widget 側で表示させたい SwiftData のモデルのファイルのターゲットを変更する必要があります。
今回はデフォルトで作成されている Item.swift
の値を Widget で表示させたいので、以下のように Target Membership
の + ボタンを押します。
Target Membership
に Widget のターゲット(CustomWidget)を追加します。
これで Widget 側でも Item.swift
の内容を参照できるようになります。
次に Item.swift
の変更を行います。
今回は変化がわかりやすいように Item
に String 型の title
を持たせるように変更します。
import Foundation
import SwiftData
@Model
final class Item {
var title: String
init(title: String) {
self.title = title
}
}
次に ContentView.swift
の修正を行います。
Item
のプロパティを変更したのでそれに応じて修正していきます。
import SwiftUI
import SwiftData
import WidgetKit
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
@State private var newItemTitle: String = ""
var body: some View {
NavigationStack {
VStack {
TextField(text: $newItemTitle) {
Text("New Item")
}
.padding()
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.title)")
} label: {
Text(item.title)
}
}
.onDelete(perform: deleteItems)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button {
if(newItemTitle.isEmpty) { return }
addItem(title: newItemTitle)
} label: {
Label("Add Item", systemImage: "plus")
}
}
}
.padding()
}
}
private func addItem(title: String) {
withAnimation {
let newItem = Item(title: title)
modelContext.insert(newItem)
try? modelContext.save()
// Widget の記録更新
WidgetCenter.shared.reloadAllTimelines()
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
try? modelContext.save()
// Widget の記録更新
WidgetCenter.shared.reloadAllTimelines()
}
}
}
ContentView
に含まれる以下のコードでは、 modelContext.save()
で SwiftData のデータを保存し、reloadAllTimelines
メソッドで Widget の再読み込みを行なっています。
これで Widget に表示するデータを最新に保つことができるようになります。
詳しくは こちら のドキュメントをご参照ください。
try? modelContext.save()
WidgetCenter.shared.reloadAllTimelines()
アプリ側では SwiftData のデータ追加、削除のみを行うシンプルな処理を実装しています。
これでアプリ側の実装は完了です。
2. Widget 側の実装
次に Widget 側の実装を行います。
CustomWidget/CustonWidget.swift
で SwiftData のデータを表示できるように実装していきます。
以下のように、@Query
で SwiftData に格納されているデータを呼び出して model
に代入します。
そして、データを ForEach で表示させます。
今回はデータを参照するだけのシンプルな実装なので、必要に応じてスタイルの変更等を行います。
struct CustomWidgetEntryView : View {
var entry: Provider.Entry
@Query var model: [Item]
var body: some View {
VStack {
LazyVStack(content: {
ForEach(model, id: \.self) { item in
Text(item.title)
}
})
}
}
}
次に CustomWidget
に含まれる CustomWidgetEntryView
に対して、使用したいモデルの modelContainer
を適用します。
これで Widget 側でも Item
モデルのデータにアクセスできるようになります。
struct CustomWidget: Widget {
let kind: String = "CustomWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
CustomWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
// modelContainer の追加
.modelContainer(for: [Item.self])
}
}
}
この状態でアプリを実行し、Widget を追加してから SwiftData に含まれるデータを変更してみます。
以下の動画のように、変更がすぐに反映されるかと思います。
以上です。
まとめ
最後まで読んでいただいてありがとうございました。
WidgetKit と SwiftData の間のやり取りは App Groups を用いて紐付けを行うことで実現できることがわかりました。
誤っている点やもっと良い書き方があればご指摘いただければ幸いです。
参考
Discussion