Make and Parse an API call using SwiftUI

Chase
8 min readFeb 9, 2023

--

Making an API call is a common occurrence in almost every app. Here, we will walk though one way of making and parsing an API call using Swift and SwiftUI.

In our example we will request data from the endpoint (we will use https://jsonplaceholder.typicode.com/posts), receive the response in JSON, convert the JSON response into a Swift object, and then display that data in a list in SwiftUI.

Possible points of failure

When making a call to an external endpoint that we may or may not control, there are several steps in the process that could cause our request to fail. For example, we may have mistyped the address of the end point, maybe the server was down, maybe we didn’t have the correct authentication or authorization we needed to access that end point, etc.

One of our responsibilities as a developer is to make sure we code defensively, meaning that we should try to protect our code from failing on any error situation that we can reasonably assume.

In our example, we aren’t reaching out to an authenticated endpoint, so we won’t try protecting ourselves against a specific bad auth status. We do however still have some things to defend against, like a bad URL, bad request, bad response, etc. For our example we will create an enumeration of the errors that we can reasonably expect to defend against.

enum NetworkError: Error {
case badUrl
case invalidRequest
case badResponse
case badStatus
case failedToDecodeResponse
}

Create a Generic parsing service to fetch the data

If you think the code below looks too complicated, don’t leave just yet. You’ve got this; let’s keep going. While using Generics in Swift is a study in and of itself, at its most basic level a Generic type is exactly what it sounds like. It is a placeholder that can hold whatever type you want to pass into it. In Swift code you will usually see them represented as a capital “T”.

In the example below, we create a downloadData function that takes a URL from which we will try to fetch the data, and then it will try to return that data as whatever type we want. In our case, we want an array of Post objects. It will also try to ensure that we are doing our part as developers by safely handling any errors that may be returned along the journey of our request. For the sake of this example, we are doing a simple print statement to help us know what errors may have occurred in our process.

class WebService {
func downloadData<T: Codable>(fromURL: String) async -> T? {
do {
guard let url = URL(string: fromURL) else { throw NetworkError.badUrl }
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
guard let decodedResponse = try? JSONDecoder().decode(T.self, from: data) else { throw NetworkError.failedToDecodeResponse }

return decodedResponse
} catch NetworkError.badUrl {
print("There was an error creating the URL")
} catch NetworkError.badResponse {
print("Did not get a valid response")
} catch NetworkError.badStatus {
print("Did not get a 2xx status code from the response")
} catch NetworkError.failedToDecodeResponse {
print("Failed to decode response into the given type")
} catch {
print("An error occured downloading the data")
}

return nil
}
}

Parsing a JSON object into a Swift struct

Lets looks at one piece of example data from the Posts endpoint:

{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},

This is the object that we will model in Swift. In our example, we will call this a Post object. As we can see from above, our Post object has the following keys: “userId,” “id,” “title,” and “body.” Since Swift is a typed language, we will want to assign a type to the values of the keys.

Based on the Post object, it seems reasonable to say that the “userId” and “id” can be an Integer, and the “title” and “body” values would be a String type. For more information on how we know what types these values are, we can research “Type Inference” in Swift.

Now that we know the keys, and the types for the values in our Post object, we are ready to create that Post object using Swift. In Swift, these types of objects are normally created using a “struct” and are often referred to as Models (the first M from the MVVM pattern).

struct Post: Identifiable, Codable {
let userId: Int
let id: Int
let title: String
let body: String
}

You may be asking yourself why we added “Identifiable” and “Codable” to our Post struct.

Identifiable is a protocol that will be useful once we display this data in a List in SwiftUI. It lets SwiftUI know that each of these values already has a unique key assigned to it.

Codable is wrapper around a set of protocols in Swift that will allow us to decode JSON into our Post model type, and also encode our Post model type back into JSON (should we need to make a post request to the endpoint).

Since for our example we only want to Decode the data from JSON to Swift, we could replace the call of Codeable with Decodable. However, the overhead of adding the extra conformance to Encodable is minimal, and we get the added benefit of being able to convert the Swift object back into JSON. I will continue to use Codeable in the example.

Building out our View Model

One of the most common patterns that we use to display data in a SwiftUI view is to use a View Model (the VM of MVVM).

Our View Model is a simple class that conforms to ObservableObject so that when the data in our view model changes, it will tell SwiftUI to update the view. Using the “@Published” wrapper on the postData variable tells SwiftUI, “When this value changes, I want whatever view it is used in to be re-rendered.” We will also add “@MainActor” before the class to the compiler know that this code will be run on the main thread.

@MainActor class PostViewModel: ObservableObject {
@Published var postData = [Post]()

func fetchData() async {
guard let downloadedPosts: [Post] = await WebService().downloadData(fromURL: "https://jsonplaceholder.typicode.com/posts") else {return}
postData = downloadedPosts
}
}

Building out our view in SwiftUI

Finally we get to build our view so that we can see the data we requested (this is the V of the MVVM pattern). SwiftUI makes this part simple and relatively easy.

First we want to initialize our View Model, and let our view know that we want the view model to change state, so we add “@StateObject” to our vm variable.

For our example, we want to display the data in some sort of list format. In SwiftUI, this is as simple as adding a List to our view. Since we want a list of postData, we pass that variable into the list as its only argument. If our Post object did not conform to Identifiable, we would have to pass a second argument with a unique ID for each item in our list. For more information on using a List view in SwiftUI, check out my other article below.

Styling components in SwiftUI is as easy as adding a modifier to a view. In the example below, we are adding a default padding to a view, changing the color, font style, font size, and adding a limit to how many lines each piece of text will display. We are also setting up our layout using HStack (horizontal stack) and VStack (vertical stack).

The onAppear modifier at the bottom of the code runs each time our list appears on the screen. Inside this check, we look to see if the postData array is empty, and if it is, we start a task that fetches and then assigns the postData in the View Model.

struct ContentView: View {
@StateObject var vm = PostViewModel()

var body: some View {
List(vm.postData) { post in
HStack {
Text("\(post.userId)")
.padding()
.overlay(Circle().stroke(.blue))

VStack(alignment: .leading) {
Text(post.title)
.bold()
.lineLimit(1)

Text(post.body)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
}
.onAppear {
if vm.postData.isEmpty {
Task {
await vm.fetchData()
}
}
}
}
}

Putting it all together

Putting all the code together, we end up with the following:

import SwiftUI

struct Post: Identifiable, Codable {
let userId: Int
let id: Int
let title: String
let body: String
}

enum NetworkError: Error {
case badUrl
case invalidRequest
case badResponse
case badStatus
case failedToDecodeResponse
}

class WebService: Codable {
func downloadData<T: Codable>(fromURL: String) async -> T? {
do {
guard let url = URL(string: fromURL) else { throw NetworkError.badUrl }
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
guard let decodedResponse = try? JSONDecoder().decode(T.self, from: data) else { throw NetworkError.failedToDecodeResponse }

return decodedResponse
} catch NetworkError.badUrl {
print("There was an error creating the URL")
} catch NetworkError.badResponse {
print("Did not get a valid response")
} catch NetworkError.badStatus {
print("Did not get a 2xx status code from the response")
} catch NetworkError.failedToDecodeResponse {
print("Failed to decode response into the given type")
} catch {
print("An error occured downloading the data")
}

return nil
}
}

class PostViewModel: ObservableObject {
@Published var postData = [Post]()

func fetchData() async {
guard let downloadedPosts: [Post] = await WebService().downloadData(fromURL: "https://jsonplaceholder.typicode.com/posts") else {return}
postData = downloadedPosts
}
}

struct ContentView: View {
@StateObject var vm = PostViewModel()

var body: some View {
List(vm.postData) { post in
HStack {
Text("\(post.userId)")
.padding()
.overlay(Circle().stroke(.blue))

VStack(alignment: .leading) {
Text(post.title)
.bold()
.lineLimit(1)

Text(post.body)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
}
.onAppear {
if vm.postData.isEmpty {
Task {
await vm.fetchData()
}
}
}
}
}

And here is what that data looks like in our preview window:

Screenshot of the example data that we downloaded from the endpoint in this project running on an iOS simulator.

Congratulations! Now you know how to get data from an API using Swift, as well as how to parse and display the data.

Take Your List to the Next Level by Filtering it with Search

If you want to take your list to the next level, check out the following tutorial to learn how to add search to your list: https://medium.com/@jpmtech/how-to-add-search-to-your-swiftui-app-2d724bf72c16

If you got value from this article, please consider following me, clapping for this article, or sharing it to help others more easily find it.

If you have any questions on the topic, or know of another way to accomplish the same task, feel free to respond to the post or share it with a friend and get their opinion on it.

If you want to learn more about native mobile development, you can check out the other articles I have written here: https://medium.com/@jpmtech

If you want to see apps that have been built with native mobile development, you can check out my apps here: https://jpmtech.io/apps

Thank you for taking the time to check out my work!

--

--