Custom GraphQL types on Swift projects
By default, GraphQL supports only a handful of basic types that we can use on the schema definition. This list includes Int
, Float
, String
, Boolean
and ID
. But as you can easily understand relying solely on these types is quite restrictive. What if we want to add a date field? Or some other kind of data?
Hopefully for us, GraphQL offers plenty of flexibility when it comes to constructing a model with custom types; be it a custom object type, a custom scalar type, enums or a list to name a few.
And that’s what we are going to investigate in this post. We will see how we can use all these custom types on Swift projects.
Before we start
In a previous post, I described how to create a simple GraphQL server in Swift using Vapor and an iOS app to fetch information from this server using the Apollo iOS client. For that post, I used a simple Post
type with an id
and a title
property.
This time, we are going one step further and we will enrich the Post
type by adding some more properties. We are going to introduce a custom scalar type that we will use for the id of the model, a Date field for the date that the post was published, a list of enums for the tags and a custom object for the author of the post.
As a side note before we start, the code in this post is a continuation of the code from the previous post that I mentioned earlier. If you want to go through the code as you read this post, you can find on Github a branch on the version of the project at the end of the previous post (server & client) and a branch with the version of the code at the end of this post (server & client). If you want to learn more about the setup and how to run those projects, either refer to my previous post or to the README file of each project.
Following the same pattern as in the previous post, let’s start with the updates on the server side and then we will update the iOS app.
Server
To begin with, let’s introduce three new types. The first one will be a new structure named CustomUUID
that we will as a type for the id
property. The second one will be an enum
named Tag
that we are going to use for the type of the list of tags and lastly a structure name Author
to represent the author of the post.
// CustomUUID.swift
struct CustomUUID: Codable {
let value: UUID
}
// Tag.swift
enum Tag: String, Codable {
case swift = "Swift"
case vapor = "Vapor"
case graphql = "GraphQL"
}
// Author.swift
struct Author: Codable {
let id: CustomUUID
let name: String
let twitter: String
}
Once all those types are added, we can update our Post
model to look like the following snippet:
// Post.swift
struct Post: Codable {
let id: CustomUUID
let title: String
let publishedAt: Date
let tags: [Tag]
let author: Author
}
After that, we will update the FieldKeyProvider
extension for the Post
and add a new key for each new property. We will later use those keys on the schema definition to map the fields of the GraphQL schema to the properties of the Post
structure. The final version of the Post
extension will look like this:
// GraphQL+FieldKeyProvider.swift
extension Post: FieldKeyProvider {
typealias FieldKey = FieldKeys
enum FieldKeys: String {
case id
case title
case publishedAt
case tags
case author
}
}
Since we have add a new structure, the Author
, we will also add an extension for this structure to add conformance to the FieldKeyProvider
protocol. The extension will be like in the following snippet:
// GraphQL+FieldKeyProvider.swift
extension Author: FieldKeyProvider {
typealias FieldKey = FieldKeys
enum FieldKeys: String {
case id
case name
case twitter
}
}
Now, we can move on and update the definition of the GraphQL schema:
// Schema.swift
enum Schemas {
static var postSchema = Schema<PostController, Request>([
Enum(Tag.self, [
Value(.swift)
.description("About Swift"),
Value(.vapor)
.description("About Vapor"),
Value(.graphql)
.description("About GraphQL"),
])
.description("Tags"),
Scalar(CustomUUID.self)
.description("My custom UUID"),
Scalar(Date.self)
.description("Date Type"),
Type(Author.self, fields: [
Field(.id, at: \.id),
Field(.name, at: \.name),
Field(.twitter, at: \.twitter)
]),
Type(Post.self, fields: [
Field(.id, at: \.id),
Field(.title, at: \.title),
Field(.publishedAt, at: \.publishedAt),
Field(.tags, at: \.tags),
Field(.author, at: \.author),
]),
Query([
Field(.posts, at: PostController.fetchPosts),
]),
])
}
In this snippet, we have added the definition of the enum and all of its cases, the definition of the CustomUUID
and Swift’s Date
, a definition of the new Author
type and updated the definition of the Post
type to include the new fields.
WARNING: The order of the definitions in the schema does matter. If we were to place the definition of the
CustomUUID
after the definition of thePost
, we would get an exception sayingFatal error: 'try!' expression unexpectedly raised an error: Cannot use type "CustomUUID" for field "id". Type does not map to a GraphQL type.
.
Lastly, we are going to update the data that the GraphQL server will return to the client and add values for the new fields:
// PostController.swift
private lazy var author = Author(
id: CustomUUID(value: UUID()),
name: "Ioannis Diamantidis",
twitter: "@diamantidis_io"
)
private lazy var posts = [
Post(
id: CustomUUID(value: UUID()),
title: "My first post",
publishedAt: Date(),
tags: [.swift, .graphql, .vapor],
author: self.author
)
]
NOTE: I could have used
UUID
directly instead of theCustomUUID
, but for the sake of demonstration, I preferred to use a custom container structure. If you want to use theUUID
, you can follow the same logic as with theDate
type.
And this is it for the server side! You can now build and run the project!
The code with all those changes is also available on GitHub.
Let’s now jump on to the client side and the iOS app!
iOS
First and foremost, we will get the updated version of the GraphQL schema. With the server running, run the following command from the root directory of the iOS project to update the schema.json
.
apollo schema:download --endpoint=http://127.0.0.1:8080/graphql iOSGraphQL/GraphQL/schema.json
Once this is done, let’s open Xcode and update the query file(AllPosts.graphql
) to add the new fields. We are going to add the tags
and the publishedAt
in the same way as we added the id
and title
fields, but for the author
field, we are going to use the concept of GraphQL’s fragments. Fragments are reusable components that you can use to split the query definition in smaller chunks and use them in multiple queries.
In our case, we will define a fragment on the Author
type and then use it on the AllPosts
query to fetch the author of the post.
The final version of the query file will be like the following snippet:
fragment AuthorDetails on Author {
id
name
twitter
}
query AllPosts {
posts {
id
title
publishedAt
tags
author {
...AuthorDetails
}
}
}
Now, let’s jump back to the Terminal and run the following command from the root directory to update API.swift
.
./Pods/Apollo/scripts/run-bundled-codegen.sh codegen:generate \
--target=swift \
'--includes=./**/*.graphql' \
--localSchemaFile=./path/to/GraphQL/schema.json \
./path/to/GraphQL/API.swift
If you build and run the app right now, you will get some errors like Type of expression is ambiguous without more context
and Use of unresolved identifier 'CustomUUID'
.
To fix those errors, we will have to add definition for the new structures that we introduced on the GraphQL schema.
Let’s start with the Author
which will have the following definition:
// Author.swift
struct Author {
var id: CustomUUID
var name: String
var twitter: String
init(author: AuthorDetails) {
self.id = author.id
self.name = author.name
self.twitter = author.twitter
}
}
In this snippet, we map the properties of the structure AuthorDetails
to the properties of our domain model. AuthorDetails
is the structure that the Apollo iOS client generated when we run the run-bundled-codegen.sh
command.
Next, let’s add the definition for the CustomUUID
:
// CustomUUID.swift
public struct CustomUUID: JSONDecodable {
let value: UUID
public init(jsonValue value: JSONValue) throws {
guard let stringValue = (value as AnyObject)["value"] as? String, let uuid = UUID(uuidString: stringValue) else {
throw JSONDecodingError.couldNotConvert(value: value, to: CustomUUID.self)
}
self.value = uuid
}
}
Here, we make CustomUUID
conform to Apollo’s protocol JSONDecodable
and fulfill the init(jsonValue value: JSONValue)
requirement. JSONDecodable
is a protocol that we use to add the logic about decoding the values of custom scalar types.
The same applies for the publishedAt
field and its Date
type, with the only difference being that this time we will add an extension to the Swift’s Date
structure instead of adding a new one.
extension Date: JSONDecodable {
public init(jsonValue value: JSONValue) throws {
guard let timeInterval = try? TimeInterval(jsonValue: value) else {
throw JSONDecodingError.couldNotConvert(value: value, to: Date.self)
}
self = Date(timeIntervalSinceReferenceDate: timeInterval)
}
}
Lastly, we will update the Post
structure, where we will add the new fields and update the initializer to instantiate them.
// Post.swift
struct Post {
let id: CustomUUID
let title: String
let publishedAt: Date
let tags: [Tag]
var author: Author
init(post: AllPostsQuery.Data.Post) {
self.id = post.id
self.title = post.title
self.publishedAt = post.publishedAt
self.tags = post.tags
self.author = Author(author: post.author.fragments.authorDetails)
}
}
Now we are ready to build and run the app. If you do so, you should be able to see the post object with all its properties on the console!
The code with all those changes is also available on GitHub.
Conclusion
And that’s about it! In this post, we have seen how to use GraphQL’s features like custom scalar types, enums, lists and custom objects to enhance the Post
model with fields like the uuid
, the publishedAt
, the tags
and the author
.
Knowing about those possibilities is really valuable when working with GraphQL and can make a huge difference when it comes to designing a GraphQL schema.
In the posts to come, we are going to see how further improve this project by adding support for sorting, filtering, creating a new post, editing an existing, deleting, etc. So stay tuned and follow me on Twitter should you want to get notified once these posts are published or you have a question or comment about this post.
Thanks for reading this post, and see you next time!