저번 UIKit으로 Pagination을 구현해보았는데 이번에는 SwiftUI로 Pagination을 구현해보려고 한다.
아래는 저번 UIKit으로 구현한 내용을 포스팅한 것이다.
[UIKit] UITableView + Pagination 구현
Pagination Pagination(페이지 매기기) 또는 Paging(페이징) 이란 문서를 전자 페이지 또는 인쇄된 페이지 등 개별 페이지로 나누는 프로세스이다.우리가 사용하는 웹페이지에서 쉽게 Pagination이 구현한
kimkhuna99.tistory.com
일단 나는 아래 블로그 내용을 가지고 진행하였다.
API Response
API Request URL 은 https://api.jikan.moe/v4/top/anime?page=1 이 API를 참고하여 만들었다.
애니메이션 순위를 나타내는 API로 추정된다.
Pagination
API 에서 Pagination을 위한 데이터를 제공한다.
- last_visiable_page : Pagination의 마지막 페이지
- has_next_page : 다음 페이지가 있는지에 대한 여부
- current_page : 현재 표시된 페이지
Model
JSON 형식의 데이터를 파싱하기 위한 Model을 생성하기 위해 아래와 같이 구현하였다.
첫 번째로는 API Response의 모델이다. Pagination에 대한 정보와 data로 나누어져 있다.
struct JikanMoeResponse: Decodable, Equatable {
let pagination: Pagination
let data: [Anime]
}
Pagination에 대한 구조체는 아래 코드를 참고
struct Pagination: Decodable, Equatable {
let lastVisiblePage: Int
let hasNextPage: Bool
let currentPage: Int
enum CodingKeys: String, CodingKey {
case lastVisiblePage = "last_visible_page"
case hasNextPage = "has_next_page"
case currentPage = "current_page"
}
}
데이터 내에는 애니메이션의 대한 정보와 포스터 이미지 URL 등으로 API에서 제공하고 있다.
struct Anime: Identifiable, Decodable, Equatable {
var id: Int
let url: String
let image: AnimeImage
let title: String
let episodes: Int?
let rank: Int?
let score: Double?
enum CodingKeys: String, CodingKey {
case id = "mal_id"
case url = "url"
case image = "images"
case title = "title"
case episodes = "episodes"
case rank = "rank"
case score = "score"
}
}
이미지 URL에 대한 구조체도 아래를 참고
struct AnimeImage: Decodable, Equatable {
let jpg: ImageDetails
let webp: ImageDetails
}
struct ImageDetails: Decodable, Equatable {
let imageUrl: String
let smallImageUrl: String
let largeImageUrl: String
enum CodingKeys: String, CodingKey {
case imageUrl = "image_url"
case smallImageUrl = "small_image_url"
case largeImageUrl = "large_image_url"
}
}
ViewModel
ViewModel 내에는 Pagination 로직과 API 통신 로직으로 구현하였다.
@MainActor
class AnimeKitMainViewModel: ObservableObject {
@Published var animes: [Anime] = []
var page: Int = 1
var lastVisiblePage: Int = -1
/// API 통신
func loadAnimes() async {
guard let url = URL(string: "https://api.jikan.moe/v4/top/anime?page=\(page)") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
if data.isEmpty { return }
let decodedData = try JSONDecoder().decode(JikanMoeResponse.self, from: data)
lastVisiblePage = decodedData.pagination.lastVisiblePage // 데이터의 마지막 페이지를 할당
animes.append(contentsOf: decodedData.data)
page += 1 // 함수가 다시 실행될 때 다음 페이지를 렌더링할 수 있도록 페이지를 증가
} catch {
print(error)
}
}
/// 렌더링된 요소의 ID를 기준으로 사용자가 목록의 끝에 도달했는지 확인하는 함수
func shouldLoadPagination(id: Int) async {
// 배열의 마지막 id를 확인하고 page 특정 요소에 도달 여부 확인
if animes.last?.id == id && lastVisiblePage >= page {
await loadAnimes()
}
}
/// 새로고침
func refresh () async {
page = 1
lastVisiblePage = -1
animes.removeAll()
await loadAnimes()
}
}
View
View 내에 LazyVGrid로 구현하여 요소들이 보이게 구현하였고 GridItem 내에서 ViewModel 내에 shouldLoadPagination 함수로 Pagination 확인한다.
struct AnimeKitMainView: View {
@StateObject var viewModel = AnimeKitMainViewModel()
let columns: [GridItem] = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(viewModel.animes) { anime in
AnimeCard(anime: anime)
.onAppear {
Task {
await viewModel.shouldLoadPagination(id: anime.id)
}
}
.padding(.bottom)
}
}
}
.navigationTitle("Top Animes")
.padding()
}
.onAppear {
Task {
await viewModel.loadAnimes()
}
}
.refreshable {
Task {
await viewModel.refresh()
}
}
}
}
Anime를 표시하기 위한 UI `AnimeCard` 추가
struct AnimeCard: View {
let anime: Anime
var body: some View {
VStack(alignment: .leading) {
// Image
AsyncImage(url: URL(string: anime.image.jpg.imageUrl)) { image in
image.resizable()
.scaledToFit()
.frame(width: 200, height: 230)
} placeholder: {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.frame(width: 200, height: 200)
}
// Title
Text(anime.title)
.lineLimit(1)
.padding(.horizontal)
// Score
Label {
Text(String(format: "%2.f", anime.score ?? 0))
.fontWeight(.light)
} icon: {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
.padding(.horizontal)
}
}
}
결과

Pagination은 제일 많이 쓰는 기술 중 하나라서 SwiftUI와 UIKit으로 구현해보았다. 확실히 SwiftUI에서 구현하는 방식이 쉽다고 느껴졌다. 다음에는 SwiftUI 내에서 AsyncImage에서 캐싱 처리에 대하여 공부해봐야겠다. 코드는 아래 깃헙을 참고
GitHub - kyeonghunkim0/Pagination_SwifUI
Contribute to kyeonghunkim0/Pagination_SwifUI development by creating an account on GitHub.
github.com
'Develop > SwiftUI' 카테고리의 다른 글
| [Combine] ObservableObject, @StateObject, @ObservedObject, @EnvironmentObject (0) | 2025.11.08 |
|---|---|
| [Combine] Combine (1) - Combine이란? (0) | 2025.08.18 |
| TCA(The Composable Architecture) (7) | 2025.08.04 |
| [SwiftUI] Managing user interface state (0) | 2025.01.27 |
| [SwiftUI] GeometryReader (2) | 2024.10.13 |