複数のUIWindowを利用してFiNCアプリの遷移構造を分けた話

taktem
FiNC Tech Blog
Published in
9 min readOct 26, 2018

はじめに

こんにちは。FiNC TechnologiesのiOS開発を担当している西村です。

FiNCでは、アプリの規模が大きいこともあり、負債化を防ぐために、様々な機能をモジュール単位で切り分けられるように試行錯誤を重ねています。

その中で、今回はUIWindowを複数併用した遷移構造の整理のお話をしたいと思います。

きっかけと課題

この構造を作ることになったきっかけは、アプリのどこからでも呼ばれうる、コンシェルジュ的な機能(以下: コンシェルジュ)を作りたいという企画がはじまりでした。

FiNCアプリは、複数のドメインから成り立っています。
※ライフログや、メディアページ、タイムラインなど

今回の企画は、これを統括してコントロールする、上位概念としてキャラクターを作りたいというものです。

結果的にできあがったのが、すでにリリース済のアプリでもご覧いただけるこの画面です。

チャット形式でFiNCのマスコットキャラクターが様々な案内をしてくれるというものです。
この会話の流れによって、アプリ内の各種機能に誘導されたり、場合によってはアプリ外のサービスにリンクされたりすることもあります。

この場合、例えば以下のような機能が考えられます。

  • コンシェルジュとの対話をきっかけに、画面の裏では別の遷移が走っている
  • いかなる状況下においても、最前面に出現可能
  • 裏ではアプリの本来の機能が動きうるので、上記条件を満たした上で、Facebookログインや各種ダイアログの機能を阻害しない

課題に対するアプローチ

このオーダーを安全に実現するためには、iOSアプリの基本的な遷移構造から切り離した、独立の遷移構造を別途共存できるようにすれば良いのではないかと考えました。

遷移構造を並列に持つ、というのは、もちろん管理コストがあがり、多用すれば考慮すべき例外パターンも増えてしまいます。
今回の場合は、以下のような理由により、メリットの方が大きいと考えて採用しました。

  • オンボーディング的な機能や、アプリ全体のガイドをする上で、裏側で別途画面遷移を終えた状態を作れるようにしたい。
  • コンシェルジュをモジュールとして分離させ、不要なドメイン知識を持たせないようにし、具体的な処理は呼び出した側が通知を受け取って、画面遷移の要否レベルすらもコンシェルジュは認知しないようにするため

UIWindowを利用した構造分割

以下、具体的機能と構造の話を区別するために、このコンシェルジュ的な機能を呼び出す側を”アプリ本体”、呼び出されるコンシェルジュ的機能の方を”サブモジュール”と便宜上呼ぶことにします。

複数のUIWindowの構成

通常、iOSアプリは、1個のWindowをベースに動いています。
AppDelegateに`var window: UIWindow?`がはじめから宣言されていますが、意識的にWindow操作を行った経験がある方は少ないんじゃないでしょうか。
キーボード出現時などに一時的に他のWindowが利用されることはありますが、基本的には開発者が意識して行う必要もないように作られています。

これに対して今回は

サブモジュールを表示する際に、新しくUIWindowを生成し、別の独立した遷移構造を持って表示するようにしています。
基本的な処理フローとしては

  • 通常modalで遷移処理を行うであろうタイミングで、UIWindowを生成し、新しく前面に配置する
  • サブモジュール内で起こったイベント情報は、すべてRxSwiftのStreamとしてアプリ本体へ通知し、必要であればアプリ本体が処理を行う
  • サブモジュールの役割が終わる、もしくはアプリ本体から終了の命令を受けたらサブモジュールはUIWindowを破棄する

という流れで、図にすると以下のようなフローになっています。

設計上の具体的な特徴として以下のようなものがあります。

イベントの渡し方

アプリ本体は、サブモジュールのイベント通知をsubscribeし、必要に応じて処理を行い、必要であればサブモジュールに新たに処理を命令します。

この時、サブモジュール側は、自分が通知しているイベントが引き起こす具体的な処理には関心がありません。

例えば以下の例のようなパターンがあります。

  • 非ログイン時に、ログイン前提の処理を受けた場合、アプリ本体がログイン処理を行う必要がある
  • 他のドメインの機能をオーバーレイ表示で呼び出す必要がある

このような場合、サブモジュール内で直接機能を呼び出すべきではないので、イベント通知用のRxSwiftのStreamを用意しています。
アプリ本体から呼び出す際に、イベント通知に対応する可能性がある場合は、このStreamをsubscribeしておき、通知を受けた時に必要に応じて処理を行うようにします。

WindowLevelの管理

前述したオーバーレイ表示なども踏まえて、UIWindowを扱う場合はWindowLevelを管理する必要があります。
都度Levelを数値として直接指定して管理していくのは危険なので、予めWindowLevelを定義し、役割毎にアサインしておくことで、特に意識しなくても、重なり順が守られるようになります。

冒頭のコンシェルジュ機能を例とした場合、
アプリ本体、コンシェルジュのチャット画面、一時的に表示されるログイン画面のそれぞれにWindowLevelの定義をアサインしておき、間違えてコンシェルジュチャットの裏にログイン画面が隠れてしまった、のような自体に陥らないようになっています。

ライフサイクルの管理

このサブモジュールは、遷移構造として独立しているので、基本的にはどこかでインスタンスが保持されているわけではありません。
サブモジュールが自身のインスタンスを保持しておき、役目を終えたら終了のイベント通知を投げて自身のインスタンスを破棄します。

結果

裏で遷移を行えるというのは思いの外実用的で、特に、サインアップフローをこのコンシェルジュのチャット機能を用いて行う際に、以下のような恩恵がありました。

画面生成の並列化

  • サインアップフローで決定したユーザーステータスに合わせて、初期画面の配置設定が柔軟になる
  • サインアップフローが完了した時点ですでにコンテンツの画面表示は完了しているので、ローディングの待ち時間などのストレスを無くすことができる

疎結合化の強制

frameworkを独立したことで、サブモジュールのドメインが持つべき機能を明確に意識する必要があり、アプリ本体と密結合状態を作り出すことが実質できないようになっています。
これにより、チャット上からアプリ本体の具体的なコンテンツに直接Push遷移してまた戻ってきたい、のようなオーダーは対応できませんが、本来混ざるべきではない遷移構造を避けられたという見方もできるかと思います。

実際は、WindowLevelをコントロールして、イベント通知を受けたアプリ本体がサブモジュールより全面にコンテンツをフローティングして表示することは可能なので、見え方が整理できていれば、コンテンツの流れを阻害することもありません。

注意点

ただし、冒頭でも記載した通り、むやみに採用すると管理が複雑になってしまうため、適切に必要性を判断し、現在どういうWindowを使っているか、各WindowLevelの定義をどうするか、などは集約管理して、各自が関係性を意識せずに済むような構造を守るのが大事です。

総括すると、制約はできるものの、全体的にはメリットが大きいのではないかと感じました。

現状の課題

ログのコントロール

上記制約を許容した上で出る懸念としては、ログの取得方法に独自の条件が加わってしまうことです。
例えば、コンシェルジュからのイベント通知によりアプリ本体が遷移した場合、そのViewDidLoadやViewDidAppearでスクリーン計測を行ってしまうと、実際のユーザー体験とはログ状況が異なってしまいます。
こちらに関しては、データ解析のチームと話し合って工夫してはいますが、より効率的なログ出力の方法を模索したいところです。

今後の展望

現在、この構造はFiNCアプリのために作ったもので、構造としてアプリのドメイン情報が混ざっている部分が残っています。
ここから必要な責務を見直し、このWindow設計の部分のみを切り出してライブラリとして公開することも考えています。

iOS・Androidエンジニア募集中

最後になりますが株式会社FiNC Technologiesでは、iOS・Androidエンジニアを募集しています。ご興味がある方はぜひこちらからご応募下さい。

--

--