1
1

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 1 year has passed since last update.

UITableViewを一番下までスクロールした時に、APIを叩く

Last updated at Posted at 2022-06-06

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

参考

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?