【Swift】UITableViewにindexPathをinsertした後scrollさせる


## やりたかっこと

前提として

- 一番下までスクロールしたらAPIを叩いてロードする仕組みを実装している
- APIから読み込んでいるフラグをクラスの変数として保持している
- フラグが`true`の場合はロードしない

というクラスを組んだ場合、フラグを`false`にするタイミングによっては`UITableView#scrollToRowAtIndexPath:atScrollPosition:animated`を使った段階でまたAPIをロードしてしまいます。

それを阻止した処理ができたので書いておきます。

## ソースコード

```swift:DataSource.swift
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)
	}
}
```

```swift:ViewController.swift
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()
    }
}
```

読んでもらえればだいたいわかるかと思います...。

## 補足

今回ずっと戦っていたのが以下の処理のところになります。

```swift
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)`が呼び出される仕組みとなっています。

## 終わりに

やっと綺麗に思ったとおりのことができて、嬉しくて投稿しました。