Swift is the Apple-backed programming language powering all the iOS applications on your shiny new iPhone.
As more software comes to phone applications, working with GraphQL endpoints in Swift becomes more and more important. We can use the power of Swift’s native language constructs and frameworks in order to rapidly build a solution that can pull data from Hygraph.
In this example, we’ll work with Swift’s native URLSession
, setting up services and tooling to allow us to call the Hygraph API. With this tooling, we can retrieve Hygraph products, convert them into native Swift models using Codable, and display those products in a list efficiently.
Instead of creating a Hygraph project from scratch to follow along, you can use the endpoint https://api-eu-central-1.hygraph.com/v2/ck8sn5tnf01gc01z89dbc7s0o/master
. All of the Hygraph examples repo on Github use this endpoint, and no authentication is required.
#Getting Started
In XCode, let's create a new project. Select the multi-platform app option, and provide your project a name and organization identifier.
Once the project has loaded, we'll need to do some prep work to get ready for making GraphQL API calls. For this project, we'll be using URLSession
and Swift's async
/await
support, so we'll need to create some extensions and services to support this.
#Prepping ,[object Object]
Create a new file, and call it URLSession+Async
. Ensure this file is added to both the iOS and Mac targets.
We'll add in a function to retrieve data using a URLRequest
with async
support.
extension URLSession {func getData(from urlRequest: URLRequest) async throws -> (Data, URLResponse) {try await withCheckedThrowingContinuation { continuation inlet task = self.dataTask(with: urlRequest) { data, response, error inguard let data = data, let response = response else {let error = error ?? URLError(.badServerResponse)return continuation.resume(throwing: error)}continuation.resume(returning: (data, response))}task.resume()}}}
This method wraps dataTask(with: URLRequest)
to allow support for async
.
#Creating our GraphQL Operation Structure
We'll need a structure to allow us to create GraphQL operations, which can be sent to our GraphQL API.
Create a class called GraphQLOperation
, and fill it with the following code:
struct GraphQLOperation : Encodable {var operationString: Stringprivate let url = URL(string: "your graphql endpoint")!enum CodingKeys: String, CodingKey {case variablescase query}init(_ operationString: String) {self.operationString = operationString}func encode(to encoder: Encoder) throws {var container = encoder.container(keyedBy: CodingKeys.self)try container.encode(operationString, forKey: .query)}func getURLRequest() throws -> URLRequest {var request = URLRequest(url: url)request.httpMethod = "POST"request.setValue("application/json", forHTTPHeaderField: "Content-Type")request.httpBody = try JSONEncoder().encode(self)return request}}
This structure can take a string, and convert it into a URLRequest
to be sent to the API. It does this using Encodable
, which is part of the Swift Codable
functionality for automatic encoding / decoding of objects.
#Creating our Result Object
We'll need the ability to parse the API response, so let's create GraphQLResult
:
struct GraphQLResult<T: Decodable>: Decodable {let object: T?let errorMessages: [String]enum CodingKeys: String, CodingKey {case datacase errors}struct Error: Decodable {let message: String}init(from decoder: Decoder) throws {let container = try decoder.container(keyedBy: CodingKeys.self)let dataDict = try container.decodeIfPresent([String: T].self, forKey: .data)self.object = dataDict?.values.firstvar errorMessages: [String] = []let errors = try container.decodeIfPresent([Error].self, forKey: .errors)if let errors = errors {errorMessages.append(contentsOf: errors.map { $0.message })}self.errorMessages = errorMessages}}
Because this structure is a Decodable
enabled structure, it is able to decode our GraphQL responses into one of two values: our provided object (a product, for example) or error messages.
#Creating our GraphQL API
Lastly, create one more new file, and call it GraphQLAPI
. Again, make sure this is added to both iOS and Mac targets.
In this file, we'll create a method to take a GraphQLOperation
, make the request with our URLSession
extension, and decode the result into our objects, if possible by proxy of the GraphQLResult
structure.
class GraphQLAPI {func performOperation<Output: Decodable>(_ operation: GraphQLOperation) async throws -> Output {// Get the URLRequest from the provided operationlet request: URLRequest = try operation.getURLRequest()// Make the API calllet (data, _) = try await URLSession.shared.getData(from: request)// Attempt to parse into our `Output`let result = try JSONDecoder().decode(GraphQLResult<Output>.self, from: data)guard let object = result.object else {print(result.errorMessages.joined(separator: "\n"))throw NSError(domain: "Error", code: 1)}return object}}
#Retrieving Products
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}
Ensure this object is Decodable
in order to work with our operations, and Identifiable
so that it can be displayed in a list later on.
Next, let's create the operation:
swift
extension GraphQLOperation {
static var LIST_PRODUCTS: Self {
GraphQLOperation(
"""
{
products {
id
name
description
price
}
}
"""
)
}
}
This is a plain GraphQL string, wrapped in our GraphQLOperation
structure in order to work with our API class.
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(GraphQLOperation.LIST_PRODUCTS)) ?? []}}
This uses our LIST_PRODUCTS
operation, passing it to our GraphQLAPI
's performOperation
method. Should the operation complete 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, in this case returning an empty list. We could also handle the error here in some other way by just using try
with a do/catch
.
#Displaying Products
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 need 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 Hygraph, checkout the example for this project on Github