概要
- 【参考】親記事
React Native (Expo)でWidgetKitの実装での学びについて書いています。具体的には以下の2つです。
- 特定バージョン(
iOS17+)の機能を使用した時のビルドエラーの対応 - ウィジェットのサイズ(小・中・大)ごとに表示レイアウトを切り替えたい
前提条件
-
apple target widgetでウィジェットを実装
背景
①EAS Build が containerBackground で落ちる(iOS バージョン不整合)
EAS Build の iOS ビルドが以下のエラーで失敗した。
✖ Build failed
🍏 iOS build failed:
The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:
- 'containerBackground(_:for:)' is only available in iOS 17.0 or newer
- 'widget' is only available in iOS 17.0 or newer
Refer to "Xcode Logs" below for additional, more detailed logs.
原因は表示ロジックの中で使用していた.containerBackground(.background, for: .widget)がiOS 17.0+ 限定であり、expo-target.config.js の deploymentTarget は "16.0" に設定されており、バージョン不整合が発生していました。
deploymentTarget: "16.0" |
deploymentTarget: "17.0" |
|
|---|---|---|
.containerBackground(.background, for: .widget) |
❌ コンパイルエラー | ✅ |
.background(Color(.systemBackground)) |
✅ | ✅ |
ゴール: iOS 16 と iOS 17 の両方でビルドが通り、ウィジェットが正常に動作する。
②サイズ(小・中・大)ごとに適したウィジェット表示ができない
当初のウィジェットは .systemSmall と .systemMedium で同一レイアウト(今月のページ数のみ)を使っており、Medium サイズの横長スペースを活かせていませんでした。また .systemLarge は未対応でした。
結果として、サイズ別の表示情報を以下のように設計しました。
| サイズ | 表示内容 |
|---|---|
| Small(≈ 155×155pt) | 連続読書日数(ストリーク) + 今日のページ数 |
| Medium(≈ 329×155pt) | 今日のページ数 / ストリーク / 今月のページ数 の3列グリッド |
| Large(≈ 329×345pt) | 今月のページ数(大)+ 3列グリッド + 読書中の本のタイトル・著者 |
ゴール: ユーザーが選んだサイズに応じて情報密度が最適化されたウィジェットを提供する。
どうやって解決したのか
①の解決
対応方法は2つあり、アプリ全体のサポート対象iOSバージョン方針に合わせて選択します。
方法1:#available 分岐で iOS 16/17 を両対応する
iOS 16 サポートを維持したまま対応する場合はこちらになります。swiftファイル記述している @ViewBuilder 拡張を追加し、#available(iOSApplicationExtension 17.0, *) でランタイム分岐させます。
// containerBackgroundを呼び出していた箇所で、
// 下記のwidgetBackgroundを呼び出す(置き換える)。
extension View {
@ViewBuilder
func widgetBackground() -> some View {
if #available(iOSApplicationExtension 17.0, *) {
containerBackground(.background, for: .widget)
} else {
background(Color(.systemBackground))
}
}
}
方法2:expo-target.config.js の deploymentTarget を "17.0" に上げる
iOS 16 のサポートを切ってよい場合はデプロイのターゲットを変更するだけで良いです。こちらは非常に単純で、上記の#available 分岐が不要になり、containerBackground をそのまま使えます。
// expo-target.config.js
module.exports = [
{
// ...
deploymentTarget: '17.0', // "16.0" → "17.0" に変更
// ...
},
];
まとめ
2つの方法のトレードオフをまとめると以下の通りです。
方法1:#available 分岐 |
方法2:deploymentTarget を上げる |
|
|---|---|---|
| iOS 16 サポート | ✅ 維持できる | ❌ 切り捨てになる |
| コードの複雑さ | △ 分岐が増える | ◎ シンプルなまま |
app.json の変更 |
不要 | 必要 |
②の解決:@Environment(\.widgetFamily) でサイズ分岐
PageWidgetView に @Environment(\.widgetFamily) を導入し、サイズごとにビューを切り替えました。
// Widget View (dispatcher)
struct PageWidgetView: View {
@Environment(\.widgetFamily) var widgetFamily
let entry: PageEntry
var body: some View {
switch widgetFamily {
case .systemSmall:
// 小サイズのウィジェット(個別に実装)
SmallWidgetView(entry: entry)
case .systemMedium:
// 中サイズのウィジェット(個別に実装)
MediumWidgetView(entry: entry)
case .systemLarge:
// 大サイズのウィジェット(個別に実装)
LargeWidgetView(entry: entry)
default:
SmallWidgetView(entry: entry)
}
}
}
// Widget
struct PageCountWidget: Widget {
let kind = "PageCountWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: PageProvider()) { entry in
PageWidgetView(entry: entry)
}
.configurationDisplayName("App Name")
.description("Description ...")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
【参考】表示されるウィジェット(小・中・大)
感想
最大の教訓はWidgetKit は「別アプリ」だと思って設計する、でした。ウィジェットの実装では「メインアプリと同じ感覚でデータを扱える」と思っていましたが、実際はウィジェットはメインアプリとは完全に独立した別プロセスであり、SQLiteやファイルシステムなども直接共有できませんでした。今回のウィジェットの実装は勉強になりました。引き続き個人開発進めていこうと思います。
最後までお読みいただきありがとうございました。