SwiftNIO Tutorial: Practical Guide for Asynchronous Problems
In this tutorial, you’ll solve common asynchronous problems about promises and futures in SwiftNIO by building a quotations app. By Joannis Orlandos.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftNIO Tutorial: Practical Guide for Asynchronous Problems
15 mins
The Quote Repository
The starter project has already set up the basic routes so that Quotanizer can insert quotes into the repository and retrieve them again.
The app should get the quotes from QuoteRepository
, but you haven’t implemented the repository’s functionality yet.
Your next step is to provide the code to insert a new quote and to retrieve a random quote.
Adding Quotes to Quotanizer
Repositories are an abstraction of interactions with data and databases. Many developers combine the repository pattern with dependency injection, which enables you to write tests for individual pieces of code.
In QuoteRepository.swift replace the insert(_:)
implementation with the following:
func insert(_ quote: Quote) -> EventLoopFuture<Void> {
// 1
do {
// 2
let json = try JSONEncoder().encode(quote)
let entity = DataEntity(data: json)
// 3
return QuoteRepository.database.addEntity(entity).hop(to: eventLoop)
} catch {
// 4
return eventLoop.makeFailedFuture(error)
}
}
Now, break down what’s happening above:
- A do-catch block catches errors thrown from encoding the quote.
- Encode the quote as JSON.
- Insert the quote in the database and return the result on the repository’s event loop.
- Return a failed EventLoopFuture containing the JSONEncoder’s error, if an error arises.
Instead of throwing errors, this code wraps the caught errors in an EventLoopFuture
, which already uses a success and failure state. Returning an EventLoopFuture
and throwing errors from the same place is an anti-pattern.
Every EventLoopFuture
relies on an EventLoop
. On completion of the future, handlers such as map
and flatMap
run on the event loop’s thread. For this reason, hopping to the correct event loop is important. If SwiftNIO ends ups on the wrong event loop, unexpected behavior may occur.
During development, these programming errors will crash the app. But if you’re on a release build, you have to be even more careful. SwiftNIO will not check for programming errors, so they can go unseen for a long time.
Completing the Repository
Finally, to complete the repository, implement findAndDeleteOne()
:
func findAndDeleteOne() -> EventLoopFuture<Quote?> {
// 1
return QuoteRepository.database.getAllEntities().flatMap { entities in
// 2
guard let entity = entities.randomElement() else {
// 3
return self.eventLoop.makeSucceededFuture(nil)
}
// 4
return QuoteRepository.database.deleteOne(by: entity.id).flatMapThrowing {
// 5
return try JSONDecoder().decode(Quote.self, from: entity.data)
}
// 6
}.hop(to: eventLoop)
}
Here’s what the code above does:
- Reads all quotes from the database.
- Selects a random quote.
- Returns
nil
in anEventLoopFuture
if there are no quotes. - Removes the quote from the database, to ensure that it’s only returned once.
- Decodes the quote from JSON.
- Finally, the resulting
EventLoopFuture
hops to the repository’s event loop.
Quotanizer should be working now, but is it really? Your next step is to test the app to ensure there you haven’t overlooked any errors.
Testing the App
Build and run the app and visit http://localhost:1111. Make sure your target is My Mac.
When visiting the website, you’ll see a server error. The route expects to receive a quote from the repository, but the database has no quotes yet!
Open RESTed, or a different HTTP client of your choice, and make a POST request with the URL http://localhost:1111.
Add a parameter with the name text
, then add any quote you like and make sure the request is JSON-encoded. Next, run the request by clicking Send Request. You’ll see the response in the panel on the right.
Now, you can visit the URL from your web browser: http://localhost:1111 again. The quote you inserted will appear.
If you refresh, however, you’ll see another server error. That’s because when the app serves a quote, it then removes it, so the database is empty again.
Showing the error when the database is empty is not the way you want the app to work. You’ll use helpers to fix that error in the next section!
Creating Helpers for Futures
Helpers are great tools for common tasks such as unwrapping optionals. When you write extensions in Swift, futures become more powerful and code less cluttered and error-prone, as well as more readable.
If you use Vapor, you can leverage its helpers. The framework provides helpers for most use cases on the web. But for this app, there are no helpers other than the ones SwiftNIO provides.
Open QuoteResponder.swift and, in respond(to:)
, look at the GET
case:
case (.GET, "/"):
return getQuote(for: request)
It doesn’t implement any error handling, which is why you see a server error in the browser when there’s no quote to retrieve.
To fix this, add error handling by replacing that case with the following code:
case (.GET, "/"):
return getQuote(for: request).flatMapErrorThrowing { error in
guard case QuoteAPIError.notFound = error else {
throw error
}
return HTTPResponse(status: .notFound, body: nil)
}
flatMapErrorThrowing(_:)
transforms errors into failure cases. Here, you check if the error is QuoteAPIError.notFound
, in which case you respond with the Not Found status, or status code 404 — and that fixes the issue.
Build and run the project again, and go to http://localhost:1111 one final time. You should see an actual HTTP 404 Not Found error, as expected.
Unwrapping Variables
Your service is functioning now, but your error handling can be prettified quite a bit.
Look at getQuote(for:)
and you’ll see regular optional unwrapping. As your project grows, you’ll unwrap a lot of variables. To make optional unwrapping less verbose, add the following extension in QuoteResponser.swift:
extension EventLoopFuture {
func unwrapOptional<Wrapped>(orThrow error: Error)
-> EventLoopFuture<Wrapped> where Value == Wrapped? {
flatMapThrowing { optional in
guard let wrapped = optional else {
throw error
}
return wrapped
}
}
}
This adds a generic helper to EventLoopFuture, which will indirectly return either an instance of the generic type Wrapped
or an error. The future’s Value
must be an optional wrapping an instance of Wrapped
.
This gives you the power to rewrite getQuote(for:)
as such:
private func getQuote(for request: HTTPRequest)
-> EventLoopFuture<HTTPResponse> {
let repository = QuoteRepository(eventLoop: request.eventLoop)
return repository.findAndDeleteOne()
.unwrapOptional(orThrow: QuoteAPIError.notFound)
.flatMapThrowing { quote in
let body = try HTTPBody(json: quote, pretty: true)
return HTTPResponse(status: .ok, body: body)
}
}
This change doesn’t add anything functional, but it improves the code a bit. Take a look at the original code:
return repository
.findAndDeleteOne()
// 1
.flatMapThrowing { quote in
guard let quote = quote else {
throw QuoteAPIError.notFound
}
...
}
It uses an explicit optional binding in a guard statement, throwing an error if unwrapping fails. Compare it with the improved version:
return repository
.findAndDeleteOne()
// 1
.unwrapOptional(orThrow: QuoteAPIError.notFound)
.flatMapThrowing { quote in
...
}
Here, you delegate the unwrapping to unwrapOptional(orThrow:)
, which takes care of throwing the passed error if unwrapping fails. It’s easier to read, and also less ambiguous to write.