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 3 of 4 of this article. Click here to view the first page.

Refreshing Files

Whenever you upload a new file, you need to restart the app to see it. Obviously, this isn't ideal.

In this section, you'll use the refreshable view modifier and NotificationCenter to update the list of URLs.

Thankfully, it's super easy to add pull-to-refresh functionality in SwiftUI.

Open ContentView.swift and add the refreshable view modifier after the closing brace for the List view.

.refreshable {
  server.loadFiles()
}

This solves the main issue of having to restart the app to preview new uploads. It would be even better if it happened automatically — in real time — as new files are added or removed.

You can do this by using NotificationCenter.

To add this functionality, open FileServer.swift and a notification observer within the initializer, after the configuration step.

NotificationCenter.default.addObserver(forName: .serverFilesChanged, object: nil, queue: .main) { _ in   
  self.loadFiles()
}

To trigger this notification, open FileWebRouteCollection.swift and add a new function called notifyFileChange() like so:

func notifyFileChange() {
  DispatchQueue.main.async {
    NotificationCenter.default.post(name: .serverFilesChanged, object: nil)
  }
}

Be sure to call it whenever the files inside the documents directory are altered. A good place to do this is before the return statement within the uploadFilePostHandler function.

This triggers the notification when a file has finished uploading successfully, which then tells the FileServer to reload the files, and passes the results back to the UI via a published property.

Run the app and experiment with uploading files through your browser — and watch the iOS app update before your very eyes!

Downloading Files

A file hosting server is nothing without the ability for people to download those files. Enabling downloads is super easy because you've done most of the work already. All you need is a new route to handle such a request.

Open FileWebRouteCollection.swift and create a new handler called downloadFileHandler.

func downloadFileHandler(_ req: Request) throws -> Response {
  guard let filename = req.parameters.get("filename") else {
    throw Abort(.badRequest)
  }
  let fileUrl = try URL.documentsDirectory().appendingPathComponent(filename)
  return req.fileio.streamFile(at: fileUrl.path)
}

This rejects any requests made without a filename, fetches that file from storage, then returns it in the response.

Finally, register the new download route in the collection by adding it to the routes builder within the boot(routes:) function at the top of the file.

routes.get(":filename", use: downloadFileHandler)

The colon syntax marks this particular URL path component as a parameter and exposes its value through the request. Unlike other path components that are explicit, this one can be any string value. For example, if you want to download a file named MyDocument.txt you would open myServer:8080/MyDocument/ in your browser. Vapor will take the MyDocument portion and make it accessible through req.parameters.get("filename") as seen in the previous code block.

Build and run again, but this time navigate to your server in a web browser and click a file.

One of two things will happen. The file will open in the browser or download to your device. Hooray!

That leaves one last thing to take care of: file deletion.

Deleting Files on the Web

To maintain acceptable levels of storage space on your device, you're going to want a way to clean up the files on your server. The delete buttons on the web side of things are ready to go.

So, open FileWebRouteCollection.swift and add a new route to handle deletion requests.

func deleteFileHandler(_ req: Request) throws -> Response {
  guard let filename = req.parameters.get("filename") else {
    throw Abort(.badRequest)
  }
  let fileURL = try URL.documentsDirectory().appendingPathComponent(filename)
  try FileManager.default.removeItem(at: fileURL)
  notifyFileChange()
  return req.redirect(to: "/")
}

This is similar to the download handler. Both reject requests for missing filename parameters, and both construct a URL based on that filename. However, instead of returning the file, it's removed. Finally, a notification triggers to inform the iOS portion of the app that the UI needs updating, and the redirect refreshes the webpage.

Register the deletion route by adding this code to the boot(routes:) function:

routes.get("delete", ":filename", use: deleteFileHandler)

This new route is similar to the download route — except it has an extra path component. Going with the same example as before, while you can download a file with the name MyDocument.txt from myServer:8080/MyDocument/, you can delete that same file by navigating to myServer:8080/delete/MyDocument/. This is the same request that's made when clicking the delete button on the website.

Deleting Files on the App

File deletion wouldn't be complete without being able to do it from the app, too.

Open FileServer.swift and add a new delete function at the bottom of the class:

func deleteFile(at offsets: IndexSet) {
  // 1
  let urlsToDelete = offsets.map { fileURLs[$0] }
  // 2
  fileURLs.remove(atOffsets: offsets)
  // 3
  for url in urlsToDelete {
    try? FileManager.default.removeItem(at: url)
  }
}

An IndexSet is a collection of unique integers. This is great for when you need to delete multiple items at a time.

Here's what this code does:

  1. Creates a list of URLs up for deletion using the provided indexes.
  2. Removes those urls from the array that feeds the app's UI.
  3. Deletes files at those URLs from the device.

Open ContentView.swift and add the onDelete(perform:) view modifier after the closing brace of the ForEach.

.onDelete(perform: server.deleteFile)

Run the app, and you'll now be able to delete files from the server and within the app!

Finally, if you'd rather not have to look up the device name and port when connecting to the server, add the following code after the .refreshable view modifier inside ContentView.swift:

.toolbar {
  ToolbarItem(placement: .principal) {
    Text(ProcessInfo().hostName + ":\(server.port)")
  }
}

This puts the device name and port above the list of files, making it far easier to share this information with others on the same network.

Congratulations! You successfully created an iOS app with a built-in server!

Accessing over the Internet

Just a word on security: You must take precautions and not leave your app running unattended.

If you'd like to allow access to your app from anywhere, you may need to disable firewalls or change some port settings on your network router. Or, you could disable the Wi-Fi on your device and connect to it using cell services.

You'll then need your network's public IP address, which you can find by typing "what's my ip" into a web search. That IP address and port number should be all you need to make a connection.