diff --git a/Sources/ACMNetworking/ACMNetworking.swift b/Sources/ACMNetworking/ACMNetworking.swift index 6c5b61d..7d3b31e 100644 --- a/Sources/ACMNetworking/ACMNetworking.swift +++ b/Sources/ACMNetworking/ACMNetworking.swift @@ -6,7 +6,20 @@ import Foundation /// ACMNetworking, make requests easily public class ACMNetworking: NSObject { - private var task: URLSessionDataTask? + var mainEndpoint: ACMBaseEndpoint? + var session: URLSession? + var requestTask: URLSessionDataTask? + var downloadTask: URLSessionDownloadTask? + var taskProgress: NSKeyValueObservation? + + /// Predefined variables + var logger: ACMBaseLogger? { + mainEndpoint?.logger + } + + var stringUtils: ACMStringUtils? { + mainEndpoint?.stringUtils + } /// Public Init function /// For creating object with SDK @@ -14,49 +27,33 @@ public class ACMNetworking: NSObject { super.init() } - /// Main request function - /// - /// - Parameters: - /// - endpoint: base endpoint that keeps all endpoint information - /// - currentRetryCount(Optional): retry request count - /// - onSuccess: Callback for success scenario - /// - onError: Callback for error scenario - /// - /// - Returns: - /// - Void - public func request(to endpoint: ACMBaseEndpoint, - currentRetryCount: Int? = 0, - onSuccess: ACMGenericCallbacks.ResponseCallback, - onError: ACMGenericCallbacks.ErrorCallback) - { - guard let urlRequest = generateURLRequest(endpoint: endpoint) else { return } - - task = endpoint.session(delegate: self).dataTask(with: urlRequest) { [weak self] data, response, error in - guard let self else { return } - - self.handleNilErrorResponse(with: endpoint, error: error, onError: onError) - self.handleNilResponse(with: endpoint, response: response, onError: onError) - self.handleConnectivityError(with: endpoint, error: error, onError: onError) - - guard let data = self.handleData(with: endpoint, data: data, onError: onError) else { return } - guard let httpResponse = self.handleHttpResponse(with: endpoint, response: response, onError: onError) else { return } - - // Check if response is in valid http range - guard self.validateResponse(with: httpResponse) else { - self.executeRetry(with: endpoint, httpResponse: httpResponse, data: data, currentRetryCount: currentRetryCount, onSuccess: onSuccess, onError: onError) - return - } - - self.cancel() - - self.handleResult(with: endpoint, data: data, onSuccess: onSuccess, onError: onError) - } - task?.resume() + /// Public destroy function + deinit { + print("ACMNetworking deinited") } /// Cancels the current network request public func cancel() { - task?.cancel() - task = nil + cancelRequestTask() + cancelDownloadTask() + session?.invalidateAndCancel() + session = nil + taskProgress?.invalidate() + taskProgress = nil + mainEndpoint = nil + } + + private func cancelRequestTask() { + if requestTask?.state == .running { + requestTask?.cancel() + } + requestTask = nil + } + + private func cancelDownloadTask() { + if downloadTask?.state == .running { + downloadTask?.cancel() + } + downloadTask = nil } } diff --git a/Sources/ACMNetworking/Library/Callbacks/ACMGenericCallbacks.swift b/Sources/ACMNetworking/Library/Callbacks/ACMGenericCallbacks.swift index 411c5de..b5b559b 100644 --- a/Sources/ACMNetworking/Library/Callbacks/ACMGenericCallbacks.swift +++ b/Sources/ACMNetworking/Library/Callbacks/ACMGenericCallbacks.swift @@ -14,4 +14,8 @@ public enum ACMGenericCallbacks { public typealias InfoCallback = ((Bool?, Error?) -> Void)? /// Success callback with generic response for closures public typealias ResponseCallback = ((T) -> Void)? + /// Success callback with generic response for closures + public typealias DownloadCallback = ((ACMDownloadModel) -> Void)? + /// Progress callback with generic response for closures + public typealias ProgressCallback = ((ACMProgressModel) -> Void)? } diff --git a/Sources/ACMNetworking/Library/Constants/ACMNetworking/ACMNetworkingConstants.swift b/Sources/ACMNetworking/Library/Constants/ACMNetworking/ACMNetworkingConstants.swift index acb6972..3f6cdcd 100644 --- a/Sources/ACMNetworking/Library/Constants/ACMNetworking/ACMNetworkingConstants.swift +++ b/Sources/ACMNetworking/Library/Constants/ACMNetworking/ACMNetworkingConstants.swift @@ -3,5 +3,5 @@ // enum ACMNetworkingConstants { - static var configOverride: Bool = false + static var configOverride: Bool? = false } diff --git a/Sources/ACMNetworking/Library/Constants/NetworkMessages/ACMNetworkConstants.swift b/Sources/ACMNetworking/Library/Constants/NetworkMessages/ACMNetworkConstants.swift index d7caca7..c4246a6 100644 --- a/Sources/ACMNetworking/Library/Constants/NetworkMessages/ACMNetworkConstants.swift +++ b/Sources/ACMNetworking/Library/Constants/NetworkMessages/ACMNetworkConstants.swift @@ -42,6 +42,8 @@ public enum ACMNetworkConstants { public static var httpBodyMultipart = "Multipart data, length: %@" /// Info message for retry mechanism public static var httpRetryCount = "Current retry count is %d, total retry count is %d" + /// Error message if data is invalid and could not be parsed + public static var genericErrorMessage = "Generic error : %@" } public extension ACMNetworkConstants { @@ -52,12 +54,12 @@ public extension ACMNetworkConstants { } /// Header for holding multipart header with content type - static func multipartHeader(model: ACMMultipartContentTypeModel) -> ACMHeaderModel { - ACMHeaderModel(field: "Content-Type", value: ACMStringUtils.shared.merge(list: [ + static func multipartHeader(model: ACMMultipartContentTypeModel, utils: ACMStringUtils?) -> ACMHeaderModel { + ACMHeaderModel(field: "Content-Type", value: utils?.merge(list: [ model.type, " ", model.boundary, - ])) + ]) ?? "") } /// Header for holding multipart accept type diff --git a/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Extensions.swift b/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Extensions.swift index 9276372..fe676b8 100644 --- a/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Extensions.swift +++ b/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Extensions.swift @@ -7,60 +7,65 @@ import Foundation extension ACMNetworking { func baseRequest(to endpoint: ACMBaseEndpoint) -> URLRequest? { guard let urlRequest = endpoint.urlRequest else { - ACMBaseLogger.error(ACMNetworkConstants.urlRequestErrorMessage) + endpoint.logger?.error(ACMNetworkConstants.urlRequestErrorMessage) return nil } - let info = ACMStringUtils.shared.merge(list: [ + var infoList = [ ACMNetworkConstants.httpRequestType, - endpoint.method.rawValue, - ]) - ACMBaseLogger.info(info) + ] + + if let methodRaw = endpoint.method?.rawValue { + infoList.append(methodRaw) + } + + let info = endpoint.stringUtils?.merge(list: infoList) + endpoint.logger?.info(info) if let url = endpoint.url { - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.httpURLMessage, "\(url)", ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) } if let authHeader = endpoint.authHeader { - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.httpAuthHeadersMessage, "\(authHeader)", ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) } if let headers = endpoint.headers, headers.count > 0 { - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.httpHeadersMessage, "\(headers)", ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) } if let queryItems = endpoint.queryItems, queryItems.count > 0 { - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.httpQueryItemsMessage, "\(queryItems)", ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) } if let params = endpoint.params { - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.httpBodyMessage, params.paramsRaw, ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) } else if let data = endpoint.mediaData { - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.httpBodyMessage, String(format: ACMNetworkConstants.httpBodyMultipart, "\(data.length)"), ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) } return urlRequest @@ -75,13 +80,13 @@ extension ACMNetworking: URLSessionTaskDelegate { /// - task: URL session task /// - didFinishCollecting: Metrics that gathered public func urlSession(_: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - let message = ACMStringUtils.shared.merge(list: [ + let message = mainEndpoint?.stringUtils?.merge(list: [ "didFinishCollecting", task.description, "metrics", "\(metrics.taskInterval)", ]) - ACMBaseLogger.info(message) + logger?.info(message) } /// URL Session taskIsWaitingForConnectivity @@ -90,11 +95,11 @@ extension ACMNetworking: URLSessionTaskDelegate { /// - session: URL Session /// - task: URL session task public func urlSession(_: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { - let message = ACMStringUtils.shared.merge(list: [ + let message = mainEndpoint?.stringUtils?.merge(list: [ "taskIsWaitingForConnectivity", task.description, ]) - ACMBaseLogger.info(message) + logger?.info(message) } /// URL Session didSendBodyData @@ -106,7 +111,7 @@ extension ACMNetworking: URLSessionTaskDelegate { /// - totalBytesSent /// - totalBytesExpectedToSend public func urlSession(_: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - let message = ACMStringUtils.shared.merge(list: [ + let message = mainEndpoint?.stringUtils?.merge(list: [ "task", task.description, "didSendBodyData", @@ -116,6 +121,6 @@ extension ACMNetworking: URLSessionTaskDelegate { "totalBytesExpectedToSend", "\(totalBytesExpectedToSend)", ]) - ACMBaseLogger.info(message) + logger?.info(message) } } diff --git a/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Handle+Extensions.swift b/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Handle+Extensions.swift index ef2c3f1..9ed7c8d 100644 --- a/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Handle+Extensions.swift +++ b/Sources/ACMNetworking/Library/Extensions/ACMNetworking+Handle+Extensions.swift @@ -10,7 +10,7 @@ import Foundation extension ACMNetworking { func generateURLRequest(endpoint: ACMBaseEndpoint) -> URLRequest? { guard let urlRequest = baseRequest(to: endpoint) else { - ACMBaseLogger.error(ACMNetworkConstants.urlRequestErrorMessage) + endpoint.logger?.error(ACMNetworkConstants.urlRequestErrorMessage) return nil } return urlRequest @@ -22,11 +22,12 @@ extension ACMNetworking { func handleNilErrorResponse(with endpoint: ACMBaseEndpoint, error: Error?, onError: ACMGenericCallbacks.ErrorCallback) { guard error == nil else { cancel() - let message = ACMStringUtils.shared.merge(list: [ + + let message = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.errorMessage, error?.localizedDescription ?? "", ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: error?.localizedDescription, endpoint: endpoint)) return } @@ -38,11 +39,11 @@ extension ACMNetworking { func handleNilResponse(with endpoint: ACMBaseEndpoint, response: URLResponse?, onError: ACMGenericCallbacks.ErrorCallback) { guard response != nil else { cancel() - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.errorMessage, ACMNetworkConstants.responseNullMessage, ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.responseNullMessage, endpoint: endpoint)) return } @@ -54,11 +55,11 @@ extension ACMNetworking { func handleConnectivityError(with endpoint: ACMBaseEndpoint, error: Error?, onError: ACMGenericCallbacks.ErrorCallback) { if error?.isConnectivityError ?? false { cancel() - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.errorMessage, ACMNetworkConstants.dataNullMessage, ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.dataNullMessage, endpoint: endpoint)) return } @@ -70,11 +71,11 @@ extension ACMNetworking { func handleData(with endpoint: ACMBaseEndpoint, data: Data?, onError: ACMGenericCallbacks.ErrorCallback) -> Data? { guard let data = data else { cancel() - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.errorMessage, ACMNetworkConstants.dataNullMessage, ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.dataNullMessage, endpoint: endpoint)) return nil } @@ -87,11 +88,11 @@ extension ACMNetworking { func handleHttpResponse(with endpoint: ACMBaseEndpoint, response: URLResponse?, onError: ACMGenericCallbacks.ErrorCallback) -> HTTPURLResponse? { guard let httpResponse = response as? HTTPURLResponse else { cancel() - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.errorMessage, ACMNetworkConstants.httpStatusError, ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.httpStatusError, endpoint: endpoint)) return nil } @@ -109,14 +110,14 @@ extension ACMNetworking { extension ACMNetworking { /// Execute retry mechanism func executeRetry(with endpoint: ACMBaseEndpoint, httpResponse: HTTPURLResponse, data: Data, currentRetryCount: Int?, onSuccess: ACMGenericCallbacks.ResponseCallback, onError: ACMGenericCallbacks.ErrorCallback) { - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.errorMessage, ACMNetworkConstants.httpStatusError, "-\(httpResponse.statusCode)", ACMNetworkConstants.responseInfoMessage, String(data: data, encoding: .utf8) ?? "", ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) // MARK: Retry mechanism @@ -128,7 +129,7 @@ extension ACMNetworking { if let currentRetryCount = currentRetryCount, currentRetryCount < maxRetryCount { let nextRetryCount = currentRetryCount + 1 - ACMBaseLogger.info(ACMStringUtils.shared.merge(list: [ + endpoint.logger?.info(endpoint.stringUtils?.merge(list: [ String(format: ACMNetworkConstants.httpRetryCount, nextRetryCount, maxRetryCount), ])) request(to: endpoint, currentRetryCount: nextRetryCount, onSuccess: onSuccess, onError: onError) @@ -143,11 +144,11 @@ extension ACMNetworking { func handleResult(with endpoint: ACMBaseEndpoint, data: Data, onSuccess: ACMGenericCallbacks.ResponseCallback, onError: ACMGenericCallbacks.ErrorCallback) { do { let dataString = String(data: data, encoding: .utf8) ?? "" - let info = ACMStringUtils.shared.merge(list: [ + let info = endpoint.stringUtils?.merge(list: [ ACMNetworkConstants.responseInfoMessage, dataString, ]) - ACMBaseLogger.info(info) + endpoint.logger?.info(info) if endpoint.isStream == true { let components = dataString @@ -169,31 +170,31 @@ extension ACMNetworking { onSuccess?(responseObject) } } catch let DecodingError.dataCorrupted(context) { - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ context.debugDescription, ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) } catch let DecodingError.keyNotFound(key, context) { - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ "Key \(key) not found: \(context.debugDescription)", "codingPath: \(context.codingPath)", ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) } catch let DecodingError.valueNotFound(value, context) { - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ "Value \(value) not found: \(context.debugDescription)", "codingPath: \(context.codingPath)", ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) } catch let DecodingError.typeMismatch(type, context) { - let message = ACMStringUtils.shared.merge(list: [ + let message = endpoint.stringUtils?.merge(list: [ "Type \(type) mismatch: \(context.debugDescription)", "codingPath: \(context.codingPath)", ]) - ACMBaseLogger.error(message) + endpoint.logger?.error(message) } catch let e { let errorMessage = String(format: ACMNetworkConstants.dataParseErrorMessage, e.localizedDescription) - ACMBaseLogger.warning(errorMessage) + endpoint.logger?.warning(errorMessage) onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: errorMessage, endpoint: endpoint)) } } diff --git a/Sources/ACMNetworking/Library/MainCalls/ACMNetworking+Download.swift b/Sources/ACMNetworking/Library/MainCalls/ACMNetworking+Download.swift new file mode 100644 index 0000000..976bdc6 --- /dev/null +++ b/Sources/ACMNetworking/Library/MainCalls/ACMNetworking+Download.swift @@ -0,0 +1,80 @@ +// +// ACMNetworking+Download.swift +// +// +// Created by burak on 22.12.2023. +// + +import UIKit + +public extension ACMNetworking { + /// Main request function + /// + /// - Parameters: + /// - endpoint: base endpoint that keeps all endpoint information + /// - currentRetryCount(Optional): retry request count + /// - onSuccess: Callback for success scenario + /// - onError: Callback for error scenario + /// + /// - Returns: + /// - Void + func download(to endpoint: ACMBaseEndpoint, + currentRetryCount _: Int? = 0, + onSuccess: ACMGenericCallbacks.DownloadCallback, + onProgress: ACMGenericCallbacks.ProgressCallback = nil, + onError: ACMGenericCallbacks.ErrorCallback = nil) + { + guard let urlRequest = generateURLRequest(endpoint: endpoint) else { return } + + session = endpoint.session(delegate: self) + downloadTask = session?.downloadTask(with: urlRequest) { [weak self] url, urlResponse, error in + guard let self else { return } + + self.handleNilErrorResponse(with: endpoint, error: error, onError: onError) + self.handleNilResponse(with: endpoint, response: urlResponse, onError: onError) + self.handleConnectivityError(with: endpoint, error: error, onError: onError) + + guard let url, + let urlResponse, + let httpResponse = self.handleHttpResponse(with: endpoint, response: urlResponse, onError: onError), + let pathExtension = httpResponse.url?.pathExtension + else { + self.cancel() + return + } + + self.cancel() + + do { + let searchUrl = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let destinationDirectory = searchUrl + .appendingPathComponent("ACMNetworking") + + if !FileManager.default.fileExists(atPath: destinationDirectory.relativePath) { + try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true) + } + + let destination = destinationDirectory.appendingPathComponent(url.lastPathComponent) + .appendingPathExtension(pathExtension) + + try FileManager.default.moveItem(at: url, to: destination) + + let data = try Data(contentsOf: destination) + + let model = ACMDownloadModel(data: data, localURL: destination, response: urlResponse) + onSuccess?(model) + } catch let e { + let errorMessage = String(format: ACMNetworkConstants.genericErrorMessage, e.localizedDescription) + endpoint.logger?.warning(errorMessage) + onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: errorMessage, endpoint: endpoint)) + } + } + + taskProgress = downloadTask?.progress.observe(\.fractionCompleted, changeHandler: { progress, _ in + let model = ACMProgressModel(progress: progress.fractionCompleted) + onProgress?(model) + }) + + downloadTask?.resume() + } +} diff --git a/Sources/ACMNetworking/Library/MainCalls/ACMNetworking+Request.swift b/Sources/ACMNetworking/Library/MainCalls/ACMNetworking+Request.swift new file mode 100644 index 0000000..f2cc3aa --- /dev/null +++ b/Sources/ACMNetworking/Library/MainCalls/ACMNetworking+Request.swift @@ -0,0 +1,61 @@ +// +// ACMNetworking+Request.swift +// +// +// Created by burak on 22.12.2023. +// + +import UIKit + +public extension ACMNetworking { + /// Main request function + /// + /// - Parameters: + /// - endpoint: base endpoint that keeps all endpoint information + /// - currentRetryCount(Optional): retry request count + /// - onSuccess: Callback for success scenario + /// - onError: Callback for error scenario + /// + /// - Returns: + /// - Void + func request(to endpoint: ACMBaseEndpoint, + currentRetryCount: Int? = 0, + onSuccess: ACMGenericCallbacks.ResponseCallback, + onProgress: ACMGenericCallbacks.ProgressCallback = nil, + onError: ACMGenericCallbacks.ErrorCallback = nil) + { + guard let urlRequest = generateURLRequest(endpoint: endpoint) else { return } + + mainEndpoint = endpoint + + session = mainEndpoint?.session(delegate: self) + + requestTask = session?.dataTask(with: urlRequest) { [weak self] data, response, error in + guard let self else { return } + + self.handleNilErrorResponse(with: endpoint, error: error, onError: onError) + self.handleNilResponse(with: endpoint, response: response, onError: onError) + self.handleConnectivityError(with: endpoint, error: error, onError: onError) + + guard let data = self.handleData(with: endpoint, data: data, onError: onError) else { return } + guard let httpResponse = self.handleHttpResponse(with: endpoint, response: response, onError: onError) else { return } + + // Check if response is in valid http range + guard self.validateResponse(with: httpResponse) else { + self.executeRetry(with: endpoint, httpResponse: httpResponse, data: data, currentRetryCount: currentRetryCount, onSuccess: onSuccess, onError: onError) + return + } + + self.cancel() + + self.handleResult(with: endpoint, data: data, onSuccess: onSuccess, onError: onError) + } + + taskProgress = requestTask?.progress.observe(\.fractionCompleted, changeHandler: { progress, _ in + let model = ACMProgressModel(progress: progress.fractionCompleted) + onProgress?(model) + }) + + requestTask?.resume() + } +} diff --git a/Sources/ACMNetworking/Library/Manager/ACMBaseEndpoint.swift b/Sources/ACMNetworking/Library/Manager/ACMBaseEndpoint.swift index b761f66..0b25f7f 100644 --- a/Sources/ACMNetworking/Library/Manager/ACMBaseEndpoint.swift +++ b/Sources/ACMNetworking/Library/Manager/ACMBaseEndpoint.swift @@ -8,13 +8,21 @@ import Foundation /// /// Base endpoint struct for holding endpoint information public struct ACMBaseEndpoint { + // MARK: Logger + + var logger: ACMBaseLogger? + + // MARK: String utils + + var stringUtils: ACMStringUtils? + // MARK: Config var config: ACMPlistModel? // MARK: Override fetching config file - var configOverride: Bool = false + var configOverride: Bool? = false // MARK: API host @@ -22,7 +30,7 @@ public struct ACMBaseEndpoint { // MARK: API scheme, Default https - var scheme: ACMBaseScheme = .https + var scheme: ACMBaseScheme? = .https // MARK: The path for api access @@ -42,19 +50,31 @@ public struct ACMBaseEndpoint { // MARK: Default method: GET - var method: ACMBaseMethod = .get + var method: ACMBaseMethod? = .get // MARK: Generated URL for making request var url: URL? { - var components = URLComponents() - components.scheme = scheme.rawValue - components.host = host - components.path = updatedPath - components.queryItems = queryItems - return components.url + if let downloadURL { + var components = URLComponents(string: downloadURL) + components?.queryItems = queryItems + return components?.url + } else { + var components = URLComponents() + if let rawScheme = scheme?.rawValue { + components.scheme = rawScheme + } + components.host = host + components.path = updatedPath + components.queryItems = queryItems + return components.url + } } + // MARK: Raw URL string for bypassing host, path + + var downloadURL: String? + // MARK: Auth header var authHeader: ACMAuthModel? @@ -75,12 +95,14 @@ public struct ACMBaseEndpoint { var urlRequest: URLRequest? { guard let url = url else { - ACMBaseLogger.error(ACMNetworkConstants.errorURLMessage) + logger?.error(ACMNetworkConstants.errorURLMessage) return nil } var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = method.rawValue + if let methodRaw = method?.rawValue { + urlRequest.httpMethod = methodRaw + } if let header = authHeader { urlRequest.setValue(header.rawHeader, forHTTPHeaderField: ACMNetworkConstants.headerAuthorization) @@ -117,13 +139,14 @@ public struct ACMBaseEndpoint { return URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) } - init(config: ACMPlistModel? = nil, configOverride: Bool, host: String? = nil, scheme: ACMBaseScheme, path: String = "", queryItems: [URLQueryItem]? = nil, params: [String: Any?]? = nil, headers: NSMutableDictionary? = nil, method: ACMBaseMethod, authHeader: ACMAuthModel? = nil, mediaData: NSMutableData? = nil, retryCount: Int? = nil, isStream: Bool = false) { + init(config: ACMPlistModel? = nil, configOverride: Bool? = nil, host: String? = nil, scheme: ACMBaseScheme? = nil, path: String = "", queryItems: [URLQueryItem]? = nil, params: [String: Any?]? = nil, headers: NSMutableDictionary? = nil, method: ACMBaseMethod? = nil, authHeader: ACMAuthModel? = nil, mediaData: NSMutableData? = nil, retryCount: Int? = nil, isStream: Bool = false, downloadURL: String? = nil) { if let config = config { self.config = config } else { - self.config = ACMPlistUtils.shared.config() + self.config = ACMPlistUtils().config() } - ACMBaseLogger.shared.config = self.config + logger = ACMBaseLogger(config: self.config) + stringUtils = ACMStringUtils() ACMNetworkingConstants.configOverride = configOverride if let host = host { self.host = host @@ -140,6 +163,7 @@ public struct ACMBaseEndpoint { self.mediaData = mediaData self.retryCount = retryCount self.isStream = isStream + self.downloadURL = downloadURL } } diff --git a/Sources/ACMNetworking/Library/Manager/ACMBaseLogger.swift b/Sources/ACMNetworking/Library/Manager/ACMBaseLogger.swift index 9f16a1b..3e04360 100644 --- a/Sources/ACMNetworking/Library/Manager/ACMBaseLogger.swift +++ b/Sources/ACMNetworking/Library/Manager/ACMBaseLogger.swift @@ -6,12 +6,11 @@ import Foundation import os.log final class ACMBaseLogger { - /// shared instance - static var shared = ACMBaseLogger() - var config: ACMPlistModel? - private init() {} + init(config: ACMPlistModel? = nil) { + self.config = config + } /// is BaseLoggerging enable var isEnabled: Bool { @@ -21,29 +20,29 @@ final class ACMBaseLogger { /// BaseLogger for success. Will add ✅ emoji to see better /// /// - Parameter message: BaseLoggerging message - static func info(_ message: String) { - guard ACMBaseLogger.shared.isEnabled else { return } - ACMBaseLogger.shared.debug(type: "✅", message: message) + func info(_ message: String?) { + guard isEnabled else { return } + debug(type: "✅", message: message ?? "") } /// BaseLogger for warning. Will add ⚠️ emoji to see better /// /// - Parameter message: BaseLoggerging message - static func warning(_ message: String) { - guard ACMBaseLogger.shared.isEnabled else { return } - ACMBaseLogger.shared.debug(type: "⚠️", message: message) + func warning(_ message: String?) { + guard isEnabled else { return } + debug(type: "⚠️", message: message ?? "") } /// BaseLogger for error. Will add ❌ emoji to see better /// /// - Parameter message: BaseLoggerging message - static func error(_ message: String) { - guard ACMBaseLogger.shared.isEnabled else { return } - ACMBaseLogger.shared.debug(type: "❌", message: message) + func error(_ message: String?) { + guard isEnabled else { return } + debug(type: "❌", message: message ?? "") } private func debug(type: Any?, message: String) { - guard ACMBaseLogger.shared.isEnabled else { return } + guard isEnabled else { return } DispatchQueue.main.async { os_log("%@", type: .debug, "\(type ?? "") -> \(message)") } diff --git a/Sources/ACMNetworking/Library/Manager/ACMEndpoint.swift b/Sources/ACMNetworking/Library/Manager/ACMEndpoint.swift index 691a77a..f1924d1 100644 --- a/Sources/ACMNetworking/Library/Manager/ACMEndpoint.swift +++ b/Sources/ACMNetworking/Library/Manager/ACMEndpoint.swift @@ -13,6 +13,7 @@ public final class ACMEndpoint { /// Init method for creating new object public init() {} + var mainEndpoint: ACMBaseEndpoint? var config: ACMPlistModel? var configOverride: Bool = false var host: String? @@ -29,6 +30,7 @@ public final class ACMEndpoint { var mediaData: NSMutableData? var retryCount: Int? var isStream: Bool = false + var downloadURL: String? /// Set config model /// @@ -103,6 +105,18 @@ public final class ACMEndpoint { return self } + /// Sets the raw url string + /// + /// - Parameters: + /// - rawURL: Given url string + /// + /// - Returns + /// - Self + public func set(downloadURL: String) -> Self { + self.downloadURL = downloadURL + return self + } + /// Sets the request method /// /// - Parameters: @@ -262,13 +276,13 @@ public final class ACMEndpoint { let contentType = ACMNetworkConstants.multipartContentType let boundary = contentType.boundary let fileModel = fileData?.fileModel - let fileName = ACMStringUtils.shared.merge(list: [ + let fileName = mainEndpoint?.stringUtils?.merge(list: [ ProcessInfo.processInfo.globallyUniqueString, fileModel?.ext ?? "", - ]) + ]) ?? "" let endpoint = set(method: .post) - .add(header: ACMNetworkConstants.multipartHeader(model: contentType)) + .add(header: ACMNetworkConstants.multipartHeader(model: contentType, utils: mainEndpoint?.stringUtils)) .add(header: ACMNetworkConstants.multipartDataAccept) let body = NSMutableData() @@ -289,13 +303,13 @@ public final class ACMEndpoint { " \($0.key)=\($0.value);" }.joined(separator: "") - let contentDisposition = ACMStringUtils.shared.merge(list: [ + let contentDisposition = mainEndpoint?.stringUtils?.merge(list: [ "Content-Disposition: form-data;", paramsRaw, "\r\n", ]) - if let data = contentDisposition.toData { + if let data = contentDisposition?.toData { body.append(data) } } @@ -330,6 +344,8 @@ public final class ACMEndpoint { let streamSupported = isStream || hasStreamEnabledAsParam - return ACMBaseEndpoint(config: config, configOverride: configOverride, host: host, scheme: scheme, path: path, queryItems: queryItems, params: params, headers: headers, method: method, authHeader: authHeader, mediaData: mediaData, retryCount: retryCount, isStream: streamSupported) + mainEndpoint = ACMBaseEndpoint(config: config, configOverride: configOverride, host: host, scheme: scheme, path: path, queryItems: queryItems, params: params, headers: headers, method: method, authHeader: authHeader, mediaData: mediaData, retryCount: retryCount, isStream: streamSupported, downloadURL: downloadURL) + + return mainEndpoint ?? ACMBaseEndpoint() } } diff --git a/Sources/ACMNetworking/Library/Models/ACMDownloadModel.swift b/Sources/ACMNetworking/Library/Models/ACMDownloadModel.swift new file mode 100644 index 0000000..703fe2d --- /dev/null +++ b/Sources/ACMNetworking/Library/Models/ACMDownloadModel.swift @@ -0,0 +1,14 @@ +// +// ACMDownloadModel.swift +// +// +// Created by burak on 22.12.2023. +// + +import Foundation + +public struct ACMDownloadModel { + public var data: Data? + public var localURL: URL? + public var response: URLResponse? +} diff --git a/Sources/ACMNetworking/Library/Models/ACMProgressModel.swift b/Sources/ACMNetworking/Library/Models/ACMProgressModel.swift new file mode 100644 index 0000000..1f62ee4 --- /dev/null +++ b/Sources/ACMNetworking/Library/Models/ACMProgressModel.swift @@ -0,0 +1,20 @@ +// +// ACMProgressModel.swift +// +// +// Created by Burak on 28.12.2023. +// + +import Foundation + +public struct ACMProgressModel { + public var progress: Double + public func formatted(with format: String = "%.2f") -> String { + return String(format: format, progress) + } + + public func formattedPercentage(with format: String = "%.f") -> String { + let percentageProgress = progress * 100 + return String(format: format, percentageProgress) + } +} diff --git a/Sources/ACMNetworking/Library/Utils/Plist/ACMPlistUtils.swift b/Sources/ACMNetworking/Library/Utils/Plist/ACMPlistUtils.swift index baa94ec..751bdf7 100644 --- a/Sources/ACMNetworking/Library/Utils/Plist/ACMPlistUtils.swift +++ b/Sources/ACMNetworking/Library/Utils/Plist/ACMPlistUtils.swift @@ -8,8 +8,6 @@ import Foundation /// /// Utility class for plist public final class ACMPlistUtils { - public static let shared = ACMPlistUtils() - public func config(type _: T.Type? = ACMPlistModel.self) -> T? { guard let data = getData(), let plist = getList(with: data, type: T.self) else { throwFatalError(with: ACMPropertyListSerializationError.fileNotParsed) diff --git a/Sources/ACMNetworking/Library/Utils/String/ACMStringUtils.swift b/Sources/ACMNetworking/Library/Utils/String/ACMStringUtils.swift index 9311912..7afb2b3 100644 --- a/Sources/ACMNetworking/Library/Utils/String/ACMStringUtils.swift +++ b/Sources/ACMNetworking/Library/Utils/String/ACMStringUtils.swift @@ -6,8 +6,7 @@ /// /// String utility for making string actions easily public final class ACMStringUtils { - static let shared = ACMStringUtils() - + public init() {} func merge(list: [String]) -> String { return list.joined(separator: " ") }