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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

Template and Result

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.

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.