【Swift】WidgetKit使用時に、入力された文字をUserDefaults経由でWidgetに表示する方法

公開:2020/10/09
更新:2020/10/12
9 min読了の目安(約8400字TECH技術記事

前回こちらの記事iOS14.0から追加されたWidgetKitを簡単に実装する方法
で、WidgetKitを使用する方法を記載しましたが、
今回はWidgetKitをExtensionとして追加後、UserDefaults経由で値を共有する方法を記載します。

環境

  • Xcode: 12.0
  • Swift5

下準備

※前提として既にTargetからWidgetKitが追加されていることとします

1. CapabilityからAppGroupを追加

TargetをWidgetApp(WidgetKitを追加した元となるProject)に選択し、CapabilityからAppGroupをダブルクリックしAppGroupを追加します。
スクリーンショット 2020-09-29 11.47.46.png
以下のようにAppGroupsの欄が追加されていればOK
スクリーンショット 2020-09-29 11.58.32.png

2.AppGroupsに共通のIdentifierを登録する

a.AppGroupsの「+」を押下すると以下のようなダイアログが表示されるので、開発チームを選択する
スクリーンショット 2020-09-29 12.02.01.png

b.AppGroupsで使用する共通のIdentifierの入力を求めらるので以下のように入力する

group.{プロジェクト作成時のBundle Identifier}.xxxxx

例)
スクリーンショット 2020-09-29 12.00.20.png

※注意

xxxxx部分は自由に登録可能ですが、xxxxx.yyyのように二つ以上ピリオドで繋げてしまうと、
AppStore申請時にエラーで弾かれてしまいますので注意してください。

OK押下後、以下のようにAppGroup用のIdentifierが登録されていればOKです。
※チェックボックスにチェックがついていない場合は、付けるようにしてください。
スクリーンショット 2020-09-29 12.00.36.png

####3.WidgetExtension側にもAppGroupsを追加する
ほぼ手順は先ほど行った手順と同じです。

a.Extensionとして追加したTarget(WidgetAppExtension)を選択し、Capbility>AppGroupsを追加する
スクリーンショット 2020-09-29 13.19.15.png

b. 「+」ボタンを押下しAppGroupsに共通Identifierを登録する
こちら側は、開発者チームを選択したところで終了するはずです。
そうすると手順2で追加したIdentifierが表示されていると思うので、チェックボックスにチェックマークをつけてください。
スクリーンショット 2020-09-29 12.02.22.png

以上でUserDefaultsを使用する下準備が終了です。

実装

今回は、以下のような内容の実装をしていきます。
・画面側で入力された文字列をUserDefaultsに保存
・UserDefaultsに保存された文字列をWidgetに表示する

1.画面入力用のテキストフィールド、ボタンを配置する

ContentView.swift

struct ContentView: View {
    @State private var text = ""
    var body: some View {
        VStack {
            TextField("文字入力", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding(.horizontal)
            Button(action: {
                // ボタン押下時
            }){
                Text("文字を保存する")
            }
        }
    }
}

2.UserDefaultsに保存処理を追加

今回はボタン押下時にテキストフィールドの値を保存します
ContentView.swift

struct ContentView: View {
    @State private var text = ""
    var body: some View {
        VStack {
            TextField("文字入力", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding(.horizontal)
            Button(action: {
                // ボタン押下時
                let userDefaults = UserDefaults(suiteName: "group.com.sample.yajima.WidgetApp.WidgetExtension")
                if let userDefaults = userDefaults {
                    userDefaults.synchronize()
                    userDefaults.setValue(text, forKeyPath: "inputText")
                }
            }){
                Text("文字を保存する")
            }
        }
    }
}

このようにsuiteNameに先ほど登録したIdentifierを指定することによってUserDefaultsの値を共有することができます。
userDefaults.synchronize()は不要との記事も見かけましたが、私は追加しないと動作しませんでした。

3.UserDefaultsから値取得処理

WidgetAppExtension.swift

    // struct Provider: TimelineProvider内
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        /* 追記ここから  */
        var text = ""
        let userDefaults = UserDefaults(suiteName: "group.com.sample.yajima.WidgetApp.WidgetExtension")
        if let userDefaults = userDefaults {
            text = userDefaults.string(forKey: "inputText") ?? ""
        }
        /* ここまで  */
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }

値保存時と同様に、IdentifierをsuiteNameに指定し、他は普段通りのUserDefaultsの使用方法と同じです。

4.WidgetにUserDefaultsから取得した値を表示する

前回の記事から何も触っていなければ、
デフォルトのWidgetには時間が表示される処理が記載されていると思うので、
そちらに追加で、文字列が表示されるようにしてみます。

WidgetAppExtension.swift

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), text: "")
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), text: "")
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        var text = ""
        let userDefaults = UserDefaults(suiteName: "group.com.sample.yajima.WidgetApp.WidgetExtension")
        if let userDefaults = userDefaults {
            text = userDefaults.string(forKey: "inputText") ?? ""
        }
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            // UserDefaultsから取得した文字列をセット
            let entry = SimpleEntry(date: entryDate, text: text)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let text: String
}

struct WidgetAppExtensionEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
        Text(entry.text)
    }
}

@main
struct WidgetAppExtension: Widget {
    let kind: String = "WidgetAppExtension"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WidgetAppExtensionEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

struct WidgetAppExtension_Previews: PreviewProvider {
    static var previews: some View {
        WidgetAppExtensionEntryView(entry: SimpleEntry(date: Date(),text: ""))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

以上で、UserDefaultsに保存された値をWidgetKitに表示させることができます。

ボタン押下後すぐにWidgetの表示内容を更新したい場合

ボタン押下時に以下処理を呼び出すことで更新が可能となります。

WidgetCenter.shared.reloadAllTimelines()

ContentView.swift

import SwiftUI
import WidgetKit

struct ContentView: View {
    @State private var text = ""
    var body: some View {
        VStack {
            TextField("文字入力", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding(.horizontal)
            Button(action: {
                // ボタン押下時
                // AppGroups追加時に設定したIdentifier
                let userDefaults = UserDefaults(suiteName: "group.com.sample.yajima.WidgetApp.WidgetExtension")
                if let userDefaults = userDefaults {
                    userDefaults.synchronize()
                    userDefaults.setValue(text, forKeyPath: "inputText")
                }
                // Widgetを更新
                WidgetCenter.shared.reloadAllTimelines()
            }){
                Text("文字を保存する")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

動作

0929gif2.gif

このように入力した文字がWidgetKitに反映されるようになりました!
何か私の認識違い等ございましたらご指摘いただけますと幸いです。

本記事のような実装で以下のアプリをリリースしました。
まだまだ機能が乏しいですが、よろしければインストールしてみてください!
Accent Widget

備考

アプリ公開しました!よろしければインストールお願いします。
とらんぽ

Twitter始めました!よろしければフォローお願いします。
@yajima_tohshu