Running a Web Server on iOS with Vapor

With Vapor, your iOS app can be both the client and the server to control your data — or even other devices. This tutorial will show you how to get started with client-server communication in the same process. By Beau Nouvelle.

5 (4) · 3 Reviews

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

Creating Routes

In this section, you’ll create four routes to process the incoming requests to your server.

Create a new file in the server folder and name it FileWebRouteCollection.swift.

Start the file by conforming to RouteCollection and adding the boot() protocol method.

import Vapor

struct FileWebRouteCollection: RouteCollection {
  func boot(routes: RoutesBuilder) throws {
  }
}

Application uses this function for route registration.

The first route is filesViewHandler. It’s the entry point to your server and handles any requests sent to "deviceIP":8080.

In fact, this is the only View route your server needs for this project! It will display the entire list of uploaded files, allow users to upload new files and even download them.

func filesViewHandler(_ req: Request) async throws -> View {
  let documentsDirectory = try URL.documentsDirectory()
  let fileUrls = try documentsDirectory.visibleContents()
  let filenames = fileUrls.map { $0.lastPathComponent }
  let context = FileContext(filenames: filenames)
  return try await req.view.render("files", context)
}

This loads the contents of the document’s directory accessible by the iOS app, generates a list of file names and passes them through to the view renderer.

To fix this, change the return type to include Vapor as a prefix: Vapor.View.

Note: If you import both SwiftUI and Vapor in the same file, you may see compiler errors regarding the View type being returned in filesViewHandler(_:).

To fix this, change the return type to include Vapor as a prefix: Vapor.View.

At the bottom of the file, create the FileContext.

struct FileContext: Encodable {
  var filenames: [String]
}

Take a quick look in server/Views/files.leaf and you’ll see the filenames property on FileContext in use starting at line 24.

...
#for(filename in filenames):
...

This will display all of the filenames to the user in a list, and from there they can download a specific file by clicking on it.

Now, add your new route to the boot function to access it.


routes.get(use: filesViewHandler)

All “get” requests made to the root of your server will now pass through this route.

Open FileServer.swift.

Inside the do statement, before try app.start(), register the FileWebRouteCollection.

try app.register(collection: FileWebRouteCollection())

Build and run.

Open localhost:8080 in your web browser, and you’ll see two buttons.

Choose File and Upload Bootstrap web buttons

You’ll find that the upload button doesn’t work yet because that route doesn’t exist. Time to create it!

Uploading Files

Open FileWebRouteCollection.swift, and at the bottom of the file beneath the FileContext struct, create a new one called FileUploadPostData.

struct FileUploadPostData: Content {
  var file: File
}

Inside FileWebRouteCollection, add a new function to handle file uploads.

func uploadFilePostHandler(_ req: Request) throws -> Response {
  // 1
  let fileData = try req.content.decode(FileUploadPostData.self)
  // 2
  let writeURL = try URL.documentsDirectory().appendingPathComponent(fileData.file.filename)
  // 3
  try Data(fileData.file.data.readableBytesView).write(to: writeURL)
  // 4
  return req.redirect(to: "/")
}

Here's how it works:

  1. Decode the content of the incoming request into a FileUploadPostData object.
  2. Generate a URL based on the documents directory and name of the uploaded file.
  3. Write the file to the generated URL.
  4. If successful, refresh the page by redirecting the browser to the root URL.

Register this new POST route in boot().

routes.post(use: uploadFilePostHandler)

Decoding the web request contents into the FileUploadPostData object isn’t magic.

Open files.leaf and look at the form block.

The first input field is “file” for both name and type. Vapor uses this name parameter to map the file data in the request to the FileUploadPostData.file property.

<form method="POST" action="/" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="submit" class="btn btn-primary" value="Upload"/>
</form>

Now for the exciting part — build and run.

Navigate to http://localhost:8080 in your web browser and upload a file.

List of uploaded files in a web browser with with buttons to upload more.

Delete doesn’t work yet, but now that you can send files to your iOS device through a web browser, it’s time to create a way to preview them.

Previewing Files

Bundled within the starter project is a struct called FileView. This is a UIViewControllerRepresentable wrapper for QLPreviewController, which is part of Apple's QuickLook framework and is capable of opening a few different file types including images, videos, music and text.

You'll use FileView to preview the uploaded files in the iOS app.

Before you can do that, the iOS side of your project needs to have access to these files. In this guide, you'll be using the FileServer. However, in a much larger project, it would be better to move that responsibility to a dedicated file management object.

Open FileServer.swift and add a new property near the top of the class:

@Published var fileURLs: [URL] = []

This is the single source of truth for all files stored inside the documents directory of your app. SwiftUI listens to any changes in the array of URLs and updates the UI accordingly.

To load these files, create a new function and name it loadFiles()

func loadFiles() {
  do {
    let documentsDirectory = try FileManager.default.url(
      for: .documentDirectory,
      in: .userDomainMask,
      appropriateFor: nil,
      create: false)
    let fileUrls = try FileManager.default.contentsOfDirectory(
      at: documentsDirectory,
      includingPropertiesForKeys: nil,
      options: .skipsHiddenFiles)
    self.fileURLs = fileUrls
  } catch {
    fatalError(error.localizedDescription)
  }
}

This function looks inside the documents directory for your app and returns the URLs for all visible files within. You’ll use this whenever you need to refresh the list of files in the iOS portion of your project.

A great place to call this is in the onAppear method inside ContentView.swift — that way, when your app launches, it will populate the file list with files.

Add it after you start the server:

...
.onAppear {
  server.start()
  server.loadFiles()
}
...

To actually preview the files, you'll also need to replace the Text view with this code:

NavigationView {
  List {
    ForEach(server.fileURLs, id: \.path) { file in
      NavigationLink {
        FileView(url: file)
      } label: {
        Text(file.lastPathComponent)
      }
    }
  }
}

This loops over all the URLs found by the loadFiles() function and creates a navigation link to a FileView for previewing.

Build and run.

You'll see something similar to the following image, depending on which files you uploaded. If you don't see any files, try uploading some through your browser.

You'll need to build and run again for them to appear.

iOS UI with a list of file names.

Tapping on a row will open that file.