次のページの読み込みに関してはこちらに記載しています。
https://qiita.com/shiz/items/f0f663f8fb926d914990
SwiftUIを使っていく上でListを扱っているときに
TableViewのように一覧を引っ張ってデータを更新する
いわゆるPullToRefreshを
SwiftUIのみで実現する実装方法がわからなかったため
調べてみました。
結論としては明確な方法がまだ見つかっていないのですが
一応それらしく実現する方法ががありましたので
記録として残しておきます。
もし他に良い実装などございましたら
教えて頂けましたらうれしいです🙇🏻♂️
今回必要となるものは
GeometryReader
です。
※ Bata版のためSnapshotは載せられないのでコードだけ示させていただきます🙇🏻♂️
※ 細かい処理などは省略しています🙇🏻♂️
こちらを参考にしています。
https://www.youtube.com/watch?v=m2o8iMU2LQA
実装
内容としてはQiitaのAPIから記事の一覧を取得します。
データの取得
データを取得します。
import Combine
import Foundation
final class ListViewModel: ObservableObject {
@Published var items: [QiitaItem] = []
@Published var isLoading = false
private var cancellables: Set<AnyCancellable> = []
private let perPage = 20
func refresh() {
if !isLoading {
getQiitaList(page: 1, perPage: perPage) { [weak self] result in
self?.handleResult(result)
}
}
}
private func getQiitaList(page: Int, perPage: Int,
completion: @escaping (Result<[QiitaItem], Error>) -> Void) {
let parameters: [String: Any] = [
"page": page,
"per_page": perPage,
]
guard let url = URL(string: "https://qiita.com/api/v2/items"),
let request = makeGetRequest(url: url, parameters: parameters) else {
return completion(.failure(APIError.requestError))
}
fetch(request: request) { result in
completion(Result {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode([QiitaItem].self, from: result.get())
})
}
}
private func fetch(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
isLoading = true
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
return completion(.failure(APIError.responseError(error)))
}
guard let httpResponse = response as? HTTPURLResponse else {
return completion(.failure(APIError.invalidResponse(response)))
}
guard (200 ..< 300) ~= httpResponse.statusCode else {
return completion(.failure(APIError.invalidStatusCode(httpResponse.statusCode)))
}
guard let data = data else {
return completion(.failure(APIError.noResponseData))
}
return completion(.success(data))
}.resume()
}
private func makeGetRequest(url: URL, parameters: [String: Any]) -> URLRequest? {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return nil
}
components.queryItems = parameters.map { (arg) -> URLQueryItem in
let (key, value) = arg
return URLQueryItem(name: key, value: String(describing: value))
}
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
return request
}
private func handleResult(_ result: Result<[QiitaItem], Error>) {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.isLoading = false
switch result {
case .success(let items):
self.items.append(contentsOf: items)
case .failure(let error):
print(error)
}
}
}
}
enum APIError: Error {
case requestError
case responseError(Error)
case invalidResponse(URLResponse?)
case invalidStatusCode(Int)
case noResponseData
}
画面への表示
次に画面に表示する実装です。
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: ListViewModel
var body: some View {
NavigationView {
List {
// frameを取得してyの位置がある値を超えた時にrefreshメソッドを呼び出す
GeometryReader { g -> Text in
let frame = g.frame(in: .global)
if frame.origin.y > 250 {
self.viewModel.refresh()
return Text("Loading...")
} else {
return Text("")
}
}
ForEach(self.viewModel.items) { item in
Text(item.title)
}
}.navigationBarTitle("検索結果")
}
}
}
ポイントは
GeometryReader { g -> Text in
let frame = g.frame(in: .global)
if frame.origin.y > 250 {
self.viewModel.refresh()
return Text("Loading...")
} else {
return Text("")
}
}
でframe
を取得し
origin.y
の位置でListが引っ張られていることを検知します。
今回はText
で「Loading...」という文字を出していますが、
戻り値を別のViewに変更することも可能です。
まとめ
GeometryReader
を利用することで
PullToRefreshのような動きをすることができました。
しかし、どの位置でRefreshをしようとしているのかの判断を
手動でしなければならなかったり
ちょっと強引な気もしています。
何か標準の機能として出てくれば嬉しいですね😃
(あって気付いていないだけでしたらぜひ教えてください💦)