APIを叩いて、20件ずつデータを取得する方法について少し詰まったポイントがあったので、残しておきます。
UITableViewが一番下に行ったのを検知
まず、scrollViewDidScroll
内で、tableView
のスクロールを検知することができ、下記のコードによって一番下に行ったのを検知することができます。
ViewController.swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentOffsetY = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.height
let distanceToBottom = maximumOffset - currentOffsetY
if distanceToBottom < 0 && self.tableView.isDragging {
print("一番下を検知したよー!")
// ここで、任意の処理を記載(今回は、APIを叩く)
}
}
APIを叩く
次に、ViewModelにAPIを叩く処理を記載します。
ここは、ライブラリなどを使用していただいても結構です。
DataModel.swift
struct DataModel {
let id: Int
let title: String
let contents: String
init(json: [String: Any]) {
self.id = json["id"] as? Int ?? 0
self.title = json["title"] as? String ?? ""
self.contents = json["contents"] as? String ?? ""
}
init() {
self.id = 0
self.name = ""
self.contents = ""
}
}
ViewModel.swift
final class ViewModel {
var dataArray = [DataModel]()
// pageが1増えるたびに20件ずつ取得するAPIを想定します。
private var currentPage = 1
func fetchData(handler: @escaping (_ result: [DataModel]?, _ error: String?) -> Void) {
guard let url = URL(string: "https://example.com/data?page=\(self.currentPage)") else { return }
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
if let error = error {
handler(nil, error.localizedDescription)
return
}
guard let data = data, let response = response as? HTTPURLResponse else {
handler(nil, "data or response is nil")
return
}
if response.statusCode == 200 {
do {
let object = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
guard let results = object?["result"] as? NSArray
else { return }
// dataの重複を防ぐために、配列の中身を空にします。
dataArray = []
dataArray = results.map { DataModel(json: $0 as? [String: Any] ?? [:]) }
handler(dataArray, nil)
// データの取得が完了するたびに、次ページを取得するために以下を記載
currentPage += 1
} catch {
handler(nil, error.localizedDescription)
}
} else {
handler(nil, "statusCode error")
}
}
task.resume()
}
}
ロードのステイタスを管理
上記のままでは、スクロールした際に一番下を複数回検知し、一気に何度もデータを読み込むようになってしまうと思うので、ロードのステイタスを管理するために、以下の処理を追記します。
これで、一番下までスクロールした際に次のデータのみを取得することができます。
※上記のViewModel
に対してコメント部分のみ追記があります。
ViewModel.swift
final class BookListViewModel {
// 追記: ロードのステイタスを管理します。
private enum Status {
case initial // 初期状態
case loading // ロード中
case completion // ロード完了
case error // エラー
}
var dataArray = [DataModel]()
private var currentPage = 1
// 追記: 最初は.initialを設定
private var status = Status.initial
func fetchData(handler: @escaping (_ result: [DataModel]?, _ error: String?) -> Void) {
// 追記: .loadingの時以外、処理を実行する。
guard self.status != .loading else { return }
// 追記: ステイタスを.loadingに変更
self.status = .loading
guard let url = URL(string: "https://example.com/data?page=\(self.currentPage)") else { return }
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
if let error = error {
handler(nil, error.localizedDescription)
// 追記: エラー
status = .error
return
}
guard let data = data, let response = response as? HTTPURLResponse else {
handler(nil, "data or response is nil")
// 追記: エラー
status = .error
return
}
if response.statusCode == 200 {
do {
let object = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
guard let results = object?["result"] as? NSArray
else { return }
dataArray = []
dataArray = results.map { DataModel(json: $0 as? [String: Any] ?? [:]) }
handler(dataArray, nil)
currentPage += 1
// 追記: ロード完了
status = .completion
} catch {
handler(nil, error.localizedDescription)
// 追記: エラー
status = .error
}
} else {
handler(nil, "statusCode error")
// 追記: エラー
status = .error
}
}
task.resume()
}
}
ViewControllerでデータの取得とtableViewの更新を行う
setUpdata
でデータの取得とtableViewの更新を行う
ViewController.swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentOffsetY = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.height
let distanceToBottom = maximumOffset - currentOffsetY
if distanceToBottom < 0 && self.tableView.isDragging {
print("一番下を検知したよー!")
self.setUpData()
}
}
func setUpData() {
self.activityIndicator.startAnimating()
self.viewModel.fetchData { result, error in
DispatchQueue.main.async { [self] in
activityIndicator.stopAnimating()
if let error = error {
print(error)
}
if let result = result {
dataArray.append(contentsOf: result)
tableView.reloadData()
}
}
}
}
参考