The problem:

In Vapor, I was trying to make a network call to retrieve data and have my route return that data.

When I was dabbling in iOS development, I’d make a network call, the function calling that network call would complete, and when that data would finally arrive I’d be able to update my view accordingly within the completion handler. In Vapor, I don’t just need to get that data at some point; I need to get that data before my route returns a response to the incoming request.

    app.get("withClosure") {req -> String in
        Networkmanager.shared.standardNetworkRequest { result in
            switch result {
            case .success(let imageString):
                print(imageString)
            case .failure(let error):
                print(error)
            }
        }
        return "I return immediately!"
    }

The standardNetworkRequest:

   func standardNetworkRequest(completed: @escaping ((Result<String,Error>) -> Void)) {
        
        // create URL, call completion handler with failure if there's an error
        guard let url = URL(string: "https://file-examples-com.github.io/uploads/2017/10/file_example_JPG_100kB.jpg") else {
            completed(.failure(CustomErrors.standardError))
            return
        }
        let task = URLSession.shared.downloadTask(with: url) { url, response, error in
        // call completion handler with failure if there's an error
            if let error = error {
                completed(.failure(error))
                return
            }
       // verifies response and status code, call completion handler with failure if there's an error
            guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
                completed(.failure(CustomErrors.standardError))
                return
            }
       // verifies URL, call completion handler with failure if there's an error
            guard let url = url else {
                completed(.failure(CustomErrors.standardError))
                return
            }
        // encodes image to bas64 string, call completion handler's success with the string, call completion handler with failure in catch block
            do {
                let image           = try Data(contentsOf: url)
                let encodedImage    = image.base64EncodedString()
                completed(.success(encodedImage))

            } catch {
                completed(.failure(CustomErrors.standardError))
            }
        }
        task.resume()
    }

The Solution:

I knew that Vapor relies heavily on Promises/Futures, and while I was able to use them when provided (for example, returning data from Fluent database calls using them), I had no idea how to implement them into my custom network calls.

In short, I figured out how to create a promise and have my network call return an EventLoopFuture<String>:

    func downloadPicture(req: Request) -> EventLoopFuture<String> {
        
        // create promise of type String
        let promise = req.eventLoop.makePromise(of: String.self)
        
        // create URL, set the promise's future result if there's an error
        guard let url = URL(string: "https://file-examples-com.github.io/uploads/2017/10/file_example_JPG_100kB.jpg") else {
            promise.fail(CustomErrors.standardError)
            return promise.futureResult
        }
        let task = URLSession.shared.downloadTask(with: url) { url, response, error in
        // set the promise's future result if there's an error
            if let error = error {
                promise.fail(error)
                return
            }
       // verifies response and status code, set the promise's future result if there's an error
            guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
                promise.fail(CustomErrors.standardError)
                return
            }
       // verifies URL, set the promise's future result if there's an error
            guard let url = url else {
                promise.fail(CustomErrors.standardError)
                return
            }
        // encodes image to bas64 string, set the promise's future result with the string, set to error in catch block
            do {
                let image           = try Data(contentsOf: url)
                let encodedImage    = image.base64EncodedString()
                promise.succeed(encodedImage)
            } catch {
                promise.fail(CustomErrors.standardError)

            }
        }
        task.resume()
        // return promise's futureResult
        return promise.futureResult
    }

So what’s different?

  1. I removed the completion handler and I added a return value of EventLoopFuture<String>.
  2. I added a promise of type String
  3. Where I initially had my completion handler in my downloadTask, I replaced it with promise.fail or promise.succeed
  4. At the end of my network call, I’m returning the futureResult of that promise.

In short, I’m telling my function that I’ll be returning a future result of my network call instead of accessing my completion handler.

So my updated route looks like this:

    app.get("withPromise") {req -> EventLoopFuture<String> in
        return Networkmanager.shared.downloadPicture(req: req).map { imageString in
            return imageString
        }
    }

If you aren’t familiar with Vapor’s EventLoopFutures, I’ll explain what’s going on here.

Instead of returning a String, I’m returning an EventLoopFuture<String>. Swift uses .map to access the value of the EventLoopFuture<String>. In the .map closure, I tell Swift I’m naming my returned String ‘imageString’, and I then return that String.

This wasn’t something I necessarily needed in any project, but it did help me understand EventLoopFutures a little bit better. Hopefully by comparing these two examples it will shed a little light on the concept for you as well.