UITableViewのスクロール位置を戻したい！

![Xcode-12.0](https://img.shields.io/badge/Xcode-12.0-brightgreen) ![iOS-14.0](https://img.shields.io/badge/iOS-14.0-brightgreen) ![Swift-5.3](https://img.shields.io/badge/Swift-5.3-brightgreen)

# はじめに
`UITableView` で `reloadData()` をしたときにトップへスクロールしたいときがたまにあります。でもこれが結構めんどくさい。。。  
色々方法を試したので備忘録として記載します。

やりたいことは「ボタン押下したときにリロード＋スクロール位置をトップに戻したい」ということ！

こんな感じ（before が最下部表示で after が before の状態からリロード+トップへスクロールした状態です)

| before | after |
| --- | --- |
|![before](https://user-images.githubusercontent.com/34936885/95737721-1d9e4600-0cc3-11eb-879f-a760546ca1f3.png)|![after](https://user-images.githubusercontent.com/34936885/95737749-24c55400-0cc3-11eb-859a-18fe0dd36ccc.png)|

テーブルは[前回の記事](https://qiita.com/am10/items/babaf83186386978d60c)で書いたヘッダーが消えたり出たりする `grouped` スタイルのテーブルでやります！
（before はヘッダー非表示、after はヘッダー表示です）

# 方法
テーブルをトップにスクロールする方法として `setContetnOffset` と `scrollToRow` があり、 `reloadData` の前にスクロールさせるのか後にスクロールさせるのかで４パターンできると思います。

## 実行結果１
実装はこんな感じ

```swift
// パターン１（setContetnOffset先スクロール）
tableView.setContentOffset(.zero, animated: false)
tableView.reloadData()

// パターン２（scrollToRow先スクロール）
tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)
tableView.reloadData()

// パターン３（setContetnOffset後スクロール）
tableView.reloadData()
tableView.setContentOffset(.zero, animated: false)

// パターン４（scrollToRow後スクロール）
tableView.reloadData()
tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)
```

結果はこんな感じ

| パターン１ | パターン２ | パターン３ | パターン４ |
| --- | --- | --- | --- |
|![1](https://user-images.githubusercontent.com/34936885/95736793-aa480480-0cc1-11eb-8689-656157cf0b9c.png)|![2](https://user-images.githubusercontent.com/34936885/95736830-b9c74d80-0cc1-11eb-8261-54457c9bbd43.png)|![3](https://user-images.githubusercontent.com/34936885/95736865-c6e43c80-0cc1-11eb-9a43-720347e1a452.png)|![4](https://user-images.githubusercontent.com/34936885/95736901-d1063b00-0cc1-11eb-8070-46943e589e46.png)|

パターン１とパターン４（でも row 設定なのでヘッダーまではスクロールしてない）はいけてそうだけどパターン２、３が中途半端な位置になっている。。。

## 実行結果２
なんかわからんけどとりあえずコンテンツサイズが確定してない=レイアウトが中途半端な状態だから上のような結果になるんだろうと思い `layoutIfNeeded()` 呼べばいいんじゃね？ということで下記のように実装してみた。

```swift
// パターン１（setContetnOffset先スクロール）
tableView.setContentOffset(.zero, animated: false)
tableView.layoutIfNeeded()
tableView.reloadData()

// パターン２（scrollToRow先スクロール）
tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)
tableView.layoutIfNeeded()
tableView.reloadData()

// パターン３（setContetnOffset後スクロール）
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(.zero, animated: false)

// パターン４（scrollToRow後スクロール）
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)
```

結果はこんな感じ

| パターン１ | パターン２ | パターン３ | パターン４ |
| --- | --- | --- | --- |
|![l_1](https://user-images.githubusercontent.com/34936885/95736915-d794b280-0cc1-11eb-990a-7986ff95b7f6.png)|![l_2](https://user-images.githubusercontent.com/34936885/95736927-dfeced80-0cc1-11eb-82d2-e12d0c0307a3.png)|![l_3](https://user-images.githubusercontent.com/34936885/95736948-e8452880-0cc1-11eb-9a4d-ca7a225f9a0e.png)|![l_4](https://user-images.githubusercontent.com/34936885/95736961-eda27300-0cc1-11eb-8709-b50814f695bd.png)|

全パターンいい感じにいけてそう:tada:

## 実行結果３
`layoutIfNeeded()` を呼ぶことでそれぞれ思った通りの動作になってますがあまいです！セルの高さが固定なら問題ないのですがセルの高さが可変の場合はちょっと。。。:confounded:

下記のような２パターンのセルを用意します。

<img width="250" alt="cell" src="https://user-images.githubusercontent.com/34936885/95737224-4d008300-0cc2-11eb-81b9-6edcf4b7e468.png">

実行結果２の処理を試してみると結果はこんな感じ（ちなみに `layoutIfNeeded` ないパターンだとパターン４以外は中途半端なスクロールになりました）

| パターン１ | パターン２ | パターン３ | パターン４ |
| --- | --- | --- | --- |
|![b_l_1](https://user-images.githubusercontent.com/34936885/95737405-905af180-0cc2-11eb-90d4-f625b4ef8a4f.png)|![b_l_2](https://user-images.githubusercontent.com/34936885/95737419-99e45980-0cc2-11eb-8079-1fa7074b7f7c.png)|![b_l_3](https://user-images.githubusercontent.com/34936885/95737434-a10b6780-0cc2-11eb-8ddd-97844905370d.png)|![b_l_4](https://user-images.githubusercontent.com/34936885/95737450-a7014880-0cc2-11eb-9781-d5998278bca1.png)|

パターン３以外はいけてそうです:clap:おそらく `tableView(_ :estimatedHeightForRowAt:)` で適切な値を返してやるとパターン３でもうまくいくと思います。２パターンとかならいいですがここにオートレイアウトとか関与してくると色々高さ計算がめんどくさくなります。。。

セルの高さに関しては下記の記事に色々丁寧に記載されていたので参考に（私はあんまりわかってない。。。）  
[UITableViewのrowHeightやestimatedRowHeightに何を設定すると良いのか](https://qiita.com/masashi-sutou/items/bb8ac89c717dcbe56123)

# 結論
今回は計算とかいろいろめんどくさいのとヘッダーまでスクロールしたかったので下記を採用しました。（たぶんいけてそう:v:）

```swift
// パターン１（setContetnOffset先スクロール）
tableView.setContentOffset(.zero, animated: false)
tableView.layoutIfNeeded()
tableView.reloadData()
```

動作はこんな感じ

![scroll](https://user-images.githubusercontent.com/34936885/95737569-dca63180-0cc2-11eb-8543-741537d58daa.gif)

# 20201024追記
[コメント](https://qiita.com/am10/items/e9e7eae5665d2c8bc85f#comment-35489cd34fcc3f04b7cf)でいただきましたが下記のようにスクロール位置を `.top` から `.bottom` に変更するといけそうです:tada:

```swift
tableView.reloadData()
tableView.scrollToRow(at: .init(row: 0, section: 0), at: .bottom, animated: false)
```

`layoutIfNeeded` も必要ないですしこれが一番いいんじゃないかなと思います:relaxed:

# おわりに
`reloadData` とトップへスクロールの組み合わせはわりとやることあるんですが結局どういう方法がいいのかな？といつも悩んでしまいます。。。

下記のようなやり方もあるみたいです。

[iOS TableView reload and scroll top](https://stackoverflow.com/questions/48017955/ios-tableview-reload-and-scroll-top/50029233)

他なにかいい方法ご存知であればぜひご教授ください:raised_hands: