skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    I came across DispatchGroup and that's one way which helped me to solve my problem. Sharing the code for it below.

    I will also try Joakim's suggestion and will share it's code as well.

    private func processRequest(_ request: NSMutableURLRequest) -> (data: Data?, response: URLResponse?, error: Error?) {
        let dispatchGroup = DispatchGroup()
        var result: (data: Data?, response: URLResponse?, error: Error?) = (nil, nil, nil)
        dispatchGroup.enter()
            
        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)
                    result = (nil, nil, error)
                    dispatchGroup.leave()
                }
            }
                
            if let error = error {
                result = (nil, nil, error)
                dispatchGroup.leave()
            }
    
            result = (data, response, nil)
            dispatchGroup.leave()
        })
            
        dataTask.execute()
            
        dispatchGroup.wait()
        return result
    }
    
    private func processRequestWithToken(_ request: NSMutableURLRequest) async throws -> (data: Data?, response: URLResponse?) {
        try await acquireToken()
        let result = processRequest(request)
        if let error = result.error {
            throw error
        }
            
        return (data: result.data, response: result.response)
    }
    

  2. This problem of chaining completion handlers is often called "Callback Hell" or "The Pyramid of Doom" and is specifically addressed by the Async/Await paradigm.

    A challenge to answering your question is that the answer is simply "learn to use async/await" and apply it to your problem domain.

    I recommend you start with "Meet async/await in Swift" which describes the pyramid of doom and how async/await addresses the issue:

    https://developer.apple.com/videos/play/wwdc2021/10132/

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search