見出し画像

REALITY iOSのウィジェットを作ってみた話 - REALITY Advent Calendar 2022 #25

この記事はREALITY Advent Calendar 2022の25日目の記事です。
昨日はホンダさんの「REALITYのアバターでノベルゲームやってみたくない?
」でした。

今年のREALITY Advent Calendarも今日が最終日!

かむいは開発合宿で初めて幹事を担当し、同じく幹事となったtakashiさん・ナオヤさんにおんぶに抱っこで準備を進めて来ました。最後の任務がこの記事の執筆です。アドベントカレンダーに投稿するまでが開発合宿!
早速やっていきましょ〜!

REALITYをウィジェットで見たい!

iOS14から登場したウィジェットは、様々なサイズでホーム画面やロック画面にアプリの情報をいち早く表示できる機能です。

例えばOS標準の天気アプリでは、最高気温・最低気温・現在の気温などアプリを使用してる人のほとんどが関心を持つ情報を表示します。

実現するためにはWidgetKitを使用します。

REALITYも多機能な分、ウィジェットを使って色んな情報がいち早く見れたら良いのに…と思っている方も多いのではないでしょうか。

REALITYのどんな情報がわかると嬉しいだろう?

今回は以下のようなウィジェットを作って見ました。

自分のアバター画像・名前・ランクを表示しタップすると自身のプロフィール画面に遷移するウィジェットです。押しポイントは

  • 自分の可愛い or カッコいいアバターを常にホーム画面で見れる

  • タップすると動く自分のアバターが見れる画面にすぐ飛べる

  • プロフィールを編集するとウィジェットにもすぐに反映される

ウィジェットは1つのアプリに対し複数作ることが可能なので、まずは自身のアバターに重きを置いたウィジェットを作ってみました。

他にも、例えばOS標準の写真アプリでは「あなたへのおすすめ」「最近のハイライト」などのカテゴリからランダムで写真をウィジェットに表示してくれます。NEXT REALITYに用意した「REALITYのアルバム」から、ランダムにREALITYの写真をウィジェットに表示してくれる機能もあると良いですね。(「思い出アルバム」と名付けたかったけど、夏のイベントで使われていたので作る際は良いネーミングを募集したいところ…)

Widget Extensionを使った実装

続いてウィジェットの実装について紹介します。

データの表示

WidgetはSwiftUIのみをサポートしており、シンプルなView表現であれば簡単に実現ができます。逆にUIKitは使えずUIViewRepresentableでUIKitの力を頼ることも出来ません。現状SwiftUIが得意としないView表現については実装は難しいと言えます。

データの取得・更新

ウィジェットに表示するデータの取得は自由なタイミングで行うことはできません。ホーム画面やロック画面に置かれるため端末のパフォーマンスに直に影響するためです。

そのため重要となってくるのがTimelineというウィジェットのデータを取得する仕組みです。WidgetKitに備わっているこのオブジェクトは以下の構成要素から成り立っています。

  1. Entries…Viewを更新する日付を保持しているTimelineEntryの配列。あらかじめ静的に更新できるようTimelineEntryを複数セットできる。

  2. ReloadPolicy…次の更新時期についてシステム側にリクエストする仕組み。ポリシーの種類には最後のTimelineEntryが表示された後にTimelineを更新する「atEnd」や、指定時間に更新する「after(date:)」などがある。

時間軸で見るTimelineを使ったウィジェット描画の流れ

今回はアプリ側で名前やアバター画像を編集した際にもウィジェットを更新したいため、WidgetCenterでアプリ側からウィジェットを更新する仕組みも使います。

ウィジェットは複数用意できると前述しましたが、特定のウィジェットに対する更新の書き方も出来ますし、今回であれば1つのウィジェットだけなので以下の様な書き方で1アプリに紐づくウィジェット全体に対する更新を促しても大丈夫です。

WidgetCenter.shared.reloadAllTimelines()

またランクはどうでしょうか。ランク機能は週単位で計測を行い、毎週日曜日の日を跨いだタイミングで集計を行い翌朝に反映されます。そのためTimelineを取得するTimelineProvider.getTimeline()にて以下の様な実装を行います。

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<RankWidgetEntry>) -> Void) {
  Task {
    let repository = UserRepositoryImpl(apiClient: UserApiClient())
    let response = try await repository.fetchUserEntry()
    var entries = [UserEntry]()

    let nowEntry = UserEntry(date: Date(), rank: response.rank, name: response.name, image: response.image)
    entries.append(nowEntry)

    for i in 1..<4 {
      let numberOfDiffDayFromMon = Calendar.current.component(.weekday, from: Date()) - 2
      if let day = findStartTime(day: (i*7)-numberOfDiffDayFromMon, hour: 6) {
        let entry = UserEntry(date: day, rank: response.rank, name: response.name, image: response.image)
        entries.append(entry)
      }
    }

    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
  }

  private func findStartTime(day: Int, hour: Int) -> Date? {
    let calendar = Calendar(identifier: .gregorian)
    guard let designatedDate = calendar.date(byAdding: DateComponents(day: day), to: Date()) else {
      return nil
    }
    let startOfDay = calendar.startOfDay(for: designatedDate)
    return calendar.date(byAdding: DateComponents(hour: hour), to: startOfDay)
  }
}

静的にView更新を続けて行えるよう4つTimelineを用意し、今の日付のEntryの他は毎週月曜日の午前6時にViewを更新するように指定します。これでプロフィール側に更新がなくとも、毎週月曜日の6時にはウィジェットの更新を行います。リロードポリシーには「atEnd」を指定し、4週目のウィジェット表示を終えた後に再度Timelineをリクエストするようにします。

画面遷移

ウィジェットをタップした際に画面遷移する仕組みは2つ用意されています。

1つはSwiftUIのViewModifierであるLinkです。こちらはウィジェットに複数のタップ領域を指定できる反面、ウィジェットの小サイズでは利用できない仕様のため今回は採用できませんでした。
もう1つはwidgetURLです。こちらはタップ領域が全体に限定される反面、シンプルで勝手が良いためこちらを採用しました。

実は大変だった下準備

ウィジェットはアプリ本体とは別にモジュール(Widget Extension)を用意し機能を実現するため、ウィジェットからAPIを叩くために必要な通信処理を外部に切り出す必要がありました。

これまでその仕組みを用意できておらず、ウィジェットやApp Clipなどの機能を作ることに腰が重くなっていました。
しかし、6月にリリースしたシェアシート上でURLをREALITYに共有できる機能でもこちらの仕組みが必要となり、大規模なリファクタリングを実施し機能実現に至りました。

ウィジェットの通信に関わる部分を
リファクタリングしたクラス図
わかりづらいですが大変だったんです…涙

開発合宿期間は2泊3日のため、この部分を含めた実装では時間がとても足りません。今回のウィジェットは時前に頑張った下地があった上での取り組みとなりました。

開発合宿で取り組めたこと

今回のREALITY Advent Calendarで発表された取り組み全てがそうですが、こんな機能が欲しい!と思ったアイデアもまとまった時間を用意できなければ形に持っていくのは困難です。

開発合宿は修学旅行の様なワイワイ企画ではありつつ、限られた時間の中で如何に良いものをアウトプットするかという腕試しの場でもあり、且つ近年続いたオンラインでの業務から、久々に仲間と顔を合わせてプログラミングするというチームビルディングの側面が強い催しだと思います。

また実施することのメリットは大きいですが、この時間を割くことを理解してくれるエンジニア以外の同僚や上長との信頼関係があってこそだと思ので、来年も企画していけるように日々の業務に励んでいきたいと改めて感じました。

圡善旅館さん、お世話になりました!!

さいごに

今年のREALITY Advent Calendarはいかがだったでしょうか。

欲しいと思う機能のオンパレードだったので、REALITYに導入されるものがあると嬉しいですね!ウィジェットも早くリリースできるように頑張りたいと思います!💪

来年も開発合宿の実現と合わせて、そこでお披露目した機能をまたアドベントカレンダーで発表できる日を楽しみにしています。

それではメリークリスマスー!🎄🎅🍰✨