Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

【iOS】ミラティブにウィジェット機能を実装した際の開発Tips

こんにちは、iOS エンジニアの千吉良です。iOS14 にはウィジェット機能が新しく搭載*1されて、アプリ側で対応をすることで iOS 端末のホーム画面に独自のウィジェットを置けるようになりました。ミラティブでも、たまにはオシャレしたいよねということで昨年ウィジェット機能に対応しました。まだまだ対応しているアプリは少ないですが、ホーム画面に置いておくとアプリへの愛着も増すし、 SwiftUI での開発が経験できて今後対応していくであろう新しい開発環境を経験できるという点にもメリットがあります。今回はミラティブで導入したウィジェット機能について、いくつかの実装に触れてご紹介します。

ウィジェットの機能

ミラティブのウィジェット上では、フォローしている友だちの最新の配信情報を確認することができます。ウィジェット上にはユーザーのアイコンと名前が並び、アイコン上に設置されているマークによって対象の配信が配信中かどうかを知ることができます。ウィジェット上の情報は定期的、またはアプリを操作した際に更新されるので、アプリを開かなくても端末のホーム画面で新着の配信情報を確認することができます。

f:id:naru-jpn:20210115140755p:plain
ウィジェット機能の概要

ウィジェットの種類

f:id:naru-jpn:20210115143512p:plain
Small サイズと Medium サイズ

ウィジェットは Large, Medium, Small の3種類のサイズを作成することができます。その中でも一部のサイズにのみ対応するということができて、ミラティブでは Medium, Small サイズのウィジェットに対応しています。

ウィジェットの構成を表す WidgetConfiguration には2種類あって、情報を表示するだけの StaticConfiguration とユーザーがウィジェットをカスタマイズできる*2 IntentConfiguration とがありますが、ミラティブのウィジェットでは配信情報を表示するのみなので StaticConfiguration を選択しています。

@main
struct WidgetMain: Widget {
  var body: some WidgetConfiguration {
    // StaticConfiguration を選択
    StaticConfiguration(kind: kind, provider: Provider()) { entry in
      WidgetEntryView(entry: entry)
    }
    .configurationDisplayName(displayName)
    .description(description)
    // supportedFamilies でサポートしたい WidgetFamily を指定
    .supportedFamilies([.systemSmall, .systemMedium])
  }
}

ウィジェットとアプリとの連携

ウィジェットは URL Scheme を介してアプリと連携します。ウィジェット上に URL Scheme を設定する手段として、ウィジェットの任意の場所をタップした際に使用される widgetURL と、ウィジェット上の特定の View をタップした際に使用される Link の2種類があります。

この URL Scheme の設定に関して、Small サイズのウィジェットと Medium サイズのウィジェットでは、それぞれできることに差があります。

f:id:naru-jpn:20210115145324p:plain
widgetURL と Link

Small と Medium で共通してウィジェット全体に対して widgetURL を設定することができます。Medium サイズ以上のウィジェットでは、特定の View に対して Link を設定できるようになっています。

つまり、Medium サイズ以上のウィジェットでは複数のコンテンツを配置してそれぞれアプリと連携をするということができますが、Small サイズのウィジェットではアプリと連携するための導線は1種類しか設置できません。これはシステム上の制約なので、ウィジェットを設定する段階で考慮をする必要があります。*3

var body: some View {
  switch family {
  case .systemSmall:
    if let item = entry.items.first {
      // small の場合は widget 全体に widgetURL を設定している
      FollowCatalogSmallView(date: entry.date, item: item)
        .widgetURL(URL(string: "mirr:///live/\(item.liveId)"))
    }
    ...
var body: some View {
  HStack(spacing: 12) {
    ForEach(0..<items.count) { offset in
      if let url = URL(string: "mirr:///live/\(items[offset].liveId)") {
        // medium ではそれぞれのユーザーのViewに対して Link を設定している
        Link(destination: url) {
          FollowCatalogItemView(item: items[offset])
        }
        ...

ウィジェットの自動更新

f:id:naru-jpn:20210115153959p:plain
ウィジェットの更新時間

ウィジェット上のコンテンツは、Timeline*4 と呼ばれる表示する内容とそれを表示する時間の組をあらかじめ指定しておくことで、システムに表示の制御を任せています。ウィジェット上のコンテンツは自動的に更新をすることができて、「指定した最後のコンテンツが表示されたら」「何分後に」といったような更新タイミングを指定することができます。

ミラティブのウィジェットには更新時間が表示されています。ウィジェット内のコンテンツはアプリケーションの操作によっても更新されますが、それとは別に15分毎に自動で更新を行なっています。

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
  ...
  let timeline: Timeline<TimelineEntry>
  if let date = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) {
    // 15分後に更新するように更新ポリシーを設定
    timeline = Timeline(entries: [entry], policy: .after(date))
  }
  ...
  completion(timeline)
}

この15分後という値についてはもっと頻繁に行なってもよいようなものですが、ウィジェットのシステム上、あまり頻繁に更新処理を行うと制限がかけられて更新が出来なくなってしまうという事象があるようでした。*5 配信情報のようにリアルタイム性のあるデータを表示するにあたり、表示されるデータに数分のラグがある場合を踏まえて、ウィジェット内には更新時間も表記するようにしています。

おまけ(NSKeyedArchiver の Widget Extension 上での振る舞い)

ウィジェット機能の開発中に遭遇したのですが、アプリ本体でエンコードした HTTPCookie のバイナリデータがウィジェット側でデコードできないという問題があることがわかりました。詳しく調べてみると、「全く同じ情報を持つ HTTPCookie のオブジェクトを NSKeyedArchiver でバイナリデータに変換した際、アプリ本体とウィジェットでできるバイナリデータが微妙に違う」という現象があるようでした。

Xcode12.1 で新規作成したプロジェクト上でも再現したのでミラティブのみで発生する問題ではないだろうと判断し、ネット上に情報もなく意図と異なる挙動だったのでフォーラムに投稿をしています。*6 NSKeyedArchiver から作られたバイナリデータは plutil というコマンドを使って中身を覗くことができて、その出力結果もフォーラムに記載しています。もし何かご存知の方がいらっしゃったら投稿へ返信いただけると嬉しいです。

おわりに

今回はミラティブのウィジェット機能についてご紹介しました。ミラティブは現在 iOS12.4 以上のサポートとなっているのでアプリ本体には SwiftUI をなかなか導入しづらい構成になっているのですが、ウィジェット機能を取り入れることで SwiftUI を使用した開発を行うことができ、プレビュー機能をどう運用/活用していくかなど今後 SwiftUI を導入する際に解消していきたい課題などの把握にも繋がりました。ミラティブのウィジェットもぜひホーム画面に置いてみてね!

We are hiring!

ミラティブでは一緒にアプリを作ってくれる iOS エンジニアを募集中です!気軽にご連絡ください!

www.mirrativ.co.jp

speakerdeck.com

*1:https://support.apple.com/ja-jp/HT207122

*2:Making a Configurable Widget

*3:Apple Developer Documentation 内のセクション Respond to User Interactions に記述があります。ウィジェット内の複数箇所で widgetURL を設定した場合には挙動が未定義であるなどの仕様には、注意が必要です。

*4:https://developer.apple.com/documentation/widgetkit/timeline

*5:これに関するフォーラムの投稿 WidgetKit won't request a new timeline at the end。Apple の Staff が1分間隔の更新では間隔が短すぎるので15間隔にしてみてはどうかという旨の返信をしています。

*6:Subtle difference of NSKeyedArchiver binary format on application target and widget extension