😬

[Swift]CollectionViewで、PullToRefresh中にTopInsetを変更した時の挙動

2023/04/09に公開

概要

UICollectionViewController で、UIScrolRefreshを用いた、PullToRefreshをしている最中に CollectionView.contentInset.topの値を変更すると、CollectionViewのセルが元の場所に戻らなくなる。
(添付動画では、pullToRefresh完了後に、1つ目のセルが上部にめり込んでいる)

原因

  • conetntInsetAdjustmentBehavior = .neverで、pullToRefreshの最中にtopInsetの値を変更したこと。
  • 試した時の環境(Xcode13.4, iOS15)

解決策

  1. pullToRefreshの最中にtopInsetの値を変更しない。
  2. pullToRefresh中にtopInsetの値を変更するときは、endRefreshingで引かれる値をあらかじめ足しておく。
  3. contentInset.adjustmentBehaviorをデフォルトのautomaticのままにする。

検証(ソースコード github)

簡単なdemoプロジェクトを作って、何が起きているかを検証する。

ViewDidAppear周り

  • contentInset.adjustmentBehaviorneverに設定している。
  • refreshControlで、pullToRefreshのときに、self.pullToRefreshが呼ばれるように設定。
  • contentInset.top50を設定し、上部に余白を持たせている。
CollectionViewController
//MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        
        //default is .automatic
        collectionView.contentInsetAdjustmentBehavior = .never
        
        printInsetTop(message: "ViewDidAppear")
    }
    
    private func setup() {
        collectionView.backgroundColor = .white
        
        //Refresh Control
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
        refreshControl.addTarget(self, action: #selector(self.pullToRefresh), for: .valueChanged)
        collectionView.addSubview(refreshControl)
        
        // set topInset so that we can see the 1st cell entirely
        self.setCollectionViewTopInset()
    }

    private func setCollectionViewTopInset() {
        collectionView.contentInset.top = 50
    }

// MARK: - Debug
    private func printInsetTop(message: String) {
        print("# \(message)")
        print("+ contentInset:         \(collectionView.contentInset.top)")
        print("+ adjustedContentInset: \(collectionView.adjustedContentInset.top)")
        print("")
    }

pullToRefresh周り

  • stardLoadingの中で、topInsetの値をいじる。
  • startLoadingの2秒後に、endLoadingを呼び、pullToRefreshを終了させる。
  • startLoadingendLoadingにおいて、topInsetの値をプリントする。
CollectionViewController
// MARK: - Pull To Refresh
    @objc func pullToRefresh() {
        startLoading()
        DispatchQueue.main.asyncAfter (deadline: .now() + 2) {
            self.endLoading()
        }
    }
    
    private func startLoading() {
        printInsetTop(message: "startLoading before modify inset")
        setCollectionViewTopInset()
        printInsetTop(message: "startLoading after modify inset")
    }
    
    private func endLoading() {
        printInsetTop(message: "startLoading before endRefreshing")
        refreshControl.endRefreshing()
        printInsetTop(message: "startLoading after endRefreshing")
    }

何が起きていたのか

上記のコードを用いて、pullToRefreshを行った時の、collectionViewtopInsetの値をprintしてみると下のようになる。

# ViewDidAppear
+ contentInset:         50.0

# startLoading before modify inset
+ contentInset:         110.0

# startLoading after modify inset
+ contentInset:         50.0

# startLoading before endRefreshing
+ contentInset:         50.0

# startLoading after endRefreshing
+ contentInset:         -10.0 
  • ViewDidAppearの時点では、設定した通り50となっている。

  • pullToRefreshによって、startLoadingが呼ばれた時点では、110となっている。

  • startLoadingの中で、topInset=50と設定したため、値が50に戻っている。

  • refreshControl.endRefreshingが呼ばれる前は、50のまま。

  • refreshControl.endRefreshingが呼ばれた後に、-10となっており、これが上部にめり込んだ原因。

考察

  • pullToRefresh中には、画面上部にloadingIndicatorが表示される。
    そのためのスペースを用意するために、pullToRefreshが始まったタイミングで、collectionViewによって、topInsetの値が変更される。(今回のデモでは、元の値 +60されていた)

  • pullToRefreshが終わる時には、loadingIndicatorのために用意したスペースを元に戻す必要があるので、refreshControl.endRefreshingが呼ばれるタイミングで、topInsetの値が変更される。(今回のデモでは、-60されていた)

  • pullToRefreshの途中で、topInsetの値を変更したため、もとに戻らなくなった。

    • (通常の場合): start:50 -> start Refresh: 50 + 60 -> end Refresh: 50 + 60 - 60 -> end: 50
    • (demoの場合): start:50 -> start Refresh: 50 + 60 -> 値をセット: 50 -> end Refresh: 50 - 60 -> end: -10

解決策

1.pullToRefreshの最中にtopInsetの値を変更しない

  • topInset以外の値の変更(e.g. bottomInset)なら、この問題は発生しなかった。

2. pullToRefresh中にtopInsetの値を変更するときは、endRefreshingで引かれる値をあらかじめ足しておく

  • setCollectionViewTopInset()の中で、以下のようにする。
    collectionView.contentInset.top = 50 + (refreshControl.isRefreshing ? 60 : 0)

3. contentInset.adjustmentBehaviorをデフォルトのautomaticのままにする

  • こうすると、pullToRefresh中にtopInsetを変更しても、この問題は発生しない。
  • contentInset.adjustmentBehavior = .automaticにしたときの、topInsetの値の変化。
# ViewDidAppear
+ contentInset:         50.0
+ adjustedContentInset: 50.0

# startLoading before modify inset
+ contentInset:         50.0
+ adjustedContentInset: 160.0

# startLoading after modify inset
+ contentInset:         50.0
+ adjustedContentInset: 160.0

# startLoading before endRefreshing
+ contentInset:         50.0
+ adjustedContentInset: 160.0

# startLoading after endRefreshing
+ contentInset:         50.0
+ adjustedContentInset: 100.0

Discussion