
【iOS14】WidgetKitを使ってみた!
2021.12.20
WidgetKitの実装方法をまとめてみた

(株)ライトコードの小林(こばやし)です!
iOS14からホーム画面にWidget機能が追加されましたね。
筆者自身気になってはいたものの実装したことがありませんでした!
そこで、この機会に色々触ってみつつ実装方法をまとめてみました。
開発環境
開発環境は以下のとおりです。
MacOS | 10.15.4 |
Swift | 5.3 |
Xcode | 12.0.1 |
Widget/WidgetKitとは
Widgetとは、iOS14から追加された機能であり、ホーム画面にアプリに関する情報を一目でわかる形で表示することができるものです。

Widget 自体は、SwiftUI でできた View であり、Timeline という機能によっては時間の経過とともに View が更新されていきます。
また、WidgetKit とはそれらの機能を実現するためのフレームワークで、これを用いて Widget を構築していきます。
Widgetの元となるアプリを実装する
それでは早速、Widget を実装していきましょう!
…と言いたいところですが、その前に Widget の元となるアプリが必要になるので、まずは事前準備として ToDo を表示するだけの簡単なアプリを作っていきます。
プロジェクト作成
Xcode のメニューバーから
File > New > Project...
を開いて、App を選択します。
プロジェクト名を入力し、Interface は SwiftUI、Life Cycle は SwiftUI App で作成します。
Preview を起動すると「Hello, World!」と表示される View が確認できます。
アプリ実装
次に、ToDo を表示するためのアプリの実装を行っていきます。
ディレクトリ構造は下記のようになっています。

今回の記事は Widget の実装がメインとなるので、このアプリの実装の説明は省略し、ソースコードのみを載せる形にします。
1 2 3 4 5 6 7 8 9 10 | import SwiftUI @main struct ToDoManagerApp: App { var body: some Scene { WindowGroup { ContentView() } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | import SwiftUI struct ContentView: View { @State var todo1Active: Bool = false @State var todo2Active: Bool = false @State var todo3Active: Bool = false var body: some View { NavigationView { List { NavigationLink( destination: DetailView(todo: .todo_1), isActive: $todo1Active) { TableRow(todo: .todo_1) } NavigationLink( destination: DetailView(todo: .todo_2), isActive: $todo2Active) { TableRow(todo: .todo_2) } NavigationLink( destination: DetailView(todo: .todo_3), isActive: $todo3Active) { TableRow(todo: .todo_3) } } .navigationBarTitle("ToDo List") .onOpenURL(perform: { (url) in todo1Active = url == ToDo.todo_1.url todo2Active = url == ToDo.todo_2.url todo3Active = url == ToDo.todo_3.url }) } } } struct TableRow: View { let todo: ToDo var body: some View { HStack { UserIcon(todo: todo) Overview(todo: todo) .padding() } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import SwiftUI struct DetailView: View { let todo: ToDo var body: some View { VStack { HStack { UserIcon(todo: todo) Overview(todo: todo) .padding() } Text("担当者: \(todo.user.name)").padding() Text("説明: \(todo.bio)").padding() } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import Foundation struct ToDo: Identifiable { var id: Int let title: String let user: User let deadline: String let url: URL let bio: String static let todo_1 = ToDo( id: 1, title: "素材を集める", user: User.wizard, deadline: "2020/10/8", url: URL(string: "todo:///todo_1")!, bio: "武器を作るための素材を集めます。" ) static let todo_2 = ToDo( id: 2, title: "武器を作る", user: User.fighter, deadline: "2020/10/10", url: URL(string: "todo:///todo_2")!, bio: "敵のボスを倒すために武器を作ります。" ) static let todo_3 = ToDo( id: 3, title: "敵のボスを倒す", user: User.hero, deadline: "2020/10/15", url: URL(string: "todo:///todo_3")!, bio: "近くの村を襲った敵のボスを倒しにいきます。" ) static let availableToDoList = [todo_1, todo_2, todo_3] } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import Foundation struct User: Identifiable { let id: String let name: String let icon: String static let hero = User( id: "hero", name: "勇者", icon: "🦸♂️" ) static let fighter = User( id: "fighter", name: "格闘家", icon: "👨🎤" ) static let wizard = User( id: "wizard", name: "魔法使い", icon: "🧙" ) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import SwiftUI struct UserIcon: View { var todo: ToDo var body: some View { ZStack { Circle().fill(Color.green) .frame(maxWidth: 50, maxHeight: 50) Text(todo.user.icon) .font(.largeTitle) .multilineTextAlignment(.center) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import SwiftUI struct Overview: View { var todo: ToDo var body: some View { VStack(alignment: .leading) { Text(todo.title) .font(.title) .fontWeight(.bold) .minimumScaleFactor(0.25) Text("締め切り: \(todo.deadline)") .minimumScaleFactor(0.5) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import SwiftUI struct ToDoView: View { var todo: ToDo var body: some View { HStack { VStack(alignment: .leading) { HStack { UserIcon(todo: todo) VStack { Text("締め切り") .minimumScaleFactor(0.1) .lineLimit(1) Text(todo.deadline) .minimumScaleFactor(0.1) .lineLimit(1) } } Text(todo.title) .font(.title) .fontWeight(.bold) .minimumScaleFactor(0.1) .lineLimit(1) } } .padding() } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import SwiftUI struct AllToDoListView: View { var body: some View { VStack(alignment: .leading, spacing: 48, content: { ForEach( ToDo.availableToDoList.sorted { $0.deadline.toDate() < $1.deadline.toDate() }) { todo in Link(destination: todo.url) { HStack { VStack(alignment: .leading) { HStack { UserIcon(todo: todo) VStack { Text("締め切り") .foregroundColor(.black) Text(todo.deadline) .minimumScaleFactor(0.1) .lineLimit(1) .foregroundColor(.black) } } } Text(todo.title) .font(.title) .fontWeight(.bold) .foregroundColor(.black) .minimumScaleFactor(0.1) .lineLimit(1) } } } }) } } |
1 2 3 4 5 6 7 8 9 10 11 12 | import Foundation extension String { func toDate() -> Date { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy/MM/dd" dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo") guard let date = dateFormatter.date(from: self) else { return Date()} return date } } |
Build&Run すると、下記のようなアプリが立ち上がります。

それでは、このアプリを使って Widget を作っていきましょう!
Widgetを実装する
アプリの準備が完了したので、Widget を実装していきます。
Widgetを追加する
まずは Widget を追加しましょう。
メニューバーから
File > New > Target...
を開いて「widget」を検索します。
ProductName 等を入力して、Finish を押すとプロジェクト配下に新しく Widget用ディレクトリが追加されます。
ToDoManagerWidget.swift を開いて Preview を見てみると現在時刻を確認できる Widget が表示されます。
また、作成した WidgetExtension を下記画像のように Model と View の Target Membership に追加します。
Widgetを実装する
それではアプリ用の Widget を実装していきましょう!
まずは、 configurationDisplayName と description を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @main struct ToDoManagerWidget: Widget { let kind: String = "ToDoManagerWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in ToDoManagerWidgetEntryView(entry: entry) } // Widget設定画面でのタイトル .configurationDisplayName("ToDoの詳細") // Widget設定画面での説明文 .description("ToDoの詳細を確認することができます") } } |
これらで設定した値は、Widget選択時に表示されるタイトルと説明文になります。

次は、Widget に表示する View 周りを実装していきます。
SimpleEntity に対し、View に渡したいデータ項目(今回の場合はToDo)を追加します。
1 2 3 4 5 | struct SimpleEntry: TimelineEntry { let date: Date // 追加 let todo: ToDo } |
SimpleEntity を使用している箇所で引数不足のエラーが出てくるので、それぞれ引数を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { // 引数追加 SimpleEntry(date: Date(), todo: .todo_1) } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { // 引数追加 let entry = SimpleEntry(date: Date(), todo: .todo_1) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { // 引数追加 let entries: [SimpleEntry] = [SimpleEntry(date: Date(), todo: .todo_1)] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } |
続いて、EntryView で表示したい View を body に追加します。
1 2 3 4 5 6 7 8 | struct ToDoManagerWidgetEntryView : View { var entry: Provider.Entry var body: some View { // 事前に作成したToDoViewを設定 ToDoView(todo: entry.todo) } } |
この状態で一度実行してみましょう!

いい感じですね!
Widgetのサポートサイズを設定する
サポートできるサイズには systemSmall 、 systemMedium 、 systemLarge の3つがあります。
また、 systemSmallはタップ領域が一つだけですが、 systemMediumと systemLargeは複数設置できたりします。
デフォルトではすべてのサイズがサポートされている状態ですが、下記画像のように systemLargeサイズだと余白が多くもったいない感じがします。

別の View を使って表示したいのでここでは systemSmallと systemMediumのみをサポートするように設定します。
まずは、supportFamilies の設定を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @main struct ToDoManagerWidget: Widget { let kind: String = "ToDoManagerWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in ToDoManagerWidgetEntryView(entry: entry) } .configurationDisplayName("ToDoの詳細") .description("ToDoの詳細を確認することができます") // サポートするサイズを設定 .supportedFamilies([.systemSmall, .systemMedium]) } } |
次に、WidgetKit が提供している widgetFamily の環境変数を利用して、 systemSmallでの表示と systemMediumでの表示を少し変えてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | struct ToDoManagerWidgetEntryView : View { var entry: Provider.Entry @Environment(\.widgetFamily) var family @ViewBuilder var body: some View { switch family { case .systemSmall: ZStack { ToDoView(todo: entry.todo) } .widgetURL(entry.todo.url) default: ZStack { HStack(alignment: .center) { ToDoView(todo: entry.todo) Text(entry.todo.bio) .padding() } .padding() .widgetURL(entry.todo.url) } } } |
これで Build&Run してみると…

systemMediumサイズもいい感じになりました!
Widgetに表示するデータを変更できるようにする
これまでは表示しているデータは魔法使いの ToDo である「素材を集める」しか表示してませんでしたが、他のユーザーのToDoにも表示切り替えができるようにしたいですね。
選択できるようにするには IntentDefinition を追加する必要があります。
まずは、File > New > File... で Intent を検索して、「SiriKit Intent Definition File」を選択し追加します。
作成した intentdefinitiontarget ファイルの Target Membership に Widget が含まれていることを確認します。
次に、intentdefinitiontarget ファイルを開き左下の+ボタンを押して New Enum で Enum を追加し、下記のように編集します。
同様に+ボタンを押して New Intent で Intent を追加し、下記のように編集します。
続いて、Widget を編集していきます。
まず、StaticConfiguration をIntentConfiguration に変更し、intent の引数に先程作成した UserSelectionIntent を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @main struct ToDoManagerWidget: Widget { let kind: String = "ToDoManagerWidget" var body: some WidgetConfiguration { // IntentConfigurationに変更 IntentConfiguration(kind: kind, intent: UserSelectionIntent.self, provider: Provider()) { entry in ToDoManagerWidgetEntryView(entry: entry) } .configurationDisplayName("ToDoの詳細") .description("ToDoの詳細を確認することができます") .supportedFamilies([.systemSmall, .systemMedium]) } } |
IntentConfiguration を使うには IntentTimeProvider が必要なので、Provider の TimelineProviderをIntentTimeProvider に変更します。
また、選択したユーザーのToDoが表示されるように修正を加えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | struct Provider: IntentTimelineProvider { typealias Entry = SimpleEntry typealias Intent = UserSelectionIntent func todo(for configuration: UserSelectionIntent) -> ToDo { switch configuration.user { case .hero: return .todo_3 case .fighter: return .todo_2 case .wizard: return .todo_1 default: return .todo_1 } } func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), todo: .todo_1) } func getSnapshot(for configuration: UserSelectionIntent, in context: Context, completion: @escaping (SimpleEntry) -> Void) { let entry = SimpleEntry(date: Date(), todo: .todo_1) completion(entry) } func getTimeline(for configuration: UserSelectionIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) { let selectedToDo = todo(for: configuration) let entries: [SimpleEntry] = [SimpleEntry(date: Date(), todo: selectedToDo)] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } |
Build&Runしてみると…

ユーザー選択ができるようになりましたね!
WidgetBundleを設定する
最後に、サポートしていなかった systemLargeサイズの Widget を作成していきましょう。
新しくファイルを作成し systemLargeサイズ用の Widget を実装します。
実装内容は今までの説明と同様です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import WidgetKit import SwiftUI struct LeaderboardProvider: TimelineProvider { func placeholder(in context: Context) -> LeaderboardEntry { LeaderboardEntry(date: Date()) } func getSnapshot(in context: Context, completion: @escaping (LeaderboardEntry) -> ()) { let entry = LeaderboardEntry(date: Date()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let entries: [LeaderboardEntry] = [LeaderboardEntry(date: Date())] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } struct LeaderboardEntry: TimelineEntry { let date: Date } struct LeaderboardWidgetEntryView : View { var entry: LeaderboardProvider.Entry var body: some View { AllToDoListView() .padding() } } @main struct LeaderboardWidget: Widget { let kind: String = "LeaderboardWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: LeaderboardProvider()) { entry in LeaderboardWidgetEntryView(entry: entry) } .configurationDisplayName("ToDoの詳細") .description("3人分のToDoの詳細を確認することができます") .supportedFamilies([.systemLarge]) } } |
これで systemLargeサイズの Widget ができたので、実行してみましょう!…と言いたいところですが、このままでは実行できません。
なぜなら、複数の Widgetに@main を設定することはできないからです。
このような場合は、WidgetBundle を設定する必要があります。
下記のように Bundle を作成し@main を設定することで、一つの @main で事足りるようになります。
1 2 3 4 5 6 7 8 | @main struct ToDoBundle: WidgetBundle { @WidgetBundleBuilder var body: some Widget { ToDoManagerWidget() LeaderboardWidget() } } |
これで、Build&Run を実行すると…

ちゃんと systemLargeサイズの Widget が表示されましたね!
さいごに
今回は WidgetKit を使ってアプリに Widget 機能を追加してみました。
今回は静的なデータを表示するのみでしたが、ある一定の時間になったらWidgetを更新したり、アプリ内から更新イベントを受け取って Widget を更新したりなどもできます。
この記事が WidgetKit を使った実装への取っ掛かりになったら嬉しいです!
(自分もWidget機能のあるアプリを作ってリリースしてみよう…!)
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来