メルカリiOSアプリでは、検索画面や保存した検索結果画面などのいくつかの画面で、アプリがデータを非同期で取得しているときに実際のコンテンツのレイアウトに似せたキラッと光るViewをLoading Indicatorとして表示しています。
この実際のコンテンツに似せた骨組みだけの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()
を呼び出します。
また、ShimmerView
はShimmerReplicatorView
というリスト形式のSkelton Viewを作るのに特化したAPIが用意されていますので、UICollectionView
やUITableView
等のAPIを利用せずにリスト型のLoading Indicatorを素早く作ることができます。(ShimmerReplicatorView
については、下の節で詳しく説明します。)
ShimmerView API について
ShimmerView
の各APIの概要はGitHubのREADMEに説明されていますので、ぜひご覧ください。ShimmerView
APIは以下のような点を考慮してデザインしました。
ShimmerView
を導入することによる既存UIへの副作用を最小限にする- エレガントなアニメーション
ShimmerView
を使ったSkelton Viewの開発は簡単、迅速に行える
次の節からそれぞれについて説明していきたいと思います。
既存UIへの副作用を最小限にする
ShimmerView
はUIView
のサブクラスで、ShimmerView
を使ったCustom Loading IndicatorのViewは本来コンテンツを表示するViewと別に存在することになります。
既存のViewのレイアウトコードを再利用することはできないので一見メンテナンスが面倒になるように思われますが、Custom Loading Indicatorのコードの中身はとてもシンプル(ほぼレイアウトのコードのみ)でわかりやすいので、今のところメンテナンスのコストはそれほどかかっていません。
メルカリでは既存のViewで使われていたUIActivityIndicatorView
を置き換える形で導入を進めています。
エレガントなアニメーション
ShimmerView
はCAGradientLayer
によってグラデーションのアニメーションを表示していますが、より効果的なEffectにするために以下のような特徴を持っています。
ShimmerSyncTargetを使って複数のViewのShimmer Effectを時間的、空間的に同期させる。
画面上に複数のShimmerView
を配置する場合、それぞれのViewが別々にShimmer Effectを適応されるよりも、時間的、空間的に同期されていたほうがエフェクトに統一感が出ます。
Syncされていない | Syncされている |
---|---|
複数のShimmerView
を同期させたい場合、それらのすべてのViewを内包する親View、またはViewControllerを作成し、ShimmerSyncTarget
として指定してください。内包されているShimmerView
はアニメーションを始めるときに自分のResponder Chainを遡り直近のSync Targetを見つけ、そこで指定されているShimmerViewStyle
、アニメーションのスタート時間、相対座標を計算してShimmer Effectを他のShimmerView
と同期させます。
グラデーションの補完関数
CAGradientLayer
は、デフォルトではプロパティのcolors
で指定された色をstartPoint
とendPoint
間で直線的に補完しているのですが、その直線的に補完されたグラデーションをアニメーションさせたときに(感覚的ではありますが)あまりなめらかに表示されないことがありました。ShimmerView
ではグラデーションの補完関数として、Smoothstep関数を利用しています。
線形補完 | SmoothStep関数での補完 |
---|---|
ShimmerViewを使ったSkelton Viewの開発は簡単、迅速に行える
メルカリにはEntityのリスト表示するような画面(検索結果画面、保存した検索条件一覧画面など)が多数あります。そのような画面の場合、大抵はView生成時のAPIアクセスでアイテム一覧を取得し、そのAPIアクセス中にLoading Indicatorを表示させます。このようなリスト画面はUITableView
やUICollectionView
を使って構築されますが、DelegateやDatasource等のDelegate FunctionをLoading Indicatorのために毎回設定しているのではLoading Indicator制作に時間がかかりすぎてしまいます。また、UITableView
やUICollectionView
といった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
を使って縦方向にセルを複製することでUITableView
やUICollectionView
を利用せずリスト型のUIを生成しています。以下のスクリーンショットのように、二段目以降のセルは実際にはUIView
ではなく一段目のセルのレイヤーが複製されたものになっています。
スクリーン | 実際のView構成 |
---|---|
最後に
以上、ShimmerView
の紹介でした。このブログ記事を通して、Shimmer Effectを使ったLoading Indicatorに興味を持っていただければ幸いです。