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 rootGraphQLResult
- Models which can contain
PostCollection
andPost
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.