読者です 読者をやめる 読者になる 読者になる

iOS アプリの UI でこれだけはおさえたい、読み込み中の体験を向上させる基本 UI パターン3つ

ホリデー株式会社 *1 の多田です。Holiday ( https://haveagood.holiday/ ) というサービスの開発を行っています。

アプリを通してユーザに価値を届けるためには、アプリの細部のインタラクションを軽視することはできません。細かい部分に気を配り使い心地を良くしてこそ、サービスで本当に実現したい価値をユーザにまっすぐ届けることができるためです。

iOS アプリの使い心地を良くするための基本的なインタラクションを以前当ブログで投稿した記事でいくつか紹介しましたが、今回は前回紹介しなかったインタラクションのうち、「読み込み中」の UI の基本パターンについて取り上げようとおもいます。

はじめに:なぜ読み込み中の UI を考えなくてはいけないのか

Holiday iOS アプリでは、基本的にデータはクライアント側で持たずサーバと通信して表示するデータを受け取っており、受け取ったデータを表示するまでにはどうしてもある程度時間がかかります。もちろん、根本的な待ち時間を短くするために、API を高速化することや、レスポンスの最適化、リクエストのタイミングの工夫などは必要不可欠です。ですが、どこまでそのような最適化を行っても、特にモバイルアプリの場合はネットワークが不安定な環境を避けることができないため、読み込み時間がある程度かかる場合のことを想定しなくてはいけません。

読み込み時間がかかる場合にできることは色々と考えられますが、凝ったことをせずとも適切な UI を用いることで読み込み中の体験を良くすることができます。そこで、簡単に実現できる読み込み中の基本 UI パターンを3つ、Holiday iOS アプリでの例とともにご紹介します。


おまけ:ネットワーク環境が悪い状態を開発中に再現するために

シミュレータ上や実機でネットワーク環境が悪い状態を再現するためには、Network Link Conditioner を使うと良いです。


アクティビティインジケータ

f:id:tdksk:20151201234102g:plain

最も基本的な読み込み中の UI は、UIKit で提供されているアクティビティインジケータです。アクティビティインジケータを表示することで、処理が停止せずに進んでいることを示すことができます。

アクティビティインジケータは画面内に馴染むため、データを取得するまでの間に表示するものとして最も手軽で無難な選択肢の一つです。遷移後の画面でデータ取得までの空の画面に表示すべきものがないなら、アクティビティインジケータを表示しておくと画面を見ているユーザを安心させることができます。

APIKit を使ったサンプルコードは下記のとおりです。

// リクエストを送る前にアクティビティインジケータを表示(画面の初期化時に `activityIndicator` をビューにセットしておく必要がある)
activityIndicator.startAnimating()

Session.sendRequest(request) { result in
    // レスポンスを受け取ったらアクティビティインジケータを非表示
    activityIndicator.stopAnimating()

    // データ取得後の処理
    // ...
}

HUD

f:id:tdksk:20151201234127g:plain

アクティビティインジケータの他には、HUD が読み込み中の UI として広く使われています。SVProgressHUDMBProgressHUD などのライブラリを使うことで簡単に導入することができます。

HUD は画面の最前面に覆いかぶさるように表示されるため、どのような画面でも使うことができるという意味では便利なのですが、良くも悪くも目立つので使いどころには気をつけなくてはいけません。例えば画面遷移後のデータ読み込み中に表示するものとしては、前述のアクティビティインジケータのほうが読み込み前後での画面の変化が少なく違和感を与えないため良さそうです。また、HUD を表示している時はユーザの動作を妨げている(ように見える)ため、本当にユーザに何もさせず待たせるべき箇所なのかを考える必要があります。

これらを踏まえると、HUD を使うことに適している場面は、処理が終わることに最も注目しており、処理が終わるまでは画面遷移するべきでないところと言えそうです。そのため、Holiday では読み込み中というよりも以下のような POST や PATCH の処理中の場面で HUD を使っています。

  • ユーザ登録、ログイン処理
    • 成功するまで先に進むことができない
    • 失敗した場合必要であれば入力項目を書き換えて再度実行する必要がある
  • ある程度ボリュームのある内容の投稿
    • 投稿が完了されたことを確認するまではユーザが安心できない
    • 失敗した場合リトライする必要がある

後者の具体例としては、フォトレポ *2 を投稿する場面が挙げられます。投稿されたものは投稿する時と同じ画面に表示するため、サーバ側の処理を待たずにクライアント側で先に表示を変えてしまうこともできますが、この場合は処理が完了するまでに時間がかかる *3 ことや、処理中であることを明確に伝えたほうが安心感があること、またチャットなどのように短い時間で連投するものではないことなどを考慮し、処理中は HUD を表示して処理が完了した後に投稿内容を反映させる *4 方法を取りました。

// リクエストを送る前に HUD を表示
SVProgressHUD.show()

Session.sendRequest(request) { result in
    switch result {
    case .Success(let response):
        // HUD を消す
        SVProgressHUD.dismiss()

        // 成功結果を画面に反映する処理
        // ...
    case .Failure(let error):
        // HUD でエラーメッセージを表示
        SVProgressHUD.showErrorWithStatus("送信できませんでした。もう一度お試しください。")
    }
}

プレースホルダ

f:id:tdksk:20151201234209g:plain

アクティビティインジケータも HUD も、読み込み中であることはユーザに伝えやすいものの、多用するとうっとおしい印象を与えてしまいます(操作の度に HUD を表示したり、一画面内に複数のアクティビティインジケータを使用したり…)。そのため、ある程度読み込みが発生していることが予期できそうな場所に関しては、読み込み中であることを明示しないというのも一つの選択肢です。

読み込み中であることを予期させるために、プレースホルダを使うと効果的です。プレースホルダを使うことで、データの読み込みの完了を待たずに先にレイアウトだけを画面上に反映し、体感読み込み速度を上げると同時にデータ取得後の画面との差分が少なくすることができます。

プレースホルダは読み込みに時間がかかる画像に対して良く利用されます。画像の読み込みまでプレースホルダ画像を表示するためには SDWebImagesd_setImageWithURL:placeholderImage: メソッドを使うと簡単に実現できます。

Holiday では画像のプレースホルダだけでなく、いくつかの画面でセルのプレースホルダを利用しています。例えばおでかけプラン詳細画面ではプラン内のスポットをコレクションビューセルで複数表示していますが、事前にスポットの数だけ読み込んでおき、プラン詳細画面ではスポットの内容が読み込まれるまでスポットの数だけセルのプレースホルダを表示しています。

override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
    // 初回ロード時は Plan モデルに紐づく Spot モデルが取得できていないので、事前に Plan モデルに保存されているスポット数 `spotCounts` 分だけセルを表示
    return plan.spots.count > 0 ? plan.spots.count : plan.spotCounts.toInt()
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(SpotCollectionViewCell.reuseIdentifier, forIndexPath: indexPath) as SpotCollectionViewCell

    // スポットデータが取得できているときはそのデータでセルを描画し、そうでない場合は空データでセルを描画
    let spot: Spot? = plan.spots.count > indexPath.item ? plan.spots[indexPath.item] : nil
    cell.updateWithSpot(spot)

    return cell
}

おまけ:文字のプレースホルダ

f:id:tdksk:20151201234340p:plain

セルや画像だけでなく、文字部分もプレースホルダとして表示するパターンも少し前から流行って使われ続けています *5 。文字中心のコンテンツだとセルや画像のプレースホルダだとスカスカになるため、データ読み込み後のイメージを想起させるためには有効です。また、Facebook などはプレースホルダ部分の色をアニメーションで若干変化させることにより読み込み中であることを示しています。

Holiday では読み込み中のプレースホルダとしては利用していませんが、おでかけプラン投稿画面などで BLOKK というフォントを用いて文字のプレースホルダを設置しています。


おわりに

今回は iOS アプリの読み込み中の UI の中でも基本的で手軽に実現できる3つの例を紹介しました。もちろんここで紹介したものが正解とは思っておらず、実現したいことを達成するための一つの参考になれば良いと思っています。

なお、Holiday ではプロダクトを良いものにするためにこだわりを持ってサービス開発をしたいエンジニアを募集しています。興味をお持ちいただけた方はぜひご応募ください。

*1:クックパッドの100%子会社

*2:写真とひとことでおでかけプラン作者に感謝のフィードバックを伝えることができるもの

*3:写真のアップロードをバックグラウンドで先に行うと送信後の完了時間は短くなりますが、まだそこまでは実現できていません

*4:処理が完了した時は投稿したものが画面に反映されるため、HUD で成功した旨のメッセージを出していません

*5:Facebook, Slack, Medium, Booking Now, TVShow Time などで見ることができます

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/