投稿の経緯
前回投稿したWidgetKitで天気予報アプリ作ってみた~位置情報取得&保存編~の続編です。
今回は「Widget Extension」を追加して「WidgetBundle」を設定するところまでを書こうと思います。
前回の記事を見てない方は先に↓こちら↓を確認してください。
開発環境
Swift 5.5
Xcode 13.2.1
サンプルプロジェクト
GitHubにPushしています。気になる方はご覧ください。
https://github.com/ken-sasaki-222/WeatherWidget
Widget Extensionの追加
File → New → Target → Widget Extensionを選択。今回はWidget
という名前で作成します。
Include Configuration Intentとは
Widget Extensionを追加するタイミングで「Include Configuration Intent」という項目が表示されます。こちらを有効にすることで、ウィジェットで表示する内容をユーザーが選択できるようになります。
有効にしていない場合は、ユーザーが表示内容を選択できないタイプのウィジェットになります。
選択できる状態を「IntentConfiguration」、選択できない状態を「StaticConfiguration」と言います。
今回は有効にしなくて大丈夫です。そのまま「Widget Extension」を追加しましょう。
Widgetファイルを分割する
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
@main
struct Widget: Widget {
let kind: String = "Widget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct Widget_Previews: PreviewProvider {
static var previews: some View {
WidgetEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
「Widget Extension」を追加するとこのようにWifget.swift
ファイルが作成されます。
これは好みの問題ですが、このファイルにまとめてEntryやViewを書いてしまうと見通しが悪くなるのでいくつかのファイルに分けたいと思います。
それでは順番に対応しましょう。
TimelineProvider
import WidgetKit
import SwiftUI
struct MediumWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> MediumWidgetEntryModel {
MediumWidgetEntryModel(date: Date())
}
func getSnapshot(in context: Context, completion: @escaping (MediumWidgetEntryModel) -> ()) {
let entry = MediumWidgetEntryModel(date: Date())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<MediumWidgetEntryModel>) -> ()) {
var entries: [MediumWidgetEntryModel] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = MediumWidgetEntryModel(date: entryDate)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
MediumWidgetProvider
ファイルを作成してコードを移行します。
ミディアムサイズのウィジェット専用の「TimelineProvider」です。このstructはウィジェットで扱うデータを定義して、タイムラインにエントリーします。
TimelineEntry
import WidgetKit
import SwiftUI
struct MediumWidgetEntryModel: TimelineEntry {
let date: Date
}
MediumWidgetEntryModel
ファイルを作成してコードを移行します。
ミディアムサイズのウィジェットのタイムラインに渡す「TimelineEntry型」のモデルとして扱います。
View
import WidgetKit
import SwiftUI
struct MediumWidgetView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text("Hello, World!")
}
}
struct MediumWidgetView_Previews: PreviewProvider {
static var previews: some View {
Group {
MediumWidgetView()
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .light)
MediumWidgetView()
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .dark)
}
}
}
WidgetEntryView
を削除して新しくMediumWidgetView
を作成します。ウィジェットのViewを管理します。
このタイミングで@Environment(\.colorScheme) var colorScheme
を定義して、プレビュー画面でダークモードとライトモード両方のViewを確認できるようにしておきましょう。後々便利です。
Widget
import WidgetKit
import SwiftUI
@main
struct MediumWidgetMaster: Widget {
let kind: String = "MediumWidgetMaster"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: MediumWidgetProvider()) { entry in
MediumWidgetView()
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemMedium])
}
}
新しくMediumWidgetMaster
ファイルを作成してコードを移行します。このタイミングで.supportedFamilies
でミディアムサイズを指定しておきましょう。
WidgetBundleの設定
コチラの記事と
公式ドキュメントがわかりやすいです。
「WidgetBundle」の中に複数のウィジェットを配置することで、異なるレイアウトのウィジェットが作りやすくなります。
現状はミディアムサイズのウィジェットのみの開発ですが、いつでも別サイズを追加できるように「WidgetBundle」を設定しておきましょう。
import WidgetKit
import SwiftUI
@main
struct WidgetBundle: SwiftUI.WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
MediumWidgetMaster()
}
}
これで設定完了です。WidgetBundleに@main
を付与して、WidgetMasterの@main
は外しておきましょう。
現在のファイル構成はこんな感じです。
ウィジェットとかWatchってTimeLineがあって構成が特殊だから、だいたいいつもこんな感じにまとめてます。
おすすめのまとめ方とかあれば教えてください!
ウィジェットをビルドする
ビルドターゲットを「Widget Extension」に変更してアプリをビルドしましょう。
ウィジェットが表示されれば開発準備OKです!
おわりに
今回はWidgetKitで天気予報アプリ作ってみた~Widget Extension追加編~について書きました。
次回はタイムライン作成の記事を書こうと思っています。
続きが気になる方は↓こちら↓から
ご覧いただきありがとうございました。
こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。
お知らせ
現在副業でiOSアプリ開発案件を募集しています。
Twitter DMでご依頼お待ちしております!
↓活動リンクはこちら↓
https://linktr.ee/sasaki.ken