SwiftDataをiCloudとApp Groupsに対応して利用する
SwiftDataはiOS 17以降で使えるフレームワークです。SwiftUIと相性よく作られている、CoreData的な存在です。
最近個人アプリで利用しましたが、
- iCloudで複数デバイス間でデータを同期
- App Groupsでウィジェット側でもデータを取得
という構成にしました。このあたりを設定するのに良いドキュメントが見つからなかったので、このZennで手順を紹介します。
(PR)作った個人アプリはこちらです!
速習 SwiftData
SwiftDataについて簡単に紹介します。
クラスに @Model
をつけます。
import Foundation
import SwiftData
@Model
final class Diary {
var title: String
var content: String
var createdAt: Date
init(title: String, content: String, createdAt: Date) {
self.title = title
self.content = content
self.createdAt = createdAt
}
}
コンテナを初期化し、 modelContainer
に渡します。
import SwiftUI
import SwiftData
@main
struct SampleApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Diary.self,
])
let modelConfiguration = ModelConfiguration(schema: schema)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
Viewから以下のように利用します。
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// クエリ
@Query private var diaries: [Diary]
var body: some View {
NavigationView {
List {
ForEach(diaries) { diary in
VStack(alignment: .leading) {
Text(diary.title)
Text(diary.content)
Text(diary.createdAt, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
}
.toolbar {
ToolbarItem {
Button(action: addDiary) {
Label("Add", systemImage: "plus")
}
}
}
}
}
// データの追加と永続化
private func addDiary() {
let newDiary = Diary(title: "Title",
content: "Content",
createdAt: .now)
modelContext.insert(newDiary)
}
}
これだけでデータの読み書きが実現できます。
@Model
をつけたクラスのプロパティは@Published
をつける必要はなく、値が変更されるとViewが更新されます。
iCloudに対応する
プロジェクトの作成
iCloudに対応させる方法です。
新規プロジェクト作成時、StorageでSwiftData
を選び、「Host in CloudKit」 にチェックを入れます。
CloudKitをONにする
「XcodeのTargets > Signing & Capabilities > iCloud」からメニューを開き、「+」ボタンを押します。
containerのidentifierを問われるので、値を入力して設定します(慣習的に iCloud.
から始めることが多いようです)。
追加した直後は赤字になりますが、少し待つと通常の色になります。
「CloudKit Console」をクリックしてブラウザで開いてみましょう。
CloudKit Databaseを選択。
このような画面が表示されます。
CloudKit Databaseについては後述しますので、いったんXcodeに戻ります。
Entityの定義を修正する
CloudKitを使う際の注意として、プロパティをoptionalにするかデフォルト値をセットする必要があります。また、@Attribute(.unique)
なども利用できません。
Xcodeのコンソールエラーをを見ながら修正していきます。
今回は以下のようにデフォルト値をセットしました。
@Model
final class Diary {
var title: String = ""
var content: String = ""
var createdAt: Date = Date.now
// ...
}
iCloudにサインインして挙動を確認する
ビルドして挙動を確認してみましょう。
iPhoneのシミュレータを2台立ち上げ、同じApple IDでサインインします。一方でデータを追加すると、もう一方でもデータが同期されます。
(シミュレータだと一度画面を切り替える必要がありますが、実機ではリアルタイムに同期されます)
App Groupsに対応する
Targetの作成
続いてApp Groupsの対応です。
まずはアプリにWidgetを追加しましょう。
New > Target > Widget Extension からターゲットを追加します。
App GroupsをONにする
「Targets > Signing & Capabilities」から「App Groups」を追加します。
「+」ボタンからidentifierを追加します(慣習的にgroup.
から始まる値が多いようです)。
Widget側のTargetsでもApp GroupsをONにして、同じグループにチェックを入れます。
共通で使うファイルのTarget Membershipを更新する
ウィジェット側から利用するクラスのTarget Membershipを編集していきます。
(ここは他にも色々やり方があると思うので、お好みで)
まずはEntityのファイルを編集。
ModelContainerもウィジェットから使いたいので、別ファイルに切り出してTarget Membershipを編集します。
// SharedModelContainer.swift
import Foundation
import SwiftData
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Diary.self
])
let modelConfiguration = ModelConfiguration(schema: schema)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
これでウィジェットから呼び出す準備はOKです。
ウィジェット側でSwiftDataからデータを取得する
ウィジェットからデータを呼び出します。
struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task { @MainActor in
var entries: [SimpleEntry] = []
// SwiftDataからデータ取得
let context = sharedModelContainer.mainContext
let diaries = (try? context.fetch(FetchDescriptor<Diary>())) ?? []
// 取得したデータを使ってentriesに追加
}
}
}
これで本体とウィジェットから共通のデータにアクセスできました。
CloudKit Consoleを確認する
CloudKit Consoleの使い方にも触れておきます。
リリース時は忘れずにDeploy Scheme Changesする
開発時はsandbox環境につながっています。リリースの前に本番環境へ反映する必要があります。
操作はCloudKit Consoleの「Deploy Scheme Changes...」メニューから行います
接続先はentitlementsに設定が書かれているので、常に本番に接続されるように変更することも可能です。
RecordNameを追加してクエリする
iCloudに保存されたデータを確認したい場合、ひと手間必要です。
Indexes > Entity と選択し、「Add Basic Index」します。
"recordName"を選び、Index Typeを"Querable"として追加します。
追加できたらクエリしてみます。デフォルトでプライベート領域にデータが書かれる設定になっているので、以下のように指定します。
- DBは「Private Database」
- Zoneは「com.apple.coredata.cloudkit.zone」
- RECORD_TYPEは対象のType
画像のようにデータ表示が取得できます。
iCloudにデータを送ったものしか変更に検知されない
モデルクラスに加えた変更は自動的にiCloud側に反映されます。
ただし、iCloudにサインインした状態で一度モデルを更新する必要があります。
コードに追加しただけではiCloudに反映されませんので気をつけましょう。
その他
以下の制限があったので書いておきます。
- enumをPredicateのなかの条件式で使えない。永続化するのはenumではなくRawValueにして対応
- booleanのプロパティがiCloudに保存できなかった。原因不明だが、文字列にして対応
おわりに
SwiftDataをiCloudとApp Groupsに対応する手順を書きました。
どなたかの参考になれば幸いです。
Discussion