Version: 1.1.0

Differences from Relay in JavaScript

Relay.swift is not intended to replace the official Relay framework for JavaScript. We love Relay, and we want to bring the same benefits that it brings to building web apps to a new environment: building native apps for Apple platforms with Swift.

Relay.swift was created by spending a bunch of time heads down in Relay's source code, understanding how it does what it does. For the most part, the API should be the same or similar to the original. When things diverge in a significant way, it's usually because of fundamental underlying differences in the two environments. This page explains some of those differences.

Relay.swift vs. Relay#

Relay is written in JavaScript using Flow to provide a type system. Flow is very different from Swift's type system because for the most part, Flow types are structural. This means that a value is of a certain type based on the structure of the value. As long as two types require the same structure, they are effectively the same type: if a value conforms to one of them, it conforms to the other. Swift doesn't support structural typing at all: instead, Swift has exclusively nominal types. In Swift, the name of a type matters, and values know the name of their type. This means that if we define two structs in Swift with the exact same fields, we still can't treat a value of one of those types as the other type.

Relay takes advantage of structural types in Flow both in its internals and in the type definitions that its compiler generates for query data:

  • It is able to use plain-old JavaScript objects to represent query data and enforce types on the structure of those objects at the boundary between the Relay framework and the app without any explicit conversion code.
  • For nested fields in query data, Relay only needs to generate named types for the top-level: the structure of nested fields is embedded inside those types without needing to named and defined elsewhere.
  • For input types and enums, which can be reused across many queries, mutations, and fragments, Relay can generate types in each file that uses them. Because the structure of the types match, values can be used interchangeably with them.

Relay.swift has to take a different approach, both to how it stores records and how it generates types, to work within the bounds of nominal typing:

  • Internally, Relay.swift stores records with dictionaries, similar to the JS objects Relay uses, because the shape of records is heterogenous and not known ahead of time. But when data is read out of the store, it has to be converted into named Swift structs that are generated by the Relay compiler. This is only way to express the types of specific fields in Swift, so that consumers of the data can only access the fields specified in the query.
  • Every nested field in those structs needs its own named type as well. The compiler can't just generate these based on the GraphQL schema and reuse them either, because queries may alias fields or select different fields for the same object type in different places. To handle this, nested fields each get a nested struct type named after both the type name in the schema and the field name in the query, preventing collisions.
  • This approach doesn't work for input types and enums, because their values should be usable in any place that expects them. If two different fields in a query are of the same enum type, values from one of those fields should be comparable to values from the other. For these types, the compiler generates an enum for GraphQL enums and a struct for GraphQL input types the first time it generates a file that uses the type. Swift thankfully doesn't care which file the type is in: anything in the module can use it.

RelaySwiftUI vs. react-relay#

There are other differences between Relay.swift and Relay on the UI side.

First, while in Relay.swift you still write your queries inside tagged strings in your source code, they can't actually stand in for the compiled version of the query. Relay uses a Babel plugin to transform tagged graphql string literals into imports of the compiled version from the Relay compiler, but Swift doesn't have an equivalent way of doing arbitrary transforms to code as it is compiled.

Instead, you still tag your GraphQL queries in Relay.swift using the graphql function, but it only serves to flag the query to the Relay compiler. Once the compiled version of the query is generated, you can reference the generated type explicitly when calling into Relay. This is the easiest way to preserve type information about the query variables and response.

Relay.swift uses property wrappers to achieve a similar effect in SwiftUI to what hooks do in React. If you've tried the experimental hooks API for Relay, Relay.swift's property wrappers should feel at least a little familiar. They're a little more finicky about how you use them, but for the most part, we can accomplish a lot of the same things with property wrappers in SwiftUI that React libraries can do with hooks.