Created
August 27, 2024 23:21
-
-
Save vurgunmert/19f854f2d250bf91cf215758c8a029f2 to your computer and use it in GitHub Desktop.
Network Manager Apple iOS tvOS Swift
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
// | |
// 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