Skip to content

Instantly share code, notes, and snippets.

@lgastler
Created January 28, 2023 19:28
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 lgastler/c2f761a7d36b91dfdb2bf74e28db2347 to your computer and use it in GitHub Desktop.
Save lgastler/c2f761a7d36b91dfdb2bf74e28db2347 to your computer and use it in GitHub Desktop.
Swift NetworkStack
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 }
}
}
@lgastler
Copy link
Author

We could add additionalPathComponents, f.e. for use cases like /api/users/1 like this using fetch():

func fetch<T>(_ resource: Endpoint<T>, additionalPathComponent: String? = nil, with data: Data? = nil) async throws -> T {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment