Article

SwiftUI Decoding JSON (iOS 13 Beta 5)

First Published: August 7th, 2019
Last Updated: 20 days ago

Watch Video
Duration: 4m 25s

Summary

Several API changes in the iOS & iPad0S 13 Beta 5 release affect getting data from a JSON API. For example, BindableObject is replaced by the ObservableObject protocol from the Combine framework, the property wrapper @ObjectBinding is replaced by @ObservedObject, and properties marked with @Published automatically emit updates. This short video shows you how to fetch and decode data from a JSON REST API using the Beta 5 API.

Setup

In Xcode 11 Beta 5, start a new project (⇧⌘N), select “Single View App” (iOS), enter “TodoList” in the “Product Name” input field, and ensure “Use SwiftUI” is selected.

Starting Code

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

JSON Data

We will get our JSON data from JSONPlaceholder, a fake REST API. The URL is https://jsonplaceholder.typicode.com/todos/. This endpoint returns a list of 200 To Do items.

The JSON data looks like this:

let todosEndpoint = "https://jsonplaceholder.typicode.com/todos/"

// JSON returned by "https://jsonplaceholder.typicode.com/todos/":
//    [
//        {
//            "userId": 1,
//            "id": 1,
//            "title": "delectus aut autem",
//            "completed": false
//        },
//        {
//            "userId": 1,
//            "id": 2,
//            "title": "quis ut nam facilis et officia qui",
//            "completed": false
//        },
//        ...
//    ]

Todo Model

Create a Todo struct, to hold each To Do item, and make it conform to the Codable and Identifiable protocols. In the JSON data, the userId and id fields are Ints, the title field is a String, and the completed field is a Bool. Add the properties that we want to decode using Coding Keys.

struct Todo: Codable, Identifiable {
    let userId, id: Int
    let title: String
    let completed: Bool

    enum CodingKeys: CodingKey {
        case userId, id, title, completed
    }
}

Add a typealias for the array of Todos.

typealias Todos = [Todo]

Get Data from REST API

Now, create a class that will facilitate the downloading of the To Do items from the REST API. Call it TodoDownloader, and make it conform to the ObservableObject protocol. Note that ObservableObject has replaced BindableObject, which was used in previous betas. Any property marked with the @Published property wrapper emits changes, which will be watched by observers.

Create an initializer. Get the URL from the endpoint string. Set up a shared dataTask on a URLSession with this URL. In the completion handler, set up a do/catch block. Guard against the returned data being nil.

try to decode, using the JSONDecoder, on the Todos type, from the data. Dispatch this async task to return to the main queue after it completes. If there is an error, print it out. resume operation of the dataTask.

class TodoDownloader: ObservableObject {
    @Published var todos: Todos = [Todo]()

    init() {
        guard let url = URL(string: todosEndpoint) else { return }
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                guard let data = data else { return }
                let todos = try JSONDecoder().decode(Todos.self, from: data)
                DispatchQueue.main.async {
                    self.todos = todos
                }
            } catch {
                print("Error decoding JSON: ", error)
            }
        }.resume()
    }
}

Show List of To Do Items

Go to the ContentView. Use the ObservedObject property wrapper to listen to changes in the todoData, which was downloaded. Note that ObservedObject has replaced ObjectBinding, which was used in previous betas. Remove the default Text. Add a NavigationView. List the todos from the todoData, with their title text. Finally, add a navigationBarTitle.

struct ContentView: View {
    @ObservedObject var todoData: TodoDownloader = TodoDownloader()

    var body: some View {
        NavigationView {
            List(self.todoData.todos) { todo in
                Text(todo.title)
            }
        .navigationBarTitle(Text("To Do List"))
        }
    }
}

Build and run.

Final Code

ContentView.swift
import SwiftUI

let todosEndpoint = "https://jsonplaceholder.typicode.com/todos/"

// JSON returned by "https://jsonplaceholder.typicode.com/todos/":
//    [
//        {
//            "userId": 1,
//            "id": 1,
//            "title": "delectus aut autem",
//            "completed": false
//        },
//        {
//            "userId": 1,
//            "id": 2,
//            "title": "quis ut nam facilis et officia qui",
//            "completed": false
//        },
//        ...
//    ]

struct Todo: Codable, Identifiable {
    let userId, id: Int
    let title: String
    let completed: Bool

    enum CodingKeys: CodingKey {
        case userId, id, title, completed
    }
}

typealias Todos = [Todo]

class TodoDownloader: ObservableObject {
    @Published var todos: Todos = [Todo]()

    init() {
        guard let url = URL(string: todosEndpoint) else { return }
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                guard let data = data else { return }
                let todos = try JSONDecoder().decode(Todos.self, from: data)
                DispatchQueue.main.async {
                    self.todos = todos
                }
            } catch {
                print("Error decoding JSON: ", error)
            }
        }.resume()
    }
}

struct ContentView: View {
    @ObservedObject var todoData: TodoDownloader = TodoDownloader()

    var body: some View {
        NavigationView {
            List(self.todoData.todos) { todo in
                Text(todo.title)
            }
        .navigationBarTitle(Text("To Do List"))
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif