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.
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
Running a Web Server on iOS with Vapor
25 mins
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
.
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.
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:
- Decode the content of the incoming request into a
FileUploadPostData
object. - Generate a URL based on the documents directory and name of the uploaded file.
- Write the file to the generated URL.
- 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.
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.
Tapping on a row will open that file.