LoginSignup
17
10

More than 3 years have passed since last update.

【iOS14】iOSアプリにウィジェットを導入する【WidgetKit】

Posted at

ウィジェットとは

iOS14からWidgetExtensionを利用してアプリ毎に用意されたWidgetをホーム画面のどの部分にもユーザーが自由に配置することができるようになりました。
詳しくは公式
iPhone や iPod touch でウィジェットを使う

導入方法

メニュー → File → New → Target
スクリーンショット 2020-10-01 18.30.43.png

Widget Extensionを選択 ターゲット名を入力
スクリーンショット 2020-10-01 18.31.04.png

ここではSampleWidgetという名前にしました
このようなものができます
スクリーンショット 2020-10-01 18.38.46.png
以下のファイルが自動生成されます
スクリーンショット 2020-10-01 18.38.54.png

実行してみる

何も変更を加えずシミュレータで実行すると以下のようにウィジェットが出現します。
スクリーンショット 2020-10-01 18.48.56.png

SampleWidget.swiftファイル内で定義されている
SampleWidgetEntryViewが表示されています。

SampleWidget.swift
struct SampleWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

Provider.Entryという構造体に定義されたdateという日時を入れた変数を表示するText型のViewです。

SampleWidget.swift
@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種類のサイズを選択できるようになります。
スクリーンショット 2020-10-02 12.02.17.pngスクリーンショット 2020-10-02 12.02.08.png

アプリとの連携

ウィジェットをタップした時の処理

ウィジェットをタップするとアプリが起動します。
ViewにwidgetURLを実装することによってアプリ側でハンドリングすることができます。
.widgetURL(URL(string: "widget-link://"))

SampleWidget.swift
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を実装してハンドリングします。

SceneDelegate.swift
   // 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")
        }
    }
SampleApp.swift
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を使います。
例として以下のようにします。

SampleWidget.swift
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")!)
        }
    }
}

スクリーンショット 2020-10-02 12.21.58.png

これで上記のようにタップ領域ごとにハンドリングすることができます。

Widgetを更新する

Timelineという概念により実装側で更新頻度を設定する方法
アプリ側から強制的に更新をさせるという方法
があります。

しかしどちらにせよ実際の更新はOSによって制御されており、非常に短い間隔であったりリアルタイム性を持たせるであったり厳密な時間を定義した更新はできないようです。

こちらは自動生成されたデフォルトのSampleWidget.swiftのTimelineに関する実装の内容です。

SampleWidget.swift
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という概念の理解にも時間がかかってしまい記事の作成に苦労しました。

実際の使い方に関しては、例えばライブ配信系のアプリであれば直近の配信スケジュールを表示だったり、自分のフォロワー数とランキングの遷移を表示といったものが考えられると思います。
野球速報のアプリならばその日の試合日程や途中経過が表示されていてタップすればアプリ内の詳細な試合のデータのページにそのまま遷移することができる、といった感じです。
ユーザーの知りたい情報をアプリを起動せずともホーム画面に表示しておけるというのは非常に便利な機能だと思うので、アプリ開発の幅も広がったと感じました。

ありがとうございました。

17
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
10