Skip to content

Instantly share code, notes, and snippets.

@mayoralito
Created June 18, 2020 21:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mayoralito/ada0d7a557911665fce957f1eda1553e to your computer and use it in GitHub Desktop.
Save mayoralito/ada0d7a557911665fce957f1eda1553e to your computer and use it in GitHub Desktop.
// File: APIService.swift
import Foundation
import Combine
enum APIServiceError: Error {
case responseError
case parseError(Error)
}
protocol APIRequestType {
associatedtype Response: Decodable
var path: String { get }
var queryItems: [URLQueryItem]? { get }
var headers: [String: Any]? { get }
var mockFilename: String? { get }
}
extension APIRequestType {
var mockFilename: String? {
return nil
}
}
protocol APIServiceType {
func response<Request>(from request: Request) -> AnyPublisher<Request.Response, APIServiceError> where Request: APIRequestType
}
// ------------------------------------------------
// API
struct APIService: APIServiceType {
private let baseURL: URL
init(baseURL: URL = URL(string: ApplicationSettings.Environment.baseURL())!) {
self.baseURL = baseURL
}
func response<Request>(from request: Request) -> AnyPublisher<Request.Response, APIServiceError> where Request : APIRequestType {
let pathURL = URL(string: request.path, relativeTo: baseURL)!
var urlComponents = URLComponents(url: pathURL, resolvingAgainstBaseURL: true)!
urlComponents.queryItems = request.queryItems
let _headers = request.headers!
var request = URLRequest(url: urlComponents.url!)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("no-cache", forHTTPHeaderField: "Cache-Control")
request.addValue("*/*", forHTTPHeaderField: "Accept")
request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("iOS-\(UUID().uuidString)", forHTTPHeaderField: "reqId")
if let sessionData = UserPreferencesParser.shared.get(preference: .sessionData) as? String, !sessionData.isEmpty {
request.addValue("bearer \(sessionData)", forHTTPHeaderField: "authorization")
}
_ = _headers.map {
request.addValue(String(describing: $0.value), forHTTPHeaderField: $0.key)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return URLSession.shared.dataTaskPublisher(for: request)
.map { data, urlResponse in
return data
}
.mapError { _ in APIServiceError.responseError }
.decode(type: Request.Response.self, decoder: decoder)
.mapError(APIServiceError.parseError)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
// File: APIServiceMock.swift
import Foundation
import Combine
struct APIServiceMock: APIServiceType {
private let baseURL: URL
init(baseURL: URL = URL(string: ApplicationSettings.Environment.baseURL())!) {
self.baseURL = baseURL
}
func response<Request>(from request: Request) -> AnyPublisher<Request.Response, APIServiceError> where Request : APIRequestType {
let filename = request.mockFilename
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
fatalError("Couldn't find \(String(describing: filename)) in main bundle.")
}
return URLSession.shared.dataTaskPublisher(for: URLRequest(url: file))
.map { data, urlResponse in
return data
}
.mapError { _ in APIServiceError.responseError }
.decode(type: Request.Response.self, decoder: decoder)
.mapError(APIServiceError.parseError)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
// File: ProfileRequest.swift
import Foundation
struct ProfileRequest: APIRequestType {
typealias Response = ProfileResponse
var path: String { return "/path/to/profile" }
var queryItems: [URLQueryItem]? {
return []
}
var headers: [String : Any]? {
return [
"key": "value"
]
}
var mockFilename: String? {
return "some-file.json" // if this attribute return content then the class would fetch the info from the local JSON instead of API.
}
}
// File: ProfileViewModel.swift
final class ProfileViewModel {
private let apiService: APIServiceType
init(apiService: APIServiceType = APIService()) {
self.apiService = apiService
// other configs
}
}
// File: AppDelegete.swift
func someMethod() {
let mockData = false
var profileView: ProfileView?
if !mockData {
// Load the View passing API default configs
profileView = ProfileView(viewModel: .init())
} else {
// Load the View passing MockAPI Settings
profileView = ProfileView(viewModel: .init(apiService: APIServiceMock()))
}
// add to your hostviewcontroller
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment