diff --git a/EasyCode.xcodeproj/project.pbxproj b/EasyCode.xcodeproj/project.pbxproj index 11a18df..8aff6a7 100644 --- a/EasyCode.xcodeproj/project.pbxproj +++ b/EasyCode.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ BB3C76C82CFCDFDF005217AF /* View+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3C76C72CFCDFDF005217AF /* View+extension.swift */; }; BB3C76CA2CFD502D005217AF /* UIApplication+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3C76C92CFD502D005217AF /* UIApplication+extension.swift */; }; BB8A14152C36649C00F18CE8 /* UnmanagedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB8A14142C36649C00F18CE8 /* UnmanagedWrapper.swift */; }; + BBA9A1632DB77423002BC7FB /* FaceAnalysis.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBA9A1622DB77423002BC7FB /* FaceAnalysis.swift */; }; BBAFFBC52C340D12005703B0 /* EasyCode.docc in Sources */ = {isa = PBXBuildFile; fileRef = BBAFFBC42C340D12005703B0 /* EasyCode.docc */; }; BBAFFBCB2C340D12005703B0 /* EasyCode.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBAFFBC02C340D12005703B0 /* EasyCode.framework */; }; BBAFFBD02C340D12005703B0 /* EasyCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAFFBCF2C340D12005703B0 /* EasyCodeTests.swift */; }; @@ -145,6 +146,7 @@ BB3C76C72CFCDFDF005217AF /* View+extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+extension.swift"; sourceTree = ""; }; BB3C76C92CFD502D005217AF /* UIApplication+extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+extension.swift"; sourceTree = ""; }; BB8A14142C36649C00F18CE8 /* UnmanagedWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnmanagedWrapper.swift; sourceTree = ""; }; + BBA9A1622DB77423002BC7FB /* FaceAnalysis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceAnalysis.swift; sourceTree = ""; }; BBAFFBC02C340D12005703B0 /* EasyCode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EasyCode.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BBAFFBC32C340D12005703B0 /* EasyCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EasyCode.h; sourceTree = ""; }; BBAFFBC42C340D12005703B0 /* EasyCode.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = EasyCode.docc; sourceTree = ""; }; @@ -331,6 +333,7 @@ children = ( BBAFFBED2C341A98005703B0 /* Array+extension.swift */, BBBD51EF2C36A1E500557CD3 /* AVCaptureDevice+extension.swift */, + BBE498C92D2D06930085233A /* BackgroundTaskManager.swift */, BBAFFBDB2C340DF2005703B0 /* BinaryFloatingPoint+extension.swift */, BBCC22D42C35208F00CC19C9 /* BiometricIDAuth.swift */, BBAFFC012C342126005703B0 /* Character+extension.swift */, @@ -348,6 +351,7 @@ BBAFFC292C342877005703B0 /* Encodable+extension.swift */, BBAFFBF12C341C31005703B0 /* EncryptionCryptoProtocol.swift */, BBAFFBF32C341C59005703B0 /* EncryptionError.swift */, + BBA9A1622DB77423002BC7FB /* FaceAnalysis.swift */, BBAFFC152C342349005703B0 /* HostingView.swift */, BBBD51EB2C369B5700557CD3 /* ImageFilter.swift */, BBAFFBFD2C341FE4005703B0 /* JailbreakDetection.swift */, @@ -395,7 +399,6 @@ BB3B162F2CEC96910049F5F2 /* DI */, BB3B16422CEC979D0049F5F2 /* Keychain */, BB3C2EF92C6A0D3B00149867 /* Network Layer */, - BBE498C92D2D06930085233A /* BackgroundTaskManager.swift */, ); path = Source; sourceTree = ""; @@ -607,6 +610,7 @@ BB3C2F0B2C6A10F800149867 /* ServiceLocator.swift in Sources */, BBAFFBEE2C341A98005703B0 /* Array+extension.swift in Sources */, BB3B16312CEC96980049F5F2 /* DependencyInjector.swift in Sources */, + BBA9A1632DB77423002BC7FB /* FaceAnalysis.swift in Sources */, BBAFFC182C342430005703B0 /* UIViewController+extension.swift in Sources */, BB3C76CA2CFD502D005217AF /* UIApplication+extension.swift in Sources */, BBBCE97C2CFCBAE70061DB04 /* Typealiases.swift in Sources */, diff --git a/EasyCode/Source/FaceAnalysis.swift b/EasyCode/Source/FaceAnalysis.swift new file mode 100644 index 0000000..e4ef3b6 --- /dev/null +++ b/EasyCode/Source/FaceAnalysis.swift @@ -0,0 +1,87 @@ +// +// FaceAnalysis.swift +// EasyCode +// +// Created by Zhanibek Lukpanov on 22.04.2025. +// + +import Foundation +import UIKit.UIImage +import Vision +import CoreImage + +public class FaceAnalysis { + + public init() {} + + private let context = CIContext() + private lazy var detector = CIDetector( + ofType: CIDetectorTypeFace, + context: context, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh] + ) + + public func isSmileDetected(in image: UIImage) -> Bool { + guard let ciImage = CIImage(image: image), let detector else { return false } + + let options: [String: Any] = [ + CIDetectorSmile: true, + CIDetectorImageOrientation: NSNumber(value: image.imageOrientation.rawValue) + ] + + guard let faceFeatures = detector.features(in: ciImage, options: options) as? [CIFaceFeature], + let faceFeature = faceFeatures.first else { + return false + } + + return faceFeature.hasSmile + } + + public func areEyesOpen(in image: UIImage) -> Bool { + guard let ciImage = CIImage(image: image), let detector else { return false } + + let options: [String: Any] = [ + CIDetectorImageOrientation: NSNumber(value: image.imageOrientation.rawValue), + CIDetectorEyeBlink: true + ] + + guard let faceFeatures = detector.features(in: ciImage, options: options) as? [CIFaceFeature], + let face = faceFeatures.first else { + return false + } + + return !face.leftEyeClosed && !face.rightEyeClosed + } + + open func isMouthOpen(in image: UIImage) -> Bool { + guard let ciImage = CIImage(image: image) else { return false } + + let request = VNDetectFaceLandmarksRequest() + let handler = VNImageRequestHandler(ciImage: ciImage, options: [:]) + var isMouthOpen = false + + do { + try handler.perform([request]) + if let firstFace = request.results?.first as? VNFaceObservation, + let landmarks = firstFace.landmarks, + let outerLips = landmarks.outerLips { + + let maxY = outerLips.normalizedPoints.max(by: { $0.y < $1.y })?.y ?? 0 + let minY = outerLips.normalizedPoints.min(by: { $0.y < $1.y })?.y ?? 0 + let maxX = outerLips.normalizedPoints.max(by: { $0.x < $1.x })?.x ?? 0 + let minX = outerLips.normalizedPoints.min(by: { $0.x < $1.x })?.x ?? 0 + + let verticalOpenAmount = maxY - minY + let horizontalExpandAmount = maxX - minX + + let verticalThreshold: CGFloat = 0.27 + let horizontalThreshold: CGFloat = 0.2 + + isMouthOpen = verticalOpenAmount > verticalThreshold && horizontalExpandAmount > horizontalThreshold + } + } catch { + print("Face landmarks detection failed:", error.localizedDescription) + } + return isMouthOpen + } +} diff --git a/EasyCode/Source/Network Layer/NetworkManager.swift b/EasyCode/Source/Network Layer/NetworkManager.swift index 6eb2d07..deb0965 100644 --- a/EasyCode/Source/Network Layer/NetworkManager.swift +++ b/EasyCode/Source/Network Layer/NetworkManager.swift @@ -10,11 +10,25 @@ import Foundation /// `NetworkManager` is a class responsible for managing network requests in your application. It provides /// methods for making standard HTTP requests, handling multipart form data, and supports SSL pinning for enhanced security. /// The class also includes request logging capabilities and offers both callback-based and async/await APIs. +/// public class NetworkManager: NSObject { + public enum SessionType { case main, multipart } + + private let logger = ConsoleLogger() + private var longPollTimer: Timer? + /// The `URLSession` used for making network requests. It is lazily initialized with the default configuration /// and uses `self` as the delegate. - public lazy var session = URLSession(configuration: .default, delegate: self, delegateQueue: .main) + private lazy var session: URLSession = { + let confirguration = URLSessionConfiguration.default + confirguration.waitsForConnectivity = true + confirguration.timeoutIntervalForResource = 30 + confirguration.timeoutIntervalForRequest = 30 + return URLSession(configuration: confirguration, delegate: self, delegateQueue: .main) + }() + + private lazy var multipartSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main) /// A boolean value that indicates whether SSL pinning is enabled. When enabled, the manager will perform SSL /// pinning by validating the server's SSL certificate against the certificates included in `certDataItems`. @@ -26,8 +40,6 @@ public class NetworkManager: NSObject { /// An array of `Data` objects representing the SSL certificates to be used for SSL pinning. public var certDataItems = [Data]() - private var longPollTimer: Timer? - /// Initializes a new instance of `NetworkManager`. public override init() {} @@ -66,7 +78,10 @@ public class NetworkManager: NSObject { /// /// - Parameters: /// - endpoint: An object conforming to `EndPointProtocol` that defines the request details. - /// - isNeedToResumeImmediatly: A boolean value indicating whether the request should start immediately. + /// - retryCount: The number of retry attempts to make in case of a timeout (`NSURLErrorTimedOut`) or cancellation + /// (`NSURLErrorCancelled`). Default is `1`, meaning one initial attempt plus one retry. + /// - retryDeadline: The delay in seconds before each retry. Default is `0` (retry immediately). + /// - isNeedToPerformImmediatly: A boolean value indicating whether the request should start immediately. /// - completion: A closure that is called with the response object conforming to `NetworkResponseProtocol`. /// - Returns: The `URLSessionDataTask` for the request, or `nil` if the request could not be created. /// @@ -83,6 +98,8 @@ public class NetworkManager: NSObject { @discardableResult public func request( _ endpoint: EndPointProtocol, + retryCount: Int = 1, + retryDeadline: TimeInterval = 0, isNeedToPerformImmediatly: Bool = true, completion: @escaping (NetworkResponseProtocol) -> Void ) -> URLSessionDataTask? { @@ -91,9 +108,7 @@ public class NetworkManager: NSObject { let identifiedRequest = IdentifiedRequest(request: request) let row = LoggerRow(request: identifiedRequest) if isNeedToLogRequests { - ConsoleLogger().log(request: request) - } - if NetworkGlobals.isLoggerEnabled { + logger.log(request: request) DispatchQueue.main.async { NetworkGlobals.loggerViewController.insert(row: row) } @@ -103,17 +118,35 @@ public class NetworkManager: NSObject { guard let manager = self else { return } if let response = response as? HTTPURLResponse, manager.isNeedToLogRequests { - ConsoleLogger().log(request: request, response: response, responseData: data, error: error) + manager.logger.log(request: request, response: response, responseData: data, error: error) } - let response = manager.composeResponse(data: data, response: response, error: error) - if NetworkGlobals.isLoggerEnabled { + let composedResponse = manager.composeResponse(data: data, response: response, error: error) + if manager.isNeedToLogRequests { DispatchQueue.main.async { - NetworkGlobals.loggerViewController.update(id: identifiedRequest.id, response: response) + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) } } - completion(response) + if let error { + let nsError = error as NSError + if retryCount > 0, nsError.code == -1001 || nsError.code == -999 { + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + retryDeadline) { + _ = manager.request( + endpoint, + retryCount: retryCount - 1, + retryDeadline: retryDeadline, + isNeedToPerformImmediatly: isNeedToPerformImmediatly, + completion: completion + ) + } + } + } else { + completion(composedResponse) + } } if isNeedToPerformImmediatly { task.resume() } @@ -124,7 +157,10 @@ public class NetworkManager: NSObject { /// /// - Parameters: /// - endpoint: An object conforming to `EndPointProtocol` that defines the request details. - /// - isNeedToResumeImmediatly: A boolean value indicating whether the request should start immediately. + /// - retryCount: The number of retry attempts to make in case of a timeout (`NSURLErrorTimedOut`) or cancellation + /// (`NSURLErrorCancelled`). Default is `1`, meaning one initial attempt plus one retry. + /// - retryDeadline: The delay in seconds before each retry. Default is `0` (retry immediately). + /// - isNeedToPerformImmediatly: A boolean value indicating whether the request should start immediately. /// - multiPartParams: An array of `MultipartFormDataParameter` representing the form data. /// - completion: A closure that is called with the response object conforming to `NetworkResponseProtocol`. /// - Returns: The `URLSessionDataTask` for the request, or `nil` if the request could not be created. @@ -146,6 +182,8 @@ public class NetworkManager: NSObject { @discardableResult public func multiPart( _ endpoint: EndPointProtocol, + retryCount: Int = 1, + retryDeadline: TimeInterval = 0, isNeedToResumeImmediatly: Bool = true, with multiPartParams: [MultipartFormDataParameter], completion: @escaping (NetworkResponseProtocol) -> Void @@ -156,7 +194,6 @@ public class NetworkManager: NSObject { request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var data = Data() - for param in multiPartParams { data.append("\r\n--\(boundary)\r\n") data.append("Content-Disposition: form-data; name=\"\(param.name)\"; filename=\"\(param.fileName)\"\r\n") @@ -164,35 +201,51 @@ public class NetworkManager: NSObject { data.append(param.data) data.append("\r\n") } - data.append("--\(boundary)--\r\n") let identifiedRequest = IdentifiedRequest(request: request) let row = LoggerRow(request: identifiedRequest) if isNeedToLogRequests { - ConsoleLogger().log(request: request) - } - if NetworkGlobals.isLoggerEnabled { + logger.log(request: request) DispatchQueue.main.async { NetworkGlobals.loggerViewController.insert(row: row) } } - let task = session.uploadTask(with: request, from: data) { [weak self] data, response, error in + let task = multipartSession.uploadTask(with: request, from: data) { [weak self] data, response, error in guard let manager = self else { return } if let response = response as? HTTPURLResponse, manager.isNeedToLogRequests { - ConsoleLogger().log(request: request, response: response, responseData: data, error: error) + manager.logger.log(request: request, response: response, responseData: data, error: error) } - let response = manager.composeResponse(data: data, response: response, error: error) - if NetworkGlobals.isLoggerEnabled { + let composedResponse = manager.composeResponse(data: data, response: response, error: error) + if manager.isNeedToLogRequests { DispatchQueue.main.async { - NetworkGlobals.loggerViewController.update(id: identifiedRequest.id, response: response) + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) } } - completion(response) + if let error { + let nsError = error as NSError + if retryCount > 0, nsError.code == -1001 || nsError.code == -999 { + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + retryDeadline) { + _ = manager.multiPart( + endpoint, + retryCount: retryCount - 1, + retryDeadline: retryDeadline, + isNeedToResumeImmediatly: isNeedToResumeImmediatly, + with: multiPartParams, + completion: completion + ) + } + } + } else { + completion(composedResponse) + } } if isNeedToResumeImmediatly { task.resume() } @@ -203,7 +256,11 @@ public class NetworkManager: NSObject { /// Sends an HTTP request using async/await and returns the response. /// - /// - Parameter endpoint: An object conforming to `EndPointProtocol` that defines the request details. + /// - Parameters: + /// - endpoint: An object conforming to `EndPointProtocol` that defines the request details. + /// - retryCount: The number of retry attempts to make in case of a timeout (`NSURLErrorTimedOut`) or cancellation + /// (`NSURLErrorCancelled`). Default is `1`, meaning one initial attempt plus one retry. + /// - retryDeadline: The delay in seconds before each retry. Default is `0` (retry immediately). /// - Returns: A `NetworkResponseProtocol` object representing the response, or `nil` if the request could not be created. /// /// # Example @@ -219,55 +276,84 @@ public class NetworkManager: NSObject { /// } /// ``` @discardableResult - public func request(_ endpoint: EndPointProtocol) async -> NetworkResponseProtocol? { + public func request( + _ endpoint: EndPointProtocol, + retryCount: Int = 1, + retryDeadline: UInt64 = 0 + ) async -> NetworkResponseProtocol? { guard let request = endpoint.makeRequest() else { return nil } - let identifiedRequest = IdentifiedRequest(request: request) - let row = LoggerRow(request: identifiedRequest) - - if isNeedToLogRequests { - ConsoleLogger().log(request: request) - } - if NetworkGlobals.isLoggerEnabled { - DispatchQueue.main.async { - NetworkGlobals.loggerViewController.insert(row: row) - } - } - - do { - let (data, response) = try await session.data(for: request) - if let response = response as? HTTPURLResponse, isNeedToLogRequests { - ConsoleLogger().log(request: request, response: response, responseData: data, error: nil) + var attempts = 0 + let maximumAttempts = retryCount + 1 + while attempts < maximumAttempts { + let identifiedRequest = IdentifiedRequest(request: request) + let row = LoggerRow(request: identifiedRequest) + if isNeedToLogRequests { + logger.log(request: request) + await MainActor.run { NetworkGlobals.loggerViewController.insert(row: row) } } - let composedResponse = composeResponse(data: data, response: response, error: nil) - if NetworkGlobals.isLoggerEnabled { - DispatchQueue.main.async { - NetworkGlobals.loggerViewController.update(id: identifiedRequest.id, response: composedResponse) + do { + let (data, response) = try await session.data(for: request) + if let response = response as? HTTPURLResponse, isNeedToLogRequests { + logger.log(request: request, response: response, responseData: data, error: nil) } - } - - return composedResponse + let composedResponse = composeResponse(data: data, response: response, error: nil) + if isNeedToLogRequests { + await MainActor.run { + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) + } + } + return composedResponse + } catch { + let nsError = error as NSError + if (nsError.code == -1001 || nsError.code == -999) && attempts < retryCount { + attempts += 1 + if isNeedToLogRequests { + logger.log(request: request, response: nil, responseData: nil, error: error) + let composedResponse = composeResponse(data: nil, response: nil, error: error) + await MainActor.run { + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) + } + } + if retryDeadline > 0 { + try? await Task.sleep(nanoseconds: retryDeadline * 1_000_000_000) + } + } else { + if isNeedToLogRequests { + logger.log(request: request, response: nil, responseData: nil, error: error) + } + let composedResponse = composeResponse(data: nil, response: nil, error: error) + if isNeedToLogRequests { + await MainActor.run { + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) + } + } - } catch { - if isNeedToLogRequests { - ConsoleLogger().log(request: request, response: nil, responseData: nil, error: error) - } - let composedResponse = composeResponse(data: nil, response: nil, error: error) - if NetworkGlobals.isLoggerEnabled { - DispatchQueue.main.async { - NetworkGlobals.loggerViewController.update(id: identifiedRequest.id, response: composedResponse) + return composedResponse } } - - return composedResponse } + + return nil } /// Sends a multipart form-data request using async/await and returns the response. /// /// - Parameters: /// - endpoint: An object conforming to `EndPointProtocol` that defines the request details. + /// - retryCount: The number of retry attempts to make in case of a timeout (`NSURLErrorTimedOut`) or cancellation + /// (`NSURLErrorCancelled`). Default is `1`, meaning one initial attempt plus one retry. + /// - retryDeadline: The delay in seconds before each retry. Default is `0` (retry immediately). /// - multiPartParams: An array of `MultipartFormDataParameter` representing the form data. /// - Returns: A `NetworkResponseProtocol` object representing the response, or `nil` if the request could not be created. /// @@ -286,6 +372,8 @@ public class NetworkManager: NSObject { @discardableResult public func multiPart( _ endpoint: EndPointProtocol, + retryCount: Int = 1, + retryDeadline: UInt64 = 0, with multiPartParams: [MultipartFormDataParameter] ) async -> NetworkResponseProtocol? { guard var request = endpoint.makeRequest() else { return nil } @@ -294,7 +382,6 @@ public class NetworkManager: NSObject { request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var data = Data() - for param in multiPartParams { data.append("\r\n--\(boundary)\r\n") data.append("Content-Disposition: form-data; name=\"\(param.name)\"; filename=\"\(param.fileName)\"\r\n") @@ -302,50 +389,69 @@ public class NetworkManager: NSObject { data.append(param.data) data.append("\r\n") } - data.append("--\(boundary)--\r\n") - let identifiedRequest = IdentifiedRequest(request: request) - let row = LoggerRow(request: identifiedRequest) - if isNeedToLogRequests { - ConsoleLogger().log(request: request) - } - if NetworkGlobals.isLoggerEnabled { - DispatchQueue.main.async { - NetworkGlobals.loggerViewController.insert(row: row) - } - } - do { - let (responseData, response) = try await session.upload(for: request, from: data) - if let response = response as? HTTPURLResponse, isNeedToLogRequests { - ConsoleLogger().log(request: request, response: response, responseData: responseData, error: nil) + var attempts = 0 + let maximumAttempts = retryCount + 1 + while attempts < maximumAttempts { + let identifiedRequest = IdentifiedRequest(request: request) + let row = LoggerRow(request: identifiedRequest) + if isNeedToLogRequests { + logger.log(request: request) + await MainActor.run { NetworkGlobals.loggerViewController.insert(row: row) } } - let composedResponse = composeResponse(data: responseData, response: response, error: nil) - - if NetworkGlobals.isLoggerEnabled { - DispatchQueue.main.async { - NetworkGlobals.loggerViewController.update( - id: identifiedRequest.id, - response: composedResponse - ) + do { + let (responseData, response) = try await multipartSession.upload(for: request, from: data) + if let response = response as? HTTPURLResponse, isNeedToLogRequests { + logger.log(request: request, response: response, responseData: responseData, error: nil) } - } - - return composedResponse + let composedResponse = composeResponse(data: responseData, response: response, error: nil) + if isNeedToLogRequests { + await MainActor.run { + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) + } + } + return composedResponse + } catch { + let nsError = error as NSError + if (nsError.code == -1001 || nsError.code == -999) && attempts < retryCount { + attempts += 1 + if isNeedToLogRequests { + logger.log(request: request, response: nil, responseData: nil, error: error) + let composedResponse = composeResponse(data: nil, response: nil, error: error) + await MainActor.run { + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) + } + } + if retryDeadline > 0 { + try? await Task.sleep(nanoseconds: retryDeadline * 1_000_000_000) + } + } else { + if isNeedToLogRequests { + logger.log(request: request, response: nil, responseData: nil, error: error) + } + let composedResponse = composeResponse(data: nil, response: nil, error: error) + if isNeedToLogRequests { + await MainActor.run { + NetworkGlobals.loggerViewController.update( + id: identifiedRequest.id, + response: composedResponse + ) + } + } - } catch { - if isNeedToLogRequests { - ConsoleLogger().log(request: request, response: nil, responseData: nil, error: error) - } - let composedResponse = composeResponse(data: nil, response: nil, error: error) - if NetworkGlobals.isLoggerEnabled { - DispatchQueue.main.async { - NetworkGlobals.loggerViewController.update(id: identifiedRequest.id, response: composedResponse) + return composedResponse } } - - return composedResponse } + + return nil } /// Starts a long-polling process to periodically send HTTP requests to the specified endpoint. @@ -374,14 +480,22 @@ public class NetworkManager: NSObject { public func startLongPolling( endpoint: EndPointProtocol, interval: TimeInterval, - completion: @escaping (_ response: NetworkResponseProtocol, _ stop: () -> Void) -> Void + retryCount: Int = 1, + retryDeadline: TimeInterval = 0, + isNeedToPerformImmediatly: Bool = true, + completion: @escaping (_ response: NetworkResponseProtocol, _ stop: @escaping () -> Void) -> Void ) { stopLongPolling() longPollTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in guard let self else { return } - self.request(endpoint) { response in + self.request( + endpoint, + retryCount: retryCount, + retryDeadline: retryDeadline, + isNeedToPerformImmediatly: isNeedToPerformImmediatly + ) { response in let stopClosure = { [weak self] in guard let self else { return } self.stopLongPolling() @@ -390,6 +504,10 @@ public class NetworkManager: NSObject { } } + if let longPollTimer { + RunLoop.main.add(longPollTimer, forMode: .common) + } + longPollTimer?.fire() } @@ -400,9 +518,33 @@ public class NetworkManager: NSObject { longPollTimer?.invalidate() longPollTimer = nil } + + /// Sets a new `URLSession` instance for the specified session type. + /// + /// This method allows you to replace the session used for either `.main` or `.multipart` + /// operations. Useful when you need a custom session configuration for specific request types. + /// + /// - Parameters: + /// - sessionType: The type of session to configure (`.main` or `.multipart`). + /// - session: The new `URLSession` instance to assign for the given type. + /// + /// Example: + /// ```swift + /// let customSession = URLSession(configuration: .default) + /// setNewSession(for: .main, session: customSession) + /// ``` + public func setNewSession(for sessionType: SessionType, session: URLSession) { + switch sessionType { + case .main: + self.session = session + case .multipart: + multipartSession = session + } + } } // MARK: - URLSessionDelegate + extension NetworkManager: URLSessionDelegate { /// Handles SSL pinning by validating the server's certificate against the stored certificates. @@ -416,57 +558,55 @@ extension NetworkManager: URLSessionDelegate { didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { - DispatchQueue.global(qos: .utility).async { [weak self] in - guard let manager = self else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } + var disposition: URLSession.AuthChallengeDisposition = isSSLPinningEnabled ? .cancelAuthenticationChallenge + : .performDefaultHandling + var urlCredential: URLCredential? - var disposition: URLSession.AuthChallengeDisposition = .cancelAuthenticationChallenge - - guard manager.isSSLPinningEnabled else { - completionHandler(.performDefaultHandling, nil) - return - } - - guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let serverTrust = challenge.protectionSpace.serverTrust else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(disposition, nil) + return + } - var secError: CFError? - let isServerTrusted = SecTrustEvaluateWithError(serverTrust, &secError) + if !isSSLPinningEnabled { + completionHandler(.performDefaultHandling, nil) + return + } - if #available(iOS 15.0, *) { - if isServerTrusted, - let certificates = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate], - let serverCertificate = certificates.first { - let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data + let isTrusted = SecTrustEvaluateWithError(serverTrust, nil) + if !isTrusted { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } - for localCertData in manager.certDataItems where serverCertificateData == localCertData { - let credential = URLCredential(trust: serverTrust) - disposition = .useCredential - completionHandler(disposition, credential) - return - } - } + let serverCertificateData: Data? + if #available(iOS 15.0, *) { + if let certificates = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate], + let firstCert = certificates.first { + serverCertificateData = SecCertificateCopyData(firstCert) as Data } else { - let serverCertificates = (0.. Void)? = nil) { + if let presented = presentedViewController { + presented.dismiss(animated: animated) { [weak self] in + self?.dismissAllPresentedControllers(completion: completion) + } + } else { + completion?() + } + } + + func setAlert(title: String, message: String? = nil, actions: [UIAlertAction]? = nil) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + defer { present(alertController, animated: true) } + guard let actions else { return } + actions.forEach { alertController.addAction($0) } + } } // MARK: - MFMailComposeViewControllerDelegate