From iOS to Server-side Swift
I’ve been working on various iOS apps for over a decade. In this time, I did also experiment with other projects, including an Objective-C based http server. After the advent of Swift, I’ve read glimpses of something called Vapor. I was curious what it was.
And I finally got the time to look into it. Discussing it with my friend Klemen, he pointed out I could also take a look at GraphQL while I’m at it.
I thereby set out on an adventure, use Vapor, GraphQL and Composable Architecture for the client, to build a complete project, displaying a simple list of stars and their planets.
On the Server
For the sake of simplicity I decided to use an in memory sqlite database behind Fluent. I like how Fluent abstracts away interfacing with the database, so in case I wanted to change my backing db, I wouldn’t have to change anything in my source code. In order to support GraphQL, I decided to adopt GraphQLKit and GraphiQLVapor. This allowed me to get something working really quickly.
The two main points for supporting GraphQL in your Vapor app for me are:
- Create your resolver
- Create the schema
Resolver is the object GraphQLVapor will ask to perform queries or mutations. I put all my database access code in there.
final class StarsController {
func fetchStars(request: Request,
_: NoArguments,
group: EventLoopGroup) throws -> EventLoopFuture<[Star]> {
group.next().makeFutureWithTask {
return try await Star.query(on: request.db).with(\.$planets).all()
}
}
...
}
In the sample above, I wanted to use async/await from Fluent, but wasn’t sure how to couple it with GraphQLKit’s requests, which is why I have it wrapped up in an EventLoopGroups future.
Schema is a description for GraphQL, of what entities are available and what queries or mutations can be performed.
let starsSchema = try! Schema<StarsController, Request> {
Scalar(UUID.self)
Type(Star.self) {
Field("id", at: \.id)
Field("name", at: \.name)
Field("planets", with: \.$planets)
}
...
Query {
Field("stars", at: StarsController.fetchStars)
Field("planets", at: StarsController.fetchPlanets)
Field("starsPlanets", at: StarsController.fetchStarsPlanets) {
Argument("starID", at: \.starID)
}
}
Mutation {
Field("createStar", at: StarsController.createStar) {
Argument("name", at: \.name)
}
...
}
}
Once this is done, tell the Vapor application to register the newly created schema.
app.register(graphQLSchema: starsSchema, withResolver: StarsController())
With the above code, we have a working GraphQL endpoint in our app. We can already make queries or perform mutations.
To enable browsing the GraphQL implementation directly on the server, I used GraphiQL-Vapor, just adding the following line after registering the GraphQL schema.
app.enableGraphiQL(on: "explore")
With this, I can make queries and mutations directly at localhost:8080/explore
On the Client
Googling for an iOS GraphQL client lead me straight to Apollo. Once added as a dependency to the client project, I needed to tell Apollo where to get the schema and what operations can be performed. This is done in a codegen-config.json
file.
{
"input" : {
"operationSearchPaths" : [
"**/*.graphql"
],
"schemaSearchPaths" : [
"**/*.graphqls"
]
},
"schemaDownloadConfiguration": {
"downloadMethod": {
"introspection": {
"endpointURL": "http://localhost:8080/graphql",
"httpMethod": {
"POST": {}
},
"includeDeprecatedInputValues": false,
"outputFormat": "SDL"
}
},
"downloadTimeout": 60,
"headers": [],
"outputPath": "./graphql/schema.graphqls"
}
}
I had to specify the server location and operations that I want to do in order for Apollo to generate objects I could then use in my client app.
query Stars {
stars {
id
name
planets {
id
name
}
}
}
query PlanetsOfAStar($starID: UUID!) {
starsPlanets(starID: $starID) {
id
name
}
}
mutation NewStar($name: String!) {
createStar(name: $name) {
id
}
}
mutation NewPlanet($name: String!, $starID: UUID!) {
createPlanet(name: $name, starID: $starID) {
id
}
}
After that it was time to structure the application so it can display and create Stars and Planets.
I decided to introduce model objects separately in the Client domain. Even though this essentially duplicates models, it allows me to stay flexible in the future in case I would want to swap Apollo for a different GraphQL client implementation. It also frees me from coupling the business logic with the network layer.
Conclusion
I am very excited about my first step into the world of server-side swift. There is certainly a lot more for me to explore here. Vapor is built on top of SwiftNIO and there are other frameworks similar to Vapor worth exploring as well.
The main take-away for me here is the knowledge, that I can now provide backend services for any of my projects, without leaving the world of Swift.
Feel free to take a look around the project on my GitHub
References:
GraphQL Vapor Template where I figured out how to set up the schema.
Getting Started with Apollo where I figured how to get Apollo client up and running.
Fluent where I learned how to interface with the database.