Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- uitableview
- ribs
- 스위프트
- HIG
- collectionview
- 리팩토링
- UICollectionView
- Human interface guide
- UITextView
- rxswift
- clean architecture
- ios
- tableView
- 애니메이션
- 리펙터링
- RxCocoa
- Observable
- 클린 코드
- SWIFT
- map
- 리펙토링
- Refactoring
- swiftUI
- Xcode
- swift documentation
- combine
- uiscrollview
- MVVM
- Clean Code
- Protocol
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 5. GraphQL - Apollo의 fetch 예제 (Pagination) 본문
1. GraphQL - 개념
3. GraphQL - Apollo 모델 생성 (generate model)
4. GraphQL - Apollo의 request pipeline, Interceptor, token, header
5. GraphQL - Apollo의 fetch 예제 (Pagination)
Apollo 준비
- 1~3 개념을 참고하여 튜토리얼 Sandbox에서 아래 쿼리 생성 & generate
query LaunchList {
launches {
hasMore
cursor
launches {
id
site
mission {
name
missionPatch(size: SMALL)
}
}
}
}
- Apollo 접근 singleton 정의
import Apollo
class Network {
static let shared = Network()
let apollo = ApolloClient(url: URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")!)
private init() {}
}
TableView에 얻은 데이터 뿌리기
- image도 뿌려야되기 때문에, 이미지 캐싱 관련 Kingfisher 프레임워크 준비
- 코드로 레이아웃 작성을 편리하게 하기위해서 SnapKit 프레임워크 준비
pod 'Kingfisher'
pod 'SnapKit'
- placeholder 이미지 존재
- tableView에 아래처럼 데이터를 뿌리도록 구현
- Apollo는 graphQL을 다루고 있으므로, graphQL은 보통 전체 에러를 내려주기보단 부분 에러를 내려주기 때문에 .success의 response에서 error처리도 수행
- .failure에 해당되는 case는 network error를 의미
- 단순히 데이터를 가져오는 것이기 때문에 fetch(query:) 호출
// ViewController.swift
private var dataSource = [LaunchListQuery.Data.Launch.Launch]()
// in viewDidLoad
Network.shared.apollo
.fetch(query: LaunchListQuery()) { [weak self] result in
guard let ss = self else { return }
defer { ss.tableView.reloadData() }
switch result {
case .success(let graphQLResult):
if let launchConnection = graphQLResult.data?.launches {
ss.dataSource.append(contentsOf: launchConnection.launches.compactMap { $0 })
}
if let errors = graphQLResult.errors {
let message = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
// print(message)
}
case .failure(let error):
print(error)
}
}
- 나머지 UI를 포함한 전체 코드
// ViewController.swift
import UIKit
import SnapKit
import Kingfisher
final class ViewController: UIViewController {
private let tableView: UITableView = {
let view = UITableView()
return view
}()
private var dataSource = [LaunchListQuery.Data.Launch.Launch]()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "리스트"
self.view.addSubview(self.tableView)
self.tableView.register(LaunchListCell.self, forCellReuseIdentifier: "LaunchListCell")
self.tableView.dataSource = self
self.tableView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
Network.shared.apollo
.fetch(query: LaunchListQuery()) { [weak self] result in
guard let ss = self else { return }
defer { ss.tableView.reloadData() }
switch result {
case .success(let graphQLResult):
if let launchConnection = graphQLResult.data?.launches {
ss.dataSource.append(contentsOf: launchConnection.launches.compactMap { $0 })
}
if let errors = graphQLResult.errors {
let message = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
// print(message)
}
case .failure(let error):
print(error)
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LaunchListCell", for: indexPath) as! LaunchListCell
let data = self.dataSource[indexPath.row]
cell.prepare(
imageUrlString: data.mission?.missionPatch,
preferredSize: LaunchListCell.Constants.imageSize,
title: data.mission?.name,
desc: data.site
)
return cell
}
}
final class LaunchListCell: UITableViewCell {
enum Constants {
static let imageSize = CGSize(width: 40, height: 40)
}
private let thumbnailImageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
return view
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .label
return label
}()
private let descLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .secondaryLabel
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.addSubview(self.thumbnailImageView)
self.contentView.addSubview(self.titleLabel)
self.contentView.addSubview(self.descLabel)
self.thumbnailImageView.snp.makeConstraints {
$0.top.left.equalToSuperview()
$0.size.equalTo(Constants.imageSize)
$0.bottom.lessThanOrEqualToSuperview()
}
self.titleLabel.snp.makeConstraints {
$0.top.equalTo(self.thumbnailImageView)
$0.left.equalTo(self.thumbnailImageView.snp.right)
$0.right.lessThanOrEqualTo(12)
}
self.descLabel.snp.makeConstraints {
$0.top.equalTo(self.titleLabel.snp.bottom)
$0.left.equalTo(self.thumbnailImageView.snp.right)
$0.bottom.lessThanOrEqualToSuperview()
$0.right.lessThanOrEqualTo(12)
}
}
required init?(coder: NSCoder) { fatalError() }
override func prepareForReuse() {
super.prepareForReuse()
self.prepare(imageUrlString: nil, preferredSize: .zero, title: nil, desc: nil)
}
func prepare(imageUrlString: String?, preferredSize: CGSize, title: String?, desc: String?) {
self.thumbnailImageView.image = nil
if
let imageUrlString = imageUrlString,
let url = URL(string: imageUrlString)
{
self.thumbnailImageView.kf.setImage(
with: url,
placeholder: UIImage(named: "placeholder"),
options: [
.processor(DownsamplingImageProcessor(size: preferredSize)),
.progressiveJPEG(ImageProgressive(isBlur: false, isFastestScan: true, scanInterval: 0.1))
],
completionHandler: { result in
print(result)
}
)
}
self.titleLabel.text = title
self.descLabel.text = desc
}
}
Pagination
- cursor (페이지 값에 해당되는 값을 저장해놓기 위해 전역에 아래 프로퍼티 선언)
// ViewController.swift
private var lastConnection: LaunchListQuery.Data.Launch?
- cursor값을 가지고 데이터를 조회해야 하므로, graphql 쿼리 수정
- $cursor 라는 파라미터 추가
query LaunchList($cursor:String) {
launches(after:$cursor) {
hasMore
cursor
launches {
id
site
mission {
name
missionPatch(size: SMALL)
}
}
}
}
- cursor값이 nil일 경우, 첫번째 페이지를 받을 수 있으므로 lastConnection이 nil이면 첫번째 페이지를 호출하고, 아니면 다음 페이지를 호출 (cursor값을 전달)하도록 구현
override func viewDidLoad() {
super.viewDidLoad()
...
self.loadMoreLaunchesIfTheyExist()
}
private func loadMoreLaunchesIfTheyExist() {
guard let connection = self.lastConnection else {
self.loadMoreLaunches(from: nil)
return
}
guard connection.hasMore else { return }
self.loadMoreLaunches(from: connection.cursor)
}
private func loadMoreLaunches(from cursor: String?) {
Network.shared.apollo.fetch(query: LaunchListQuery(cursor: cursor)) { [weak self] result in
guard let ss = self else { return }
defer { ss.tableView.reloadData() }
switch result {
case .success(let graphQLResult):
if let launchConnection = graphQLResult.data?.launches {
ss.lastConnection = launchConnection
ss.dataSource.append(contentsOf: launchConnection.launches.compactMap { $0 })
}
if let errors = graphQLResult.errors {
let message = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
print(message)
}
case .failure(let error):
print("network error - \(error)")
}
}
}
- pagination을 위해 tableView의 scorllViewDidScroll(_:)메소드를 사용하기 위해 델리게이트 할당 및 메소드 준수
- scroll에 관한 Pagination 개념은 이전 Pagination 개념 포스팅 글 참고
self.tableView.delegate = self
...
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentHeight = scrollView.contentSize.height
let yOffset = scrollView.contentOffset.y
let heightRemainBottomHeight = contentHeight - yOffset
let frameHeight = scrollView.frame.size.height
if heightRemainBottomHeight < frameHeight {
self.loadMoreLaunchesIfTheyExist()
}
}
}
* 전체 코드: https://github.com/JK0369/ExApolloApp
* 참고
https://github.com/apollographql/apollo-ios/blob/main/docs/source/tutorial/tutorial-pagination.md
https://www.apollographql.com/docs/ios/tutorial/tutorial-query-ui/
'iOS framework' 카테고리의 다른 글
[iOS - swift] xUnique (파일, 폴더 sorting 관리, pbxproj 솔팅 관리) 프레임워크 (0) | 2022.03.23 |
---|---|
[iOS - swift] RxGesture 사용 방법 (스와이프, UIPageControl) (0) | 2022.03.12 |
[iOS - swift] 4. GraphQL - Apollo의 request pipeline, Interceptor, token, header (0) | 2022.03.03 |
[iOS - swift] 3. GraphQL - Apollo 모델 생성 (generate model) (0) | 2022.03.02 |
[iOS - swift] 2. GraphQL - Apollo 사용 개념 (0) | 2022.03.01 |
Comments