Working with Swift, GraphQL, and Grafbase

Working with Swift, GraphQL, and Grafbase

Swift is a powerful programming language that is easy and intuitive enough to get started with, even if you're someone new to programming.

Using Swift you can build amazing experiences and native applications for iOS, iPadOS, macOS, tvOS, and watchOS.

In this guide we'll explore adding a GraphQL backend (Grafbase), and add an API service layer with Swift to your existing application.

Let's begin by creating a GraphQL backend using the Grafbase CLI.

In the root of your existing Swift application run the following:

npx grafbase init --template https://github.com/grafbase/grafbase/tree/main/examples/swift

Then open the generated grafbase/schema.graphql file. You should see the following schema that defines your backend:

type Post @model {
  title: String!
  body: String!
  comments: [Comment]
}

type Comment @model {
  message: String!
  post: Post
}

We can use the Grafbase CLI to run the GraphQL backend locally:

npx grafbase dev

You should see a success message that your GraphQL Playground and API is running at http://localhost:4000.

In this guide we'll only explore querying data from our backend, so you will need to populate the backend using a GraphQL mutation inside the playground.

mutation {
  postCreate(
    input: {
      title: "Swift + GraphQL!"
      body: "Hello from Grafbase."
      comments: [
        { create: { message: "GraphQL is awesome!" }
        { create: { message: "Another comment from Grafbase" } }
      ]
    }
  ) {
    post {
      id
    }
  }
}

To use URLSession with await/async we will create an extension on URLSession.

Create a file and add the following:

import Foundation

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()
        }
    }
}

We will then create a GraphQL layer on top of URLSession and Codable which can make API calls, and return objects:

import Foundation

struct GraphQLOperation: Encodable {
    var operationString: String

    private let url = "http://localhost:4000/graphql"

    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 {
        guard let url = URL(string: self.url), self.url != "" else {
            fatalError("Please fill in your URL")
        }
        var request = URLRequest(url: url)

        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(self)

        return request
    }
}

class GraphQLAPI {
    func performOperation<Output: Decodable>(_ operation: GraphQLOperation) async throws -> Output {
        let request: URLRequest = try operation.getURLRequest()

        let (data, _) = try await URLSession.shared.getData(from: request)

        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
    }
}

You'll want to change the value for url from http://localhost:4000/graphql to the API endpoint Grafbase provides once you deploy, or use environment variables with the Xcode Schema Editor for a better developer experience.

Now create a file that contains the following GraphQL query:

import Foundation

extension GraphQLOperation {
    static var LIST_POSTS: Self {
        GraphQLOperation(
            """
                {
                    postCollection(first:10) {
                        edges {
                            node {
                            id
                            title
                            body
                            comments(first: 10) {
                                edges {
                                node {
                                    id
                                    message
                                }
                                }
                            }
                            }
                        }
                        }
                }
            """
        )
    }
}

We now have the basic API layer needed to make requests. You can add more queries and mutations to this file. The API will now be able to convert the GraphQL data into a Codable object.

So we can interface correctly with the API and have typed responses, we will need to create the Codable models.

There are three main areas we need:

  • A model to represent Edge's, Node's and our root GraphQLResult
  • Models which can contain PostCollection and Post data
  • A Model which can contain Comment data.

First, create a new file for Edge's, Node's and the GraphQLResult:

import Foundation

struct Edge<T: Decodable>: Decodable {
    let edges: [Node<T>];
}

struct Node<T: Decodable>: Decodable{
    let node: T;
}

struct GraphQLResult<T: Decodable>: Decodable {
    var object: T?
    var 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)
        do {
            self.object = try container.decodeIfPresent(T.self, forKey: .data)
        } catch {
            print(error)
        }

        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
    }
}

Our system will make use of generics in order to cast appropriately to the correct objects.

As well as this, our GraphQLResult object will be responsible for decoding the root object and also storing errors, if they occur.

Next we want both the PostCollection and Post models:

import Foundation

struct PostCollection: Decodable {
    let postCollection: Edge<Post>
}

struct Post: Decodable, Identifiable, Hashable {
    var id: String = UUID().uuidString
    let title: String
    var body: String = ""
    var comments: Edge<Comment>?

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func ==(lhs: Post, rhs: Post) -> Bool {
        return lhs.id == rhs.id
    }
}

Here we make sure that our models are Decodable, while also making sure the Post is Identifiable and Hashable so that it can be used in some list views later on.

We've added a reference to Comment here, but we don't yet have the Comment model, so let's add that too:

struct Comment: Decodable, Identifiable, Hashable {
    var id: String = UUID().uuidString
    let message: String
}

Finally, let's add a service layer which can make a request to the API and return us a PostCollection.

In a new file, add the following:

import Foundation

class APIService {
    let api: GraphQLAPI = GraphQLAPI()

    func listPosts() async -> PostCollection? {
        return (
            try? await self.api.performOperation(GraphQLOperation.LIST_POSTS)
        )
    }
}

With all this in place, we can now start creating our views.

Inside the main view, we can load and list the Post entries:

import SwiftUI

struct ContentView: View {

    @State var posts: [Post] = []
    @State private var selectedPost: Post?
    let apiService: APIService = APIService()

    func loadPosts() async {
        self.posts = await self.apiService.listPosts()?.postCollection.edges.map({ $0.node }) ?? []
    }

    var body: some View {
        NavigationSplitView {
            List(self.posts, id: \.id, selection: $selectedPost) { post in
                NavigationLink(post.title, value: post)
            }
            .navigationTitle("Posts")
        } detail: {
            if let post = self.selectedPost {
                DetailView(post: post).navigationTitle(post.title)
            } else {
                Text("Select a post")
            }
        }
        .onAppear {
            Task.init {
                await self.loadPosts()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(posts: [
            Post(title: "Post 1"),
            Post(title: "Post 2"),
            Post(title: "Post 3"),
            Post(title: "Post 4"),
            Post(title: "Post 5"),
        ])
    }
}

Here we make use of NavigationSplitView and List in order to get some basic UI in place which can work across multiple devices, and display simple amounts of data.

We also use onAppear and Task.init in order to load our Post's once the view has loaded.

Within our List, we make use of the $selectedPost and NavigationLink in order to make sure we're storing a reference to the Post the user has selected, and finally in our detail block, if there is a $selectedPost, we show a DetailView and pass that Post through.

We don't yet have a DetailView, so let's add that in a new file:

import SwiftUI

struct DetailView: View {
    var post: Post

    var body: some View {
        VStack(alignment: .leading) {
            Text(post.title).font(.subheadline)
                .padding(.bottom, 10)
            Spacer()
            Text(post.body).padding(.bottom, 10)
            Spacer()
            if (post.comments?.edges ?? []).count > 0 {
                Text("Comments").font(.headline).padding(.bottom, 10)
                List((post.comments?.edges ?? []).map({$0.node}), id: \.id) { comment in
                    Text(comment.message)
                }
            }
        }
        .padding()
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView(
            post: Post(
                title: "Title"
            )
        )
    }
}

With this, we take the Post that has been given to the DetailView, and show some of its basic information, such as its title and body.

Additionally, if this Post has comments, we also show those comments in another List.

That's it! We now have a basic Swift application which can pull the content from a GraphQL backend and show it in the UI.

Get Started

Deploy your Swift backend in minutes with Grafbase.