TableViewでのデータの読込・リフレッシュ・更新についての処理

# はじめに
Qiitaが提供しているAPIを使用して、`TableView`に記事を表示する際に、データの__読込__・__リフレッシュ__・__更新__について実装しましたので備忘録として投稿します。


# 概要
1. `QiitaAPI`の初期取得
2. 取得している`QiitaAPI`のリフレッシュ
3. `TableView`下部までスクロールした際に`QiitaAPI`追加取得

※上記にフォーカスをあたて説明になります。


# 動作環境
【Xcode】Version 12.0.1
【Swift】Version 5.3


# 実装コード

```swift:CustomTableView.swift
import UIKit

class CustomTableView: UITableView {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    override init(frame:CGRect, style: UITableView.Style) {
        super.init(frame: frame, style: .plain)
        setup()
    }
        
    private func setup() {
        let refreshControl = UIRefreshControl()
        refreshControl.attributedTitle = NSAttributedString(string: "読み込み中")
        self.refreshControl = refreshControl
        // UITableViewが空のときの線(セパレーター)を消すために、空のViewを設定
        self.tableFooterView = UIView()
    }
        
    func addTargetToRefreshControl(_ target: Any?, action: Selector, event: UIControl.Event) {
        self.refreshControl?.addTarget(target, action: action, for: event)
    }

    func beginRefreshing() {
        guard let refreshControl = self.refreshControl else { return }
        refreshControl.beginRefreshing()
        refreshControl.sendActions(for: .valueChanged)
        self.contentOffset.y = -self.bounds.height
    }
    
    func endRefreshing() {
        guard let refreshControl = self.refreshControl else { return }
        refreshControl.endRefreshing()
    }
    
}
```

```swift:ViewController.swift
class ViewController: UIViewController {
    
    @IBOutlet private weak var tableView: CustomTableView!

    private var reloading: Bool = false
    private var page: Int = 20

    # ・・・省略・・・

    override func viewDidLoad() {
        super.viewDidLoad()
        # ・・・省略・・・
        tableView.addTargetToRefreshControl(self, action: #selector(self.refreshArticlesAction), event: .valueChanged)
        // refreshControlを呼び出し、ローディングする
        tableView.beginRefreshing()
               
        # ・・・省略 （Qiitaからデータ取得する処理） ・・・
        tableView.endRefreshing()
        tableView.reloadData()
    }
        
    @objc private func refreshArticlesAction() {
        // 取得数を初期値にする
        page = 20
       
        # ・・・省略 （Qiitaからデータ取得する処理） ・・・
        tableView.endRefreshing()
        tableView.reloadData()
    }
    
}

# ・・・省略・・・

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.contentOffset.y > 0 {
            let height = scrollView.frame.size.height
            let contentYoffset = scrollView.contentOffset.y
            let distanceFromBottom = scrollView.contentSize.height - contentYoffset
            if distanceFromBottom < height {
                // 何回も呼ばれてしまうので、リロード中は変数：reloadingをtrueにしておく
                if !reloading && page < 100 {
                    // 取得数を20追加にする
                    if page <= 100 { page += 20 }
                    // リロード処理のフラグ変更を「true」
                    reloading.toggle()

                    # ・・・省略 （Qiitaからデータ取得する処理） ・・・
                    // リロード処理のフラグ変更を「false」
                    reloading.toggle()
                    tableView.reloadData()
                }
            }
        }
    }
}
```

# 実装詳細

### 1. `QiitaAPI`の初期取得
![output1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692635/8554fa52-b57d-8b1b-53b6-f4d8f0c88e75.gif)

* `viewDidLoad`で`refreshControl.beginRefreshing()`を呼び出してローディング開始。
* `Qiita`からデータ取得。
* `refreshControl.endRefreshing()`でローディング停止し、`tableView.reloadData()`で内容更新します。


### 2. 取得している`QiitaAPI`のリフレッシュ
![output3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692635/4654f33a-a6e3-5959-d526-61a4b72bd5e7.gif)

* `UIRefreshControl`のインスタンスに`addTarget`で指定しています。

```swift:CustomTableView.swift
func addTargetToRefreshControl(_ target: Any?, action: Selector, event: UIControl.Event) {
    self.refreshControl?.addTarget(target, action: action, for: event)
}
```

```swift:ViewController.swift
tableView.addTargetToRefreshControl(self, action: #selector(self.refreshArticlesAction), event: .valueChanged)
```

* 下に引っ張る度に`@objc private func refreshArticlesAction()`が呼ばれるので、初期取得数 __20__ に変更し、`Qiita`からデータ取得。（省略しておりますが、初期取得数は`QiitaAPI`の取得数になります）
* `refreshControl.endRefreshing()`でローディング停止し、`tableView.reloadData()`で内容更新します。


### 3. `TableView`下部までスクロールした際に`QiitaAPI`追加取得
![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692635/a61a877c-a108-83cf-24d2-0b9d32efb478.gif)

* `ScrollView`のデリゲートメソッドを使い、下までスクロールされたか検出します。
* `distanceFromBottom`何回も呼ばれるので`Bool`型の変数`reloading`を用意しました。
* `QiitaAPI`の追加取得数は __20__ ずつ増やすことにし、__100__ で上限としました。
* `tableView.reloadData()`で内容更新します。

```swift:ViewController.swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.contentOffset.y > 0 {
        let height = scrollView.frame.size.height
        let contentYoffset = scrollView.contentOffset.y
        let distanceFromBottom = scrollView.contentSize.height - contentYoffset
        if distanceFromBottom < height {
            // 何回も呼ばれてしまうので、リロード中は変数：reloadingをtrueにしておく
            if !reloading && page < 100 {
                // 取得数を20追加にする
                if page <= 100 { page += 20 }
                // リロード処理のフラグ変更を「true」
                reloading.toggle()

                # ・・・省略 （Qiitaからデータ取得する処理） ・・・
                // リロード処理のフラグ変更を「false」
                reloading.toggle()
                tableView.reloadData()
            }
        }
    }
}
```


# 参考
* [TableViewをスクロールしてデータ更新（上・下それぞれバージョン）
](https://qiita.com/john-rocky/items/082bb2df0bf6e238b0d6)