프로젝트에서 Alamofire 사용했다가 Sparrow SAST 에 위험 경고가 많이 잡혀서 삭제하고 블러그 여기저기 조합해서 URLSession 으로 초기 버전을 만들었다.

 

import Foundation

enum NetworkManagerError: Error {
    case error(errorString: String), invalidResponse(code: Int), invalidData
    
    var description: String {
        switch self {
        case .error(errorString: let error):
            return "\(error)"
        case .invalidResponse(code: let code):
            return "response error code : \(code)"
        case .invalidData:
            return "Invalied Data"
        }
    }
}

class NetworkManager: NSObject {
    
    typealias Percentage = Double
    typealias ProgressHandler = (Percentage) -> Void
    typealias CompletionHandler = (Result<Void, Error>) -> Void
    
    
    static let shard = NetworkManager()
    
    private lazy var session = URLSession(configuration: .default,
                                          delegate: self,
                                          delegateQueue: .main)
    
    
    private var progressHandlersByTaskID = [Int : ProgressHandler]()
    private var completionHandlersByTaskID = [Int : CompletionHandler]()
    
    override init() {
        super.init()
    }
    
    private func generateStringData(_ params: Dictionary<String, Any>? ) -> Data? {

        var paramString: String = ""
        if let p = params {
            for (key,value) in p {
                if paramString.count == 0 {
                    paramString = "\(key)=\(value)"
                }
                else {
                    paramString = paramString + "&\(key)=\(value)"
                }
            }
        }
        return paramString.data(using: .utf8)
    }
    
    /// Codable Struct Model 이용해 데이터 형태로 받아 처리할 수 있음.
    func requestDecodable<T:Decodable>(url: URL, method: HTTPMethod,_ params: Dictionary<String, Any>?, of type: T.Type = T.self, completion: @escaping (Result<T?, NetworkManagerError>) -> Void) {
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        
        if let parameter = generateStringData(params) {
            request.httpBody = parameter
        }
        
        let task = session.dataTask(with: request, completionHandler: { data, response, error in
            
            guard error == nil else {
                completion(.failure(.error(errorString: error?.localizedDescription ?? "")))
                return
            }
            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                completion(.failure(.invalidResponse(code: (response as? HTTPURLResponse)!.statusCode)))
                return
            }
            
            guard let data = data else {
                completion(.failure(.invalidData))
                return
            }
            
            do {
                let response = try JSONDecoder().decode(T.self, from: data)
                completion(.success(response))
                
            } catch let error {
                completion(.failure(.error(errorString: error.localizedDescription )))
            }
        })
        task.resume()
        
    }
    
    func request(url: URL, _ params: Dictionary<String, Any>?, completion: @escaping (Result<Dictionary<String, Any>?, NetworkManagerError>) -> Void) {
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        
        if let parameter = generateStringData(params) {
            request.httpBody = parameter
        }
        
        let task = session.dataTask(with: request, completionHandler: { data, response, error in
            
            guard error == nil else {
                completion(.failure(.error(errorString: error?.localizedDescription ?? "")))
                return
            }
            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                completion(.failure(.invalidResponse(code: (response as? HTTPURLResponse)!.statusCode)))
                return
            }
            
            guard let data = data else {
                completion(.failure(.invalidData))
                return
            }
            
            do {
                guard let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] else {
                    return
                }
                
                completion(.success(json))
                
            } catch let error {
                completion(.failure(.error(errorString: error.localizedDescription )))
            }
        })
        task.resume()
    }
    
    
    func downloadRequest(url: URL,
                         progressHandler: @escaping ProgressHandler,
                         completionHandler: @escaping CompletionHandler) {
        
        var request = URLRequest(url: url)
        request.addValue("", forHTTPHeaderField: "Accept-Encoding")
        
        let task = session.downloadTask(with: request)
        progressHandlersByTaskID[task.taskIdentifier] = progressHandler
        completionHandlersByTaskID[task.taskIdentifier] = completionHandler
        task.resume()
    }
    
    func uploadFile(at fileURL: URL,
                    to targetURL: URL,
                    progressHandler: @escaping ProgressHandler,
                    completionHandler: @escaping CompletionHandler) {
        
            var request = URLRequest(
                url: targetURL,
                cachePolicy: .reloadIgnoringLocalCacheData
            )
            
            request.httpMethod = "POST"

            let task = session.uploadTask(
                with: request,
                fromFile: fileURL,
                completionHandler: { data, response, error in
                    // Validate response and call handler
                    if error != nil {
                        completionHandler(.failure(error!))
                    } else {
                        completionHandler(.success(()))
                    }
                }
            )

            progressHandlersByTaskID[task.taskIdentifier] = progressHandler
            task.resume()
        }
}

// MARK: SessionDelegate
extension NetworkManager: URLSessionDelegate {
    
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
            completionHandler(.rejectProtectionSpace, nil)
        }
        if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        }
    }
}

// MARK: SessionDownloadDelegate
extension NetworkManager: URLSessionDownloadDelegate {
    // 다운로드 성공
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        
        log(direction: .ETC, ofType: self, datas: "Finished Downloading to \(location)")
        
        guard let sourcURL = downloadTask.originalRequest?.url else {return}
        
        log(direction: .ETC, ofType: self, datas: "Source URL : \(sourcURL)")
        
        let fileManager = FileManager.default
        let documentURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let destinationURL = documentURL.appendingPathComponent(sourcURL.lastPathComponent)
        
        log(direction: .ETC, ofType: self, datas: "Destination URL : " ,destinationURL)
        
        try? fileManager.removeItem(at: destinationURL)
        
        do{
            try fileManager.copyItem(at: location, to: destinationURL)
            let handler = completionHandlersByTaskID[downloadTask.taskIdentifier]
            handler?(.success(()))
        }
        catch let error {
            log(direction: .ETC, ofType: self, datas: "Could not copy file to disk: \(error.localizedDescription)")
            let handler = completionHandlersByTaskID[downloadTask.taskIdentifier]
            handler?(.failure(error))
        }
    }
    
    // 다운로드 중 (프로그래스)
    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didWriteData bytesWritten: Int64,
                    totalBytesWritten: Int64,
                    totalBytesExpectedToWrite: Int64) {
        
        guard let url = downloadTask.originalRequest?.url else {
            return
        }
        
        let currentProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        
        let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, countStyle: .file)
       
        let handler = progressHandlersByTaskID[downloadTask.taskIdentifier]
        handler?(currentProgress)
    }
    
    // 다운로드 실패
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        
        if error != nil {
            let handler = completionHandlersByTaskID[task.taskIdentifier]
            handler?(.failure(error!))
        }
        
    }
}

// MARK: Upload Session
extension NetworkManager: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didSendBodyData bytesSent: Int64,
                    totalBytesSent: Int64,
                    totalBytesExpectedToSend: Int64
    ) {
        guard let url = task.originalRequest?.url else {
            return
        }
        
        let currentProgress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
        
        let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToSend, countStyle: .file)
       
        let handler = progressHandlersByTaskID[task.taskIdentifier]
        handler?(currentProgress)
    }
}

 

[Post 데이터 전송]

앱스토어에서 앱 버전정보 가져오기 예제

let url = URL(string: "https://itunes.apple.com/kr/lookup?bundleId=kr.xxx.xxxxxxx")
        
NetworkManger.shard.request(url: url!, nil) { result in
            
    switch result {
        case .success(let object):
            guard let result = (object?["results"] as? [Any])?.first as? [String: Any],
                    let version = result["version"] as? String,
                    let trackViewUrl = result["trackViewUrl"] as? String else {
                        return
                    }

            DispatchQueue.main.async {
                completion(version, trackViewUrl, nil)
            }
        case .failure(let error):
            log(direction: .ERROR, ofType: self, datas: error.description)
    }
}

 

 

[파일 다운로드]

if let url = URL(string: "https://file-examples-com.github.io/uploads/2017/10/file-example_PDF_1MB.pdf") {
            
	NetworkManger.shard.downloadRequest(url: url, progressHandler: { percentage in
		log(direction: .ETC, ofType: self, datas: "다운로드", percentage)
	}, completionHandler: { result in
		switch result {
		case .success:
			log(direction: .ETC, ofType: self, datas: "다운로드 완료")
		case .failure(let error):
			log(direction: .ETC, ofType: self, datas: "다운로드 실패", error.localizedDescription)
		}
	})
}

+ Recent posts