Clientを担当しているmasaki.hagaがお送りします。2020年4月、MercariのHome画面が新しく生まれ変わりました。リリース当初は旧ホーム画面とほとんど見分けがつかないぐらい似ていましたが、その後、様々なABテストを行い旧ホーム画面にはなかった便利な機能が追加されています。旧ホーム新ホーム旧ホーム画面のタイムラインは、バナー、いいねした商品コンポーネント、おすすめの商品の一覧という構成になっていました。新しいホーム画面では、コンポーネントの概念を一般化し、より多様なレイアウトのコンポーネントを追加し、サーバー側でそれらのコンポーネントの出し分け、内容やレイアウトを操作することができるようになりました。ホームのコンテンツを操作することで、各指標がどのように変化するかを評価し、最適化していくことを目的としています。コンポーネントの種類レイアウト閲覧した商品からおすすめを表示するコンポーネントお客様の出品状況を表示するコンポーネントお客様のMerpay残高を表示するコンポーネントなどでABテストを行い、メルカリのファーストビューに表示する最適なコンポーネントの選定を行いました。この投稿では、そのHome画面のiOS側の設計について説明します。ホーム画面の設計ホーム画面は一番お客様の目につきやすい画面であるため、各チームからの需要が高く、それぞれ独自のコンポーネントを開発することが想定されます。そのような画面で、十分な拡張性、保守性、なめらかなUXを実現するために以下の点を念頭に設計されています。二段階のAPIアクセスを行うクライアントはホーム画面で表示するデータをホームマイクロサービスから取得しますが、各コンポーネントで表示する具体的なデータはホームマイクロサービスがプロキシとして働き、他のマイクロサービスから取得します。そのため、すべてのデータを一つのAPIコールでバックエンドから取得するようにすると、Viewのインスタンスの生成から最終的なインターフェイスの描画までの時間がもっとも遅いマイクロサービスのレスポンスに依存することになります。このような依存を取り除くため、APIアクセスは二段階に分けて設計されました。1段階目のAPIコールではどのコンポーネントを表示するかについてのレイアウト情報のみを取得し、クライアントは各コンポートのデフォルトのサイズを用いてレイアウトを計算します。各コンポーネントが表示されるタイミングでコンポーネントそれぞれがAPIコールを行いコンテンツを取得します。ファーストビューに入ってくるようなコンポーネントは1段階目のAPIコールが終わった直後にコンテンツが取得されますが、スクロールの下の方に配置されているコンポーネントは、ユーザーがスクロールを行いコンポーネントが表示されようとするタイミングまでデータ取得を遅延します。各コンポーネントはUIViewControllerのサブクラスとして定義する各コンポーネントは通信、永続化、レイアウト、ナビゲーションなど様々なロジックを持ちえます。ホーム画面のベースはUICollectionViewですが、各セルをUIViewControllerにして各コンポーネントのロジックはそのUIViewController内にカプセル化しています。各コンポーネントのコンテナーとなる親のUIViewControllerはホーム画面のレイアウトと画面全体の状態の保持のみを行いますので新しいコンポーネントを追加しても非線形的にコード量が増えることを阻止できます。Updateを行う各コンポーネントはそれぞれ状態を持ち、ユーザーのインタラクションやAPIアクセスによりそれらの状態は変化していきます。各コンポーネントの状態が変化したとき、またはコンポーネントの削除、追加が行われたとき、その差分だけをアニメーションで描画し直すため、DifferenceKitを利用しています。差分更新はUIKitでもiOS13からサポートされていますが、Mercariは現在iOS12もサポートしていますので、DiffereceKitを利用しています。一度計算したサイズのキャッシングを行うホーム画面では標準のUICollectionViewとUICollectionViewFlowLayoutを利用していますが、セルフサイジングの機能は使わずにUICollectionViewDataSourceのメソッドでコンポーネントのサイズをレイアウトに渡しています(セルフサイジングを利用しなかった理由は後述します)。レイアウトの計算はメインスレッドで行われUXに影響がでやすいため、どんな複雑なレイアウトを追加してもレイアウトの計算を最小限に抑えられるような仕組みが必要です。安直な実装では、コンポーネントがUICollectionViewからデキューされる度にコンポーネントのサイズを計算しますが、それは無駄が多い方法です。各コンポーネントの状態にハッシュ値をもたせて、ハッシュ値から一度計算したサイズを取り出してくるようにしています。コンポーネントの状態が変わればハッシュ値も変わりますので、コンポーネントのサイズは再計算されます。状態によらず一定のサイズをもつコンポーネントには、クラス名などから固定のハッシュ値を作ります。}}UIScrollViewのContentOffsetは保存するいくつかのコンポーネントは横方向のUIScrollViewを持っています。あるコンポーネントのインスタンスが別のコンポーネントに再利用された時に、前の状態のUIScrollViewのContentOffsetのままだと、体験に一貫性がなくなりバグとなります。各コンポーネントの状態にはidentifierが持たせてあるので、そのidentifierでコンポーネントのscrollViewのOffsetを保存するようにして、コンポーネントがデキューされる時に前に保存されたContentOffsetはないかどうかをチェック、あれば前のContentOffsetを反映するようにしています。Updateを行う一度計算したサイズのキャッシングを行うScrollのオフセットは保存するという3つの機能は汎用性が高く、他のビューでも使うことができるため、UICollectionViewDiffableDataSourceと似たようなAPIComponentDataSourceとして切り出しています。})ローディング状態を作り込む前述の通り、各コンポーネントのコンテンツは表示されるタイミングで初めて取得されますので、通信状態によってはコンポーネントの実際の内容を表示するのに時間がかかってしまうことがあります。各コンポーネントにはローディング状態のUI(スケルトンビュー)が定義してあり、APIアクセス中に表示することでロード前とロード後のコンテンツの視差を少なくしています。困ったところ目新しい技術は特に使っていないのであまり困るような場面はありませんでしたが、その中でも特に工夫したところなどを説明します。Sizingを利用することでサイズを計算するコードを省略する予定でした。Updateを組み合わせたときに差分更新のアニメーションが乱れるというどうしても解決できないバグがあり、シンプルにサイズを計算しキャッシングしておく仕組みに変更しました。(iOS14登場以前の不具合であるため、現在は修正されているかもしれません。)タブ型に変更したときにパフォーマンスが落ちた新しいホーム画面は初期バージョンが2019年10月ごろにリリースされていたのですが、お客様の再訪率が下がったという理由から一度クローズになり、旧ホーム画面により近い複数のタブを持った画面に再設計されました。お蔵入りになった新ホーム現在のホーム画面は、このお蔵入りになった新ホームをUIPageViewControllerを使って複数設置しているのですが、タブからタブへの移動でのパフォーマンスが非常に悪いことがわかりました。これは、一つのビューコントローラーにたくさんのビューコントローラーが内包されていることが原因でした。メリカリのiOSクライアントはマイクロビューコントローラーという概念(詳細についてはmercari/Mew#Referenceを参照してください。)にしたがって設計されていて、各コンポーネント内に配置されている商品サムネイルも画面遷移やログ送信のロジックを内包するためにビューコントローラーとして設計されています。そのため、各タブが多数のビューコントローラーを持ちますが、タブを変更しようとするタイミングでビューのAppearanceMethod(viewWillAppearなど)が全ての子ビューコントローラーに対して呼ばれてしまいパフォーマンスが落ちることがわかりました。一部の子ビューコントローラーはUIの一部として使われることに特化してデザインされているので、全てのビューコントローラーに対して、このAppearanceMethodを呼ぶ必要はありません。このAppearanceMethodを子ビューコントローラーに自動的にフォワードすることを制御するAPIshouldAutomaticallyForwardAppearanceMethodsを設定することで、パフォーマンスが劇的に改善し、素早く左右にスワイプしても滑らかに表示することが可能になりました。false}ホーム画面の応用ホーム画面の一部の機能はAPIとして切り出されているのですが、汎用性の高さから、検索画面、お知らせ画面、保存した検索一覧画面など複数の画面にも使われています。検索画面ではホーム画面同様に様々なコンポーネントを試すことが多いのですが、新しいアーキテクチャを導入してからはスケーラビリティやメンテナビリティを確保しながら高速にコンポーネントを追加してテストすることが可能になりました。メルカリではミッション・バリューに共感できるiOSエンジニアを募集しています。一緒に働ける仲間をお待ちしております。Othersさんです。引き続きお楽しみください。