SwiftUI - Widget機能を使ってみる

1. Widget機能を追加する

image

プロジェクトにWidget機能を追加するには、メニューの「File > New > Target」から「Widget Extention」を選択し、ファイルを新規作成します。

This scheme has been created for the “tryWidgetExtension” target. Choose Activate to use this scheme for building and debugging. Schemes can be chosen in the toolbar or Product menu.

「このスキームは、"ファイル名Extension "ターゲット用に作成されました。有効化を選択すると、ビルドやデバッグにこのスキームが使用されます。スキームはツールバーや製品メニューで選択できます」というコメントがでるので、Activateにします。

新規作成でWidgetのファルダが作成され、下記のファイルが入っています。

  • Widget名.swift
  • Widget名.intentdefinition
  • Assets.xcassets
  • Info.plist

下記が、Widgetのテンプレートファイルの中身で、現在の時間が出力されるという内容になっています。

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] = []
    
    //現在の日付を起点として、1時間間隔で5つのエントリーからなるタイムラインを生成
    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
}

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

@main
struct TestWidget: Widget {
  let kind: String = "TestWidget" //ウィジェットを識別する文字列
  
  var body: some WidgetConfiguration {
    IntentConfiguration(
      kind: kind,
      intent: ConfigurationIntent.self, //ユーザーが編集可能なパラメーターを含むカスタムSiriKitインテント定義
      provider: Provider() //ウィジェットを更新するためのタイムラインを決定するオブジェクト
    ) { entry in
      TestWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("My Widget") //ウィジェットの名前を設定
    .description("This is an example widget.") //ウィジェットの説明を設定
  }
}

struct TestWidget_Previews: PreviewProvider {
  static var previews: some View {
    TestWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
      .previewContext(WidgetPreviewContext(family: .systemSmall))
    //.systemSmall, .systemMedium, .systemLarge
  }
}

placeholder(in:) - ウィジェットのプレースホルダーバージョンを表すタイムラインエントリを提供
getSnapshot(for:in:completion:) - ウィジェットの現在の時刻と状態を表すタイムラインエントリを提供
getTimeline(for:in:completion:) - 現在の時刻と、オプションでウィジェットを更新する将来の時刻のタイムラインエントリの配列を提供
IntentConfiguration - ウィジェットのコンテンツを説明するオブジェクト

2. 他のファイルをウィジェットに呼び出す

image

ここでは、新規にShowWidgetViewというファイルを作成し、そのファイル内容をウィジェットに呼び出してみようと思います。そのまま呼び出すとエラーになるので、呼び出すためには、ShowWidgetViewの「Target Membership > ウィジェット名Extention」にチェックを入れると呼び出せるようになります。

image

//ShowWidgetView
struct ShowWidgetView: View {
  var body: some View {
    Text("Hello, Widget!")
  }
}
struct TestWidgetEntryView : View {
  var entry: Provider.Entry
  
  var body: some View {
    VStack {
      Text(entry.date, style: .time)
      ShowWidgetView() //<<<
    }
  }
}

3. ウィジェットがサポートするサイズを設定する

ウィジェットはデフォルトでは、SmallMediumLargeとユーザー側で選択できるようになっています。選択できるサイズを制限したい場合は下記を追加します。

@main
struct TestWidget: Widget {
  let kind: String = "TestWidget"
  
  var body: some WidgetConfiguration {
    IntentConfiguration(
      kind: kind,
      intent: ConfigurationIntent.self,
      provider: Provider() 
    ) { entry in
      TestWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("My Widget") 
    .description("This is an example widget.")
    .supportedFamilies([.systemSmall]) //<<< Smallサイズのみ選択可にしたい場合
    //.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
  }
}

supportedFamilies(_:) - ウィジェットがサポートするサイズを設定

4. ウィジェットサイズで条件分岐させたい場合

image

ウィジェットサイズで条件分岐させたい場合は、@Environment(\.widgetFamily)を使用すると条件分岐が可能になります。

struct TestWidgetEntryView : View {
  var entry: Provider.Entry
  @Environment(\.widgetFamily) var family: WidgetFamily //<<<
  
  var body: some View {
    VStack {
      Text(entry.date, style: .time)
      
      ContentView()
      //vvv
      switch family {
      case .systemSmall: Text("Small")
      case .systemMedium: Text("Medium")
      case .systemLarge: Text("Large")
      default: Text("default")
      }
      
    }
  }
}

Creating a Widget Extension
Keeping a Widget Up To Date
IntentConfiguration | Apple Developer Documentation
WidgetKit | Apple Developer Documentation
WidgetFamily | Apple Developer Documentation
placeholder(in:) | Apple Developer Documentation
getSnapshot(for:in:completion:) | Apple Developer Documentation
getTimeline(for:in:completion:) | Apple Developer Documentation