By Vyacheslav Vorona, iOS developer at UniFa.
Being an iOS developer, have you ever been in a situation when you were thinking: "Alright, I want to write this kind of app. Now!" But then you instantly realize that your pet-project would also need some kind of backend and you are too busy/lazy to learn all that "server stuff"... so your enthusiasm disappears as quickly as it popped up five minutes ago. Happens to me all the time. 😅
Luckily, there are technology choices allowing you to write your backend code using the language you are used to. I looked through some of them and decided to give Vapor a try. IMHO, it is risky to use Vapor in any big and serious project right now, but for hobby programming... it is perfect. Today I am going to take a closer look at it and I hope you will join me (don't forget tea and cookies ☕️).
What are the goodies?
- You use your favorite Swift for coding
- You are doing it in your favorite Xcode with all it's debugging tools available
Cmd + R
and your server is up locally- Type checking during the compilation
- An ability to have code/models shared between your iOS app and the backend
The last point is especially intriguing and provides some opportunities you wouldn't have with a backend written with any commonly used server-side programming language. Just imagine: you change your server code and an iOS app is already aware of it. Depending on the case your changes are going to be handled automatically (and you may not even need to change anything in your client code) or your app's compilation is going to fail letting you know something is wrong. Sounds like an attractive opportunity to me. Unfortunately, there is almost no example code implementing such solutions to be found on the Internet. So the only way we have is to try to implement it by ourselves, which is what we are going to do today.
Sounds cool! What do I need?
- Vapor requires you to have Xcode 9.3+ and Swift 4.1+ (for this particular project we will need Swift 5.1 though)
- We will also need Homebrew to install Vapor
Optionally: I am going to use Postman client to test API calls, but you may use any tool you like (or not use any at all).
That's it. Let's dive into it!
Setting up the projects
Alright, first things first, since we are going to have two projects sharing some code, we need to create a workspace in Xcode. I am going to call it SchrodingerApp.workspace
. Got it? Schrodinger's cat is both dead and alive at the same time, and our code will be in the iOS app and the server at once... ah, never mind. 🙄
Next, let's download and install Vapor. Let me copy and paste some console commands from it's 'Hello, world' page for you...
Installing Vapor using Homebrew:
brew tap vapor/tap brew install vapor/tap/vapor
Easy, right? Then I will run the command below in the same folder where I've put my workspace file.
vapor new SchrodingerAppServer
It will create a new folder with the name we entered, but if you look inside, you will notice it doesn't have an .xcodeproj
file we are used to. Don't worry, Vapor can generate it for you:
cd SchrodingerAppServer vapor xcode
When everything is done, it will suggest you to open Xcode, but we still need to put our new project into the workspace, so let's refuse for now. Open SchrodingerApp.workspace
in Xcode and just drag-and-drop SchrodingerAppServer.xcodeproj
from Finder to Xcode.
You may notice that now you have a scheme called 'Run'. Also, make sure you have your Mac selected as a device to build. Go on and run it! (just Cmd + R
, as promised)
In the Xcode console we see the message which says that our brand new server is up and waiting.
Server starting on http://localhost:8080
Since iOS Simulator uses the same network as the Mac it's beeing ran at, I will use the simulator to check the localhost. So, does our server work?
Great!
We will take a closer look at the contents of our server app in a moment. But first, let's set up our client app to finish the boring part.
I am creating a new Single View iOS project named SchrodingerAppClient and pulling it's .xcodeproj
file into our workspace. Make sure you have two schemes: one for the server and one for the client.
For the next step let's designate a place to put our shared code to.
Sharing is Caring
I consider the server app as a primary one, which means the actual files containing the shared code are going to be stored inside of it's project.
To do so I'm going to create a folder called SharedAPI
("New Group" in the Xcode) in SchrodingerAppServer/Sources/App
. It will contain the code common for both of our apps.
Now I will go to the client project and add a "New Group without Folder" named SharedAPI
into SchrodingerAppClient
. This group is going to hold a reference to the SharedAPI
folder we previously created for the server app. To add the reference select the newly created group, in the "File Inspector" click a tiny folder icon and search for a SharedAPI
folder in the SchrodingerAppServer
.
Finally, we are ready to write some code! Yay! 🚀
Schrodinger's Model
I feel like our SchrodingerApp is missing something... A Cat data model, of course! I am adding a Cat.swift
file into the SchrodingerAppServer/Sources/App/SharedAPI
and here is a code for it:
final class Cat { var id: Int? var name: String var age: Int private var breedString: String var breed: Breed? { return Breed(rawValue: breedString) } init(id: Int? = nil, name: String, age: Int = 0, breed: Breed) { self.id = id self.name = name self.age = age self.breedString = breed.rawValue } } enum Breed: String { case persian = "persian" case russianBlue = "russian blue" case britishShorthair = "british shorthair" }
I guess it is pretty straightforward for now, just a couple of things to note:
var id: Int?
is in fact a requirement of theSQLiteModel
protocol which I'm going to talk about in a second- I've declared
private var breedString: String
to see how Vapor is handling private properties
Getting Fluent
Vapor provides an Object-relational mapping framework called Fluent to work with databases. It supports several database drivers:
- PostgreSQL
- MySQL
- SQLite
- MongoDB
For our project, I decided to try out SQLite as it is simple and is just nice for prototyping.
To make Cat
a full-fledged server-side model we have to adopt several protocols:
- SQLiteModel. Allows your class to represent a table in an SQLite database. Its only requirement is to have
var id: Int?
which we already implemented in our model. - Migration. Allows us to perform dynamic migrations of our model (or revert them).
- Content. Makes a model convertible to content of an HTTP message.
- Parameter. Makes a model capable of being used as a Restful route parameter.
Latter three protocols do not have any requirements by default, we just need to declare the conformance.
But here is a thing: to do so, we need to import Vapor
and import FluentSQLite
which we obviously can't do on the iOS side. To make Cat model accessible from both server and client code, I'm going to use a small trick added in Swift 5.1. - canImport()
. Add the code below right after the enum Breed
in Cat.swift
:
#if canImport(Vapor) && canImport(FluentSQLite) import FluentSQLite import Vapor extension Cat: SQLiteModel {} extension Cat: Migration {} extension Cat: Content {} extension Cat: Parameter {} #endif
This way Cat
will conform to these four protocols only in case when Vapor
and FluentSQLite
are able to be imported. This means our data model is going to be treated differently: as an SQLite model on the server-side and just as a simple object on the client-side.
To make sure everything works I'm going to add a reference to Cat.swift
into the SchrodingerAppClient
and try running both schemas ("Run" for the server and "SchrodingerAppClient" for the client). If everything is set up correctly, both should compile successfully.
Two things left to do:
- Add the Cat model to a
MigrationConfig
which is a Fluent structure managing database migrations in Fluent. To do it I will add the following line into theconfigure
function located inconfigure.swift
, right beforeservices.register(migrations)
.
migrations.add(model: Cat.self, database: .sqlite)
- (Optional) Right now the project we created doesn't persist any data between two launches of the server. To make it do so, we need to replace the this line in
configure.swift
let sqlite = try SQLiteDatabase(storage: .memory)
with this
let sqlite = try SQLiteDatabase(storage: .file(path: "db.sqlite"))
So that the database is stored in a file.
We are done with the model. Now let's teach our apps to do something with it.
POSTing Cats
Server
In this project, we will implement an endpoint for our iOS app to POST new Cat models to the server via REST API. With Vapor it is really simple. Just find a file named routes.swift
and add the code below to the bottom of the routes
function located there:
router.post("api", "cat") { request -> Future<Cat> in return try request.content.decode(Cat.self).flatMap(to: Cat.self) { cat in return cat.save(on: request) } }
Let's take a closer look at this code and see what it actually does.
router.post("api", "cat")
registers a new route at the path /api/cat
using the POST method. When a request is received, it's JSON gets decoded into Cat
using Content
protocol. Since decode(_:)
returns a Future
we need flatMap(to:)
to unwrap a Cat
when it is decoded. Then a Cat
gets saved into the database using Fluent.
Now I`m going to run the server app and try out our new endpoint using Postman:
- The URL is:
http://localhost:8080/api/cat
- My request's JSON is:
{ "name": "Murka", "age": 5, "breedString": "russian blue" }
- Make sure to set
Content-Type
header toapplication/json
And here is the server response:
{ "age": 5, "id": 1, "breedString": "russian blue", "name": "Murka" }
As you can the breedString
was handled properly (we made it private
in the model, remember?). Also, note that Fluent have set an id
for our freshly POSTed Cat.
Client
To POST some Cats from the iOS app I'm adding the method below right to the default ViewController.swift
in SchrodingerAppClient
:
private func post(_ cat: Cat) { let jsonData = try? JSONSerialization.data(withJSONObject: cat.json) var request = URLRequest(url: URL(string: "http://localhost:8080/api/cat")!) request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" request.httpBody = jsonData let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data, error == nil else { return } let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) if let responseJSON = responseJSON as? [String: Any] { print(responseJSON) } } task.resume() }
I will not describe what's going on here in detail as I believe you understand it if you are reading this article. 😉 Just a couple of things:
- Note that I am setting
application/json
as a value forContent-Type
Cat.json
is just a simple mapping I've added to the model. It is a computed property of type[String: Any]
representing a Cat instance to use in an API call
GETing Cats
Server
Next, I am going to add a GET endpoint which is going to be called by an iOS app to present a list of the Cats we've put into our Schrodinger's box. Again, with Vapor it us quite straightforward:
router.get("api", "cat") { request -> Future<[Cat]> in return Cat.query(on: request).all() }
When the server receives a GET request at the /api/cat
, it just queries all the Cats from the database and provides them to the client.
Testing new endpoint using Postman, here is the response:
[ { "age": 5, "id": 1, "breedString": "russian blue", "name": "Murka" } ]
As you can see, we received an array containing one element - the Cat we added using POST endpoint.
Client
Here is some ugly code from the client-side to get a list of the cats for presentation. I've added it as a separate method into the ViewController.swift
:
private func getCats() { var request = URLRequest(url: URL(string: "http://localhost:8080/api/cat")!) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error **in** guard let data = data, error == nil else { return } let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) if let responseJSON = responseJSON as? [[String: Any]] { self?.cats = [] for catJson in responseJSON { guard let cat = Cat.make(from: catJson) else { continue } self?.cats.append(cat) } } } task.resume() }
The only thing worth explaining here is Cat.make(from:)
method. It is just a static function decorating the initialization of Cat
and able to return nil
if some necessary data is missing.
Final preparations
User Interface I am adding some simple UI to test the features we implemented:
- Three
UITextField
s to add new Cats via POST (yeah, there should be pickers or something similar, but this project not going to the AppStore, alright? 😅) UITableView
to display the Cats the client recieves via GET
Info.plist
Another important thing to do is to modify Info.plist
in SchrodingerAppClient
as follows:
We need to set NSAllowsArbitraryLoads
key under the NSAppTransportSecurity
dictionary to YES
. Otherwise your iOS app will not be able to connect to the localhost.
Note that this is an anti-pattern and you should never do it in a real project. Apple is strongly against doing it and your app may be rejected from the AppStore.
But it's ok now, since we are just playing around. 😎
Now let's run our app and look at the results, even though they don't look as exciting as some mechanisms we implemented under the hood:
We can input some data for the new Cats, POST it...
... and then GET the Cats to see all of them in the list:
And all of it is done using a single Cat data model! 🎉
Conclusion
Let's be honest, I doubt Vapor (and backend Swift overall) is going to become a somewhat serious competitor for all those languages used for server-side development any soon. Yes, Swift is incredibly fast, has strict types and a bunch of other advantages, but it needs something more than just support from its community (even though they are awesome guys) to become a "real thing" .
BUT! Does it mean you shouldn't use it for your cute little pet-project you are writing every weekend with a beer in your hand? No! You definitely should. Finally, you can write everything from A to Z without leaving your comfort zone.
Besides, today we've learned a bit about some opportunities which mobile platform developers don't usually have. With Vapor you don't have to feel that you are writing bad code just to fit some weird architecture on the server-side. Your backend code influences your iOS app directly. And if you need to add something to the client, the server code is already aware of it. It's a great feeling, especially if you don't have much experience with backend.
I hope I could inspire you to give Vapor a try.
Happy coding! 😉
Project repo (make sure to check README)