본문 바로가기

iOS

TMDB - GET / Pagenation

728x90

TMDB API를 활용한 트렌드 미디어 GET API 연결 및 Pagenation을 구현해보자.

 

페이지네이션 구현 방법에 앞서서 먼저 페이지네이션이 무엇인지 짧게 말하자면,

페이지네이션이란?
대량의 데이터와 리소스를 분할해서 갖고 오는 방법으로 주로 서버의 데이터와 리소스를 다룰 때 사용하는 방법이다. 

 

구현 방법은 크게 세 가지로, 아래와 같다.

  1. UITableViewDelegate의 프로토콜 중 하나인, willDisplayCell 메서드를 사용하는 방법
  2. UIScrollViewDelegate의 프로토콜 중 하나인 scrollDidScroll을 통해 스크롤 뷰의 Offset을 활용하는 방법 
  3. UITableViewDataSource 프로토콜 중 하나인 Prefetching Protocol을 사용하는 방법

 

이 중에서 TMDB API의 Trending Media를 통해서 구현할 것은 2번과 3번이다.


GET 

먼저 TMDB의 Trending 정보를 GET하는 과정은 아래와 같다.

TMDB Trending

먼저, UI를 그리는 과정은 생략하겠다.

위의 이미지는 트렌드 미디어 정보를 받아서 UI에 올린 화면이다. UI의 구조는 UICollectionView를 활용해서 만들었다.

 

서버 통신을 위한 과정을 요약하자면,

  1. 서버 응답값을 받을 데이터 모델을 준비한다.
  2. 서버 요청을 위한 코드를 작성한다.
    이 때 API 명세서를 확인하여 서버에서 필요로 하는 값이 무엇인지 제대로 확인해야 한다.
  3. 서버 통신 이후 데이터 모델에 맵핑 한 후에 UI에 반영한다.

 

Response Model

API 명세서를 통해서 확인할 수 있는 응답값 중 해당 프로젝트에서 사용할 응답값만을 추려 Respones Model로 작성하면 아래와 같다.

import Foundation

// MARK: - Trend Response

struct TrendMediaResponse: Codable {
    let results: [TrendMediaData]
    let page, totalPages, totalResults: Int

    enum CodingKeys: String, CodingKey {
        case results, page
        case totalPages = "total_pages"
        case totalResults = "total_results"
    }
}

// MARK: - Result

struct TrendMediaData: Codable {
    let posterPath: String
    let backdropPath: String
    let originalTitle, title: String?
    let id: Int
    let releaseDate: String?
    let voteAverage: Double
    let adult: Bool
    let overview: String
    let genre: [Int]
    
    enum CodingKeys: String, CodingKey {
        case posterPath = "poster_path"
        case backdropPath = "backdrop_path"
        case originalTitle = "original_title"
        case title
        case id
        case releaseDate = "release_date"
        case voteAverage = "vote_average"
        case adult, overview
        case genre = "genre_ids"
    }
}

 

Request + UI 

트렌드 미디어의 API 명세서는 아래 링크를 통해서 확인할 수 있다.

https://developers.themoviedb.org/3/trending/get-trending

 

API Docs

 

developers.themoviedb.org

 

여기서 Request 관련 부분을 살펴보면,

✔️ Path Paramerters

- media_tye

- time_window

 

✔️ Query String

- api_key

를 서버에 요청해야 한다는 것을 알 수 있다. 

 

타입과 관련된 부분은 enum으로 처리할 수 있다.

enum MediaType: String, CaseIterable {
    case all
    case movie
    case tv
    case person
}

enum TimeType: String, CaseIterable {
    case day
    case week
}

 

그리고 APIManger로 ViewController와 APIManager 파일을 분리해서 작성하면,

✔️ APIManager

import Foundation

import Alamofire
import SwiftyJSON

class MediaAPIManager {
    static let shared = MediaAPIManager()
    
    private init() { }
    
    typealias completionHandler = ((Int), ([TrendMediaData])) -> Void
    
    func fetchTrendMedia(type: String, time: String, page: Int, completionHandler: @escaping completionHandler) {
        let url = URLConstant.BaseURL + URLConstant.TrendingURL + "/\(type)" + "/\(time)" + "?api_key=\(APIKey.APIKey)" + "&page=\(page)"
        
        let params: Parameters = ["media_type" : type,
                                  "time_window" : time]
        
        AF.request(url, method: .get, parameters: params).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let totalPage = json["total_pages"].intValue
                    
                    let mediaData = json["results"].arrayValue.map {
                        TrendMediaData(posterPath: $0["poster_path"].stringValue,
                                       backdropPath: $0["backdrop_path"].stringValue,
                                       originalTitle: $0["original_title"].stringValue,
                                       title: $0["title"].stringValue,
                                       id: $0["id"].intValue,
                                       releaseDate: $0["release_date"].stringValue,
                                       voteAverage: $0["vote_average"].doubleValue,
                                       adult: $0["adult"].boolValue,
                                       overview: $0["overview"].stringValue,
                                       genre: $0["genre_ids"].arrayValue.map { $0.intValue })
                    }
                    
                   completionHandler(totalPage, mediaData)
        
                }
                
            case .failure(let error):
                print(error)
            }
        }
    }
}

 

✔️ ViewController

// MARK: - Network

extension MediaViewController {
    private func fetchTrendMedia(type: String, time: String, page: Int) {
        MediaAPIManager.shared.fetchTrendMedia(type: type, time: time, page: page) { totalCount, trendMediaData in
            self.totalPage = totalCount
            self.mediaList.append(contentsOf: trendMediaData)
            
            DispatchQueue.main.async {
                self.mediaCollectionView.reloadData()
            }
        }
    }
}

이렇게 작성할 수 있다.

 


 

이 때, 위에서 확인한 Request 값 외로 다른 값을 같이 요청하는 것을 볼 수 있다,

바로 page 값이다.

이 값을 통해서 페이지네이션을 구현할 수 있다.

 

Pagenation - DataSourcePrefetching

먼저, DataSourcePrefetching Protocol을 활용한 방법을 살펴보자면 아래와 같다.

Prefetching Protocol 방법과 관련된 추가 설명은 더보기를 통해서 확인할 수 있다.

더보기
  • iOS 10 이상 지원하는 프로토콜이다.
  • 서버 통신과 같은 비동기 상황에서 페이지네이션을 쉽게 구현할 수 있다.
  • 용량이 큰 이미지를 다운받아 테이블뷰/컬렉션 뷰에 보여주려고 하는 경우에 효과적이다.
  • 스크롤 성능이 향상된다.

 

동작구조

  1. 테이블 뷰/컬렉션 뷰 생성 및 프로토콜 선언, 그리고 프로토콜 프로퍼티(prefetchDataSource)를 테이블/컬렉션 뷰에 할당한다.
  2. UITableView CellForRowAt 메서드가 호출되기 전에 미리 필요한 데이터를 로딩한다.
    a. prefetchRowAt에서 필요한 데이터를 다운 받는 등의 작업을 진행
    b. 비동기 처리 필요
  3. cellForRowAt 메서드가 호출되면 prefetchRowAt에서 미리 로딩해두었던 리소스와 데이터를 표현한다.
  4. 만약 사용자가 빠른 스크롤 등으로 화면에 셀을 보여줄 필요가 없는 경우에는 cancelPrefetchingForRowAt이 호출되고, 해당 메서드에서 관련 작업을 취소하도록 구현한다.

 

UICollectionViewDataSourcePrefetching 프로토콜을 채택해서 코드를 구현할 수 있다. 

Delegate / DataSource와 마찬가지로 채택하는 코드를 추가해야 한다.

mediaCollectionView.prefetchDataSource = self

 

프로토콜 안의 메서드 코드는 아래와 같다.

extension MediaViewController: UICollectionViewDataSourcePrefetching {
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        // 사용자가 다음에 볼 데이터를 확인하여 
        for indexPath in indexPaths {
        	// 그 데이터의 마지막 유무를 확인하여 
            if mediaList.count - 1 == indexPath.item && currentPage < totalPage {
            	// 만약 마지막이 아니라면, 다음에 올 데이터를 미리 준비해야 한다.
                // 현재 page에서 다음에 올 데이터이므로 page 값에 1을 더해서 호출한다. 
                currentPage += 1
                fetchTrendMedia(type: mediaType, time: timeType, page: currentPage)
            }
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
        
    }
}

 

이렇게 구현하면, 무한 스크롤이 가능하다. (= 페이지네이션) 

 


Pagenation - scrollViewDidScroll

또 다른 방법으로 UIScrollView Delegate를 활용할 수 있다.

테이블 뷰/컬렉션 뷰 모두 스크롤 뷰를 상속 받고 있기 때문에 사용할 수 있다.

 

스크롤뷰의 프로토콜을 사용하기 위해서는 몇가지 변수가 필요하다.

private var currentPage: Int = 1
private var totalPage: Int = 1
private var canFetchData: Bool = true

 

스크롤 시점에 따라서 다음 데이터를 패치해야 하는 유무를 따져야 한다.

extension TrendViewController: UICollectionViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    	// 사용자의 스크롤 시점에 따라서 (아래에 닿았다면)
        if mediaCollectionView.contentOffset.y > (mediaCollectionView.contentSize.height - mediaCollectionView.bounds.size.height) {
            // 그리고 다음 데이터를 불러올 수 있는 상황이 된다면 
            if canFetchData, currentPage < totalPage {
            	// 다음 데이터를 불러온다. 
                // 현재 페이지의 다음 페이지를 불러야 하므로 현재 페이지 값에 1을 더해주고,
                // 불러오는 도중에는 다음 데이터를 호출하지 말아야 하므로 canFetchData 값을 false로 지정한다.
                // 그리고 나서 서버 통신을 한다.
                currentPage += 1
                canFetchData = false

                fetchTrendMedia(type: mediaType, time: timeType, page: currentPage)
            }
        }
    }
}

 

서버 통신 코드는 아래와 같다.

extension TrendViewController {
    private func fetchTrendMedia(type: String, time: String, page: Int) {
        let url = URLConstant.BaseURL + URLConstant.TrendingURL + "/\(type)" + "/\(time)" + "?api_key=\(APIKey.APIKey)"

        let params: Parameters = ["media_type" : type,
                                  "time_window" : time]
        
        AF.request(url, method: .get, parameters: params).validate(statusCode: 200...500).responseJSON { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)

                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    self.totalPage = json["total_pages"].intValue
                    self.canFetchData = true
                    
                    ...
                }
         }
     }
}

'iOS' 카테고리의 다른 글

Kakao 다음(Daum) 검색 API 구현 (feat. Expandable Cell)  (0) 2022.08.08
Cell안의 UIButton 이벤트 처리 (+WebKit View)  (0) 2022.08.08
APIManager  (1) 2022.08.08
Pagenation  (0) 2022.08.05
.gitignore  (0) 2022.08.05