SwiftUI Decoding JSON (iOS 13 Beta 5)
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
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 Int
s, 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
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