So the basic idea of Moya is that we want some network abstraction layer that sufficiently encapsulates actually calling Alamofire directly. It should be simple enough that common things are easy, but comprehensive enough that complicated things are also easy.
따라서 Moya의 기본 아이디어는 실제로 Alamofire를 직접 호출하는 것을 충분히 캡슐화하는 네트워크 추상화 계층을 원한다는 것입니다. 일반적인 일도 쉬울 만큼 단순해야 하고, 복잡한 일도 쉬울 만큼 포괄적이어야 합니다.
라고 한다.
(설치는 spm 또는 cocoapod으로 설치가 가능하다)
https://jsonplaceholder.typicode.com << 이 사이트에 가면 무료로 api 통신을 연습할 수 있게 미리 셋팅이 되어 있다.
(저기서 간단하게 GET /posts/1로 실습을 진행한다.)
https://jsonplaceholder.typicode.com/posts/1이 링크로 들어가면
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
이런 데이터 값을 확인할 수 있다.
프로젝트에서 api 관련 폴더 구조
그 다음, https://app.quicktype.io/ (json 형식을 swift에서 사용할 수있는 data타입으로 변환해주는 사이트)
를 통해 확인해보면 다음과 같다.
저기서 CodingKeys는 리턴되는 json 형태에서 변수명을 사용자의 입맛에 맞게 변경해서 사용할 수 있게 해주는 코드이다. userID를 보면, 리턴되는 변수명은 ‘userId’이지만 나는 ‘userID’로 사용하고 싶어!라는 의미이고, 나머지 (id, title, body)는 그대로 사용하겠다는 표현이다.
하지만, 나는 api의 리턴되는 변수명 그대로 사용했다.
PostResponse.swift
import Foundation
struct GetPostData: Codable {
let id: Int
let userId: Int
let title: String
let body: String
}
구조체 명은 사용자가 원하는 이름으로 변경이 가능하다. (사이트에서는 Welcome으로 해뒀군요)
(저는 Get방식으로 Post정보를 받는다고 해서 GetPostData라고 했습니다.)
PostService.swift
전체 코드
//
// PostService.swift
// MoyaTutorial
//
// Created by yimkeul on 2023/09/14.
//
import Foundation
import Moya
enum PostService {
case getPost
}
extension PostService: TargetType {
var baseURL: URL {
return URL(string: "https://jsonplaceholder.typicode.com/")!
}
var path: String {
switch self {
case .getPost:
return "posts/1"
}
var method: Moya.Method {
switch self {
case .getPost:
return .get
}
var task: Moya.Task {
switch self {
case .getPost:
return .requestPlain
}
var headers: [String : String]? {
return ["Content-Type": "application/json"]
}
}
사용자가 사용할 메소드 정하기
enum PostService {
case getPost
}
enum으로 api 통신을 할때, 사용할 메소드를 정하는 곳이다.
Moya 라이브러리로 추상화 작업
extension PostService: TargetType {
var baseURL: URL {
return URL(string: "https://jsonplaceholder.typicode.com/")!
}
var path: String {
switch self {
case .getPost:
return "posts/1"
}
var method: Moya.Method {
switch self {
case .getPost:
return .get
}
var task: Moya.Task {
switch self {
case .getPost:
return .requestPlain
}
var headers: [String : String]? {
return ["Content-Type": "application/json"]
}
}
enum으로 정의한 Service에 Moya라이브러리로 추상화 작업을 한다. (자동 완성 됩니다.)
public struct HTTPMethod: RawRepresentable, Equatable, Hashable {
/// `CONNECT` method.
public static let connect = HTTPMethod(rawValue: "CONNECT")
/// `DELETE` method.
public static let delete = HTTPMethod(rawValue: "DELETE")
/// `GET` method.
public static let get = HTTPMethod(rawValue: "GET")
/// `HEAD` method.
public static let head = HTTPMethod(rawValue: "HEAD")
/// `OPTIONS` method.
public static let options = HTTPMethod(rawValue: "OPTIONS")
/// `PATCH` method.
public static let patch = HTTPMethod(rawValue: "PATCH")
/// `POST` method.
public static let post = HTTPMethod(rawValue: "POST")
/// `PUT` method.
public static let put = HTTPMethod(rawValue: "PUT")
/// `QUERY` method.
public static let query = HTTPMethod(rawValue: "QUERY")
/// `TRACE` method.
public static let trace = HTTPMethod(rawValue: "TRACE")
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
상황에 맞게 선택해서 사용하면 됩니다.
import Foundation
/// Represents an HTTP task.
public enum Task {
/// A request with no additional data.
case requestPlain
/// A requests body set with data.
case requestData(Data)
/// A request body set with `Encodable` type
case requestJSONEncodable(Encodable)
/// A request body set with `Encodable` type and custom encoder
case requestCustomJSONEncodable(Encodable, encoder: JSONEncoder)
/// A requests body set with encoded parameters.
case requestParameters(parameters: [String: Any], encoding: ParameterEncoding)
/// A requests body set with data, combined with url parameters.
case requestCompositeData(bodyData: Data, urlParameters: [String: Any])
/// A requests body set with encoded parameters combined with url parameters.
case requestCompositeParameters(bodyParameters: [String: Any], bodyEncoding: ParameterEncoding, urlParameters: [String: Any])
/// A file upload task.
case uploadFile(URL)
/// A "multipart/form-data" upload task.
case uploadMultipart([MultipartFormData])
/// A "multipart/form-data" upload task combined with url parameters.
case uploadCompositeMultipart([MultipartFormData], urlParameters: [String: Any])
/// A file download task to a destination.
case downloadDestination(DownloadDestination)
/// A file download task to a destination with extra parameters using the given encoding.
case downloadParameters(parameters: [String: Any], encoding: ParameterEncoding, destination: DownloadDestination)
}
PostViewModel.swift
//
// PostViewModel.swift
// MoyaTutorial
//
// Created by yimkeul on 2023/09/06.
//
import Foundation
import Moya
class PostViewModel: ObservableObject {
@Published var getPostData: GetPostData?
func requestPost() {
let provider = MoyaProvider<PostService>()
provider.request(.getPost) { result in
switch result {
case .success(let response):
do {
let decodedResponse = try JSONDecoder().decode(GetPostData.self, from: response.data)
DispatchQueue.main.async {
self.getPostData = decodedResponse
}
print("result : \(decodedResponse)")
} catch let error {
print("Decoding error: \(error.localizedDescription)")
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
}
정말 간단한 형식으로 만든 요청 함수입니다.
ObserableObject프로토콜을 사용해 처음에 작성했던 api 요청 리턴형식을 정의했던 GetPostData에 데이터를 저장합니다.
requestPost함수를 호출하면 getPostData에 값을 저장하고 사용하면 됩니다.
MoyaProvider에 제네릭타입에 어떤 URL에 어떤 방식으로 어떻게 내용을 받을것인지 정의했던 Service를 사용합니다.
provider.requset()로 api 통신을 요청을 하는데 이때, 사용자가 enum으로 어떤 이름으로 통신을 할건지 정의 했던걸 사용하면 됩니다. 클로저로 결과값에 switch로 통신에 성공했을시, json형식으로 디코딩해주고, getPostData에 값을 저장합니다. 그리고 확인하기 위해 print로 통신 값을 출력했습니다.
import SwiftUI
import Moya
struct ContentView: View {
@StateObject var postViewModel: PostViewModel = PostViewModel()
var body: some View {
VStack {
Button {
postViewModel.requestPost()
} label: {
Text("통신하기")
}
if let getPostData = postViewModel.getPostData {
Text("ID: \(getPostData.id)")
Text("UserID: \(getPostData.userId)")
Text("Title: \(getPostData.title)")
Text("Body: \(getPostData.body)")
} else {
Text("결과값이 없습니다.")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PostViewModel이 ObserableObject을 사용했으므로 @StateObject로 값을 사용하면 된다.
Button-action에 api요청 함수를 호출하게 되면 다음과 같이 데이터가 잘 통신되는걸 확인할 수 있다.
Preview
터미널 결과값
result : GetPostData(id: 1, userId: 1, title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto")
이때 버튼을 클릭해서 요청하는 방식이 아닌, 바로 api 통신을 받아오고 싶으면 .task() 또는 .onAppear()을 사용하면 된다.