やりたかっこと
前提として
- 一番下までスクロールしたらAPIを叩いてロードする仕組みを実装している
- APIから読み込んでいるフラグをクラスの変数として保持している
- フラグがtrue
の場合はロードしない
というクラスを組んだ場合、フラグをfalse
にするタイミングによってはUITableView#scrollToRowAtIndexPath:atScrollPosition:animated
を使った段階でまたAPIをロードしてしまいます。
それを阻止した処理ができたので書いておきます。
ソースコード
class DataSource: UITableViewDataSource {
// UITableViewDataSourceの処理は今回はいらないので省略
// handlerは追加する分のNSIndexPathの配列を返す
func reloadData(#handler: ([NSIndexPath] -> Void)) {
// ここでAPIとごにょごにょ
var indexPaths:[NSIndexPath] = []
//
// NSIndexPathを追加する処理
//
handler(indexPaths)
}
// handlerは追加する分のNSIndexPathの配列を返す
func addData(#handler: ([NSIndexPath] -> Void)) {
// ここでAPIとごにょごにょ
var indexPaths:[NSIndexPath] = []
//
// NSIndexPathを追加する処理
//
handler(indexPaths)
}
}
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
// これがフラグの変数
private var isLoading: Bool = false
// これは独自クラス
private var dataSource: DataSource = DataSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = dataSource
}
}
// MARK: - load data
extension ViewController {
private func reloadData() {
isLoading = true
dataSource.reloadData(handler: {[weak self] indexPaths in
self?.insertAtIndexPaths(indexPaths)
})
}
private func addData() {
isLoading = true
dataSource.reloadData(handler: {[weak self] indexPaths in
self?.insertAtIndexPaths(indexPaths)
})
}
}
extension ViewController {
// ここの処理が今回やりたかったことです。
// 今回は追加するindexPathsの頭のNSIndexPathに追加後スクロールするようにしました。
private func insertAtIndexPaths(indexPaths: [NSIndexPath]) {
CATransaction.begin()
CATransaction.setCompletionBlock({
self?.loading = false
})
self?.tableView.beginUpdates()
self?.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Bottom)
self?.tableView.endUpdates()
if let firstIndexPath = indexPaths.first {
self?.tableView.scrollToRowAtIndexPath(firstIndexPath, atScrollPosition: .Bottom, animated: true)
}
CATransaction.commit()
}
}
extension ViewController: UITableViewDelegate {
// UITableViewDelegateの処理は今回はいらないので省略
func scrollViewDidScroll(scrollView: UIScrollView) {
if isLoading || !isViewLoaded() {
return
}
let scrollValue = scrollView.contentSize.height - scrollView.bounds.height
if scrollValue > scrollView.contentOffset.y {
return
}
// 一番下までスクロールしたというこでデータを追加する
addData()
}
}
読んでもらえればだいたいわかるかと思います...。
補足
今回ずっと戦っていたのが以下の処理のところになります。
extension ViewController {
private func insertAtIndexPaths(indexPaths: [NSIndexPath]) {
CATransaction.begin()
CATransaction.setCompletionBlock({
self?.loading = false
})
self?.tableView.beginUpdates()
self?.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Bottom)
self?.tableView.endUpdates()
if let firstIndexPath = indexPaths.first {
self?.tableView.scrollToRowAtIndexPath(firstIndexPath, atScrollPosition: .Bottom, animated: true)
}
CATransaction.commit()
}
}
ここでUITableView#insertRowsAtIndexPaths:withRowAnimation:
が終わった後にUITableView#scrollToRowAtIndexPath:atScrollPosition:animated:
を実行して、すぐにisLoading
をfalse
にするとすぐにaddData()
が呼び出されてしまいました。
原因
原因は考えれば簡単なのですが、UITableView#scrollToRowAtIndexPath:atScrollPosition:animated:
はアニメーションなので終わるまで時間がかかります。
しかもスクロールアニメーションをしているのでUITableViewDelegate#scrollViewDidScroll:
がコールされます。
そこでisLoading
がfalse
になっているので、addData()
が呼び出されてしまいます。
対応
UITableView#scrollToRowAtIndexPath:atScrollPosition:animated:
が終了した後にisLoading
をfalse
にすれば大丈夫ということでCATransaction
を使うことにしました。
今回の件で説明するとCATransaction#begin()
からCATransaction#commit()
の間の処理が終了したらCATransaction#setCompletionBlock:
のblock:(() -> Void)
が呼び出される仕組みとなっています。
終わりに
やっと綺麗に思ったとおりのことができて、嬉しくて投稿しました。