Search code examples
swiftfile-uploadswiftuiprogress-barmultipartform-data

SwiftUI, how to get progress whilst uploading multipart/form-data files using await urlSession.upload(...)


iOS 15+

How do I get the upload progress from the following using await urlSession.upload?

I specifically want the Await functionality which I dont seem to get (or understand how to) using urlSession.uploadTask.

The code uploads the file fine.

I've tried using the urlSession function at the bottom but it never fires.

https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1408299-urlsession

Please note I normally code C# and I am mostly a beginner in swift.

Thank you.

NetworkManager.swift

import SwiftUI

class NetworkManager {
    
    static let shared = NetworkManager()
    
    private init() {}
    
    func uploadZipFile (
        zipFileURL: URL) async throws -> (Data, URLResponse) {
    
            let name:     String = zipFileURL.deletingPathExtension().lastPathComponent
            let fileName: String = zipFileURL.lastPathComponent
            
            let zipFileData: Data?
            
            do {
                zipFileData = try Data(contentsOf: zipFileURL)
            } catch {
                throw error
            }
            
            let uploadApiUrl: URL? = URL(string: "https://someapi.com/upload")
        
            // Generate a unique boundary string using a UUID.
            let uniqueBoundary = UUID().uuidString

            var bodyData = Data()
            
            // Add the multipart/form-data raw http body data.
            bodyData.append("\r\n--\(uniqueBoundary)\r\n".data(using: .utf8)!)
            bodyData.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
            bodyData.append("Content-Type: application/zip\r\n\r\n".data(using: .utf8)!)
            
            // Add the zip file data to the raw http body data.
            bodyData.append(zipFileData!)
    
            // End the multipart/form-data raw http body data.
            bodyData.append("\r\n--\(uniqueBoundary)--\r\n".data(using: .utf8)!)
            
            let urlSessionConfiguration = URLSessionConfiguration.default
            
            let urlSession
                = URLSession(
                    configuration: urlSessionConfiguration,
                    delegate: nil,                           // Something I need here maybe?
                    delegateQueue: nil)
            
            var urlRequest = URLRequest(url: uploadApiUrl!)
            
            // Set Content-Type Header to multipart/form-data with the unique boundary.
            urlRequest.setValue("multipart/form-data; boundary=\(uniqueBoundary)", forHTTPHeaderField: "Content-Type")
            
            urlRequest.httpMethod = "POST"
            
            let (data, urlResponse) = try await urlSession.upload(
                for: urlRequest,
                from: bodyData,
                delegate: nil   // Something I need here maybe?
            )

            return (data, urlResponse)
    }
    
    // Tried this but it never fires.
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64) {
        
        print("fractionCompleted  : \(Float(totalBytesSent) / Float(totalBytesExpectedToSend))")
            
    }
}

Solution

  • Answering my own question.

    If anybody sees anything thats wrong or bad practice then please feel free to comment.

    NetworkManager.swift

    import SwiftUI
    
    class NetworkManager: NSObject {
        
        static let shared = NetworkManager()
        
        private override init() {}
        
        func uploadZipFile (
            zipFileURL: URL) async throws -> (Data, URLResponse) {
        
                let name:     String = zipFileURL.deletingPathExtension().lastPathComponent
                let fileName: String = zipFileURL.lastPathComponent
                
                let zipFileData: Data?
                
                do {
                    zipFileData = try Data(contentsOf: zipFileURL)
                } catch {
                    throw error
                }
                
                let uploadApiUrl: URL? = URL(string: "https://someapi.com/upload")
            
                // Generate a unique boundary string using a UUID.
                let uniqueBoundary = UUID().uuidString
    
                var bodyData = Data()
                
                // Add the multipart/form-data raw http body data.
                bodyData.append("\r\n--\(uniqueBoundary)\r\n".data(using: .utf8)!)
                bodyData.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
                bodyData.append("Content-Type: application/zip\r\n\r\n".data(using: .utf8)!)
                
                // Add the zip file data to the raw http body data.
                bodyData.append(zipFileData!)
        
                // End the multipart/form-data raw http body data.
                bodyData.append("\r\n--\(uniqueBoundary)--\r\n".data(using: .utf8)!)
                
                let urlSessionConfiguration = URLSessionConfiguration.default
                
                let urlSession
                    = URLSession(
                        configuration: urlSessionConfiguration,
                        delegate: self,         
                        delegateQueue: nil)
                
                var urlRequest = URLRequest(url: uploadApiUrl!)
                
                // Set Content-Type Header to multipart/form-data with the unique boundary.
                urlRequest.setValue("multipart/form-data; boundary=\(uniqueBoundary)", forHTTPHeaderField: "Content-Type")
                
                urlRequest.httpMethod = "POST"
                
                let (data, urlResponse) = try await urlSession.upload(
                    for: urlRequest,
                    from: bodyData,
                    delegate: nil 
                )
    
                return (data, urlResponse)
        }
    }
    
    extension NetworkManager: URLSessionTaskDelegate {
        
        func urlSession(
            _ session: URLSession,
            task: URLSessionTask,
            didSendBodyData bytesSent: Int64,
            totalBytesSent: Int64,
            totalBytesExpectedToSend: Int64) {
            
            print("fractionCompleted  : \(Int(Float(totalBytesSent) / Float(totalBytesExpectedToSend) * 100))")
                
        }
        
    }