ウィジェットとは
iOS14からWidgetExtensionを利用してアプリ毎に用意されたWidgetをホーム画面のどの部分にもユーザーが自由に配置することができるようになりました。
詳しくは公式
iPhone や iPod touch でウィジェットを使う
導入方法
ここではSampleWidgetという名前にしました
このようなものができます
以下のファイルが自動生成されます
実行してみる
何も変更を加えずシミュレータで実行すると以下のようにウィジェットが出現します。
SampleWidget.swiftファイル内で定義されている
SampleWidgetEntryViewが表示されています。
struct SampleWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
Provider.Entryという構造体に定義されたdateという日時を入れた変数を表示するText型のViewです。
@main
struct SampleWidget: Widget {
let kind: String = "SampleWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
SampleWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
supportedFamiliesを追加してみました。
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
すると
デフォルトのSmallに加えてMidium, Largeの3種類のサイズを選択できるようになります。
アプリとの連携
ウィジェットをタップした時の処理
ウィジェットをタップするとアプリが起動します。
ViewにwidgetURLを実装することによってアプリ側でハンドリングすることができます。
.widgetURL(URL(string: "widget-link://"))
struct SampleWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
.widgetURL(URL(string: "widget-link://"))
}
}
アプリ側ではSceneDelegateもしくはSwiftUIのApp内onOpenURLを実装してハンドリングします。
// App launched
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _: UIWindowScene = scene as? UIWindowScene else { return }
maybeOpenedFromWidget(urlContexts: connectionOptions.urlContexts)
}
// App opened from background
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
maybeOpenedFromWidget(urlContexts: URLContexts)
}
private func maybeOpenedFromWidget(urlContexts: Set<UIOpenURLContext>) {
if let _: UIOpenURLContext = urlContexts.first(where: { $0.url.scheme == "widget-link" }) {
print("🚀 Launched from widget")
}
}
struct SampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { (url) in
print(url)
}
}
}
}
複数のリンク
systemSmallでは単一のURLしか利用できませんが、 systemMedium,systemLargeの場合はタップ領域によって複数のURLを定義できます。
その場合はwidgetURLを複数使うということはできないのでLinkを使います。
例として以下のようにします。
struct SampleWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
HStack{
Link("link 1", destination: URL(string: "widget-link://link1")!)
Link("link 2", destination: URL(string: "widget-link://link2")!)
}
}
}
これで上記のようにタップ領域ごとにハンドリングすることができます。
Widgetを更新する
Timelineという概念により実装側で更新頻度を設定する方法
アプリ側から強制的に更新をさせるという方法
があります。
しかしどちらにせよ実際の更新はOSによって制御されており、非常に短い間隔であったりリアルタイム性を持たせるであったり厳密な時間を定義した更新はできないようです。
こちらは自動生成されたデフォルトのSampleWidget.swiftのTimelineに関する実装の内容です。
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, 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, configuration: configuration)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
こちらの更新の要求が通ると、OS側から上記のgetTimelineが呼び出されるというイメージです。
上記の実装だと、getTimelineが呼び出された日時から1時間毎に5つのTimelineEntryを作成してTimelineとしています。
この時点でTimelineがシリアライズされてOS内に保持され、dateの日時になるとViewがレンダリングされるといったイメージになります。
let timeline = Timeline(entries: entries, policy: .atEnd)
このpolicyというのが肝になっています。
これはTimelineReloadPolicyとして定義されています。
public struct TimelineReloadPolicy : Equatable {
/// A policy that specifies that WidgetKit requests a new timeline after
/// the last date in a timeline passes.
public static let atEnd: TimelineReloadPolicy
/// A policy that specifies that the app prompts WidgetKit when a new
/// timeline is available.
public static let never: TimelineReloadPolicy
/// A policy that specifies a future date for WidgetKit to request a new
/// timeline.
public static func after(_ date: Date) -> TimelineReloadPolicy
atEnd → Timelineを全て経過した後に更新を要求する。
never → Timelineが経過しても更新しない
after(_ date:Date) → dateの日時が来たときに更新を要求する。
アプリ側から更新を要求する方法
WidgetCenterというクラスからアクセスします。
import WidgetKit
//~~~~~~~~~~~~~~
WidgetCenter.shared.reloadAllTimelines()
or
WidgetCenter.shared.reloadTimelines(ofKind: "SampleWidget")
//~~~~~~~~~~~~~~
終わりです
本当に触りだけの簡単な内容になってしまいましたが、個人的にはSwiftUIを全然触ったことがなくTimelineという概念の理解にも時間がかかってしまい記事の作成に苦労しました。
実際の使い方に関しては、例えばライブ配信系のアプリであれば直近の配信スケジュールを表示だったり、自分のフォロワー数とランキングの遷移を表示といったものが考えられると思います。
野球速報のアプリならばその日の試合日程や途中経過が表示されていてタップすればアプリ内の詳細な試合のデータのページにそのまま遷移することができる、といった感じです。
ユーザーの知りたい情報をアプリを起動せずともホーム画面に表示しておけるというのは非常に便利な機能だと思うので、アプリ開発の幅も広がったと感じました。
ありがとうございました。