File Handling Tutorial for Server-Side Swift Part 1
In this two-part file handling tutorial, we’ll take a close look at Server-side Swift file handling and distribution by building a MadLibs clone. By Brian Schick.
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
File Handling Tutorial for Server-Side Swift Part 1
30 mins
- Getting Started
- Using Templates
- Template Syntax
- Getting a List of Templates
- Reading Template Files
- Persisting the Selected Template
- Submitting the Completed Form
- Handling the Completed Form
- Creating Your Own Template
- Adding Routes for Your Template
- Putting It All Together
- Adding Concurrency
- Using SwiftNIO
- Reading a File Asynchronously
- Getting a List of Templates Asynchronously
- Writing a File Asynchronously
- Using Grand Central Dispatch
- Where to Go From Here
Writing a File Asynchronously
The last piece of your puzzle is to make your write operations asynchronous. Here, you hit a minor hiccup.
As it turns out, Apple initially released SwiftNIO without support for asynchronous write functionality. That's less surprising than it might seem since in production, you typically do any serious file management via APIs. You likely won't store large files on a production web server.
Nonetheless, SwiftNIO added this functionality last year.
At the time of writing, the major Swift frameworks have not yet added convenience wrappers for SwiftNIO's write functionality. When that happens, using a framework's wrapper will become a best practice.
Until then, one common solution is to use Grand Central Dispatch (GCD) to wrap FileManager
's synchronous write functionality within an asynchronous DispatchQueue
task. This ensures that write operations are non-blocking. Many production sites currently use this method, and you'll follow suit here.
Using Grand Central Dispatch
It's easy enough to wrap a synchronous method call within an asynchronous DispatchQueue
task. The challenge is ensuring that the route handler waits for the task to complete and sees its result.
Open FileController.swift, and find writeFileAsync(named:with:on:queue:overwrite:)
. Replace its contents with:
// 1
guard overwrite || !fileExists(filename) else {
return req.future(false)
}
// 2
let promise = req.eventLoop.newPromise(of: Bool.self)
// 3
queue.async {
let result = fileManager.createFile(
atPath: workingDir + filename, contents: data)
// 4
promise.succeed(result: result)
}
// 5
return promise.futureResult
Here's what you've done:
- As with the synchronous variant, you ensure you're not about to accidentally overwrite an existing file.
- You register a new promise on the
Request
object'sEventLoop
. This promise must either fail or be fulfilled before the route handler completes. - You then call the synchronous
createFile(atPath:contents:)
within aDispatchQueue
task, and dispatch it to execute asynchronously. - Importantly, you bind the results of the call to the promise, and call its
succeed
promise method. - Finally, you return the promise's
futureResult
, effectively binding the asynchronousDispatchQueue
task to the route'sEventLoop
. This ensures that the method returns theFuture
-wrapped results to its calling context.
With this in place, open WebController.swift, and locate saveTemplate(from:on:)
at the bottom of the file.
You'll update its signature to return a Future
, and replace the current guard
clause, which relies on synchronous writes, with code that works with asynchronous writes.
Replace the entire method with this:
private func saveTemplate(from data: UploadFormData, on req: Request)
throws -> Future<String> {
// 1
guard !data.title.isEmpty, !data.content.isEmpty
else { throw Abort(.badRequest) }
// 2
let title = data.title.trimmingCharacters(in: CharacterSet.whitespaces)
let filename = String(title.compactMap({ $0.isWhitespace ? "_" : $0 }))
let contentStr = data.content.trimmingCharacters(in: CharacterSet.whitespaces)
guard let data = contentStr.data(using: .utf8)
else { throw Abort(.internalServerError) }
// 3
return try FileController
.writeFileAsync(named: filename, with: data, on: req, queue: dispatchQueue)
.map(to: String.self) { success in
return success ? filename : ""
}
}
With this code you:
- Ensure the title is not empty: If it is, throw a 400 bad request error.
- Build the content string by trimming unwanted characters.
- Asynchronously write the file data to disk, and return a
Future
.
And now your app handles both read and write functionality without blocking the main thread. Bring on the web-scale, baby!
Build and run, and open your browser to localhost:8080 one final time. You haven't changed any functionality, but you can enjoy the satisfaction of knowing that your app is now ready to read and write files and to scale gracefully without blocking!
Where to Go From Here
You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
Be sure to check out Part 2 of this file handling tutorial when it's published. It focuses on how to serve up files from your Server-Side Swift app, including important nuances of doing this in the Linux and Docker environments, where most Server-Side Swift apps live.
If you're curious about SwiftNIO, Futures and Promises, check out Creating an API Helper Library for SwiftNIO.
Meanwhile, what do you think about reading and writing files in Server-Side Swift? Have you run into difficulties doing this, or have you found any helpful techniques you'd like to share? Let us know in the comments section, and Happy File-ing!