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
- UITextView
- swift documentation
- ribs
- tableView
- rxswift
- map
- HIG
- Human interface guide
- 리펙터링
- collectionview
- Protocol
- 리팩토링
- swiftUI
- combine
- ios
- Refactoring
- clean architecture
- Xcode
- 애니메이션
- RxCocoa
- Clean Code
- Observable
- 리펙토링
- uiscrollview
- MVVM
- 클린 코드
- UICollectionView
- SWIFT
- uitableview
- 스위프트
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - SwiftUI] 1. Pagination 방법 (SwiftUI에서 페이지네이션 구현 방법, Combine) 본문
iOS 응용 (SwiftUI)
[iOS - SwiftUI] 1. Pagination 방법 (SwiftUI에서 페이지네이션 구현 방법, Combine)
jake-kim 2022. 9. 28. 22:121. Pagination 방법 (페이지네이션, Combine) - 기초
2. Pagination 방법 (페이지네이션, Combine) - 메인 스레드 최적화, 이미지 캐싱
Pagination 개념
- List에서 스크롤할 때 필요한 데이터를 계속 추가적으로 받아오는 형태
- 대용량의 데이터를 한꺼번에 받아오면 부하가 크므로, 10개를 보여주고 그다음 스크롤이 마지막에 도달할때 그 다음 10개를 보여주는 형식
- API를 호출할땐 page Int 값을 다르게 보내면서 데이터를 가져오는 방법
SwiftUI 에서의 Pagination 구현 아이디어
- List의 마지막 부분에 로딩뷰를 따로 추가하고, 그 부분이 onAppear 될때 페이지네이션 수행
List {
ForEach(items) { item in
}
if !items.isEmpty {
SomeLoadingView()
.onAppear {
// 페이지네이션 수행
}
}
}
Pagination 구현 시 잘못된 방법
- Row값들이 onAppear 될때마다 해당 아이템을 체크하여 마지막 아이템일때 페이지네이션 하는 방법은 안좋은 방법
- 만약 MVVM모델, ViewModel을 사용한다면, viewModel의 상태 값을 보내는 것인데, 이 상태는 @Published 형태일 것이고 @Published 형태를 set하면 Main thread를 사용하므로 사용할때 버벅이는 현상이 발생
- MVVM 관련 개념은 이전 포스팅 글 참고
Pagination 코드 구현 - API, 모델
- 사용할 API: Unsplash Document
- 사이트에 접속 > 앱 등록 > Client key 복사해놓기
- Photo 모델 정의
import Foundation
import SwiftUI
protocol ModelType: Codable, Equatable, Identifiable { }
struct Photo: ModelType {
struct URLs: ModelType {
let regular: String
var id: String { self.regular }
}
let id: String
let urls: URLs
let width: CGFloat
let height: CGFloat
}
- API 정의
- API는 비동기 작업이므로 Combine을 사용
* Combine 개념 참고 - Unsplash Document 에 접속 > 앱 등록 > Client key 복사후 아래 "your_client_key"에 붙여넣기
- API는 비동기 작업이므로 Combine을 사용
API.swift
import Foundation
import Combine
enum APIError: Error, LocalizedError {
case unknown
case some(reason: String)
var errorDescription: String? {
switch self {
case .unknown:
return "Unknown"
case let .some(reason):
return reason
}
}
}
enum API {
private static let token = "your_client_key"
private static let photoURL = "https://api.unsplash.com/photos"
static func fetchPhotos(page: Int) -> AnyPublisher<Data, Error> {
// TODO
}
}
- fetchPhotos 구현
- 필요한 queryItem, header를 채우고 URLSession.DataTaskPublisher(request:session:)을 이용하여 Publisher 생성
static func fetchPhotos(page: Int) -> AnyPublisher<Data, Error> {
var urlComponents = URLComponents(string: photoURL)!
urlComponents.queryItems = [
.init(name: "page", value: "\(page)"),
.init(name: "per_page", value: "\(10)"),
.init(name: "order_by", value: "latest")
]
var request = URLRequest(url: urlComponents.url!)
request.allHTTPHeaderFields = ["Authorization": "Client-ID \(token)"]
return URLSession.DataTaskPublisher(request: request, session: .shared)
.eraseToAnyPublisher()
}
- Publisher에 tryMap과 mapError를 통해 error처리 코드 추가
return URLSession.DataTaskPublisher(request: request, session: .shared)
.tryMap { data, response in
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else { throw APIError.some(reason: "Invalid httpResponse") }
return data
}
.mapError { error in
if let error = error as? APIError {
return error
} else {
return APIError.some(reason: error.localizedDescription)
}
}
.eraseToAnyPublisher()
Pagination 코드 구현 - 뷰
- 필요한 @State 프로퍼티 4가지 준비
- page: 페이지네이션에 사용될 상태값
- isAppear: 뷰가 보여질때 최초 한번 초기 데이터들을 불러오기 위해서 사용할 플래그값
- photos: 페이지네이션 하면서 계속 축적될 데이터 값
- cancellables: Publisher들의 생명주기 관리할 프로퍼티
import SwiftUI
import Combine
struct ContentView: View {
@State var page = 0
@State var isAppear = false
@State var photos = [Photo]()
@State var cancellables = Set<AnyCancellable>()
var body: some View {
}
}
- body 구조
- getList(items:): 페이지네이션 되면 계속 업뎃될 뷰
- loadMorePhotos(): API를 찌르고 데이터를 가져오는 메소드
var body: some View {
NavigationView {
getList(items: $photos)
}
.onAppear {
guard !isAppear else { return }
isAppear = true
loadMorePhotos()
}
}
- 가장 간단한 loadMorePhotos() 부터 구현
- page를 하나 높여주고, API호출을 통해 photos 값 업데이트
private func loadMorePhotos() {
page += 1
self.cancellables = Set<AnyCancellable>()
API.fetchPhotos(page: page)
.sink(
receiveCompletion: {
switch $0 {
case .finished:
break
case let .failure(error):
print(error.localizedDescription)
}
}, receiveValue: { data in
guard let photos = try? JSONDecoder().decode([Photo].self, from: data) else { return }
self.photos += photos
}
)
.store(in: &self.cancellables)
}
- getRowView(photo:) 구현
- getList(item:)에서 불려질 뷰이며, 각 List마다 row에 사용될 뷰
- AsyncImage를 통해 url에 해당되는 이미지를 가져오는 것
- 핵심) frame값 정해주기: frame값을 정해주지 않으면 리스트를 스크롤할때 height값이 정해지지 않았으므로 버벅거리는 현상이 존재
@ViewBuilder
private func getRowView(photo: Photo) -> some View {
AsyncImage(
url: URL(string: photo.urls.regular),
content: { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * photo.height / photo.width)
},
placeholder: { ProgressView() }
)
}
- getList(items:) 구현
- 핵심) List 맨 마지막에 ProgressView()를 추가하고 이 뷰의 onAppear {}가 호출되면 페이지네이션 수행 (다음 데이터 로드)
- List를 구현하는 형태는 좌우 패딩을 없애고, Disclosure Indicator를 없애기 위해 구현된 형태 (자세한 내용은 이전 포스팅 글 참고)
@ViewBuilder
private func getList(items: Binding<[Photo]>) -> some View {
List {
ForEach(items.wrappedValue, id: \.id) { photo in
ZStack {
NavigationLink(
destination: {
// EmptyView는 테스트를 위해 넣은것이고, 별도의 DetailView() 정의하여 이곳에 사용할것
EmptyView()
},
label: {
// 아래 HStack 하위에 getRowView(photo:)가 label 역할을 하므로, 여기는 EmptyView() 사용 필수
EmptyView()
}
)
.opacity(0)
.buttonStyle(PlainButtonStyle())
HStack {
getRowView(photo: photo)
}
}
.listRowInsets(.init())
.padding(EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0))
}
if !items.isEmpty {
HStack {
Spacer()
ProgressView()
.onAppear {
print("여기")
loadMorePhotos()
}
Spacer()
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("photo")
}
보완점) - 다음 포스팅 글 참고 Pagination 방법 (페이지네이션, Combine) - 메인 스레드 최적화, 이미지 캐싱
- 위처럼 구현되면 스크롤 할 때 버벅이는 현상이 존재
- main thread에서 url을 가지고 이미지를 불러오는 코드가 있기 때문 (AsyncImage)
- Background Thread에서 url을 가지고 이미지를 불러오도록 수정 필요
- Image Caching 적용을 통해 조금 더 빠른 로딩이 되도록 구현
- View쪽에서 url을 받아서 처리하지 않고, UIImage 형태를 받아서 바로 UI를 그릴 수 있도록 모델을 새로 정의
'iOS 응용 (SwiftUI)' 카테고리의 다른 글
Comments