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
Persisting the Selected Template
Next, you need to add a route-handler pair to present a selected template. But first, you need to solve a problem: Since there’s no database, you need a way to persist a selected template as users navigate between pages.
Your solution will take advantage of Vapor’s SessionsMiddleware, which provides a session-specific dictionary for caching small bits of String
data.
You can’t store structs directly here, and your model objects are structs. What you can do is to “freeze-dry” them into JSON strings before presenting a new page, then “rehydrate” them back into native structs later on.
Now, open WebController.swift. Locate templatesHandler(_:)
, and insert this line just before the return
statement:
try req.session()["templates"] = context.toJSONString()
This caches the template selected by the user in the session dictionary as JSON. Downstream route handlers can now retrieve it.
Next, add this handler method at the bottom of the Route Handlers section:
func selectedTemplateGetHandler(_ req: Request) throws -> Future<View> {
guard let name = req.query[String.self, at: "name"]
else { throw Abort(.badRequest) }
let context = try getCachedTemplate(named: name, on: req)
try req.session()["selected-template"] = context.toJSONString()
return try req.view().render("selected-template", context)
}
Here, you make sure you’ve received a template name. Then, with the help of getCachedTemplate(named:on:)
, you locate the desired template from an array of templates cached earlier, and place it in a context struct. Finally, you cache just the selected template for downstream use, and present the user with a RetroLibs entry form via the selected-template.
Last, register your route by appending this to boot(router:)
:
sessions.get("selected", use: selectedTemplateGetHandler)
Nice! Build and run, and open localhost:8080 in your browser.
This time, you see just your templates, no unwanted tag-alongs as before. Even better, clicking a template takes you to a retro-chic page presenting you with a dynamic set of RetroLib words to fill in.
You have one last task to complete before you can fill in the form and see your custom story — you’ll quickly handle that now.
Submitting the Completed Form
When a user completes a form and clicks the Retro-ify Me! button, they submit a form via an HTTP POST
request. You’ll need to take the form data and zip it into a complete story in a new POST
route and handler. Make it so.
Back in WebController.swift, append this method to the struct’s extension:
private func getStory(from data: EntryFormData,
on req: Request) throws -> Future<(String,String)> {
let tagValues = data.valuesJoined.split(separator: "¶")
.map { String($0) }
guard let template = try req.session()["selected-template"]?
.toRetroLibsTemplate(), template.tags.count == tagValues.count
else {
throw Abort(.internalServerError)
}
let story: String = {
var str = ""
for (index, templatePart) in template.templateParts.enumerated() {
str += templatePart
str += index < tagValues.count ? tagValues[index] : ""
}
return str
}()
let title = template.name
return req.future((title, story))
}
This method addresses an interesting problem you'll occasionally encounter in Server-Side Swift. When you created this form, you looped through each template tag and created an <input>
object.
That's great for presentation, but when you accept form data via Codable
, a core requirement is that you return data in predictable, fully-defined Codable
– or in Vapor, wrapping Content
– structs.
How do you reconcile the unpredictable numbers of RetroLibs template inputs with Codable
requirements?
In this case, you handle this by gathering all your input values, which are all set to required
, into a delimited string you place into a single hidden <input>
field.
You then ingest just this single, hidden form input into your app, split the string into an array, then zip the story snippets and user-entered values into a single story string.
Handling the Completed Form
Now, you just need to write and register a POST
handler. First, append this method at the bottom of the Route Handlers section:
func selectedTemplatePostHandler(_ req: Request, data: EntryFormData)
throws -> Future<View> {
try getStory(from: data, on: req).flatMap(to: View.self) {
(title, story) in
let context = CompletedStory(title: title, story: story)
return try req.view().render("completed-story", context)
}
}
Here, you parse form data sent with the POST
request. Leveraging the just-added getStory(from:on:)
, you convert the entry values into a future complete story. In the flatMap
closure, you create a context
struct from the story components, and pass this to the user.
Last, register this route at the bottom of boot(router:)
:
sessions.post(EntryFormData.self, at: "selected",
use: selectedTemplatePostHandler)
Ready? Build and run, and open localhost:8080. This time, select a template, enter your most creative word choices, click Retro-ify Me!, aaaaaand... stare in wonder at your first RetroLibs story.
Creating Your Own Template
RetroLibs now hums along nicely with canned templates. But why settle for the same old pre-packaged stories when any modern-retro app worth its salt ought to let users create their own homegrown goodies?
Back in Xcode, open FileController.swift once again. Locate the stub method writeFileSync(named:with:overwrite:)
, and replace its contents with:
guard overwrite || !fileExists(filename) else { return false }
return fileManager.createFile(
atPath: workingDir + filename, contents: data)
This method simply confirms that you're not overwriting an existing file if you don't want to, then writes the file to disk.
As before, you're using FileManager
to get up and running initially, but you wouldn't use it in production. You'll make this production-worthy a bit later.
Next, open WebController.swift, and append the following method to the WebController
extension:
private func saveTemplate(from data: UploadFormData, on req: Request)
throws -> Future<String> {
guard !data.title.isEmpty, !data.content.isEmpty
else { throw Abort(.badRequest) }
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),
FileController.writeFileSync(named: filename, with: data) == true
else {
throw Abort(.internalServerError)
}
return req.future(filename)
}
This method handles the details of accepting a user-submitted story from a form, converting it to structured title
and content
strings, saving the template to disk and returning its filename. You now have the pieces you need to wire up the routes and handlers for entering and saving custom templates.
Adding Routes for Your Template
You'll now add a pair of routes and matching handlers: one for the GET
entry route and a second for the POST
route.
Append these two handler methods to the Route Handlers section:
func addTemplateGetHandler(_ req: Request) throws -> Future<View> {
try req.view().render("add-template", VoidContent())
}
func addTemplatePostHandler(_ req: Request, data: UploadFormData)
throws -> Future<View> {
try saveTemplate(from: data, on: req).flatMap(to: View.self) {
filename in
let content = UploadSuccess(filename: filename)
return try req.view().render("template-uploaded", content)
}
}
The GET
route handler doesn't need to package up any data, so you simply present the add-template
Leaf template with an empty content struct to satisfy Codable
requirements.
Your POST
handler has more work to do, but the saveTemplate(from:on:)
helper method you added earlier makes this an easy job. Since this helper passes back a future-wrapped filename, you use flatMap(to:)
to access the filename, create an UploadSuccess
struct, and present the results via the template-uploaded
template.