We recently released the first version of Apollo iOS, a strongly-typed GraphQL client for Swift apps. I’ve written about bringing GraphQL to iOS before, but in this post I’d like to go into more detail on our mapping from GraphQL types to Swift, and explain why we chose this approach. I’ll also show how the Xcode integration included with Apollo iOS gives you a convenient way of working with Swift and GraphQL side by side, and how that fits really well with the workflow enabled by the typed code generation.
For a more high-level introduction to GraphQL and its use on iOS, you may also be interested in a talk I recently gave at the Berlin GraphQL meetup. (Thanks to the Graphcool team for hosting me!)
A tale of two type systems
Underlying many of the benefits of GraphQL is the fact that it is strongly typed. Servers define a schema, a type system that precisely describes what data is available to clients. And clients define queries specifying the structure they want a response to have in terms of this type system. This means that even though responses are usually transported as weakly-typed JSON, result types are in fact fully predictable. For each field, we have the type information to know exactly what data to expect. The schema thus acts as a strongly typed and self-documenting contract between client and server.
Apollo iOS takes advantage of this type information to map responses to Swift types. All primitive GraphQL types have a natural mapping to a corresponding Swift type. This goes for built-in scalars like
Boolean, but also for schema-specific types, for which Swift types will be generated as part of the build process.
Both GraphQL and Swift have the notion of an enum type, for instance, so even though enum values are transported as strings in JSON, these values are treated as native enums in Swift. This gives you full type safety and autocompletion in Xcode. The generated Swift enum type even contains descriptions from the schema as comments, so they can be shown in Xcode’s Quick Help:
One of the safety features of Swift is that by default values can never be
nil. This means errors that would only be detected at run-time in other languages are guaranteed to be compile-time errors in Swift. When you do need to deal with situations where a value can be either present or not, you use optional types (indicated with a
? suffix on the type, as in
String?), and the Swift compiler will force you to unwrap these types before use and think carefully about the consequences of a value being
If you deserialize JSON as untyped dictionaries in Swift, you’ll have to access data by string keys and manually cast values to the right type. Both operations are potentially fallible, so every value you access will in effect be optional:
In GraphQL, all types are nullable by default, and you use a
! suffix to make a type non-nullable (as in
String!). Apollo iOS maps non-nullable GraphQL types to non-optional Swift types, and optionals are only used if the schema defines a field as nullable. This means you don’t have to deal with Swift optionals unless data is truly optional.
What’s in a model?
We’ve seen above how dealing with untyped data is a pain in a language like Swift. It also means losing the advantages of strong typing. For that reason, when interacting with a RESTful API, many developers choose to map network responses to model types to keep weak typing from spreading to their UI. In fact, there are now dozens of JSON libraries available for Swift that either make writing these mappings easier, or rely on code generation to generate the mappings for you.
Models in REST
If you’ve ever written a model layer for a RESTful API before, you’re probably used to defining one model per resource. With this approach, there would be a
Post type containing all fields returned for posts, for example, and JSON results from individual endpoints are parsed into instances of this type.
A big benefit of this over raw JSON is that these models are a lot more convenient to work with, and they also give you certain amount of type safety. Model properties are typed, so there is no need for casts. And using named properties instead of string keys means the compiler can warn you when you make a typo or try to access a field that does not exist on a specific model.
Even though this is a huge improvement over untyped data access, practical considerations often mean not all REST endpoints will return all fields for every resource, so you will likely end up with partially filled models. This means all model properties will have to be defined as optional, and you have no principled way of keeping track of what data is guaranteed to be there as a result of a particular request.
Type models vs. query models
With GraphQL, the equivalent approach would be to define a model type for every type defined in your schema. Query results could then be parsed into model instances that allow for type safe data access:
Unfortunately, these type models suffer from the same issue mentioned above: most requests will only contain a subset of the data defined in these types. This is also why even a non-nullable field like
id is optionally typed in the corresponding model. If we ask for the
id, it is guaranteed to be non-null, but queries are never required to include a field, so not every response will contain a value for
In contrast to REST however, GraphQL does have a principled way of keeping track of what data gets returned for a specific request, because queries always fully specify the structure of the response.
Query models allow you to carry this specificity over to Swift. The way this works is that Apollo iOS takes a GraphQL schema and a set of GraphQL query documents, and uses these to generate query-specific model types that only contain the fields defined in the query:
These model types are not defined globally, but are nested within a query class. There is no single
Post type, but queries like
PostDetailsQuery will each contain their own
Post type with a subset of the fields from the GraphQL type.
The benefits of static type safety
fetch(query:) method on
ApolloClient uses a query class as a generic parameter, which allows the compiler to automatically infer the type of the data returned. Type inference in Swift makes this entirely unintrusive, but it gives you complete static type safety for your data access code, catching many mistakes at build-time that would otherwise only be detected at run-time.
Your Swift code will only be able to access a field if the corresponding query asks for it. Removing a field from a query will lead to compile-time errors for any code that accesses the field. This means you can be sure at build-time that the data you access on models will actually be fetched as part of the query that returns them:
Fields defined as non-nullable in the schema are returned as non-optional types. And even if a field is defined as nullable, the benefit of query models is that these allow you to clearly differentiate between data that hasn’t been fetched (the model type simply won’t contain that property), and data that is in fact
null on the server (the model will contain an optional property with a
Keeping code and queries together
Our Xcode integration makes the mapping even more seamless, because it allows you to conveniently work with Swift and GraphQL side by side.
As you may know, Xcode allows you to show an Assistant editor to present code related to the current file in a split window pane. Apollo iOS takes advantage of this to show a matching
.graphql file for the current
.swift file. For example, you would write UI code in
PostListViewController.swift using results from a query defined in
PostListViewController.graphql. This way, you can keep data definitions and data access colocated.
If you switch to a different
.swift file, the Assistant editor in Xcode will automatically switch to the corresponding
This enables a really nice integrated development experience, where Swift and GraphQL are used together to define data dependencies for your UI components.
.graphql files will also be validated with every build, and validation errors are shown inline. So if you try to select a field that isn’t actually part of the schema or make another mistake in your query, you will get immediate feedback at build time without having to run your code:
Try it for yourself
If you haven’t done so already, please give Apollo iOS a try in your projects, and let us know what you think by opening an issue on the GitHub repository or starting a conversation on the
#ios channel on Slack.
There is much more to say about the mapping between GraphQL and Swift, especially in relation to more advanced features like fragments and type conditions. These are topics worthy of a separate post however, so keep tuned for more articles on using GraphQL on iOS!
Want to work on GraphQL developer tools full time? Join the Apollo team!
Stay in our orbit!
Become an Apollo insider and get first access to new features, best practices, and community events. Oh, and no junk mail. Ever.