研修課題の終盤に差し掛かりました。
前回の続きです。
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
は一回しか呼ばれないから。
実装例:
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()
})
}
}
public enum LoadingState {
case None
case InProgress
case Success
case Failure
}
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()
}
}
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")
}
}
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
}
}
import Alamofire
import SwiftyJSON
public protocol ResponseCollectionSerializable {
static func collection(object: JSON) -> [Self]
}
SwiftTaskについては追記します。
参考になれば幸いです。