ローディング時のズルい進捗表示


[今開発中のPlayer!](https://itunes.apple.com/jp/app/player!-re-kuang-xingsupotsuapuri/id897872395?mt=8)のログイン・登録画面で、こんな進捗表示をしていますが、これ実はフェイクだったりします(　´･‿･｀)

![2015-09-19 09_22_21.gif](https://qiita-image-store.s3.amazonaws.com/0/19398/83bbd40e-c3f9-dc8d-8419-128e5e76ba06.gif "2015-09-19 09_22_21.gif")
(Qiitaの画像サイズ制限が厳しくて粗いです。キレイなものは実際にアプリダウンロードしてご覧下さい。)

## 経緯

元々、この画面はこういう進捗表示では無く、単にインジケーターがクルクルするだけで、進捗状態が分からないものでした。
特にネットワークが悪いところだと、バグって固まってしまったのでは？とユーザーを不安にさせるようで、たまにそういう声を聞くことがありました。
登録フローは大事なところなので、そういうところでこれが原因で離脱してしまうと残念なので、改善が必要でした。

そこで、ネットワーク処理にもたつきつつもちゃんと正常に処理をしているということを示すために、進捗を表示することにしました。

ただ、例えば大きな画像などメディアファイルダウンロードなどならともかく、こういうJSONレスポンス受け取る処理って細かい進捗が取れず、それに頼ると0%から一気に100%に達するような感じになってしまい、今いちでした。

本来の目的は、「バグって固まってしまったのでは？とユーザーを不安にさせる」ような状態の解消であって、フェイクでも良いから何となくそれっぽく出来ないかなとやってみました。

## どういったフェイク進捗を行うか

ネットワーク良い状態でも悪い状態でも、ジワジワ小気味よく進捗が進んで、安心出来る感じを目指しました。

いくつか実験して見たところ、登録処理にかかる時間は概ね以下くらいでした。

- ネットワーク良い状態: 2-3秒程度
- ネットワーク悪い状態: 10秒以上かかることもあり

これを元に、以下が良さそうと思い実装したら、Gif動画のようになかなか良い感じになりました。

- ネットワーク状態が悪いときでも、90%程度まで10秒程度かけて進捗が進むようにする
- 進捗の進む間隔・進む割合を、ランダムに1秒前後開けつつ、10%前後進めるようにする
	- ランダムにすることでフェイクっぽさが消える
	- 処理中のまま10秒程度経つと、90%で止まってしまうのは許容
- ネットワークが良い状態では、進捗が半分足らずで実際の処理が終わってしまうが、そのタイミングで一気に100%まで進めて小気味良くフィニッシュした感じにしてごまかす
	- Gif動画はこのパスに近いです

この実装後は、特にこれに関してマイナス意見聞くことが無くなったので、なかなか良い塩梅で実装出来たのでは、と思っています。
僕自身、開発中に「あれ、止まった？」と思うことが無くなりました。

ちなみに、シミュレーター上で低速回線のエミュレートをする場合は、[Network Link Conditioner - NSHipster](http://nshipster.com/network-link-conditioner/)を使うと便利です。

## 実装

Swiftコードかつ[SVProgressHUDライブラリ](https://github.com/TransitApp/SVProgressHUD)を使っていますが、肝の部分は他のプラットフォームでも同様に応用出来ると思います。
本当は別ファイルに定義してある関数など引っ張ってきて、1つのコードとして載せています。

```swift
// ヘルパー的な関数群
func random(num: Int) -> Int {
    return Int(arc4random_uniform(UInt32(num)))
}
func random(min: Double, _ max: Double) -> Double {
    let random = Int(arc4random_uniform(UInt32(max*100 - min*100)))
    return min + Double(random) / 100.0
}
public func dispatchOnMainThread(delay delay: NSTimeInterval = 0, block: () -> ()) {
    if delay == 0 {
        dispatch_async(dispatch_get_main_queue()) {
            block()
        }
        return
    }
    let d = dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)))
    dispatch_after(d, dispatch_get_main_queue()) {
        block()
    }
}

private var progressCompleted = false

/** これがフェイク進捗の肝のメソッド */
func increaseProgress(progress: Float, message: String?, until: Float = 0.9) {
    if progressCompleted {
        return
    }
    SVProgressHUD.showProgress(min(progress, until), status: message, maskType: .Gradient)
    // 進捗がuntilで設定した最大値を超えたらそれ以上進捗を増やさずに待機
    if progress >= until {
        return
    }
    // ジワジワ動く間隔を0.5〜2秒でランダムに
    let delay = NSTimeInterval(random(0.5, 2))
    dispatchOnMainThread(delay: delay) {
        // 1回の進捗更新でどれくらい進むかを0.05〜0.15でランダムに設定して再帰呼び出し
        self.increaseProgress(progress + Float(random(0.05, 0.15)), message: message, until: until)
        return
    }
}

/** サインアップ処理 */
private func signUpImpl() {
    progressCompleted = false
    increaseProgress(0.05, message: message)
    // 実際のログイン処理を呼ぶ
    // ...
    
    // 終わったら100%に進める
    progressCompleted = true
    SVProgressHUD.showProgress(1, status: message, maskType: .Gradient)
}
```