Created
January 28, 2023 19:28
-
-
Save lgastler/c2f761a7d36b91dfdb2bf74e28db2347 to your computer and use it in GitHub Desktop.
Swift NetworkStack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct Endpoint<T: Decodable> { | |
var path: String | |
var type: T.Type | |
var method = HTTPMethod.get | |
var headers = [String: String]() | |
var keyPath: String? | |
} | |
extension Endpoint where T == Array<News> { | |
static let headlines = Endpoint(path: "api/users", type: Array<User>.self, keyPath: "data") | |
} | |
enum HTTPMethod: String { | |
case delete, get, patch, post, put | |
var rawValue: String { | |
String(describing: self).uppercased() | |
} | |
} | |
struct AppEnvironment { | |
var name: String | |
var baseURL: URL | |
var session: URLSession | |
static let production = AppEnvironment( | |
name: "Production", | |
baseURL: URL(string: "https://reqres.io")!, | |
session: { | |
let configuration = URLSessionConfiguration.default | |
configuration.httpAdditionalHeaders = [ | |
"ApiKey": "production-key-from-keychain" | |
] | |
return URLSession(configuration: configuration) | |
}() | |
) | |
#if DEBUG | |
static let testing = AppEnvironment( | |
name: "Testing", | |
baseURL: URL(string: "https://reqres.io")!, | |
session: { | |
let configuration = URLSessionConfiguration.ephemeral | |
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData | |
configuration.httpAdditionalHeaders = [ | |
"ApiKey": "test-key" | |
] | |
return URLSession(configuration: configuration) | |
}() | |
) | |
#endif | |
} | |
struct NetworkManager { | |
var environment: AppEnvironment | |
func fetch<T>(_ resource: Endpoint<T>, with data: Data? = nil) async throws -> T { | |
guard let url = URL(string: resource.path, relativeTo: environment.baseURL) else { | |
throw URLError(.unsupportedURL) | |
} | |
var request = URLRequest(url: url) | |
request.httpMethod = resource.method.rawValue | |
request.httpBody = data | |
request.allHTTPHeaderFields = resource.headers | |
var (data, _) = try await environment.session.data(for: request) | |
if let keyPath = resource.keyPath { | |
if let rootObject = try JSONSerialization.jsonObject(with: data) as? NSDictionary { | |
if let nestedObject = rootObject.value(forKeyPath: keyPath) { | |
data = try JSONSerialization.data(withJSONObject: nestedObject, options: .fragmentsAllowed) | |
} | |
} | |
} | |
let decoder = JSONDecoder() | |
return try decoder.decode(T.self, from: data) | |
} | |
func fetch<T>(_ resource: Endpoint<T>, with data: Data? = nil, attempts: Int, retryDelay: Double = 1) async throws -> T { | |
do { | |
print("Attempting to fetch (Attempts remaining: \(attempts))") | |
return try await fetch(resource, with: data) | |
} catch(let error) { | |
if attempts > 1 { | |
try await Task.sleep(for: .milliseconds(Int(retryDelay * 1000))) | |
return try await fetch(resource, attempts: attempts - 1, retryDelay: retryDelay) | |
} else { | |
throw error | |
} | |
} | |
} | |
func fetch<T>(_ resource: Endpoint<T>, with data: Data? = nil, defaultValue: T) async throws -> T { | |
do { | |
return try await fetch(resource, with: data) | |
} catch { | |
return defaultValue | |
} | |
} | |
} | |
struct NetworkManagerKey: EnvironmentKey { | |
static var defaultValue = NetworkManager(environment: .testing) | |
} | |
extension EnvironmentValues { | |
var networkManager: NetworkManager { | |
get { self[NetworkManagerKey.self] } | |
set { self[NetworkManagerKey.self] = newValue } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
We could add
additionalPathComponents
, f.e. for use cases like/api/users/1
like this usingfetch()
: