13
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ScrollによるPaginationの実装

Posted at

研修課題の終盤に差し掛かりました。
前回の続きです。
Alamofire,SwiftyJSON,ModelでCellをカスタマイズしてTableViewを描画する

要件:

①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()
        }
    }
}
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
    }
}

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については追記します。
参考になれば幸いです。

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?