Swift serves as the backbone for iOS applications. As more software comes to phone applications, working with GraphQL endpoints in Swift becomes more and more important. Many libraries have popped up to aid in this effort, such as Swift GraphQL which we'll be exploring today.
Swift GraphQL is a package by The Guild that lets you write GraphQL queries using the native Swift language, and it’ll take care of the rest. Swift GraphQL provides a CLI tool which can convert a GraphQL schema into native Swift. This provides you with type safety in your queries and mutations preventing you from shipping broken apps, as any invalid queries will automatically fail a build. Checks could be added to your CI tooling, allowing you to compare and analyse whether your schema has changed.
In this example we’ll get Swift GraphQL set up, with the appropriate services in our application, and use this to request products from GraphCMS and display them in a list. We’ll explore how Swift GraphQL makes selections and queries, and how it converts API data into local Swift data models.
Instead of creating a GraphCMS project from scratch to follow along, you can use the endpoint https://api-eu-central-1.graphcms.com/v2/ck8sn5tnf01gc01z89dbc7s0o/master
. All of the GraphCMS examples repo on Github use this endpoint, and no authentication is required.
Getting StartedAnchor
In XCode, let's create a new project. Select the multiplatform app option, and provide your project a name and organisation identifier.
Generating our SchemaAnchor
Swift GraphQL offers a CLI to automatically create a schema from a GraphQL endpoint.
Follow the instructions listed to create a Swift file, which is a representation of your schema.
You may have to provide type mappings in a SwiftGraphQL.yml
config to correctly handle custom types, such as dates. For example, here is a DateTime
structure for GraphCMS
:
# swiftgraphql.ymlscalars:DateTime: DateTimeScalar
import Foundationimport SwiftGraphQLstruct DateTimeScalar: Codec {private var data: Datevar raw: Intinit(from date: Date) {data = dateraw = Int(date.timeIntervalSince1970)}// MARK: - Public interfacevar value: String {let formatter = DateFormatter()formatter.locale = Locale(identifier: "fr")formatter.setLocalizedDateFormatFromTemplate("dd-MM-yyyy")return formatter.string(from: data)}// MARK: - Codec conformance// MARK: - Decoderinit(from decoder: Decoder) throws {let container = try decoder.singleValueContainer()let value = try container.decode(Int.self)data = Date(timeIntervalSince1970: TimeInterval(value))raw = value}// MARK: - Encoderfunc encode(to encoder: Encoder) throws {var container = encoder.singleValueContainer()try container.encode(Int(data.timeIntervalSince1970))}// MARK: - Mock valuestatic var mockValue = DateTimeScalar(from: Date())}
Once you have your schema generated, add it to the project by dragging it into the file pane.
Creating our GraphQLAPIAnchor
Now that we have our schema generated, we can create an API class to handle the schema operations.
Create a GraphQLAPI
file, and fill it with:
import Foundationimport SwiftGraphQLclass GraphQLAPI {func performOperation<T>(query: Selection.Query<T>) async throws -> T {try await withCheckedThrowingContinuation { continuation inSwiftGraphQL.send(query, to: "endpoint") { result inif let data = try? result.get() {continuation.resume(returning: data.data)}}}}}
This class handles performing GraphQL operations using Selection.Query
and send
provided by SwiftGraphQL
. We also wrap this with withCheckedThrowingContinuation
in order to provide async
/await
support to our application.
Retrieving ProductsAnchor
Now that we've got the prep work done, we can start retrieving our products. We'll first need our product model:
struct Product: Decodable, Identifiable {var id: String = UUID().uuidStringlet name: Stringlet description: Stringlet price: Int}
Next, we'll need to use SwiftGraphQL
to create a selection query. Let's create a ProductsQuery
file to hold this selection:
import Foundationimport SwiftGraphQLclass ProductsQuery {static var LIST_PRODUCTS: Selection<[Product], Objects.Query> {Selection.Query {return try $0.products(stage: .published,locales: [.en],selection: Selection.list(Selection.Product {Product(id: try $0.id(),name: try $0.name(),description: try ($0.description() ?? ""),price: try $0.price())}))}}}
This creates a selection for products which are only published in our en
locale, and prepares the selection to map to a list of products as defined by the model we've just created.
Finally, let's make a function to call the API using our operation. For convenience, we will create a APIService
class to handle this:
class APIService {let api: GraphQLAPI = GraphQLAPI()func listProducts() async -> [Product] {return (try? await self.api.performOperation(query: ProductsQuery.LIST_PRODUCTS)) ?? []}}
This uses our LIST_PRODUCTS
query and passes it to our GraphQLAPI
's performOperation
method. If the operation completes successfully, the API will automatically decode the response, and return the objects provided as the return type listed in our function - in this case [Product]
(a list of Product
).
By using try?
, we can default the return result if the operation does not complete successfully to return an empty list. We could also handle the error here in some other way by just using try
with a do/catch
.
Displaying ProductsAnchor
Finally, let's display the products.
In our default ContentView
(create a new SwiftUI file if you don't have one), we'll need to store our products in the state, by adding to our View structure:
@State var products: [Product] = []
We'll also require a function to retrieve our products using our previously defined function:
func loadProducts() async {self.products = APIService().listProducts()}
Finally, let's replace the body
with a list and load in the products on view appear:
swift
List(self.products, id: \.id) { product in
Text(product.name)
}.onAppear {
Task.init {
await self.loadProducts()
}
}
And voila! We should now have a working list of products displaying.
I hope you found this useful, and to learn more about Swift with GraphCMS, checkout the example for this project on Github