メルカリのShimmerViewをOSS化しました

メルカリiOSアプリでは、検索画面や保存した検索結果画面などのいくつかの画面で、アプリがデータを非同期で取得しているときに実際のコンテンツのレイアウトに似せたキラッと光るViewをLoading Indicatorとして表示しています。

f:id:hagmas_mercari:20200708110455g:plain
ShimmerEffectを使ったSearch画面

この実際のコンテンツに似せた骨組みだけのViewはSkelton Viewと呼ばれ、それにShimmer Effectというエフェクトを追加し、Loading Indicatorとして使用します。最近ではこの種のLoading IndicatorをFacebook, YouTube, Google Mapなど、さまざまなメジャーなアプリで見かけるようになりました。UIKitのUIActivityIndicatorViewを使うよりも、Loading Indicatorから実際のコンテンツへのスムーズなトランジション効果が期待できます。

この種のLoading IndicatorはもともとFacebookにより目障りにならないLoading Indicatorとして開発され、iOSではShimmerというOSSが公開されています。またGitHub上にはその他にSkelton ViewなどのOSSも公開されています。

メルカリiOSアプリチームでは、これらの既存のフレームワークを参考にしつつ、よりメルカリiOSアプリのアーキテクチャやユースケースに沿ったLoading IndicatorとそのためのAPIをデザインすることにしました。この記事ではそのOSS化したAPI、ShimmerViewの使い方と技術的な詳細を説明していきます。

GitHub: mercari/ShimmerView

ShimmerViewを使ったLoading Indicatorの例

ウェブアプリケーションであれば、ある画面を初期化した直後、その画面全体または一部のコンポーネントの内容をAPIから取得することがよくあります。サーバーからレスポンスが返ってくるまでShimmerViewを使って作成したLoading Indicatorを表示し、APIからデータが取得できたら実際のコンテンツと切り替えることでなめらかなUXを作ることができます。

ShimmerViewはコードもXibで使うことができますので、Itemの詳細画面といったレイアウトが複雑な画面でもSkelton ViewをXib上で視覚的に作ることができます。XibでShimmerViewを配置し、コード側から各ShimmerViewのstartAnimating()を呼び出します。

f:id:hagmas_mercari:20200708190653g:plain
複雑なレイアウトを含むLoading Indicatorの例

また、ShimmerViewShimmerReplicatorViewというリスト形式のSkelton Viewを作るのに特化したAPIが用意されていますので、UICollectionViewUITableView等のAPIを利用せずにリスト型のLoading Indicatorを素早く作ることができます。(ShimmerReplicatorViewについては、下の節で詳しく説明します。)

f:id:hagmas_mercari:20200708112735g:plain
リスト型のLoading Indicatorの例

ShimmerView API について

ShimmerViewの各APIの概要はGitHubのREADMEに説明されていますので、ぜひご覧ください。ShimmerView APIは以下のような点を考慮してデザインしました。

  • ShimmerViewを導入することによる既存UIへの副作用を最小限にする
  • エレガントなアニメーション
  • ShimmerViewを使ったSkelton Viewの開発は簡単、迅速に行える

次の節からそれぞれについて説明していきたいと思います。

既存UIへの副作用を最小限にする

ShimmerViewUIViewのサブクラスで、ShimmerViewを使ったCustom Loading IndicatorのViewは本来コンテンツを表示するViewと別に存在することになります。
既存のViewのレイアウトコードを再利用することはできないので一見メンテナンスが面倒になるように思われますが、Custom Loading Indicatorのコードの中身はとてもシンプル(ほぼレイアウトのコードのみ)でわかりやすいので、今のところメンテナンスのコストはそれほどかかっていません。

メルカリでは既存のViewで使われていたUIActivityIndicatorViewを置き換える形で導入を進めています。

エレガントなアニメーション

ShimmerViewCAGradientLayerによってグラデーションのアニメーションを表示していますが、より効果的なEffectにするために以下のような特徴を持っています。

ShimmerSyncTargetを使って複数のViewのShimmer Effectを時間的、空間的に同期させる。

画面上に複数のShimmerViewを配置する場合、それぞれのViewが別々にShimmer Effectを適応されるよりも、時間的、空間的に同期されていたほうがエフェクトに統一感が出ます。

Syncされていない Syncされている
f:id:hagmas_mercari:20200708145155g:plain f:id:hagmas_mercari:20200708145120g:plain

複数のShimmerViewを同期させたい場合、それらのすべてのViewを内包する親View、またはViewControllerを作成し、ShimmerSyncTargetとして指定してください。内包されているShimmerViewはアニメーションを始めるときに自分のResponder Chainを遡り直近のSync Targetを見つけ、そこで指定されているShimmerViewStyle、アニメーションのスタート時間、相対座標を計算してShimmer Effectを他のShimmerViewと同期させます。

グラデーションの補完関数

CAGradientLayerは、デフォルトではプロパティのcolorsで指定された色をstartPointendPoint間で直線的に補完しているのですが、その直線的に補完されたグラデーションをアニメーションさせたときに(感覚的ではありますが)あまりなめらかに表示されないことがありました。ShimmerViewではグラデーションの補完関数として、Smoothstep関数を利用しています。

線形補完 SmoothStep関数での補完
f:id:hagmas_mercari:20200708184145p:plain f:id:hagmas_mercari:20200708184207p:plain
ShimmerViewを使ったSkelton Viewの開発は簡単、迅速に行える

メルカリにはEntityのリスト表示するような画面(検索結果画面、保存した検索条件一覧画面など)が多数あります。そのような画面の場合、大抵はView生成時のAPIアクセスでアイテム一覧を取得し、そのAPIアクセス中にLoading Indicatorを表示させます。このようなリスト画面はUITableViewUICollectionViewを使って構築されますが、DelegateやDatasource等のDelegate FunctionをLoading Indicatorのために毎回設定しているのではLoading Indicator制作に時間がかかりすぎてしまいます。また、UITableViewUICollectionViewといったAPIはユーザーが画面をスクロールしても効率よくViewを再利用して表示できるように最適化されているので、スクロールが必要ではないLoading Indicatorにはその機能は不要になります。

この問題を解決するためにShimmerViewではShimmerReplicatorViewというAPIを用意しました。以下のように各種パラメータを設定して初期化するだけで、リスト型のLoading Indicatorを作ることができます。

let view = ShimmerReplicatorView(itemSize: .fixedHeight(120)) { () -> ShimmerReplicatorViewCell in
return UINib(nibName: "ListViewCell", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ListViewCell
}

cellProviderに繰り返し表示したいセルの初期化コードを指定すると、ShimmerReplicatorViewがViewのSizeとパラメータに応じてcellを適宜生成、自動的に配置します。

ShimmerReplicatorViewはBacking LayerがCAReplicatorLayerになっています。CAReplicatorLayerは、普段のアプリ開発ではあまり見かけないUIKitのAPIかと思いますが、与えられたパラメータに従って自身のサブレイヤを複製してくれる面白いAPIです。Listタイプの画面の場合、縦方向は同じCellが同じアニメーションで繰り返し配置されることになりますので、CAReplicatorLayerを使って縦方向にセルを複製することでUITableViewUICollectionViewを利用せずリスト型のUIを生成しています。以下のスクリーンショットのように、二段目以降のセルは実際にはUIViewではなく一段目のセルのレイヤーが複製されたものになっています。

スクリーン 実際のView構成
f:id:hagmas_mercari:20200708185735p:plain f:id:hagmas_mercari:20200708185656p:plain

最後に

以上、ShimmerViewの紹介でした。このブログ記事を通して、Shimmer Effectを使ったLoading Indicatorに興味を持っていただければ幸いです。