【iOS14】WidgetKitを使ってみた!
IT技術
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 の実装がメインとなるので、このアプリの実装の説明は省略し、ソースコードのみを載せる形にします。
1import SwiftUI
2
3@main
4struct ToDoManagerApp: App {
5 var body: some Scene {
6 WindowGroup {
7 ContentView()
8 }
9 }
10}
1import SwiftUI
2
3struct ContentView: View {
4
5 @State var todo1Active: Bool = false
6 @State var todo2Active: Bool = false
7 @State var todo3Active: Bool = false
8
9 var body: some View {
10 NavigationView {
11 List {
12 NavigationLink(
13 destination: DetailView(todo: .todo_1), isActive: $todo1Active) {
14 TableRow(todo: .todo_1)
15 }
16 NavigationLink(
17 destination: DetailView(todo: .todo_2), isActive: $todo2Active) {
18 TableRow(todo: .todo_2)
19 }
20 NavigationLink(
21 destination: DetailView(todo: .todo_3), isActive: $todo3Active) {
22 TableRow(todo: .todo_3)
23 }
24 }
25 .navigationBarTitle("ToDo List")
26 .onOpenURL(perform: { (url) in
27 todo1Active = url == ToDo.todo_1.url
28 todo2Active = url == ToDo.todo_2.url
29 todo3Active = url == ToDo.todo_3.url
30 })
31 }
32 }
33}
34
35struct TableRow: View {
36 let todo: ToDo
37 var body: some View {
38 HStack {
39 UserIcon(todo: todo)
40 Overview(todo: todo)
41 .padding()
42 }
43 }
44}
1import SwiftUI
2
3struct DetailView: View {
4 let todo: ToDo
5 var body: some View {
6 VStack {
7 HStack {
8 UserIcon(todo: todo)
9 Overview(todo: todo)
10 .padding()
11 }
12 Text("担当者: \(todo.user.name)").padding()
13 Text("説明: \(todo.bio)").padding()
14 }
15 }
16}
1import Foundation
2
3struct ToDo: Identifiable {
4 var id: Int
5 let title: String
6 let user: User
7 let deadline: String
8 let url: URL
9 let bio: String
10
11 static let todo_1 = ToDo(
12 id: 1,
13 title: "素材を集める",
14 user: User.wizard,
15 deadline: "2020/10/8",
16 url: URL(string: "todo:///todo_1")!,
17 bio: "武器を作るための素材を集めます。"
18 )
19
20 static let todo_2 = ToDo(
21 id: 2,
22 title: "武器を作る",
23 user: User.fighter,
24 deadline: "2020/10/10",
25 url: URL(string: "todo:///todo_2")!,
26 bio: "敵のボスを倒すために武器を作ります。"
27 )
28
29 static let todo_3 = ToDo(
30 id: 3,
31 title: "敵のボスを倒す",
32 user: User.hero,
33 deadline: "2020/10/15",
34 url: URL(string: "todo:///todo_3")!,
35 bio: "近くの村を襲った敵のボスを倒しにいきます。"
36 )
37
38 static let availableToDoList = [todo_1, todo_2, todo_3]
39}
1import Foundation
2
3struct User: Identifiable {
4 let id: String
5 let name: String
6 let icon: String
7
8 static let hero = User(
9 id: "hero",
10 name: "勇者",
11 icon: "🦸♂️"
12 )
13
14 static let fighter = User(
15 id: "fighter",
16 name: "格闘家",
17 icon: "👨🎤"
18 )
19
20 static let wizard = User(
21 id: "wizard",
22 name: "魔法使い",
23 icon: "🧙"
24 )
25}
1import SwiftUI
2
3struct UserIcon: View {
4 var todo: ToDo
5 var body: some View {
6 ZStack {
7 Circle().fill(Color.green)
8 .frame(maxWidth: 50, maxHeight: 50)
9 Text(todo.user.icon)
10 .font(.largeTitle)
11 .multilineTextAlignment(.center)
12 }
13 }
14}
1import SwiftUI
2
3struct Overview: View {
4 var todo: ToDo
5 var body: some View {
6 VStack(alignment: .leading) {
7 Text(todo.title)
8 .font(.title)
9 .fontWeight(.bold)
10 .minimumScaleFactor(0.25)
11 Text("締め切り: \(todo.deadline)")
12 .minimumScaleFactor(0.5)
13 }
14 }
15}
1import SwiftUI
2
3struct ToDoView: View {
4 var todo: ToDo
5 var body: some View {
6 HStack {
7 VStack(alignment: .leading) {
8 HStack {
9 UserIcon(todo: todo)
10 VStack {
11 Text("締め切り")
12 .minimumScaleFactor(0.1)
13 .lineLimit(1)
14 Text(todo.deadline)
15 .minimumScaleFactor(0.1)
16 .lineLimit(1)
17 }
18 }
19 Text(todo.title)
20 .font(.title)
21 .fontWeight(.bold)
22 .minimumScaleFactor(0.1)
23 .lineLimit(1)
24 }
25 }
26 .padding()
27 }
28}
1import SwiftUI
2
3struct AllToDoListView: View {
4 var body: some View {
5 VStack(alignment: .leading, spacing: 48, content: {
6 ForEach(
7 ToDo.availableToDoList.sorted { $0.deadline.toDate() < $1.deadline.toDate() }) { todo in
8 Link(destination: todo.url) {
9 HStack {
10 VStack(alignment: .leading) {
11 HStack {
12 UserIcon(todo: todo)
13 VStack {
14 Text("締め切り")
15 .foregroundColor(.black)
16 Text(todo.deadline)
17 .minimumScaleFactor(0.1)
18 .lineLimit(1)
19 .foregroundColor(.black)
20 }
21 }
22 }
23 Text(todo.title)
24 .font(.title)
25 .fontWeight(.bold)
26 .foregroundColor(.black)
27 .minimumScaleFactor(0.1)
28 .lineLimit(1)
29 }
30 }
31 }
32 })
33 }
34}
1import Foundation
2
3extension String {
4 func toDate() -> Date {
5 let dateFormatter = DateFormatter()
6 dateFormatter.dateFormat = "yyyy/MM/dd"
7 dateFormatter.locale = Locale(identifier: "en_US_POSIX")
8 dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
9 guard let date = dateFormatter.date(from: self) else { return Date()}
10 return date
11 }
12}
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@main
2struct ToDoManagerWidget: Widget {
3 let kind: String = "ToDoManagerWidget"
4
5 var body: some WidgetConfiguration {
6 StaticConfiguration(kind: kind, provider: Provider()) { entry in
7 ToDoManagerWidgetEntryView(entry: entry)
8 }
9 // Widget設定画面でのタイトル
10 .configurationDisplayName("ToDoの詳細")
11 // Widget設定画面での説明文
12 .description("ToDoの詳細を確認することができます")
13 }
14}
これらで設定した値は、Widget選択時に表示されるタイトルと説明文になります。
次は、Widget に表示する View 周りを実装していきます。
SimpleEntity に対し、View に渡したいデータ項目(今回の場合はToDo)を追加します。
1struct SimpleEntry: TimelineEntry {
2 let date: Date
3 // 追加
4 let todo: ToDo
5}
SimpleEntity を使用している箇所で引数不足のエラーが出てくるので、それぞれ引数を追加します。
1struct Provider: TimelineProvider {
2 func placeholder(in context: Context) -> SimpleEntry {
3 // 引数追加
4 SimpleEntry(date: Date(), todo: .todo_1)
5 }
6
7 func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
8 // 引数追加
9 let entry = SimpleEntry(date: Date(), todo: .todo_1)
10 completion(entry)
11 }
12
13 func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
14 // 引数追加
15 let entries: [SimpleEntry] = [SimpleEntry(date: Date(), todo: .todo_1)]
16
17 let timeline = Timeline(entries: entries, policy: .atEnd)
18 completion(timeline)
19 }
20}
続いて、EntryView で表示したい View を body に追加します。
1struct ToDoManagerWidgetEntryView : View {
2 var entry: Provider.Entry
3
4 var body: some View {
5 // 事前に作成したToDoViewを設定
6 ToDoView(todo: entry.todo)
7 }
8}
この状態で一度実行してみましょう!
いい感じですね!
Widgetのサポートサイズを設定する
サポートできるサイズにはsystemSmall 、systemMedium 、systemLarge の3つがあります。
また、systemSmallはタップ領域が一つだけですが、systemMediumとsystemLargeは複数設置できたりします。
デフォルトではすべてのサイズがサポートされている状態ですが、下記画像のようにsystemLargeサイズだと余白が多くもったいない感じがします。
別の View を使って表示したいのでここではsystemSmallとsystemMediumのみをサポートするように設定します。
まずは、supportFamilies の設定を追加します。
1@main
2struct ToDoManagerWidget: Widget {
3 let kind: String = "ToDoManagerWidget"
4
5 var body: some WidgetConfiguration {
6 StaticConfiguration(kind: kind, provider: Provider()) { entry in
7 ToDoManagerWidgetEntryView(entry: entry)
8 }
9 .configurationDisplayName("ToDoの詳細")
10 .description("ToDoの詳細を確認することができます")
11 // サポートするサイズを設定
12 .supportedFamilies([.systemSmall, .systemMedium])
13 }
14}
次に、WidgetKit が提供している widgetFamily の環境変数を利用して、systemSmallでの表示とsystemMediumでの表示を少し変えてみます。
1struct ToDoManagerWidgetEntryView : View {
2 var entry: Provider.Entry
3
4 @Environment(\.widgetFamily) var family
5
6 @ViewBuilder
7 var body: some View {
8 switch family {
9 case .systemSmall:
10 ZStack {
11 ToDoView(todo: entry.todo)
12 }
13 .widgetURL(entry.todo.url)
14 default:
15 ZStack {
16 HStack(alignment: .center) {
17 ToDoView(todo: entry.todo)
18 Text(entry.todo.bio)
19 .padding()
20 }
21 .padding()
22 .widgetURL(entry.todo.url)
23 }
24 }
25 }
これで 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@main
2struct ToDoManagerWidget: Widget {
3 let kind: String = "ToDoManagerWidget"
4
5 var body: some WidgetConfiguration {
6 // IntentConfigurationに変更
7 IntentConfiguration(kind: kind, intent: UserSelectionIntent.self, provider: Provider()) { entry in
8 ToDoManagerWidgetEntryView(entry: entry)
9 }
10 .configurationDisplayName("ToDoの詳細")
11 .description("ToDoの詳細を確認することができます")
12 .supportedFamilies([.systemSmall, .systemMedium])
13 }
14}
IntentConfiguration を使うには IntentTimeProvider が必要なので、Provider の TimelineProviderをIntentTimeProvider に変更します。
また、選択したユーザーのToDoが表示されるように修正を加えます。
1struct Provider: IntentTimelineProvider {
2 typealias Entry = SimpleEntry
3
4 typealias Intent = UserSelectionIntent
5
6 func todo(for configuration: UserSelectionIntent) -> ToDo {
7 switch configuration.user {
8 case .hero:
9 return .todo_3
10 case .fighter:
11 return .todo_2
12 case .wizard:
13 return .todo_1
14 default:
15 return .todo_1
16 }
17 }
18
19 func placeholder(in context: Context) -> SimpleEntry {
20 SimpleEntry(date: Date(), todo: .todo_1)
21 }
22
23 func getSnapshot(for configuration: UserSelectionIntent, in context: Context, completion: @escaping (SimpleEntry) -> Void) {
24 let entry = SimpleEntry(date: Date(), todo: .todo_1)
25 completion(entry)
26 }
27
28 func getTimeline(for configuration: UserSelectionIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
29 let selectedToDo = todo(for: configuration)
30 let entries: [SimpleEntry] = [SimpleEntry(date: Date(), todo: selectedToDo)]
31
32 let timeline = Timeline(entries: entries, policy: .atEnd)
33 completion(timeline)
34 }
35}
Build&Runしてみると…
ユーザー選択ができるようになりましたね!
WidgetBundleを設定する
最後に、サポートしていなかったsystemLargeサイズの Widget を作成していきましょう。
新しくファイルを作成しsystemLargeサイズ用の Widget を実装します。
実装内容は今までの説明と同様です。
1import WidgetKit
2import SwiftUI
3
4struct LeaderboardProvider: TimelineProvider {
5 func placeholder(in context: Context) -> LeaderboardEntry {
6 LeaderboardEntry(date: Date())
7 }
8
9 func getSnapshot(in context: Context, completion: @escaping (LeaderboardEntry) -> ()) {
10 let entry = LeaderboardEntry(date: Date())
11 completion(entry)
12 }
13
14 func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
15 let entries: [LeaderboardEntry] = [LeaderboardEntry(date: Date())]
16
17 let timeline = Timeline(entries: entries, policy: .atEnd)
18 completion(timeline)
19 }
20}
21
22struct LeaderboardEntry: TimelineEntry {
23 let date: Date
24}
25
26struct LeaderboardWidgetEntryView : View {
27 var entry: LeaderboardProvider.Entry
28
29 var body: some View {
30 AllToDoListView()
31 .padding()
32 }
33}
34
35@main
36struct LeaderboardWidget: Widget {
37 let kind: String = "LeaderboardWidget"
38
39 var body: some WidgetConfiguration {
40 StaticConfiguration(kind: kind, provider: LeaderboardProvider()) { entry in
41 LeaderboardWidgetEntryView(entry: entry)
42 }
43 .configurationDisplayName("ToDoの詳細")
44 .description("3人分のToDoの詳細を確認することができます")
45 .supportedFamilies([.systemLarge])
46 }
47}
これでsystemLargeサイズの Widget ができたので、実行してみましょう!…と言いたいところですが、このままでは実行できません。
なぜなら、複数の Widgetに@main を設定することはできないからです。
このような場合は、WidgetBundle を設定する必要があります。
下記のように Bundle を作成し@main を設定することで、一つの @main で事足りるようになります。
1@main
2struct ToDoBundle: WidgetBundle {
3 @WidgetBundleBuilder
4 var body: some Widget {
5 ToDoManagerWidget()
6 LeaderboardWidget()
7 }
8}
これで、Build&Run を実行すると…
ちゃんとsystemLargeサイズの Widget が表示されましたね!
さいごに
今回は WidgetKit を使ってアプリに Widget 機能を追加してみました。
今回は静的なデータを表示するのみでしたが、ある一定の時間になったらWidgetを更新したり、アプリ内から更新イベントを受け取って Widget を更新したりなどもできます。
この記事が WidgetKit を使った実装への取っ掛かりになったら嬉しいです!
(自分もWidget機能のあるアプリを作ってリリースしてみよう…!)
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.08.14スマホ技術 特集Android開発Android開発をJavaからKotlinへ変えていくためのお勉強DelegatedPropert...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit