NEW: Join my free 100 Days of SwiftUI challenge today! >>

Sending and receiving Codable data with URLSession and SwiftUI

Paul Hudson    @twostraws   

iOS gives us built-in tools for sending and receiving data from the internet, and if we combine it with Codable support then it’s possible to convert Swift objects to JSON for sending, then receive back JSON to be converted back to Swift objects. Even better, when the request completes we can immediately assign its data to properties in SwiftUI views, causing our user interface to update.

To demonstrate this we can load some example music JSON data from Apple’s iTunes API, and show it all in a SwiftUI List. Apple’s data includes lots of information, but we’re going to whittle it down to just two types: a Result will store a track ID, its name, and the album it belongs to, and a Response will store an array of results.

So, start with this code:

struct Response: Codable {
    var results: [Result]
}

struct Result: Codable {
    var trackId: Int
    var trackName: String
    var collectionName: String
}

We can now write a simple ContentView that shows an array of results:

struct ContentView: View {
    @State var results = [Result]()

    var body: some View {
        List(results, id: \.trackId) { item in
            VStack(alignment: .leading) {
                Text(item.trackName)
                    .font(.headline)
                Text(item.collectionName)
            }
        }
    }
}

That won’t show anything at first, because the results array is empty. This is where our networking call comes in: we’re going to ask the iTunes API to send us a list of all the songs by Taylor Swift, then use JSONDecoder to convert those results into an array of Result instances.

To make this easier to understand, let’s write it in a few stages. First, here’s the basic method stub – please add this to the ContentView struct:

func loadData() {

}

We want that to be run as soon as our List is shown, so you should add this modifier to the List:

.onAppear(perform: loadData)

Inside loadData() we have four steps we need to complete:

  1. Creating the URL we want to read.
  2. Wrapping that in a URLRequest, which allows us to configure how the URL should be accessed.
  3. Create and start a networking task from that URL request.
  4. Handle the result of that networking task.

We’ll add those step by step, starting with the URL. This needs to have a precise format: “itunes.apple.com” followed by a series of parameters – you can find the full set of parameters if you do a web search for “iTunes Search API”. In our case we’ll be using the search term “Taylor Swift” and the entity “song”, so add this to loadData() now:

guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
    print("Invalid URL")
    return
}

Next we need to wrap that URL into a URLRequest. Again, this is where we would add different customizations to control the way the URL was loaded, but here we don’t need anything so this is just a single line of code – add this to loadData() next:

let request = URLRequest(url: url)

Step 3 is to create and start a networking task with the URLRequest we just made. This can feel like a fairly odd approach when you first see it, and it has a particularly common “gotcha” – a mistake you’ll make again and again, and probably still will make in a few years.

I’ll show you the code first, then explain what it does – add this to loadData():

URLSession.shared.dataTask(with: request) { data, response, error in
    // step 4
}.resume()

URLSession is the iOS class responsible for managing network requests. You can make your own session if you want to, but it’s very common to use the shared session that iOS creates for us to use – unless you need some specific behavior, using the shared session is fine.

Our code then calls dataTask(with:) on that shared session, which creates a networking task from a URLRequest and a closure that should be run when the task completes. In our code that’s provided using trailing closure syntax, and as you can see it accepts three parameters:

  • data is whatever data was returned from the request.
  • response is a description of the data, which might include what type of data it is, how much was sent, whether there was a status code, and more.
  • error is the error that occurred.

Now, cunningly some of those properties are mutually exclusive, by which I mean that if an error occurred then data won’t be set, and if data was sent back then error won’t be set. This strange state of affairs exists because the URLSession API was made before Swift came along, so there was no nicer way of representing this either-or state.

Notice the way we call resume() on the task straight away? That’s the gotcha – that’s the thing you’ll forget time and time again. Without it the request does nothing and you’ll be staring at a blank screen. But with it the request starts immediately, and control gets handed over to the system – it will automatically run in the background, and won’t be destroyed even after our method ends.

When the request finishes, successfully or not, step 4 kicks in – that’s the closure inside the data task, and is responsible for doing something with the data or error. In our case we’re going to check whether the data was set, and if it was try to decode it into an instance of our Response struct because that’s what the iTunes API sends back. We don’t actually want the whole response, just the results array inside it so that our List will show them all.

However, there’s another catch here: URLSession automatically runs in the background, which means its completion closure will also be run in the background. By “background” I mean what’s technically known as a background thread – an independent piece of code that’s running at the same time as the rest of our program. This means the network request can be running, and even take a few seconds, without stopping our UI from being interactive.

iOS likes to have all its user interface work done on what’s called the main thread, which is the one where the program was started. This stops two pieces of code trying to manipulate the user interface simultaneously, because if all UI-related work takes place on the main thread then it can’t clash.

We want to change the results property of our view to be whatever was downloaded through the iTunes API, which in turn will update our user interface. That might work great on a background thread because SwiftUI is super smart, but honestly it’s just not worth the risk – it’s a much better idea to fetch your data in the background, decode it from JSON in the background, then actually update the property on the main thread to avoid any potential for problems.

iOS gives us a very particular way of sending work to the main thread: DispatchQueue.main.async(). This takes a closure of work to perform, and sends it off to the main thread for execution. As you can see from its name, what’s actually happening is that it’s added to a queue – a big line of work that’s waiting for execution. The “async” part is short for “asynchronous”, which means our own background work won’t wait for the closure to be run; we just add it to the queue and carry on working in the background.

So, put this final code in place of the // step 4 comment:

if let data = data {
    if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
        // we have good data – go back to the main thread
        DispatchQueue.main.async {
            // update our UI
            self.results = decodedResponse.results
        }

        // everything is good, so we can exit
        return
    }
}

// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")

That last print() line uses optional chaining and the nil coalescing operator to make sure an error is printed if it exists, otherwise give a generic error.

If you run the code now you should see a list of Taylor Swift songs appear after a short pause – it really isn’t a lot of code given how well the end result works.

Later on in this project we’re going to look at how to customize URLRequest so you can send Codable data, but that’s enough for now – please put ContentView.swift back to its original state so we can begin work.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5