【Swift UI】App GroupsでWidgetやアプリ間でデータを共有する方法!
この記事からわかること
- Swift UIでWidgetやアプリ間でデータを共有する方法
- App Groupsの使い方と実装方法
- 新規コンテナの追加方法とUserDefaultsの使い方
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.3
- iOS:18.4
- Swift:6
- macOS:Sequoia 15.6.1
アプリ内に保存しているデータを別のアプリやWidget(ウィジェット)でも共有するためには「App Groups」という仕組みを使用する必要があります。今回はApp Groupsを使ったデータの共有方法をWidgetを例に紹介していきます。Widget自体の実装方法は下記記事を参考にしてください。
App Groupsとは?
「App Groups」とは異なるアプリやWidget同士でデータを共有できる仕組みのことです。端末の内部に共有でデータを保存するためのコンテナーをコンテナーIDをつけて作成し、メンバーシップに登録してあるアプリからであればコンテナー内のデータを操作することができるようになります。メンバーシップに登録できるのは自分が開発したアプリに限られますがApp Groups使うことで関連アプリを作ったり、アプリのデータを表示させるWidgetを表示させることができるようになります。
作成できる共有コンテナーは最大1000個までです。
新規コンテナーの作成
App Groupsを使用できるようにするためには前準備が必要です。以下の流れで新しくコンテナーを作成します。コンテナーを作成したらメンバーに対象のアプリを追加する必要があります。
- アプリ側の「Signing & Capabilities」タブを開く
- 「+」ボタンから「App Groups」を追加
- 「+」ボタンを押してコンテナーID名を決めて追加して有効化(例:group.com.ame.dev.WidgetTest)
- Widget側の「Signing & Capabilities」タブを開く
- 「+」ボタンから「App Groups」を追加
- 先ほど追加したコンテナーIDにチェックを入れて有効化
コンテナーのメンバーはTargetごとに識別されるので今回はWidget側にも明示的に追加する必要があるので注意してください。
有効化すると「XXXXXXX.entitlements」というファイルが生成されます。ファイル名にDebugと付与されている場合はDebug側でしか反映されていない可能性があるので注意してください。実際にアプリをリリースする際にコンテナーIDを取得できずにクラッシュすることがあります。Debug / Releaseの両方で「XXXXXXX.entitlements」を指定する必要があるので「Build Settings」>「Code Signing Entitlements」でDebug / Releaseの両方に作成した「XXXXXXX.entitlements」が指定されているか確認してください。
データの保存と共有方法
コンテナー内に保存されるデータはUserDefaultsを使って操作できます。作成したコンテナーのUserDefaultsにアクセスするにはinit(suiteName:)形式のイニシャライザを使用します。
let userName = UserDefaults(suiteName: "group.com.ame.dev.WidgetTest")?.object(forKey: "name")
引数suiteNameに存在しないコンテナーIDを渡すとnilが返り、引数にnilを渡すとUserDefaults.standardと同義になるようです。
あとは通常のUserDefaultsを扱うように値を格納したり、取得したりするだけです。
Widgetへデータを共有する方法
既にコンテナーのメンバーシップへWidgetを追加しているのでWidgetからも共有コンテナー内のUserDefaultsからデータを取得することができます。
まずはアプリ側からUserDefaultsにデータの保存と取得をできるように記述していきます。
struct ContentView: View {
@State private var name: String = "NoName"
private func setName() {
guard let userName = UserDefaults(suiteName: "group.com.ame.dev.WidgetTest")?.object(forKey: "name") else {
return
}
self.name = userName as? String ?? ""
}
private func entryName(_ name: String) {
UserDefaults(suiteName: "group.com.ame.dev.WidgetTest")?.set(name, forKey: "name")
setName()
}
var body: some View {
VStack {
Button {
entryName("ame")
} label: {
Text("Entry")
}
Text(name)
.padding()
}.onAppear {
setName()
}
}
}
続いてWidget側からUserDefaultsからデータを取得できるように独自の構造体を生成してWidgetに組み込んでいきます。
struct ContainerGroupManager {
var name: String = "NoName"
mutating func setName() {
guard let userName = UserDefaults(suiteName: "group.com.ame.dev.WidgetTest")?.object(forKey: "name") else { return }
self.name = userName as? String ?? ""
}
}
続いてTimelineEntryとビューを修正します。entry経由でビューからデータを取得できるようにします。
struct SimpleEntry: TimelineEntry {
let date: Date
let name: String = "NoName"
}
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.name)
}
}
最後にgetTimelineメソッドを以下のように変更すればWidget側にUserDefaults内に共有されているデータを表示することができました。
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
var manager = ContainerGroupManager()
manager.setName()
let entryDate = Date()
let entry = SimpleEntry(date: entryDate)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
全体のコード
import WidgetKit
import SwiftUI
// MARK: - (1)
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(),name:ContainerGroupManager().name)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(),name:ContainerGroupManager().name)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
var manager = ContainerGroupManager()
manager.setName()
let entryDate = Date()
let entry = SimpleEntry(date: entryDate,name:manager.name)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
// MARK: - (2)
struct SimpleEntry: TimelineEntry {
let date: Date
let name: String
}
// MARK: - (3)
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.name)
}
}
// MARK: - (4)
@main
struct TestWidget: Widget {
let kind: String = "TestWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
TestWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
// MARK: - (5)
struct TestWidget_Previews: PreviewProvider {
static var previews: some View {
TestWidgetEntryView(entry: SimpleEntry(date: Date(),name: ContainerGroupManager().name))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
// MARK: - (6) 追加
struct ContainerGroupManager {
var name: String = "NoName"
mutating func setName(){
guard let userName = UserDefaults(suiteName: "group.com.ame.dev.WidgetTest")?.object(forKey: "name") else { return }
self.name = userName as? String ?? ""
}
}
共有コンテナーURLを取得する方法
作成した共有コンテナーのURLを取得するにはFileManagerクラスのcontainerURLメソッドを使用します。引数にはURLを取得したいコンテナーIDの文字列を渡します。無能な文字列を渡した場合はnilが返ります。
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.ame.dev.WidgetTest")!
これを使用することでRealmなどのライブラリでもApp Groupsを使用することが可能になります。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





