ScrollによるPaginationの実装


研修課題の終盤に差し掛かりました。
前回の続きです。
[Alamofire,SwiftyJSON,ModelでCellをカスタマイズしてTableViewを描画する](http://qiita.com/shunsukekyuwa/items/138b000aee2d63b73959)



##要件：
①APIのdataをTableViewのCellに描画する
②APIのdataは、nページある
③scrollによりiページ目のdataが描画し終わり、新たにi+1ページのdataが必要な時に通信を行い、それをcellに描画する
④scrollをトリガーにAPIへ通信するが、APIへ通信している時にscrollしても新たにAPIへ通信しないように制御する 
⑤APIの最後のページであるindexがある前提で(今回はboolean型変数のisLastを用いている。最後のページのみtrue、他はfalse)、最後のページも読んだらscrollしてもAPIへ通信しないように制御する

___
以下補足。
ここで実装する手順を考える前に、③をもう少し掘り下げてみます。
まずscrollをしたことを感知する関数として、`scrollViewDidScroll`という関数がUIScrollViewクラスには備わっているので、scroll感知にはこれを使います。
次に、scrollをしていき、i+1ページのdataが必要な時がいつか(すなわち、scrollをどれくらいしたらAPIへ通信をするのか)を定義してあげる必要があります。

④についてはやってみればわかりますが、APIへの通信は少し時間がかかります。
APIへ通信をしている間に通信の制御をせず、scrollを続けると何が起きるのかというと、iページのdataを読み込んでいる時にscrollを感知したことによりiページへの通信が複数回、同時並行で呼ばれてしまいます。
こうしたことを防ぐために、APIへ通信を始めたらそのページへの通信はシャットアウトする条件が必要で、これを通信の制御と私は表現しています。



##設計：
(a)通信をしている状態としていない状態の状態推移を追うenum、loadingStateを定義。(④に対応)
(b)③の「新たにi+1ページのdataが必要な時」とは、「iページ目のdataが初期位置からscrollされて残りが少なくなった時(私はy方向の高さが120を切ったら、という条件にしました)」と定義して評価するようにする。(③に対応)
(c)APIへのrequestをする関数を作成して、適切な時、すなわち起動時と、scrollにより③が発生した時にcallをする。この際、通信している時はlodingStateを.InProgress、APIからdataを抜き取り、インスタンス化し切ったらlodingStateを.Noneにswitchして次に読むページをincrementする。(③と④に対応)
なお、requestをする関数を作成するのは`viewDidLoad`は一回しか呼ばれないから。


##実装例：
```ProfileListViewController.swift
import UIKit
import Alamofire
import SwiftyJSON

class ProfileListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!
    var profileCells: [ProfileListItemTableViewCellModel] = [ProfileListItemTableViewCellModel]()
    var currentPage : Int = 0
    var isLast: Bool = false
    //  (a)状態推移を担うenum、LoadingState型は下記参照
    var loadingState: LoadingState = .None
    var requestable: Bool {
        return loadingState == .None && !isLast
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        
        // (c)起動時にAPIへ通信している
        self.requestObjects()
    }

    //  (c)APIへのgetrequest関数
    func requestObjects(refresh: Bool=false) {
        if  requestable == false { return }
            
        //  loading data from the currentPage in the API
        self.loadingState = .InProgress
    
        //  address to the API
        let userID: Int = 1
        let params = ["page": "\(currentPage)"]
        let request = APIRequestManager.getRequest("v123456", urlString: "/users/\(userID)/notifications", parameters: params, accessToken: true) //APIRequestManagerは下記参照
        
        //  call getRequest to the API
        //  SwiftTaskを用いています
        let task = APICollectionRequestTask<ProfileListItemTableViewCellModel>(request: request).value
        task.progress { progress in
        }.success { models in
            self.loadingState = .None
            ++self.currentPage
        
            //   add the objective data from the API to array and assign the data "isLast" from the API to the local variable "isLast"
            for model in models {
                self.profileCells.append(model)
                self.isLast = models.last!.isLast
            }
            self.tableView.reloadData()
        }.failure{ status in
            // error handling
            // user alert
            self.loadingState = .None
            print("failure:\(status)")
        }
    }

    //省略
    
extension ProfileListViewController {
    func scrollViewDidScroll(scrollView: UIScrollView) {
        // if the value of the variable "isLast" in called API equals true, willPaginate is not implemented.
        // (c)scrollによるAPIへのgetrequest
        TableView.willPaginate(scrollView, scrollable: self.requestable, completion: {
            self.requestObjects()
        })
    }
}

```

```LoadingState.swift
public enum LoadingState {
    case None
    case InProgress
    case Success
    case Failure
}
```

```APIRequestManager.swift
import Alamofire

typealias RequestParamaters = Dictionary<String, AnyObject>?

final class APIRequestManager: NSObject {

    class func getRequest(version: String, urlString: String, var parameters: RequestParamaters=nil, accessToken: Bool=false) ->  Request {
        parameters = setAccessToken(parameters, accessToken: accessToken)
        return Alamofire.request(.GET, BaseURLString + version + urlString, parameters: parameters).validate()
    }
}
```

```APICollectionRequestTask.swift
import Alamofire
import SwiftTask

final class APICollectionRequestTask<ResponseObject: ResponseCollectionSerializable> {
    typealias Progress = (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
    typealias CollectionRequestTask = Task<Progress, [ResponseObject], Int?>
    let value: CollectionRequestTask
    
    init(request: Request) {
        self.value = CollectionRequestTask { progress, fulfill, reject, configure in
            
            request.progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
                progress((bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) as Progress)
            }
            
            request.responseCollection { (_, response, result: Result<[ResponseObject]>) in
                switch result {
                case .Success(let objects):
                    fulfill(objects)
                case .Failure:
                    reject(response?.statusCode)
                }
            }
        }
    }
    
    deinit {
        debugPrint("deinit: APICollectionRequestTask")
    }
}

```

```TableView.swift
import UIKit

class TableView {
	
	//(b)の実装
	class func willPaginate(scrollView: UIScrollView, scrollable: Bool, completion: () -> Void) {
        if scrollView.contentSize.height == 0 || scrollable == false { return }
        let rest_scroll_height = (scrollView.contentSize.height + scrollView.contentInset.bottom) - (scrollView.contentOffset.y + scrollView.frame.size.height)
        if (rest_scroll_height < 120.0) {
            completion()
        }
    }
}
```


```ProfileListItemTableViewCellModel
import Foundation
import SwiftyJSON

final class ProfileListItemTableViewCellModel: NSObject, ResponseCollectionSerializable {
    var imageUrl: String = ""
    var intro: String = ""
    var name: String = ""
    var isLast: Bool = false
    
    init? (object: JSON) {
        self.imageUrl = object["image"]["thumb"]["url"].stringValue
        self.intro = object["message"].stringValue
        self.name = object["name"].stringValue
    }
    
    class func collection(object: JSON) -> [ProfileListItemTableViewCellModel] {
        let json: [JSON] = object["notifications"].array!
        var profileCells = [ProfileListItemTableViewCellModel]()
        for item in json {
            profileCells.append(ProfileListItemTableViewCellModel(object: item)!)
        }
        profileCells.last?.isLast = object["is_last"].boolValue
        return profileCells
    }
}

```

```ProfileListItemTableViewCell
import UIKit

class ProfileListItemTableViewCell: UITableViewCell {

    @IBOutlet weak var profileImage1: UIImageView!
    @IBOutlet weak var profileName1: UILabel!
    @IBOutlet weak var profileIntro1: UILabel!
    @IBOutlet weak var profileImage2: UIImageView!
    @IBOutlet weak var profileName2: UILabel!
    @IBOutlet weak var profileIntro2: UILabel!
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    // initialize the odd-numbered cell object made from ProfileListItemTableViewCellModel
    func setCell1(cellobject: ProfileListItemTableViewCellModel, atIndexPath indexPath: NSIndexPath) {
        let imageView = profileImage1 as UIImageView
        imageView.setUserAvatar(cellobject.imageUrl)
        profileName1.text = cellobject.name
        profileIntro1.text = cellobject.intro
    }
    
    // initialize the even-numbered cell object made from ProfileListItemTableViewCellModel
    func setCell2(cellobject: ProfileListItemTableViewCellModel, atIndexPath indexPath: NSIndexPath) {
        let imageView = profileImage2 as UIImageView
        imageView.setUserAvatar(cellobject.imageUrl)
        profileName2.text = cellobject.name
        profileIntro2.text = cellobject.intro
    }
}
```

```APIResponseObjectSerializable.swift
import Alamofire
import SwiftyJSON


public protocol ResponseCollectionSerializable {
    static func collection(object: JSON) -> [Self]
}
```

SwiftTaskについては追記します。
参考になれば幸いです。