Search code examples
iosswift

Returning results and throwing errors from closure within a function


I wish to throw errors and/or return request results from a closure. I went through some posts on SO and looks like people are mainly passing completion blocks to propogate error and result to the calling function.

I have a chain of functions (sometimes 3 function chain and sometimes 4-5), which means I will have to pass completion blocks to every function. Am trying to figure out if there is a better way to return result and errors from closure within a function.

BTW I am using MSGraph API here.

Code - Request functions:

private func processRequest(_ request: NSMutableURLRequest, completion: @escaping(Data?, URLResponse?, Error?) throws -> ()) rethrows {
    let dataTask: MSURLSessionDataTask = httpClient.dataTask(with: request, completionHandler: { (data, response, error) in
        if let response = response as? HTTPURLResponse {
            let statusCode = response.statusCode
            if (statusCode < 200) || (statusCode > 299) {
                let description = "Microsoft Graph service returned HTTP response status \(statusCode) for URL \(request.url?.absoluteString ?? "")"
                let error = NSError(code: statusCode, localizedDescription: description)
                try? completion(nil, nil, error)
            }
        }
        
        if let error = error {
            try? completion(nil, nil, error)
        }

        try? completion(data, response, nil)
    })
    
    dataTask.execute()
}

private func processRequestWithToken(_ request: NSMutableURLRequest, completion: (Data?, URLResponse?) -> ()) async throws {
    try await MicrosoftAccount.acquireToken()
    try processRequest(request) { (data, response, error) in
        if let error = error {
            throw error
        }
        
        completion(data, response)
    }
}

Code - Calling functions:

This below functions throws an error - Cannot convert value of type {'Bool' or '[MSGraphEvent]'} to closure result type '()', obviously because I am not returning a completion block.

public func cancelBooking(eventId: String) async throws -> Bool {
    let request = makeRequest(relativeUrl: "/me/events/\(eventId)/cancel", httpMethod: "POST")
    addStandardRequestHeaders(to: request)

    self.processRequestWithToken(request) { data, response in
        if let response = response as? HTTPURLResponse, response.statusCode == 202 {
            return true
        } else {
            return false
        }
    }
}

public func getEvents(selectedDate: Date?) async throws -> [MSGraphEvent] {
        var api = "/me/calendar/events?"

        let request = self.makeRequest(relativeUrl: api, httpMethod: "GET")
        self.processRequestWithToken(request) { data, response in
            let events: [MSGraphEvent] = try self.parseCollection(data: data)
            return events
        }
}

CancelBooking and getEvents functions are then called by another function. If I return a completion block to them, then I don't see any errors and everything works fine, but I have to pass completion block to every function in the chain.

Is there a better way I can propagate the results up in the function chain without passing completion block as parameters for every function?


Solution

  • DispatchGroup was crashing on me sometimes especially incase of bad request. Continuation worked better and the code is also nice and concise.

    private func processRequest(_ request: NSMutableURLRequest, completion: @escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void) {
        let dataTask: MSURLSessionDataTask = httpClient.dataTask(with: request, completionHandler: { (data, response, error) in
            if let response = response as? HTTPURLResponse {
                let statusCode = response.statusCode
                if (statusCode < 200) || (statusCode > 299) {
                    // create some error
                    completion(nil, nil, error)
                    return
                }
            }
            
            if let error = error {
                completion(nil, nil, error)
                return
            }
    
            completion(data, response, nil)
            return
        })
        
        dataTask.execute()
    }
    
    private func processRequestWithToken(_ request: NSMutableURLRequest) async throws -> (data: Data?, response: URLResponse?) {
        try await acquireToken()
        return try await withCheckedThrowingContinuation { continuation in
            processRequest(request) { (data: Data?, response: URLResponse?, requestError: Error?) in
                if let requestError = requestError {
                    continuation.resume(throwing: requestError)
                } else {
                    continuation.resume(returning: (data: data, response: response))
                }
            }
        }
    }