어느 API를 활용하더라도 AlamoFire
의 request
및 responseDecodable
메서드 작성 방식은 거의 동일하게 나타난다.
open func request<Parameters: Encodable>(_ convertible: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil) -> DataRequest
// Response Decodable Handler - Serialized into Decodable Type
func responseDecodable<T: Decodable>(of type: T.Type = T.self,
queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self
받아오는 타입만 다르게 설정할 뿐, 내부 구현 코드는 동일하다.
TMDB의 여러 API 호출을 담당하는 NetworkManager
에서 각 데이터를 받아오기 위한 함수들이다.
class NetworkManager {
private let header: HTTPHeaders = [
//...각자의 API 요청 권한 코드...
]
//MARK: - Trending Movie
func callRequestTrendMovie(type: DataUrl, page: Int, completionHandler: @escaping (Result<[Movie], AFError>) -> ()) {
let url = type.requestURL + "?page=\(page)"
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: TrendMovie.self) { response in
switch response.result {
case .success(let value): completionHandler(.success(value.movieList))
case .failure(let error): completionHandler(.failure(error))
}
}
//MARK: - Casting
func callRequestCastingList(type: DataUrl, movieId: Int, completionHandler: @escaping (Result<[Cast], AFError>) -> ()) {
let url = type.requestURL + "\(movieId)/credits"
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: CastingList.self) { response in
switch response.result {
case .success(let value): completionHandler(.success(value.cast))
case .failure(let error): completionHandler(.failure(error))
}
}
//MARK: - Season Lists
func callRequestSeasonList(type: DataUrl, seriesId: Int, completionHandler: @escaping (Result<[Season], AFError>) -> ()) {
let url = type.requestURL + "\(seriesId)"
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: TVDetail.self) { response in
switch response.result {
case .success(let value): completionHandler(.success(value.seasons))
case .failure(let error): completionHandler(.failure(error))
}
}
//MARK: - Episode Lists
func callReqeustEpisodeList(type: DataUrl, seriesId: Int, seasonNumber: Int, completionHandler: @escaping (Result<[Episode], AFError>) -> ()) {
let url = type.requestURL + "\(seriesId)/season/\(seasonNumber)"
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: SeasonDetail.self) { response in
switch response.result {
case .success(let value): completionHandler(.success(value.episodes))
case .failure(let error): completionHandler(.failure(error))
}
}
//MARK: - Similar Movie Lists
func callRequestSimilarMovieList(type: DataUrl, movieId: Int, page: Int, completionHandler: @escaping (Result<[SimilarMovie], AFError>) -> ()) {
let url = URL.makeDataURLString("\(movieId)"+type.requestURL+"\(page)")
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: SimilarMovieList.self) { response in
switch response.result {
case .success(let value): completionHandler(.success(value.movieList))
case .failure(let error): completionHandler(.failure(error))
}
}
//MARK: - Video Lists
func callRequestVideoLlist(type: DataUrl, movieId: Int, completionHandler: @escaping (Result<[Video], AFError>) -> ()) {
let url = URL.makeDataURLString("\(movieId)"+type.requestURL)
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: VideoList.self) { response in
switch response.result {
case .success(let value): completionHandler(.success(value.videoList))
case .failure(let error): completionHandler(.failure(error))
}
}
CTRL+C, CTRL+V 없이 순수 노가다로 작성한 감동실화
패턴이 너무나 명확하다.
Codable
을 채택한 struct type의 data 중 원하는 data를 success로 전달AFError
를 failure로 전달return type만 달라지므로 이를 Type parameter인 T로 대체해보자. API의 case를 enum으로 parameter로 같이 전달, 각 API에서 필요한 추가 data가 다르므로 optional 처리해서 필요한 data만 전달하도록 한다.
//MARK: - Generic Function for callRequest
func callRequest<T: Codable>(type: DataUrl, page: Int?, movieId: Int?, seriesId: Int?, seasonNumber: Int?, completionHandler: @escaping (Result<T, AFError>) -> ()) {
var url: String = ""
//url 설정
switch type {
case .trendMovie:
if let page = page {
url = type.requestURL + "?page=\(page)"
} else {
url = type.requestURL
}
case .movieCasting:
if let movieId = movieId {
url = type.requestURL + "\(movieId)/credits"
}
case .trendTV:
if let page = page {
url = type.requestURL + "?page=\(page)"
} else {
url = type.requestURL
}
case .seasonDetail:
if let seriesId = seriesId {
url = type.requestURL + "\(seriesId)"
}
case .episodeDetail:
if let seriesId = seriesId, let seasonNumber = seasonNumber {
url = type.requestURL + "\(seriesId)/season/\(seasonNumber)"
}
case .similarMovie:
if let movieId = movieId, let page = page {
url = URL.makeDataURLString("\(movieId)"+type.requestURL+"\(page)")
}
case .video:
if let movieId = movieId {
url = URL.makeDataURLString("\(movieId)"+type.requestURL)
}
}
//실제 원하는 배열 데이터 보다 한단계 위의 데이터 전달하기
//DataManager에서 큰 데이터 받아서 필요한 배열 데이터 확보하고 활용하기
AF.request(url, method: .get, headers: headers).validate().responseDecodable(of: T.self) { response in
switch response.result {
case .success(let value):
completionHandler(.success(value))
case .failure(let error):
completionHandler(.failure(error))
}
}
}
이를 DataManager에서 원하는 data를 가져올 때마다 호출하면 끝이 아니다.
return type을 T로 설정했기에 completionHandler로 전달된 T의 타입을 구현해야 한다.
class DataManager {
let networkManager = NetworkManager()
//예시로 2개
func setupMovieTrendList(type: DataUrl, page: Int, completionHandler: @escaping () -> ()) {
networkManager.callRequest(type: type, page: page, movieId: nil, seriesId: nil, seasonNumber: nil) { (result: Result<TrendMovie, AFError>) in
switch result {
case .success(let success):
self.trendMovieList.append(contentsOf: success.movieList)
completionHandler()
case .failure(let failure):
print("Trend Movie Error: ", failure.localizedDescription)
completionHandler()
}
}
}
func setupCastingList(type: DataUrl, movieId: Int, completionHandler: @escaping () -> ()) {
networkManager.callRequest(type: type, page: nil, movieId: movieId, seriesId: nil, seasonNumber: nil) { (result: Result<CastingList, AFError>) in
switch result {
case .success(let success):
self.castingList = success.cast
completionHandler()
case .failure(let failure):
print("Casting List Error: ", failure.localizedDescription)
completionHandler()
}
}
}
}
각 메서드마다 원하는 타입을 얻기 위해 Result 타입으로 전달된 T를 구체적으로 제시한다.
DataManager는 어차피 NetworkManager의 통신 결과만 받아서 저장하고 관리하는 역할이므로 리턴 타입 다루는 것이 크게 다르지 않다.
하지만 NetworkManager는 generic 함수로 정의해놓으니 각 case마다 따로 작성할 필요가 없다. 미래에 원하는 API가 나타날 때마다 필요한 추가 data만 parameter에 Optional type으로 작성하는 등 약간의 대응만 하면 바로 활용할 수 있는 장점이 있다.
공통 view는 Custom View Class로, 공통 method는 Generic으로