FlutterからiOSのWidgetKitを使う
こんにちは。Flutterで個人開発をしているKyoichiです。
私は、1ヶ月に使用できる目標金額から逆算してお金を管理する家計簿アプリ「Monthsave」を2022年2月にリリースしました。
このFlutterアプリでiOSのウィジェット機能を使用することになりました。しかしFlutterにはウィジェット機能を提供するパッケージがありません。よってswiftのコードを呼び出してこの機能を実装する必要があり、少々詰まりました。そこで、コピペ改変でこの機能を実装できるように記事を書きました。
※ この記事は2022/2/19にQiitaで書いた記事を修正したものである。
実装方法
- Flutterのshared_preferencesで表示したい値を保存
- FlutterのMethodCannelでswiftのコードを呼び出す
- swiftで呼び出されたメソッドをcallする
- AppGroupを用いてWidgetKitを使用する
- UserDefaultsをiOSの形式で保存し直し、Widgetを更新する
- WidgetKit側でUserDefaultsを参照し表示
Flutterのshared_preferencesで表示したい値を保存
iOSのウィジェットで表示したいデータは一旦shared_preferencesに保存する必要があります。Flutterのテンプレートであるカウンターアプリのカウントをウィジェットで表示することを考えると、まず以下のように保存します。
int _counter = 0;
final SharedPreferences _prefs = await SharedPreferences.getInstance();
_prefs.setInt('count', _counter);
FlutterのMethodCannelでswiftのコードを呼び出す
次に、ウィジェットを初期化するために、swiftのコードをMethodChannelを用いてFlutterから呼び出します。アプリ始動時やデータ更新時にこちらのメソッドを呼ぶようにします。
static const methodChannel = MethodChannel('[your app name]/sample');
try {
final int result = await methodChannel.invokeMethod('setDataForWidgetKit');
print('SET setUserDefaultsForAppGroup: $result');
} on PlatformException catch (e) {
print('ERROR setUserDefaultsData: ${e.message}');
}
swiftで呼び出されたメソッドをcallする
Xcodeを開いて、AppDelegate内のApplication関数内に次のコードを追加します。
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "[your app name]/sample",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "setDataForWidgetKit" {
self.setUserDefaultsForAppGroup(result: result)
}
result(FlutterMethodNotImplemented)
return
})
先ほど作成した、[your app name]/sampleという名前のMethodCannelの、setDataForWidgetKit関数がFlutterからinvokeされたら、swiftのsetUserDefaultsForAppGroup関数を呼び出すという意味です。
AppGroupを用いてWidgetKitを使用する
swiftでも同じであるが、WidgetKitなどのモジュールはAppExtensionsから選択して使用する。これらの機能は別のアプリと連携するときと同様に、同じAppGroupに所属させる必要がある。
Xcode上のTARGETのRunner->Signing & Capabilities -> + CapabilityからApp Groupを追加する。
グループの名前は名前は慣例的にgroup.[bundle id]とする。
次に、File -> New -> TargetからWidget Extensionを追加する。
UserDefaultsをiOSの形式で保存し直し、Widgetを更新する
先ほど呼び出したsetUserDefaultsForAppGroup関数を実装します。
Flutterではshared_preferencesに"count"というkeyで値を保存しました。swiftコード内でこの値を参照するときはkeyが"flutter.count"となります。まずこの値をswiftで取得します。
AppGroup内で共有するデータはUserDefault形式で保存する必要があるため、この値を再度保存します。最後に以下のコードを実行してウィジェットを更新します。
if #available(iOS 14.0, *) {
WidgetCenter.shared.reloadAllTimelines()
}
なお、iOS14.0以上でしかウィジェットは使用できないことを考慮しています。
一連のコードは以下のようになります。
private func setUserDefaultsForAppGroup(result: FlutterResult) {
guard let userDefaults = UserDefaults(suiteName: "group.[your bundle id]") else {
return result(FlutterError(code: "UNAVAILABLE",
message: "setUserDefaultsForAppGroup Failed",
details: nil))
}
let defaults = UserDefaults.standard
let count = defaults.value(forKey: "flutter.count") as? Int
userDefaults.setValue(count, forKey: "count")
if #available(iOS 14.0, *) {
WidgetCenter.shared.reloadAllTimelines()
}
result(true)
}
WidgetKit側でUserDefaultを参照し表示
最後にWidgetKitのテンプレートを修正して、値を表示させれば完成です。なお、WidgetKitの仕様などは今回の記事の主題ではないため省略します。
まずSimpleEntry内にプロパティを追加します。
let count: Int
次に、getSnapshot関数内に以下のコードを追加してUserDefaultからデータを参照します。
let userDefaults = UserDefaults(suiteName: "group.[your bundle id]")
let countData = userDefaults?.value(forKey: "count") as? Int
最後にmy_widgetEntryViewのbodyの値を次のように変更します。
Text(String(entry.count))
つまずいたところ
- swiftの機能であるAppGroupやUserDefaultsの仕組みをある程度理解しなければならない
- MethodChannelで呼び出したswiftのコードの実行時にエラーやログがまともに出ないため、手探りでミスを見つけなければならない。(そんなことないかも)
- WidgetKitのUIを作るとき、結局ある程度はswiftUIを理解しなければならない
- MethodChannelでは、必ずresultを返さないと例外が発生する(この仕様に気づくまでに時間がかかった)
参考資料
Discussion