うるおいらんど

【UIRefreshControl】beginRefreshingを呼び出した時にtintColorが変わらなかった話。

こんにちは。Reoです。お久しぶりです。

引越しや新生活等でバタバタしていて、なんだかんだで前回の記事から1ヶ月以上が経ってしまいました。前回の記事で書くよ!っていってた記事も全く書けていないのですが、忘れないうちに最近出会ったバグシリーズを書いていこうと思います。

さてさて、今回はUICollectionViewのUIRefreshControlを用いた時にtintColorの色が変わらない現象が発生したことについて書いていきます。

デモプロジェクト → uruly/RefreshControlTintColorDemo

 

発生した現象

  • UIRefreshControlのbeginRefresing()をviewDidAppear(_:)までのタイミングで呼びだすとtintColorの色が黒になる
  • その後手動でリフレッシュ時には色が変わっている

iOS12.1 で確認済みですが、おそらく大分前からある現象です。

UICollectionView / UITableView / UIScrollView 等のUIRefreshControlが追加できるもので確認できます。(一応私はUICollectionViewでのみ確認してあります。)

 

viewDidAppear(_:)内で呼び出した際には以下のようになります。

この動画のコードはこんな感じ。(一部はしょり)


class ViewController: UIViewController {

    @IBOutlet private weak var collectionView: UICollectionView! {
        didSet {
            collectionView.delegate = self
            collectionView.dataSource = self
            collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
            let refreshControl = UIRefreshControl()
            refreshControl.tintColor = .white
            refreshControl.addTarget(self, action: #selector(handleRefreshControl(_:)), for: .valueChanged)
            collectionView.refreshControl = refreshControl
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard let refreshControl = collectionView.refreshControl else { return }
        refreshControl.beginRefreshing()
        refreshControl.sendActions(for: .valueChanged)
        collectionView.contentOffset.y = -refreshControl.bounds.height
    }

    @objc private func handleRefreshControl(_ sender: UIRefreshControl) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.collectionView.refreshControl?.endRefreshing()
        }
    }

}

10行目で色を設定しているにも関わらず、viewDidAppear(_:)内でbeginRefresing()を呼ぶと黒色のインジケーターが表示されます。 その後手動でリフレッシュする際には設定した色が表示されます。

viewDidLoad(:)やviewWillAppear(:)で呼んだ場合にも同様に発生します。

またUIViewControllerではなく、UICollectionViewControllerを用いた場合にも同様に発生します。

 

解決策

色々調査した結果、viewDidAppear(_:)が呼び出されるタイミングでUIRefreshControlのセットアップが終わってないのかなぁと思われます。

とりあえず解決策コード。


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard let refreshControl = collectionView.refreshControl, !refreshControl.isRefreshing else {
            return
        }
        UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: { [weak self] in
            self?.collectionView.contentOffset.y = -refreshControl.bounds.height
        }, completion: { (_) in
            refreshControl.beginRefreshing()
            refreshControl.sendActions(for: .valueChanged)
        })
    }
  1. 0.25秒かけてcollectionViewを下にひっぱるアニメーション
  2. アニメーション完了後、beginRefresh()でリフレッシュを始める
  3. RefreshControlの値が変わった通知を送る(これを呼び出すことでhandleRefreshControl(_:)が呼ばれる)

手順はこの3つです。

 

実際の動きは以下の通りです。

 

 

失敗例

成功例をみたところで、失敗例と比較していこうと思います。

失敗例1: 先にbeginRefreshを呼んでしまう

一見良さそうに見えるんですが、失敗します。


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard let refreshControl = collectionView.refreshControl, !refreshControl.isRefreshing else {
            return
        }
        refreshControl.beginRefreshing()
        UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: { [weak self] in
            self?.collectionView.contentOffset.y = -refreshControl.bounds.height
        }, completion: { (_) in
            refreshControl.sendActions(for: .valueChanged)
        })
    }

結果

黒くなってしまいます。

失敗例2: animateブロックでbeginRefresingしちゃう

これはなかなか愉快な挙動します。


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard let refreshControl = collectionView.refreshControl, !refreshControl.isRefreshing else {
            return
        }
        UIView.animate(withDuration: 0.25, delay: 1, options: .curveEaseInOut, animations: { [weak self] in
            self?.collectionView.contentOffset.y = -refreshControl.bounds.height
            refreshControl.beginRefreshing()
            }, completion: { (_) in
                refreshControl.sendActions(for: .valueChanged)
        })
    }

結果

一瞬黒く表示され、その後白に変わっていきます。あとなんかいつもよりくるくるします。

 

実は成功例は、この一瞬黒く表示される間にインジケーター自体が表示されていません。

しかしこの失敗例でも実際に引っ張ってリロードする時と同じように徐々に表示されているわけではなく、アニメーションなしで表示されます。(本当は1つずつ円を描くアニメーションをして欲しいよね...)

 

Extensionを作成したよ

UIScrollView系統のサブクラスで起こる問題なので、UIScrollViewのExtensionを書きました。


import UIKit

extension UIScrollView {

    func beginRefreshing() {
        guard let refreshControl = refreshControl, !refreshControl.isRefreshing else {
            return
        }
        UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: { [weak self] in
            self?.contentOffset.y = -refreshControl.bounds.height
            }, completion: { (_) in
                refreshControl.beginRefreshing()
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                    refreshControl.sendActions(for: .valueChanged)
                }
        })
    }
}

 

こちらはcollectionViewでも問題なく使えます。beginRefreshing()を呼んだ後にすぐendRefreshing()を呼び出すと動きがかくつくので、少し待ってからsendActions(for:)を呼ぶようにしています。

 

デモを作成したので実際に動かして試してみてください。

uruly/RefreshControlTintColorDemo

 

終わりに

ブログ書くのが月1とかになってるのでもっと書きたいんですが、iOSのお仕事中に書いてることをブログに書いてしまっていいのかというのにかなり判断に悩んでしまいます。あと単純にブログを書くのに時間と体力を使うのでなかなか書き始められない…

ここまで書いておいて今更なんですが、投稿して良いかお聞きしたところOK頂けたので、とりあえず安心して公開しようと思います。

今週は少し余裕があるはずなので、ちょこちょこと書いていこうと思います。

ほいではでは〜

Comments

コメントはありません。

現在コメントフォームは工事中です。