How code generation works
Relay.swift is designed to provide a type-safe way to interact with your app's data. To do this, we use the Relay compiler from the official JavaScript Relay framework with a custom plugin to generate Swift code instead of JavaScript.
The types that are generated are based on both your GraphQL schema and any graphql()
strings found in your Swift source files. Each query
, mutation
, and fragment
in your sources will become a generated .graphql.swift
file.
Let's look at examples of what Relay.swift will generate in different situations.
#
FragmentsWe'll use the above GraphQL fragment as our example. When the Relay compiler runs, it will find this fragment and generate a file named ToDoItem_todo.graphql.swift
.
#
The fragment typeFirst, the compiler will generate a struct with the same name as the fragment. You won't generally have to use its properties and methods yourself; they're there to support Relay itself.
This is the type that you will pass to @Fragment in your SwiftUI views.
#
The Data structAll fragments must have a corresponding Data
type, so the compiler generates a structure that matches the shape of your query. If there are nested fields in the fragment, then it will generate nested types under Data
to represent that data.
The Data
type (and any nested types) conforms to Swift's Decodable
protocol. Relay includes a custom Decoder
implementation specifically for reading your types from the Relay store.
#
The Key protocolFragments also get a Key protocol, which is conformed to by any generated types where the fragment is spread. The Key protocol requires that the type includes a field to get a pointer to this fragment's data. The Key is used to pass data between different views while only exposing the right data to each one. Fragment pointers don't actually include the fragment's data. Instead, they have just enough information for Relay to be able to load it in the view that needs it.
#
Operations (queries and mutations)Again, we'll use an example from the to-do list app. The Relay compiler will generate a file called UserToDoListQuery.graphql.swift
for this query.
#
The operation typeLike fragments, operations get a struct with the same name. Instead of being initialized with a key, operations can have variables to parameterize the operation.
#
The Variables structIf the operation takes any variables, a Variables
struct will be generated with the appropriate fields for setting those variables. If it doesn't take any variables, it will instead be a type alias to EmptyVariables
, which enables some shortcuts when using the operation in Relay.swift.
Variable structs and any input
types contained in them conform to VariableDataConvertible
, another Relay.swift protocol, so there's also a field to convert the variables to VariableData
. VariableData
is a more type-safe wrapper around a dictionary, and it only supports the types that are valid in GraphQL inputs. This allows us to work with the fields of variables internally in Relay while still having them be Encodable
when they need to be sent to the network.
#
The Data structJust like fragments, operations get a Data
struct that is used to read their data from the Relay store. This example shows how nested types are generated when the query includes more than just scalar fields.
These nested types are generated specifically for the query or fragment reading the data because they need to only include the fields that were requested, not everything that's in the schema. It's even possible to alias fields in a query, giving them a name that isn't even in the schema. The types are generated so that they only include fields that are actually expected to be present.
Nested data types have a type name based on both the field's type in the GraphQL schema and the field's name in the query. We can't use just the schema type name here because there may be multiple fields with the same schema type, but with different fields selected. So we need a type per field. We could probably name them just based on the field name, but that feels weird.
In this example, we're spreading the ToDoList_user
fragment onto the user
field. We need to be able to pass this user on to the ToDoList
view, so the User_user
type also conforms to ToDoList_user_Key
and includes a fragment_ToDoList_user
property. This is enough information for the ToDoList
view to load the data it needs from Relay.swift.
#
Unions and InterfacesTODO
#
EnumsEnums, and input types as we'll see, are a bit special because they may need to be reusable across multiple operations or fragments, so we generate a type for them in the first file where we need them. Thankfully, Swift doesn't really care which file a type is defined in, so from there we can use it wherever we need it.
GraphQL enums become Swift enums. They always map to strings, and we lowercase them to get a more Swift-y case name.
Generated enums conform to both VariableValueConvertible
and ReadableScalar
, which makes them usable in both the Variables
and Data
structs mentioned above.
#
Input typesYou might think that we would treat input types similar to nested types inside Data
structs, but we don't actually want or need to. Because we can't use different subsets of fields from an input type in different places in our app, it's much better to have a single version of an input type and reuse it anywhere it's needed.
Input types end up looking a lot like the Variables
structs generated for operations, because besides being unattached to a particular operation, they're the same.