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

原因
-
conetntInsetAdjustmentBehavior = .neverで、pullToRefreshの最中にtopInsetの値を変更したこと。 - 試した時の環境(Xcode13.4, iOS15)
解決策
-
pullToRefreshの最中にtopInsetの値を変更しない。 -
pullToRefresh中にtopInsetの値を変更するときは、endRefreshingで引かれる値をあらかじめ足しておく。 -
contentInset.adjustmentBehaviorをデフォルトのautomaticのままにする。
検証(ソースコード github)
簡単なdemoプロジェクトを作って、何が起きているかを検証する。
ViewDidAppear周り
-
contentInset.adjustmentBehaviorをneverに設定している。 -
refreshControlで、pullToRefreshのときに、self.pullToRefreshが呼ばれるように設定。 -
contentInset.topに50を設定し、上部に余白を持たせている。
//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を終了させる。 -
startLoading、endLoadingにおいて、topInsetの値をプリントする。
// 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を行った時の、collectionViewのtopInsetの値を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