Working with Swift and Hygraph

As phone applications become increasingly complex, working with Swift and GraphQL endpoints. This post shows example of working with GraphQL endpoints in Swift.

Craig Tweedy
Craig Tweedy
working-with-graphql-swift-hygraph

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 StartedAnchor

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 URLSessionAnchor

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 in
let task = self.dataTask(with: urlRequest) { data, response, error in
guard 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 StructureAnchor

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: String
private let url = URL(string: "your graphql endpoint")!
enum CodingKeys: String, CodingKey {
case variables
case 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 ObjectAnchor

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 data
case 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.first
var 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 APIAnchor

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 operation
let request: URLRequest = try operation.getURLRequest()
// Make the API call
let (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 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().uuidString
let name: String
let description: String
let 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 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 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