これはフェンリル デザインとテクノロジー Advent Calendar2024 20日目の記事です。
はじめに
👦「ウィジェットの実装、ハードルが高くないですか?」
🧒「アプリ本体部分とは概念が異なるため、抽象的に理解することすら難しい」
👨「iOSやXcodeのアップデートがあるたびに、異なる実装が提供されてしまい、どんどん種類が増えて正解がわからない」
そんなみなさんに向けて、「実はそんなことないんだよ」と優しく背中を押したいという気持ちを込めて書きました。
本記事では、理論よりも具体的な実装に焦点を当てることで、動かしながら理解してもらうことを目的としています。
具体的には、プロジェクトファイルの作成から動的な設定が可能なウィジェットの実装までを行います。
1. プロジェクトファイルを作成
まず、ウィジェット練習用のプロジェクトファイルを作成します。
特別な設定は必要ありません。
2. Targetを追加
ウィジェットはアプリ本体とは別のプロセスで実行されるので、Targetを別で作成する必要があります。
File
→ New
→ Target
→ iOS
→ Widget Extension
と進みます。
下記のようにウィジェット用Targetを作成しましょう。
Widget Extension
を作成すると、下記のファイルが生成されます。
-
AppIntent
: アプリの特定の機能や操作を定義するためのファイルです。これにより、ウィジェットやショートカットから直接アプリの機能を呼び出すことが可能になります。 -
Widget
: ウィジェットの外観や動作を定義するファイルです。SwiftUIを使用して、ウィジェットのUIや表示するデータを構築します。 -
WidgetBundle
: 複数のウィジェットをまとめて提供するためのファイルです。これにより、ユーザーはアプリから複数のウィジェットを一度に利用できるようになります。
Simulator上で実行してみると、すでにウィジェットが追加されていることがわかります(簡単😃)。
エラーが出た場合
もし、ここで“~Widget” failed to launch or exited …
エラーが出る場合は、証明書の設定を見直してみてください。
Targets → Signing & Capabilities で警告が出ているかを確認してみましょう。
3. ウィジェットの見た目を変更
ウィジェットの画面はWidget
ファイル内にViewで定義されており、SwiftUIを用いたレイアウトが可能です。
試しに見た目を少し変更してみましょう。
まず、時刻表示を消してスッキリさせます。
struct EmojiWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
- Text("Time:")
- Text(entry.date, style: .time)
-
Text("Favorite Emoji:")
Text(entry.configuration.favoriteEmoji)
}
}
}
次に、背景色を変更してみましょう。
Viewに付与されている.containerBackground
モディファイアの第一引数を任意の色に変更すればOKです。
struct EmojiWidget: Widget {
let kind: String = "EmojiWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
EmojiWidgetEntryView(entry: entry)
- .containerBackground(.fill.tertiary, for: .widget)
+ .containerBackground(.orange.opacity(0.4), for: .widget)
}
}
}
変更前 | 変更後 |
---|---|
ちなみに、任意のView
を割り当てることもできます。
下記の例では、絵文字の画像に白い半透明のView
を重ねたものを背景としています。
struct EmojiWidget: Widget {
let kind: String = "EmojiWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
EmojiWidgetEntryView(entry: entry)
.containerBackground(for: .widget) {
Image("emoji")
.resizable()
.aspectRatio(contentMode: .fill)
.overlay {
Color.white.opacity(0.6)
}
}
}
}
}
4. ウィジェットの動的な設定を追加
iOS16から、ウィジェットの設定をユーザーが変更できるようになりました。みなさんは使ったことはありますか?(私はありませんでした)
実は、手順2を終えた時点で既にその機能を作成できています。
ホーム画面の「ウィジェットを編集」から、表示する絵文字を変更してみましょう。
ウィジェットの要素を追加するには、AppIntent
構造体内に@Parameter
をつけた変数を追加すればOKです。
試しに、ウィジェットの背景色をStringで定義してみましょう。
AppIntent
で定義したプロパティは、entry.configuration.{プロパティ}
の形式で取得することができます(詳細は割愛)。
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Configuration" }
static var description: IntentDescription { "This is an example widget." }
// An example configurable parameter.
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
// 追加
@Parameter(title: "Background Color", default: "White")
var backgroundColor: String
}
struct EmojiWidget: Widget {
let kind: String = "EmojiWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
EmojiWidgetEntryView(entry: entry)
.containerBackground(backgroundColor(text: entry.configuration.backgroundColor), for: .widget)
}
}
private func backgroundColor(text: String) -> Color {
switch text {
case "White":
return .white
case "Orange":
return .orange.opacity(0.4)
default:
return .clear
}
}
}
実行すると、Orange
と入力すると背景がオレンジ色になり、White
と入力すると白色に戻ることが確認できます。
5. Picker表示の追加
ここまではウィジェットの設定を文字列で変更していましたが、実は他にも様々な形式が用意されています。
最後に使用頻度の高いPicker表示について解説します。
Picker表示を行うには、@Parameter
を持つプロパティをAppEnum
プロトコルに準拠させたEnum
形式で定義する必要があります。
AppEnum
とは、Enum
形式で定義されたアプリの機能をAppIntent
を通してユーザーに提供する際に定義されるプロトコルです。
つまり、こういうことだと解釈しています。
-
単なるEnumだと、情報が足りない
Picker表示する際のタイトルやアイコン画像などの情報がないため、プロトコルによって追加したい
-
AppIntent側で情報を統一したい
AppEnumはウィジェット以外にもSiriやショートカットで使用されるため、プロトコルによって統一した情報が欲しい
詳しくは公式の説明をご覧ください。
それでは、ウィジェットの背景色をPickerで変更する処理を実装してみましょう。
まず、背景色のデータを持つEnum
を作成します。
extension ConfigurationAppIntent {
enum BackgroundColorPicker {
case white, orange
var color: Color {
switch self {
case .white:
return .white
case .orange:
return .orange.opacity(0.4)
}
}
}
}
その後、AppEnum
に準拠させます。
extension ConfigurationAppIntent {
enum BackgroundColorPicker: String, AppEnum {
case white, orange
+ static var typeDisplayRepresentation: TypeDisplayRepresentation {
+ TypeDisplayRepresentation(name: "背景色", numericFormat: "color")
+ }
+
+
+ static var caseDisplayRepresentations: [ConfigurationAppIntent.BackgroundColorPicker : DisplayRepresentation] {
+ [
+ .white: DisplayRepresentation(title: "white", subtitle: "白になります", image: .init(systemName: "snowflake")),
+ .orange: DisplayRepresentation(title: "orange", subtitle: "オレンジになります", image: .init(systemName: "carrot"))
+ ]
+ }
+
var color: Color {
switch self {
case .white:
return .white
case .orange:
return .orange.opacity(0.4)
}
}
}
}
AppEnum
に準拠させるには、下記2つのプロパティを定義する必要があります。
-
typeDisplayRepresentation
: 列挙型全体の名前を指定します。ユーザーがショートカットアプリでパラメータを選択する際に、この名前が表示されます。 -
caseDisplayRepresentations
: 各列挙型のケースに対応する表示名を指定する辞書型配列を返します。ユーザーが特定の値を選択する際に、これらの表示名が使用されます。
その後、以前の手順と同様にプロパティを書き換えます。
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Configuration" }
static var description: IntentDescription { "This is an example widget." }
// An example configurable parameter.
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
@Parameter(title: "Background Color", default: .white) // デフォルト値を変更
var backgroundColor: BackgroundColorPicker // 型を変更
}
struct EmojiWidget: Widget {
let kind: String = "EmojiWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
EmojiWidgetEntryView(entry: entry)
.containerBackground(entry.configuration.backgroundColor.color, for: .widget) // 変更
}
}
// backgroundColorメソッドを削除
}
これらを変更した後に実行し、ウィジェットの編集を行う際にPicker表示が行われたら成功です👏
ウィジェットの編集 | Picker表示 |
---|---|
おわりに
以上が最新版の実装についてでした(2024年12月時点)。今までいろいろな種類の実装を探してきた方も、まずは本記事を基に試してみてはいかがでしょうか。
ここまで画像やコードをたくさん挿入したため、手順が多く見えるかもしれませんが、内容はとても簡単です。慣れてしまえば10分程度で実装できてしまうと思います。
わかりやすいとのお声があれば、次回以降はインタラクティブなウィジェット(ウィジェット自体をタップしてアクションが実行されるもの)の実装について記事を書きたいと思います。
※ サムネにしたいと思って作成したが、記事の内容と異なるため泣く泣く下に追いやられたホーム画面