Skip to content

Instantly share code, notes, and snippets.

@hhhello0507
Created March 25, 2025 12:42
Show Gist options
  • Save hhhello0507/bad94130665822fbf7fda2f1064369a7 to your computer and use it in GitHub Desktop.
Save hhhello0507/bad94130665822fbf7fda2f1064369a7 to your computer and use it in GitHub Desktop.
Moya snippet
import Foundation
import Moya
// NetRunner 클래스에서 사용.
// 원래 NetRunner는 BaseEndpoint를 준수 받는 Endpoint (e.g. MealEndpoint, AIEndpoint)를 Generic하게 사용하고 있었음.
// 이런 방식으로 하니 각 Endpoint 별로 NetRunner를 만들어야 함.
//
// 이 프로젝트에서는 각 Endpoint 별로 다른 NetRunner를 쓸 필요성을 못 느낌.
// 따라서 NetRunner는 Generic하게 사용하는 오브젝트를 알 필요가 없음.
//
// 결론적으로 NetRunner가 특정 Endpoint를 알지 못하게 하기 위해 한 단계 더 고수준의 enum인 AnyEndpoint를 도입함.
//
// 🔽 아래 깃헙 참고해서 구현함 🔽
// https://github.com/GSM-MSG/Emdpoint/blob/master/Sources/Emdpoint/EndpointType/EndpointType.swift
// https://github.com/Team-Ampersand/Dotori-iOS/blob/master/Projects/Core/Networking/Sources/NetworkingImpl.swift
public enum AnyTarget {
case endpoint(any MyTarget)
var endpoint: any MyTarget {
switch self {
case .endpoint(let endpoint):
return endpoint
}
}
public init(_ endpoint: MyTarget) {
self = .endpoint(endpoint)
}
}
extension AnyTarget: MyTarget {
public var host: String { endpoint.host }
public var route: Route { endpoint.route }
public var baseURL: URL { endpoint.baseURL }
}
// TODO: Refresh 방식을 Moya 라이브러리에서 제공하는 거 따라가는 것을 고려해 볼 것
public enum Authorization {
case none
case refresh
}
import Foundation
import Moya
import MyFoundationUtil
public extension Encodable {
func toRequestParameters(encoding: ParameterEncoding, using encoder: JSONEncoder) -> Moya.Task {
if let data = try? encoder.encode(self),
let object = (try? JSONSerialization.jsonObject(
with: data,
options: .allowFragments
)).flatMap({ $0 as? [String: Any] }) {
let parameters = object.mapValues { value -> Any in
if let enumValue = value as? (any RawRepresentable) {
return enumValue.rawValue
}
return value
}
return .requestParameters(
parameters: parameters,
encoding: encoding
)
}
return .requestPlain
}
func toJSONParameters(using encoder: JSONEncoder = .myEncoder) -> Moya.Task {
toRequestParameters(encoding: JSONEncoding.default, using: encoder)
}
func toURLParameters(using encoder: JSONEncoder = .myEncoder) -> Moya.Task {
toRequestParameters(encoding: URLEncoding.default, using: encoder)
}
}
public enum MealEndpoint: MyTarget {
public var baseURL: URL {
URL(string: "http://localhost:1234")!
}
case fetchMeals(schoolId: Int)
}
extension MealEndpoint {
public var host: String {
"meals"
}
public var route: Route {
switch self {
case .fetchMeals(let schoolId): .get("\(schoolId)")
}
}
}
var sub = Set<AnyCancellable>()
let run = DefaultNetRunner(provider: .init(), authProvider: .init())
func request() {
print("Net")
run
.deepDive(MealEndpoint.fetchMeals(schoolId: 1393), res: String.self)
.sink {
print("Result : \($0)")
} receiveValue: {
print("Value : \($0)")
}
.store(in: &sub)
print("Net")
}
request()
RunLoop.main.run(until: Calendar.current.date(byAdding: .second, value: 10, to: .now)!)
import Moya
extension MoyaProvider {
func request(_ target: Target) async -> Result<Response, MoyaError> {
return await withCheckedContinuation { continuation in
self.request(target) { result in
continuation.resume(returning: result)
}
}
}
func request(_ target: Target) async throws -> Response {
return try await withCheckedThrowingContinuation { continuation in
self.request(target) { result in
switch result {
case .success(let response):
continuation.resume(returning: response)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
import Moya
import Foundation
/// MyTarget
/// Moya.TargetType을 추상화한 protocol
public protocol MyTarget: TargetType {
var host: String { get }
var route: Route { get }
var authorization: Authorization { get }
}
public extension MyTarget {
/**
Devide route
*/
var path: String {
route.path
}
var method: Moya.Method {
route.method
}
var task: Moya.Task {
route.task
}
/**
Just default value.
You can change another value.
*/
var headers: [String: String]? {
["Content-type": "application/json"]
}
var validationType: ValidationType {
.successCodes
}
var authorization: Authorization {
.refresh
}
}
import Foundation
import Moya
import CombineMoya
import Combine
import MyFoundationUtil
/// NetRunner
/// NetRunner가 deinit 되면 provider도 deinit됨.
/// provider가 deinit되면 request가 cancel됨.
public protocol NetRunner {
var provider: MoyaProvider<AnyTarget> { get }
var authProvider: MoyaProvider<AnyTarget> { get }
func deepDive<DTO: Decodable>(
_ target: MyTarget,
res: DTO.Type,
completion: @escaping (Result<DTO, MoyaError>) -> Void
)
func deepDive<DTO: Decodable>(
_ target: MyTarget,
res: DTO.Type
) -> AnyPublisher<DTO, MoyaError>
func deepDive<DTO: Decodable>(
_ target: MyTarget,
res: DTO.Type
) -> AnyPublisher<Result<DTO, MoyaError>, Never>
func deepDive<DTO: Decodable>(
_ target: MyTarget,
res: DTO.Type
) async -> Result<DTO, MoyaError>
func deepDive<DTO: Decodable>(
_ target: MyTarget,
res: DTO.Type
) async throws -> DTO
}
public class DefaultNetRunner: NetRunner {
public let provider: MoyaProvider<AnyTarget>
public let authProvider: MoyaProvider<AnyTarget>
public let decoder: JSONDecoder
public init(
provider: MoyaProvider<AnyTarget> = .init(),
authProvider: MoyaProvider<AnyTarget> = .init(),
decoder: JSONDecoder = .myDecoder
) {
self.provider = provider
self.authProvider = authProvider
self.decoder = decoder
}
private func selectProvider(_ target: MyTarget) -> MoyaProvider<AnyTarget> {
switch target.authorization {
case .none:
provider
case .refresh:
authProvider
}
}
public func deepDive<DTO>(
_ target: MyTarget,
res: DTO.Type,
completion: @escaping (Result<DTO, Moya.MoyaError>) -> Void
) where DTO : Decodable {
selectProvider(target).request(AnyTarget(target)) { result in
switch result {
case .success(let response):
let response = self.unwrapThrowable {
try response.filterSuccessfulStatusCodes()
.map(res, using: self.decoder)
}
completion(response)
case .failure(let error):
completion(.failure(error))
}
}
}
public func deepDive<DTO: Decodable>(
_ target: MyTarget,
res: DTO.Type
) -> AnyPublisher<DTO, MoyaError> {
return selectProvider(target)
.requestPublisher(AnyTarget(target))
.filterSuccessfulStatusCodes() // 200..<300
.map(DTO.self, using: decoder)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
public func deepDive<DTO>(
_ target: MyTarget,
res: DTO.Type
) -> AnyPublisher<Result<DTO, Moya.MoyaError>, Never> where DTO : Decodable {
return selectProvider(target)
.requestPublisher(AnyTarget(target))
.filterSuccessfulStatusCodes()
.map(DTO.self, using: decoder)
.map { Result.success($0) }
.catch { Just(Result.failure($0)) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
public func deepDive<DTO>(
_ target: MyTarget,
res: DTO.Type
) async -> Result<DTO, MoyaError> where DTO : Decodable {
return await selectProvider(target)
.request(AnyTarget(target))
.flatMap { response in
unwrapThrowable {
try response
.filterSuccessfulStatusCodes()
.map(res, using: self.decoder)
}
}
}
public func deepDive<DTO>(
_ target: MyTarget,
res: DTO.Type
) async throws -> DTO where DTO : Decodable {
return try await selectProvider(target)
.request(AnyTarget(target))
.filterSuccessfulStatusCodes()
.map(res, using: decoder)
}
private func unwrapThrowable<T>(
throwable: @escaping () throws -> T
) -> Result<T, MoyaError> {
do {
return .success(try throwable())
} catch {
if let moyaError = error as? MoyaError {
return .failure(moyaError)
} else {
return .failure(.underlying(error, nil))
}
}
}
}
//
// SwiftUIView.swift
//
//
// Created by hhhello0507 on 9/10/24.
//
import Moya
/// Route
/// Moya.TargetType에서 method, path, task를 추상화 한 struct
///
/// Ex.
/// Route.get()
/// Route.get("hello")
/// Route.get("hello").task(.requestJSONEncodable(...))
public struct Route {
struct VoidRes: Decodable {}
public let method: Moya.Method
public let path: String
public let task: Moya.Task
private init(method: Moya.Method, path: String, task: Moya.Task) {
self.method = method
self.path = path
self.task = task
}
public static func connect(_ path: String) -> Self {
Self.init(method: .connect, path: path, task: .requestPlain)
}
public static func delete(_ path: String = "") -> Self {
Self.init(method: .delete, path: path, task: .requestPlain)
}
public static func get(_ path: String = "") -> Self {
Self.init(method: .get, path: path, task: .requestPlain)
}
public static func head(_ path: String = "") -> Self {
Self.init(method: .head, path: path, task: .requestPlain)
}
public static func options(_ path: String = "") -> Self {
Self.init(method: .options, path: path, task: .requestPlain)
}
public static func patch(_ path: String = "") -> Self {
Self.init(method: .patch, path: path, task: .requestPlain)
}
public static func post(_ path: String = "") -> Self {
Self.init(method: .post, path: path, task: .requestPlain)
}
public static func put(_ path: String = "") -> Self {
Self.init(method: .put, path: path, task: .requestPlain)
}
public static func query(_ path: String = "") -> Self {
Self.init(method: .query, path: path, task: .requestPlain)
}
public static func trace(_ path: String = "") -> Self {
Self.init(method: .trace, path: path, task: .requestPlain)
}
public func task(_ task: Moya.Task) -> Self {
Self.init(method: method, path: path, task: task)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment