Skip to content

Instantly share code, notes, and snippets.

@vurgunmert
Created August 27, 2024 23:21
Show Gist options
  • Save vurgunmert/19f854f2d250bf91cf215758c8a029f2 to your computer and use it in GitHub Desktop.
Save vurgunmert/19f854f2d250bf91cf215758c8a029f2 to your computer and use it in GitHub Desktop.
Network Manager Apple iOS tvOS Swift
//
// Copyright © 2024 medienmonster GmbH. All rights reserved.
// https://medienmonster.com
//
// Originally created by Mert Vurgun.
//
import Foundation
// Use .all to log request/response details
private var logLevel: LogLevel = .all
class NetworkManager {
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
private let session: URLSession
private let jsonDecoder: JSONDecoder
private let jsonEncoder: JSONEncoder
private let logger = makeLogger(for: "NetworkManager")
init(session: URLSession = .shared, jsonDecoder: JSONDecoder = JSONDecoder(), jsonEncoder: JSONEncoder = JSONEncoder()) {
self.session = session
self.jsonDecoder = jsonDecoder
self.jsonEncoder = jsonEncoder
}
func makeRequest<T: Codable>(_ type: T.Type, request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
return try decodeResponse(type, data, response)
}
func request<T: Codable>(url: URL, method: HTTPMethod, headers: [String: String] = [:], body: Codable? = nil, responseType: T.Type = T.self) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
headers.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }
if let body = body {
request.httpBody = try jsonEncoder.encode(body)
}
request.log()
let (data, response) = try await URLSession.shared.data(for: request)
data.log()
return try decodeResponse(T.self, data, response)
}
private func decodeResponse<T: Codable>(_ type: T.Type, _ data: Data, _ response: URLResponse, _ requestUrl: String? = nil) throws -> T {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.generic(message: "Invalid response", httpCode: nil)
}
switch httpResponse.statusCode {
case 200, 201:
do {
return try jsonDecoder.decode(T.self, from: data)
} catch let decodingError as DecodingError {
throw handleDecodingError(decodingError, requestUrl)
} catch {
throw NetworkError.jsonSerialization(httpCode: httpResponse.statusCode, message: "JSON serialization error!200")
}
case 204:
if data.isEmpty {
return try createEmptyInstance(of: T.self)
} else {
throw NetworkError.jsonSerialization(httpCode: httpResponse.statusCode, message: "JSON serialization error!204")
}
case 209:
throw NetworkError.geoblocked(httpCode: httpResponse.statusCode)
case 210:
throw NetworkError.notAvailable(httpCode: httpResponse.statusCode)
case 400:
let error: StreamErrorDataModel
do {
error = try jsonDecoder.decode(StreamErrorDataModel.self, from: data)
} catch {
throw NetworkError.generic(message: "Request failed with status code \(httpResponse.statusCode)", httpCode: httpResponse.statusCode)
}
throw NetworkError.generic(message: error.message, httpCode: error.code)
case 401, 403:
throw NetworkError.unauthorized(httpCode: httpResponse.statusCode)
default:
let errorResponse = try jsonDecoder.decode(DefaultErrorDataModel.self, from: data)
throw NetworkError.generic(message: errorResponse.message ?? errorResponse.errors?.first ?? "An unexpected error has occurred. Please try again", httpCode: httpResponse.statusCode)
}
}
func deleteCookies() {
if let cookieStorage = HTTPCookieStorage.shared.cookies {
for cookie in cookieStorage {
HTTPCookieStorage.shared.deleteCookie(cookie)
}
}
}
}
private func createEmptyInstance<T: Codable>(of type: T.Type) throws -> T {
let emptyData = "{}".data(using: .utf8)!
return try JSONDecoder().decode(T.self, from: emptyData)
}
private enum LogLevel {
case none
case all
}
private extension Data {
func toString() -> String? {
return String(decoding: self, as: UTF8.self)
}
func log() {
if logLevel == .all {
print("NetworkManager:Response:Raw", String(decoding: self, as: UTF8.self))
}
}
}
private extension URLRequest {
func log() {
if logLevel == .all {
print("NetworkManager:Request:\(httpMethod ?? "") \(self)")
print("NetworkManager:Request:BODY \n \(String(describing: httpBody?.toString()))")
print("NetworkManager:Request:HEADERS \n \(String(describing: allHTTPHeaderFields))")
}
}
}
private func handleDecodingError(_ error: DecodingError, _ requestUrl: String? = nil) -> NetworkError {
switch error {
case .typeMismatch(let type, let context):
return NetworkError.jsonSerialization(httpCode: 0, message: "Type '\(type)' mismatch: \(context.debugDescription), codingPath: \(context.codingPath)")
case .valueNotFound(let type, let context):
return NetworkError.jsonSerialization(httpCode: 0, message: "Value not found for type '\(type)': \(context.debugDescription), codingPath: \(context.codingPath)")
case .keyNotFound(let key, let context):
return NetworkError.jsonSerialization(httpCode: 0, message: "\(requestUrl)\nKey '\(key)' not found: \(context.debugDescription), codingPath: \(context.codingPath)")
case .dataCorrupted(let context):
return NetworkError.jsonSerialization(httpCode: 0, message: "Data corrupted: \(context.debugDescription), codingPath: \(context.codingPath)")
@unknown default:
return NetworkError.jsonSerialization(httpCode: 0, message: "Unknown decoding error")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment