안녕하세요. 후르륵짭짭입니다.
사내에서 Network 통신을 좀 더 수월하게 사용하기 위해서
Moya 라이브러리를 사용하고 있는데, 너무 유명한 라이브러리라 정리 할까 말까 고민하다가
제가 자주 사용법을 헷갈려서 작성하려고 합니다. ㅎㅎㅎㅎ
일단 Moya 라이브러리는 정말 유명한 알라모파이어 라이브러를 Enum 타입으로 더욱 사용하기 쉽게 만들었고
테스트 또한 쉽게 할 수 있도록 했습니다. (<- 이 부분은 나중에 정리하도록 하겠습니다. )
그래서 Moya를 안 사용해본 사람은 있어도 한번만 사용해본 사람은 없을것 같단 생각이 듭니다.
만약 사내에서 Custom으로 적용할 부분이 많다면,,, 사용 안하겠지만요
일단 사내에서는 Rx+Moya 방식으로 적용하고 있지만,
여기서는 그냥 순수 Moya로 작성해보도록 하겠습니다.
** API Service 생성하기 **
Moya의 최대 장점은 이 서비스가 Enum으로 적용되어 있어 쉽게 타입을 지정 할 수 있습니다.
위에 처럼 필요한 부분에 채워넣어주기만 하면 됩니다.
- 케이스 선언 : 이 서비스가 제공하는 서비스들을 정의 해줍니다.
- BaseUrl : 기본 도메인을 작성하는 겁니다.
위에 처럼 기본적인 URL을 선언해줍니다.
그러면 해당 Case에 기본 URL로 선언해준 것으로 Domain을 설정해줍니다.
- Path : 기본 Path를 정의 해줍니다.
Path도 마찮가지 입니다.
- Method : 어떤 방식으로 통신 할 것이냐
Method는 총 get post delete put 를 주로 사용하는데, 이 외에도 여러개가 있습니다.
- task : 어떻게 데이터를 전송 할 것이냐
1. requestPlain : 기본적으로 어떤 값을 넣지 않을 때 사용합니다. (주로 GET)
2. requestData : Body에 데이터를 담아서 보낼 때 사용합니다. (주로 POST)
3. requestParameters : 파일을 다운로드 할 때 다운로드 링크를 담습니다. (주로 GET)
이 외에도 다양한 Task를 수행하는 Method들이 존재합니다
- headers : 헤더에 어떤 값을 넣을 것이냐
Header에 추가적으로 담아야 할 것이 있다면 적어주면 됩니다.
** Provider Wrapper 생성 **
기본적으로 Moya는 Provider 객체와 TargetType(서비스)만 있으면 네트워크 통신을 할 수 있습니다.
하지만 이럴경우 매번 불편하게 Provider를 선언해주고 Custom을 할 수 없게 됩니다.
따라서 저는 이 Provider를 상속 받고 좀 더 Custom하게 사용할 수 있도록 하부 기능을 만들었습니다.
위에 처럼 사용하면 request나 다른 기능을 제 입맛 대로 변환해서 사용 할 수 있습니다.
- init
endPointClosure : 통신 요청을 지정해서 할 수 있습니다. (이부분은 추후 다루겠습니다. 주로 테스트용)
stubClosure : 테스트를 할 때 사용합니다.
plugins : Custom 플러그인 입니다. 원하는 방식으로 Request와 Response를 변경 할 수 있습니다.
- requestSuccessRes
func requestSuccessRes<Model : Codable>(target : Provider, instance : Model.Type , completion : @escaping(Result<Model, MoyaError>) -> () )
위에 같이 제너릭을 사용해서 타입만 지정하면 값이 반환 될 수 있도록 하였고
상태코드가 200~300 일 때만 성공을 반환 할 수 있도록 변경 했습니다.
=> GET 호출
=> POST 호출
=> Parameter 호출
- requestDownload
case .download:
return .downloadDestination { temporaryURL, response in
print("temporaryURL : \(temporaryURL.absoluteString)")
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let desination = directory.appendingPathComponent("SwiftDocument.pdf")
return (desination , [.removePreviousFile])
}
}
위에 Task에서 Download에 다운로드할 Path를 지정해줍니다.
그리고 해당 다운로드가 완료 되면 결과 값을 반환하도록 했습니다
=> DownLoad 호출
다음 처럼 Progress Block을 지정해주면 해당 부분에
Progress : 0.01558497396955489
Complete : false
Progress : 0.03116994793910978
Complete : false
Progress : 0.04675492190866467
Complete : false
Progress : 0.29611450542154294
Complete : false
Progress : 0.5454740889344212
Complete : false
Progress : 0.7948336724472994
Complete : false
Progress : 1.0
Complete : false
위와 같이 결과가 출력이 됩니다.
그리고 지정한 FIlePath에 파일이 저장이 됩니다.
** Custom PlugIn **
원하는 방식 대로 Request와 Response가 내려온 시점에 특정 작업을 설정 할 수 있습니다.
예를들어 아래 처럼 로그를 찍을 수도 있겠죠??
URL Request - download : https://www.tutorialspoint.com/swift/swift_tutorial.pdf
URL Response - download : success(Status Code: 200, Data Length: 0)
그리고 이 PlugIn을 Provider Wrapper에 넣어줍니다.
지금 까지 Moya에 대한 사용법을 정리 해봤습니다.
여기에 작성한 것은 Moya가 어떤 것인지 정확하게 분석한 것이 아니라
사용법을 작성 한 겁니다.
큰 도움이 됐음 좋겠습니다.
** 전체 코드 **
- ViewController
import UIKit
import Moya
import RxSwift
struct VersionModel: Codable {
let version: String
let build: Int
let requiredUpdate: Bool
let comment: String
let manager: String
}
struct PostResult : Codable {
let id : Int
}
struct UserModel : Codable{
let id : Int
let name : String
let username : String
let email : String
}
struct CommentsModel : Codable {
let id : Int
let postId : Int
let name : String
let email : String
}
class ViewController: UIViewController {
let apiManager = APIManager()
let wrapper = NetworkWrapper<APIService>(plugins: [CustomPlugIn()])
override func viewDidLoad() {
super.viewDidLoad()
print("URL : \(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteString)")
// Wrapper를사용한방식()
}
private func Wrapper를사용하지않는방식(){
apiManager.version(completion: {data in
print("Real 데이터 : \(data)")
}, failure: {error in
print("Real 에러 : \(error)")
})
apiManager.testVersion { data in
print("테스트 데이터 : \(data)")
} failure: { error in
print("테스트 에러 : \(error)")
}
}
private func Wrapper를사용한방식(){
wrapper.requestSuccessRes(target: .version, instance: [UserModel].self) { result in
switch result {
case .success(let 결과):
print("Wrapper version 결과 : \(결과)")
case .failure(let error):
print("Wrapper error 실패 : \(error.localizedDescription)")
}
}
}
@IBAction func sendPost(_ sender: Any) {
let userModel = UserModel(id: 12, name: "Hello", username: "World", email: "empty")
guard let data = try? JSONEncoder().encode(userModel) else {return}
apiManager.versionPost(data: data){ data in
print("Real Post 데이터 : \(data)")
} failure: { error in
print("Real Post 실패 데이터 : \(error)")
}
apiManager.testVersionPost(data: data) { data in
print("Post 테스트 데이터 : \(data)")
} failure: { error in
print("Post 테스트 실패 데이터 : \(error)")
}
wrapper.requestSuccessRes(target: .post(data), instance: PostResult.self) { result in
switch result {
case .success(let 결과):
print("Wrapper Post 결과 : \(결과)")
case .failure(let error):
print("Wrapper error 실패 : \(error.localizedDescription)")
}
}
}
@IBAction func parameter(_ sender: Any) {
wrapper.requestSuccessRes(target: .parameter(postId: 1), instance: [CommentsModel].self) { result in
switch result {
case .success(let 결과):
print("Wrapper parameter version 결과 : \(결과)")
case .failure(let error):
print("Wrapper parameter error 실패 : \(error.localizedDescription)")
}
}
}
@IBAction func download(_ sender: Any) {
wrapper.requestDownload(target: .download) { progress in
print("Progress : \(progress.progress)")
print("Complete : \(progress.completed)")
} completion: { result in
if result {
print("Download 성공")
}
else{
print("Download Fail")
}
}
}
}
- Moya Logic
import Foundation
import Moya
enum APIService {
case version
case post(Data)
case parameter(postId : Int)
case download
}
extension APIService : TargetType {
var baseURL: URL {
switch self {
case .version , .post , .parameter:
return URL(string: "https://jsonplaceholder.typicode.com")!
case .download :
return URL(string: "https://www.tutorialspoint.com/swift/swift_tutorial.pdf")!
}
}
var path: String {
switch self{
case .version :
return "users"
case .post :
return "users"
case .parameter:
return "comments"
case .download:
return ""
}
}
var method: Moya.Method {
switch self {
case .version:
return .get
case .post :
return .post
case .parameter(_):
return .get
case .download:
return .get
}
}
var sampleData: Data {
var data : Data?
switch self {
case .version:
let version : VersionModel = VersionModel(version: "2.2.2",
build: 22,
requiredUpdate: false,
comment: "테스트 데이터입니다.",
manager: "테스트")
data = try? JSONEncoder().encode(version)
case .post(let input) :
data = input
case .parameter(postId:_):
return Data()
case .download:
return Data()
}
if data == nil {
return Data()
}
return data!
}
var task: Task {
switch self {
case .version:
return .requestPlain
case .post(let data):
return .requestData(data)
case .parameter(postId: let postId):
return .requestParameters(parameters: ["postId" : postId, "hello":postId], encoding: URLEncoding.queryString)
case .download:
return .downloadDestination { temporaryURL, response in
print("temporaryURL : \(temporaryURL.absoluteString)")
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let desination = directory.appendingPathComponent("SwiftDocument.pdf")
return (desination , [.removePreviousFile])
}
}
}
var headers: [String : String]? {
return nil
}
}
class CustomPlugIn : PluginType {
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
print("URL Request - \(target) : \(request.url?.absoluteString ?? "없음")")
return request
}
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>{
print("URL Response - \(target) : \(result)")
return result
}
}
class NetworkWrapper<Provider : TargetType> : MoyaProvider<Provider> {
init(endPointClosure : @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
stubClosure : @escaping StubClosure = MoyaProvider.neverStub,
plugins : [PluginType] ){
let session = MoyaProvider<Provider>.defaultAlamofireSession()
session.sessionConfiguration.timeoutIntervalForRequest = 20
session.sessionConfiguration.timeoutIntervalForResource = 20
super.init(endpointClosure: endPointClosure, stubClosure: stubClosure, session: session, plugins: plugins)
}
func requestSuccessRes<Model : Codable>(target : Provider, instance : Model.Type , completion : @escaping(Result<Model, MoyaError>) -> () ){
self.request(target) { result in
switch result {
case .success(let response):
if (200..<300).contains(response.statusCode) {
print("Status Code : \(response.statusCode)")
if let decodeData = try? JSONDecoder().decode(instance, from: response.data) {
completion(.success(decodeData))
}
else{
completion(.failure(.requestMapping("디코딩오류")))
}
}
else{
completion(.failure(.statusCode(response)))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func requestDownload(target : Provider, progress : @escaping ProgressBlock, completion : @escaping(Bool) -> ()){
self.request(target, progress: progress) { result in
switch result {
case .success(let response):
if (200..<300).contains(response.statusCode) {
print("Status Code : \(response.statusCode)")
completion(true)
}
else{
completion(false)
}
case .failure(_):
completion(false)
}
}
}
}
struct APIManager {
private let provider = MoyaProvider<APIService>()
let customEndpointClosure = { (target : APIService) -> Endpoint in
return Endpoint(url: URL(target: target).absoluteString, sampleResponseClosure: {.networkResponse(400, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: target.headers)
}
private var testingProvider : MoyaProvider<APIService>?
init(){
testingProvider = .init(endpointClosure: customEndpointClosure,stubClosure: MoyaProvider.immediatelyStub)
}
func version(completion : @escaping ([UserModel]) -> () , failure : @escaping (String) -> ()){
provider.request(.version, completion: { result in
switch result {
case let .success(response) :
do {
let usermodel = try JSONDecoder().decode([UserModel].self, from: response.data)
completion(usermodel)
}
catch let error{
failure(error.localizedDescription)
}
case let .failure(error):
failure(error.localizedDescription)
}
})
}
func download(){
provider.request(.download) { progress in
print("Progress : \(progress.progress)")
print("Complete : \(progress.completed)")
} completion: { result in
switch result {
case let .success(response) :
print("Download Success : \(response)")
case let .failure(error):
print("Download Fail : \(error)")
}
}
}
func versionPost(data : Data , completion : @escaping(String)->() , failure : @escaping(String)->()){
provider.request(.post(data)) { result in
switch result {
case let .success(response) :
let res = String(data: response.data, encoding: .utf8) ?? "NO Data"
completion(res)
case let .failure(error):
failure(error.localizedDescription)
}
}
}
func testVersion(completion : @escaping (VersionModel) -> () , failure : @escaping (String) -> ()){
testingProvider?.request(.version, completion: { result in
switch result {
case let .success(response) :
do {
if (200..<300).contains(response.statusCode) {
print("Status Code : \(response.statusCode)")
}
else{
print("Error Status Code : \(response.statusCode)")
}
let version = try JSONDecoder().decode(VersionModel.self, from: response.data)
completion(version)
}
catch let error{
failure(error.localizedDescription)
}
case let .failure(error):
failure(error.localizedDescription)
}
})
}
func testVersionPost(data : Data ,completion : @escaping (UserModel) -> (), failure : @escaping (String) -> ()){
testingProvider?.request(.post(data), completion: { result in
switch result {
case let .success(response) :
do {
let userModel = try JSONDecoder().decode(UserModel.self, from: response.data)
completion(userModel)
}
catch let error{
failure(error.localizedDescription)
}
case let .failure(error):
failure(error.localizedDescription)
}
})
}
}
참고 사이트 :
Moya Provider 커스텀
https://leejigun.github.io/Swift_Moya_Provider_custom
Moya :
https://github.com/Moya/Moya/blob/master/Readme.md
Moya Download :
https://gist.github.com/boboboa32/1f4305c6b5b1f660b5de7da0da473af9
Moya Test API 만들기
https://swieeft.github.io/2020/11/10/MoyaTest.html
기타
- API 검증을 위한 사이트
https://jsonplaceholder.typicode.com/
- 쿼리 스트링이란
https://velog.io/@pear/Query-String-%EC%BF%BC%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%A7%81%EC%9D%B4%EB%9E%80
'Xcode > IOS' 카테고리의 다른 글
SwiftUI) SwiftUI 체험기#1 - 다양한 Binding (0) | 2022.08.20 |
---|---|
IOS)XCFramework로 통합 Framework Package를 만들어보자 (2) | 2022.07.17 |
SwiftUI) LazyGride 대한 경험 정리 (0) | 2022.06.11 |
IOS) RxTableViewSectionedReloadDataSource를 실습해보기 (0) | 2021.10.24 |
IOS)Rx의 Publish와 Subscribe를 MVVM으로 구현해보기 (0) | 2021.09.22 |
댓글