【Swift UI】WidgetにRealmのデータを表示させる方法!
この記事からわかること
- Swift UIでWidgetにRealmのデータを表示させる方法
- App Groupsで共有コンテナー内にRealmデータベースを構築するには?
- Widget追加後にCocoa Podsを使用してライブラリを追加する方法
- App Groups使用時の@ObservedResultsの注意点
- FileManager.default.containerURLメソッドの使い方
index
[open]
\ アプリをリリースしました /
環境
- Xcode:26.0.1
- iOS:26
- Swift:6
- macOS:Tahoe 26.0.1
iOS14から実装されたWidget(ウィジェット)機能にRealmで保存しているデータを表示させる方法をまとめていきます。Realmの導入方法やWidgetの作成方法は下記記事を参考にしてください。
【SwiftUI】Realm Swiftとは?導入方法とCRUD処理のやり方
【Swift UI】Widget(ウィジェット)の実装方法!TimelineProviderとは?
WidgetにRealmのデータを表示させる方法
WidgetにRealmのデータを表示させるには前提としてRealmのデータベースをApp Groupsを使った共有コンテナー内に構築する必要があります。またWidget側にライブラリの導入をして審査に出すには少しコツが必要になります。
実装の流れ
- 新規プロジェクトを生成
- Widgetの追加
- Realmの導入
- App Groupsで新規コンテナーの作成
- コンテナーメンバーシップにアプリとWidgetを追加
- 共有コンテナーのURLを取得
- Realmの保存先URLに取得したURLを設定
まずは新規でプロジェクトを立ち上げ、さらにプロジェクト内に新しくWidgetを追加(「File」>「 New 」>「Target...」を選択後「Widget Extension」)しておきます。
これでWidget機能を持ったアプリプロジェクトファイルができました。
Widgetがある場合のCocoa Podsのインストール
次にCocoa Podsを使用してRealmを導入していきます。後述しますがSPMだと導入はできてもアーカイブのバリデーションエラーになるので注意してください。
pod initを実行するとWidgetを追加している場合はPodFileの中身がtargetごとに分かれます。
ライブラリはターゲットごとに導入する必要があるので以下のように2ヶ所に記述してpod installを実行します。
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'TestWidgetExtension' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for TestWidgetExtension
pod 'RealmSwift'
end
target 'WidgetTest' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for WidgetTest
pod 'RealmSwift'
end
App Groupsと紐づける
立ち上げたアプリからApp Groupsを追加し新規コンテナーを作成しておきます。
- アプリ側の「Signing & Capabilities」タブを開く
- 「+」ボタンから「App Groups」を追加
- コンテナーID名を決める(例:group.com.ame.dev.WidgetTest)
- Widget側の「Signing & Capabilities」タブを開く
- 「+」ボタンから「App Groups」を追加
- 先ほど追加したコンテナーにチェックを入れて有効化
作成したらWidget側からも作成したコンテナーにアクセスできるようにチェックを打っておきます。
Realmの保存先を共有コンテナーURLに変更する
続いてアプリ内にRealmデータベースを操作するためのコードを記述していきます。
まずはデータベースに保存するテーブルクラスとRealmインスタンスを操作するクラスを作成しておきます。プロジェクト内では保存先を変更したインスタンスを使用したいのでクラス内に定義した共有となるRealmインスタンスを使用することで記述が冗長にならないようにします。
import UIKit
import RealmSwift
class User: Object,ObjectKeyIdentifiable{
@Persisted(primaryKey: true) var id = UUID()
@Persisted var name:String = ""
@Persisted var age:Int = 0
}
class RealmManager {
var realm:Realm {
var config = Realm.Configuration()
config.fileURL = fileUrl
return try! Realm(configuration: config)
}
var fileUrl: URL {
let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.ame.dev.WidgetTest")!
return url.appendingPathComponent("db.realm")
}
}
Realmをインスタンス化する際に設定を指定できるのでその際に保存先URLを作成したコンテナーのURLに変更します。
containerURLメソッドを使って任意のコンテナーURLを取得し、appendingPathComponentを使ってその中にRealm用のパスを構築しています。
続いて作成したUserRealmModels.swiftをWidget側からも参照できるようにインスペクタ(右側)のTarget MemberShipのWidgetにチェックを入れておきます。
アプリ内でデータの登録処理を記述
アプリ内からデータを登録できるようにビューを構築していきます。ここでのポイントは以下の2つです。
- 共有コンテナーの場合の@ObservedResults
- アプリ側からWidgetを更新する
import SwiftUI
import RealmSwift
import WidgetKit
struct ContentView: View {
let manager = RealmManager()
@ObservedResults(User.self,configuration: Realm.Configuration(fileURL:RealmManager().fileUrl)) var users
@State var text:String = ""
func entryName(_ name:String){
let obj = User()
obj.name = name
obj.age = 26
let realm = manager.realm
try! realm.write{
realm.add(obj,update:.modified)
}
text = ""
WidgetCenter.shared.reloadAllTimelines()
}
var body: some View {
VStack{
TextField("name", text: $text).padding()
Button(action: {
entryName(text)
}, label: {
Text("Entry")
})
List(users) { user in
Text(user.name)
}.listStyle(GroupedListStyle())
}
}
}
App Groups使用時の@ObservedResults
App Groupsを使用して共有コンテナーにRealmデータベースを保存する場合は@ObservedResultsを使用する際に注意が必要です。
// ×
@ObservedResults(User.self) var users
// ◯
@ObservedResults(User.self,configuration: Realm.Configuration(fileURL:RealmManager().fileUrl)) var users
保存しているファイルURLを変更しているのでその変更を明示的に指定する必要があります。ここではRealm.Configurationを使って保存先URLを渡しています。
WidgetCenter.shared.reloadAllTimelines()
ボタンをクリックされたときにWidgetを更新するには以下のように記述します。WidgetKitをimportするのを忘れないようにしてください。
WidgetCenter.shared.reloadAllTimelines()
Widget側からRealmを取得
最後にWidget側にRealmからデータを取得して反映させるように記述すれば完成です。ここで追記するのは以下のポイントです。
- TimelineEntryにnameプロパティを追加
- Widgetのビューの調整
- getTimelineメソッド内からRealmのデータを参照
import WidgetKit
import SwiftUI
import RealmSwift
struct Provider: TimelineProvider {
// 省略
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let realm = RealmManager().realm
let obj = realm.objects(User.self).last!
var entry = SimpleEntry(date: Date())
entry.name = obj.name
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
var name:String = "NoUser"
}
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack{
Text("最後の登録者")
Text(entry.name)
}
}
}
// 省略
これで以下のような最後に登録されたユーザー名が表示されるWidgetが完成しました。
SPMでのアーカイブバリデーションエラー
WidgetにSPMを使用してライブラリを導入してみたところデバッグビルドでは問題なく動作するのですが審査のためにアーカイブを作成してアップロードすると以下のようなバリデーションエラーが発生します。
Validation failed
CFBundleIdentifier Collision. There is more than one bundle with the CFBundleIdentifier value 'realm-swift.RealmSwift' under the iOS application 'プロジェクト名.app'.
Validation failed
Invalid Bundle. The bundle at 'プロジェクト名.app/PlugIns/プロジェクトWidget名.appex' contains disallowed nested bundles.
Validation failed
Invalid Bundle. The bundle at 'プロジェクト名.app/PlugIns/プロジェクトWidget名.appex' contains disallowed file 'Frameworks'.
エラーの内容は上から以下の通りです。
- CFBundleIdentifierの衝突
- Widget内に期待しない.framework や.bundle が入っている
- .appex の中に期待しない Frameworks フォルダが存在する
Appleのガイドラインで明言されているようではないですが、実際に上記の2と3(1はただ同じライブラリが2回ロードされてしまっているだけ)はアーカイブのバリデーションでエラーになり審査にも出すことができない状態になります。Cocoa PodsとSPMで違いが出る理由はライブラリの導入構造の違いによるもののようです。
SPMでWidgetKit側でRealmライブラリを使いたい場合はCocoa Podsに移行するか、WidgetKit側でRealmを使わないでDBから読み取る仕組みを自前で構築する必要がありそうです。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。







