Swift　TableViewで無限スクロール(ページネート)を実装する方法

##Swift　TableViewで無限スクロール(ページネート)を実装する方法

普段の実装で、なんとなくTableViewの無限スクロール(ページネート)を実装してしまっていたので、改めてここに備忘録として記載したいと思います

##そもそもページネート(ページネーション)とは
ページネイト(ページネーション)とは、ページ付けと言う意味の英単語になります。
webページなどで、大量のコンテンツやリンクなどを掲載する際、一枚のページでは表示仕切れなかったり処理に時間がかかってしまったりします。
そこで、同じ様なデザインのページを複数分割し、ページ下部などに各ページへのリンクを並べた構成などにすることがあります。
この様なページ構成や、ソフトウェアにより自動的にページ分割を行う処理や機能のことをページネイト(ページネーション)と言います。

Youtubeの検索でも、検索結果が表示されてその下に「1,2,3,4,5」とあり、それぞれのページでより下層の検索結果が見れますよね?(最新順ならどんどん古いの、関連度順ならどんどん関連が古いの・・・)
その構成・機能をページネーションと言います。

##無限スクロールとは
ユーザーが多量のコンテンツをスクロールし続けるテクニックです。

インスタグラムなども、スクロールすればするだけどんどん下層のコンテンツが見れますよね？

これは下にスクロールするたびに新たな情報(Modelとか)を取得し表示していくことで実現しています。
イメージとしては、ページネートと一緒で一度に大量の情報を提示してはあらゆる処理に時間がかかってしまうので、一定数ずつ表示して、最後まで行けば2ページ目分の情報を追加、また最後まで行けば3ページ目分の情報を追加・・・・としているのです。

##TableViewでの実装方法

###STEP1: TableViewを表示されるまで
割愛します

###STEP2: 表示する情報を取得するAPIを準備(作成or流用)
詳細は割愛します。

私は自作のAPIから取得しますので、その方法に合わせて記載します。
今回はページ数を送るとその分のデータを取得できる仕様となっています。

情報獲得部分は各自が利用するAPIや方法に読み替えてください。

###STEP3：　変数を準備する
無限スクロールに利用する変数を準備しましょう。
・表示するデータを入れる配列
・現在の表示ページ数
・現在の表示ステータス

```swift:ViewControlle.swift

import UIKit
import Alamofire
import SwiftyJSON

class testViewControlle:UIViewController,UITableViewDelegate,UITableViewDataSource{

      //==========準備する変数 ここから============
      //表示するデータの配列
      var datas:[Data] = []
      //ページネーション関連変数     
      var pageCount:Int = 1
      //表示ステータス
      var displayStatus:String = "standby"
      //ページの総数
      var total_pages:Int = 1
      //==========準備する変数 ここまで============

      override func viewDidLoad() {
          super.viewDidLoad()
          
          tableView.dataSource = self
          tableView.delegate = self

          getDatas()
      }

       func numberOfSections(in tableView: UITableView) -> Int {
             // #warning Incomplete implementation, return the number of sections
             return 1
        }

       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
             // #warning Incomplete implementation, return the number of rows
             return datas.count
        }

       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
              //各自好きな様にcellを構築
              let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
              return cell
        }

        //dataを取得するためのメソッド
        func getDatas(){
            
            //ここにAPIを用いて該当ページの情報を取得する処理を記載
            //例     
             let getUelText = "https://test.com/testdata_get/" + String(pageCount)
             let getUrl = getUelText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
             AF.request(getUrl,method: .get,parameters: nil,encoding: JSONEncoding.default).responseJSON{(responce) in

                switch responce.result{
                     case .success:
                         let mJson:JSON = JSON(responce.data as Any)
                         var dateCount:Int = mJson["count"].int!
                         var mData:Data = Data()    
                         //例)ここでmDataに取得した情報をセットしておく                                        
                         self.datas.append(mData) 
                         self.tableView.reloadData()

                     case .failure(let error):
                     
                     
                 }
             }


        }

}

```

###STEP4：　スクロール位置を検知し、終わりに近づいたらdataを取得する様にする
・スクロールの位置を検知するには「scrollViewDidScroll」メソッドを用います。
・メソッドないで、現在のスクロール位置と一番下を取得し、最後に近づいただdataを取得しに行きます。
・取得しにいく時は次のページの情報を取得しにいく様、PageCountを1増やします

```swift:ViewControlle.swift

import UIKit
import Alamofire
import SwiftyJSON

class testViewControlle:UIViewController,UITableViewDelegate,UITableViewDataSource{

      //表示するデータの配列
      var datas:[Data] = []
      //ページネーション関連変数     
      var pageCount:Int = 1
      //表示ステータス
      var displayStatus:String = "standby"
      //ページの総数
      var total_pages:Int = 1

      override func viewDidLoad() {
          super.viewDidLoad()
          
          tableView.dataSource = self
          tableView.delegate = self

          getDatas()
      }

       func numberOfSections(in tableView: UITableView) -> Int {
             // #warning Incomplete implementation, return the number of sections
             return 1
        }

       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
             // #warning Incomplete implementation, return the number of rows
             return datas.count
        }

       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
              //各自好きな様にcellを構築
              let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
              return cell
        }

        //dataを取得するためのメソッド
        func getDatas(){
            
            //ここにAPIを用いて該当ページの情報を取得する処理を記載
            //例     
             let getUelText = "https://test.com/testdata_get/" + String(pageCount)
             let getUrl = getUelText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
             AF.request(getUrl,method: .get,parameters: nil,encoding: JSONEncoding.default).responseJSON{(responce) in

                switch responce.result{
                     case .success:
                         let mJson:JSON = JSON(responce.data as Any)
                         var dateCount:Int = mJson["count"].int!
                         var mData:Data = Data()    
                         //例)ここでmDataに取得した情報をセットしておく                                        
                         self.datas.append(mData) 
                         self.tableView.reloadData()

                     case .failure(let error):
                     
                     
                 }
             }


        }

           //==========STEP4 scrollViewDidScroll ここから============
           func scrollViewDidScroll(_ scrollView: UIScrollView) {
               let currentOffsetY = scrollView.contentOffset.y
               let maximumOffset = scrollView.contentSize.height - scrollView.frame.height
               let distanceToBottom = maximumOffset - currentOffsetY
               if(distanceToBottom) {
                  pageCount += 1
                  getDatas()
               }
            }
           //========== ここまで============

}

```

###STEP5：　条件を整え、「重複して取得しようとする」「もうデータがないのに追加取得しようとする」が内容にします
・現状の「scrollViewDidScroll」内では、botttomまでの距離が500以下だと何回でもgetDatasをしてしまいます　
→　displayStatus変数を用いて、重複して取得しようとしない様調整します
・現状の「scrollViewDidScroll」内では、もう取得できるデータがなくても、getDatasをしてしまいます　
→　total_pages変数を用いて、重複して取得しようとしない様調整します

```swift:ViewControlle.swift

import UIKit
import Alamofire
import SwiftyJSON

class testViewControlle:UIViewController,UITableViewDelegate,UITableViewDataSource{

      //表示するデータの配列
      var datas:[Data] = []
      //ページネーション関連変数     
      var pageCount:Int = 1
      //表示ステータス
      var displayStatus:String = "standby"
      //ページの総数
      var total_pages:Int = 1

      override func viewDidLoad() {
          super.viewDidLoad()
          
          tableView.dataSource = self
          tableView.delegate = self

          getDatas()
      }

       func numberOfSections(in tableView: UITableView) -> Int {
             // #warning Incomplete implementation, return the number of sections
             return 1
        }

       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
             // #warning Incomplete implementation, return the number of rows
             return datas.count
        }

       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
              //各自好きな様にcellを構築
              let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
              return cell
        }

        //dataを取得するためのメソッド
        func getDatas(){
             //==========STEP5 ここから============
             self.displayStatus = "loading"
             //==========ここまで============
            
            //ここにAPIを用いて該当ページの情報を取得する処理を記載
            //例     
             let getUelText = "https://test.com/testdata_get/" + String(pageCount)
             let getUrl = getUelText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
             AF.request(getUrl,method: .get,parameters: nil,encoding: JSONEncoding.default).responseJSON{(responce) in
                //==========STEP5 ここから============
                 self.displayStatus = "standby"
                //==========ここまで============

                switch responce.result{
                     case .success:
                         let mJson:JSON = JSON(responce.data as Any)
                         var dateCount:Int = mJson["count"].int!
                         total_pages = //トータルページ数が取得できるなら取得する
                         var mData:Data = Data()    
                         //例)ここでmDataに取得した情報をセットしておく                                        
                         self.datas.append(mData) 
                         self.tableView.reloadData()

                     case .failure(let error):
                         print(error)
                     
                 }
             }


        }


           func scrollViewDidScroll(_ scrollView: UIScrollView) {
               let currentOffsetY = scrollView.contentOffset.y
               let maximumOffset = scrollView.contentSize.height - scrollView.frame.height
               let distanceToBottom = maximumOffset - currentOffsetY
               //==========STEP5 ここから============
                if(distanceToBottom < 500 && displayStatus == "standby" && total_pages != pageCount){
                    pageCount += 1
                    getDatas()
                }
               //========== ここまで============
               
            }


}

```

完成!!
※もしトータルページ数が取得できない場合は、データ取得時にレスポンスが0であればもう最後である旨のフラグを立てたりすると良いかもしれません。

##まとめ
・変数を準備します。
・「scrollViewDidScroll」でスクロールの位置を取得します。
・スクロールが下に近づいたら、次のデータを取得しにいく様にします。
・取得しに行っている間に重複して作動したり、もうデータ（ページ）がないのに取得しに行ったりしない様、各種条件で制御しましょう。