Displaying JSON data in SwiftUI

Hi Francis! Hi Chris!

It would be great if you could make a tutorial explaining how to display JSON data in SwiftUI.

There’s already several tutorials out there explaining this but they all use the List container to display the data. But Lists require your struct to be Identifiable, otherwise it won’t work:

struct Todo: Codable, Identifiable {
    public var id: Int
    public var title: String
    public var completed: Bool
}

In this example the response object from the API has an “id” key. But what if it didn’t have one? Then you cannot make it Identifiable, and therefore you cannot pass the data to a List container.

Therefore, my question is: how do you display JSON data in Content View without using the List container?

Thank you!

If the JSON data does not have an id as such, then you can add one in your struct like this:

struct Employee: Codable, Identifiable {
    let id = UUID()
    let title: String
    let name: String
    let address: String
    let zipCode: Int
}
1 Like

Thank you, Chris!

Now, what if I didn’t want to display the data in a List, what If I have a single piece of String data from the API call and I just want to display it as a Text element on Content View?

Text(Todo.title)

I already tried this but it didn’t work.

Your JSON data needs to be stored in an array so the simplest way to display array data is using a List. If you want to display a single item from that list then you need to know it’s position in the array (index).

In order to create or use an array of that struct definition, an instance of the struct needs to be created in ContentView a bit like the example that follows.

struct ToDo: Hashable {
    var title: String
    var completed: Bool
}

struct ContentView: View {
    @State private var todo = [ToDo]()
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach(todo, id: \.self) { item in
                    HStack {
                        Text(item.title)
                        Text(item.completed ? "True" : "False")
                        Spacer()
                    }
                }
            }
            .padding(.horizontal)
        }
        .onAppear() {
            self.populateArray()
        }
    }
    
    func populateArray() {
        todo = []
        var trueOrFalse = false
        for i in 0...20 {
            let newItem = ToDo(title: "Item \(i)", completed: trueOrFalse)
            todo.append(newItem)
            trueOrFalse.toggle()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

All I have done is manually populated the array with some random data just for the purposes of the exercise but if your array data source is a JSON file then you would decode that data into the array. Either way you need to create an instance of that struct in ContentView in order to use it so that is done at the line:

@State private var todo = [ToDo]()
1 Like

Thank you, Chris!

I would appreciate so much if you could also post the code for the API call, the URL being: https://jsonplaceholder.typicode.com/todos

Thanks!

check out our SwiftyJSON and Alamofire articles im sure it will help you


1 Like

Thank you, Francis!

However, the examples in those tutorials are for Storyboards, not SwiftUI.

It would be great if you could make a tutorial for JSON calls and displaying the response in SwiftUI.

Thanks!

Hi Francis. There’s no viewDidLoad in SwiftUI, so where in ContentView should I put the AF request?

i just put it there to make it load at launch, you can put in inside function calls if you want

1 Like

Thank you, Francis.

I already tried that, but it doesn’t seem to work.

Would you be so kind to post the code to put an AF request inside a function call?

Hi Dav,

I meant to get back to you earlier but here is a quick method to get your JSON data from the remote server:

import SwiftUI

struct ContentView: View {
    @State private var todo: [ToDo] = []
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach(todo, id: \.self) { item in
                    HStack {
                        Text(item.title)
                        Text(item.completed ? "True" : "False")
                        Spacer()
                    }
                }
            }
            .padding(.horizontal)
        }
        .onAppear() {
            self.populateArray()
        }
    }
    
    func populateArray() {
        let url = URL(string: "https://jsonplaceholder.typicode.com/todos")
        
        URLSession.shared.dataTask(with: url!) { (data, response, error) in
            guard error == nil else {
                print(error?.localizedDescription ?? "")
                return
            }
            
            guard data != nil else {
                print("There was no data to be found at \(url!)")
                return
            }
            
            let decoder = JSONDecoder()
            
            do {
                let loaded = try decoder.decode([ToDo].self, from: data!)
                todo = loaded
            } catch {
                print("Unable to decode JSON")
            }
        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

(edit)
And the struct definition to accompany it which I put in a separate file:

struct ToDo: Codable, Hashable {
    let id: Int
    let userID: Int
    let title: String
    let completed: Bool

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id, title, completed
    }
}
1 Like

no where in the tutorial/article did i ever use the “storyboard” part of the app, its all just purely code

its as simple as putting even the basic GET in a function of your storyboard

AF.request(“https://httpbin.org/get”).responseString { response in
debugPrint(“Response: (response)”)
}

Hi Francis,

Yes, I know you are not using the Storyboard part of the project, what I meant is that you are using the ViewController class, as opposed to ContentView, which is used in SwiftUI.

Therefore, my question is: How do I use the AF request in a SwiftUI project? If I just code the AF request into a Swift file I keep getting this error message: “Expressions are not allowed at the top level”.

I know I have correctly installed the Alamofire and SwiftyJson pods because I have already used them in another Storyboard project (by Storyboard I mean not SwiftUI) so that can’t be the problem.

I would like to learn how to use Alamofire and SwiftyJson in SwiftUI.

Thank you for your help!

Hi Chris,

Thank you so much for taking your time to help me with this!

The populateArray function seems to work just fine (just a minor correction, I added “self” where it says “todo = loaded” to read “self.todo = loaded”).

However, when I run the project I get a blank screen on the simulator.

I wonder if that has something to do with the way you are declaring the “todo” var:

 @State private var todo: [ToDo] = []

I guess you are giving it an empty value () so as to fill it later with the populateArray function. But then, shouldn’t you call this function here instead of using the onAppear method? Isn’t that the reason why the variable “todo” remains empty?

Thanks!

If you create a new test project and add all of that code I provided, it should work straight up.

Look closely at the ContentView code and you will see the line of code .onAppear { } which calls the populateArray() function as soon as the program launches.

What version of Xcode are you using?

Thank you, Chris.

That’s just what I did, i created a new test project and added all of that code, but I still get a blank screen on the simulator.

I’m using Xcode 11.7

That’s strange.

Just to be sure that you have no errors here is the code I am using (Target iOS is 13.6) in one single view which includes the struct ToDo:

import SwiftUI

struct ToDo: Codable, Hashable {
    let id: Int
    let userID: Int
    let title: String
    let completed: Bool

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id, title, completed
    }
}

struct ContentView: View {
    @State private var todo: [ToDo] = []
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 10) {
                    ForEach(todo, id: \.self) { item in
                        HStack {
                            Text("userID: \(item.userID)\nID: \(item.id)\nTitle: \(item.title)\nCompleted: \(item.completed ? "True" : "False")")
                            Spacer()
                        }
                        Divider()
                    }
                }
                .padding(.horizontal)
            }
            .navigationBarTitle("SwiftUI Example")
            .onAppear() {
                populateArray()
            }
        }
    }
    
    func populateArray() {
        let url = URL(string: "https://jsonplaceholder.typicode.com/todos")
        
        URLSession.shared.dataTask(with: url!) { (data, response, error) in
            guard error == nil else {
                print(error?.localizedDescription ?? "")
                return
            }
            
            guard data != nil else {
                print("There was no data to be found at \(url!)")
                return
            }
            
            let decoder = JSONDecoder()
            
            do {
                let loaded = try decoder.decode([ToDo].self, from: data!)
                todo = loaded
            } catch {
                print("Unable to decode JSON from \(url!)")
            }
        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The reason I declare the todo array like this:

@State private var todo: [ToDo] = []

is just so that I can populate it via the populateArray() function. Yes there are other ways to achieve that by setting up a function to return an array of the type [ToDo] but I just wanted to do it simply for the moment.

Hi Chris!

Thank you again for taking your time to help me!

Yes, my target is also iOS 13.6, I’m not sure what’s wrong .

I’m getting an error message in the following line:

ForEach(todo, id: \.self) { item in

The error message says: “Type ‘()’ cannot conform to ‘View’; only struct/enum/class types can conform to protocols”. This doesn’t make sense because the “todo” var is declared as a struct, referencing the “Todo” struct.

It’s so strange that the code runs just fine for you but not for me, I’m pretty sure I copied it exactly as you wrote it. I just pasted it into ContentView.swift

@dav_leda

The only difference is that I am using Xcode 12 but I deliberately set the target iOS to 13.6 given that you are using Xcode 11.7, so this is weird.

I just changed my version to add the conformance parameter Identifiable to the struct:

struct ToDo: Codable, Hashable, Identifiable {
    let id: Int
    let userID: Int
    let title: String
    let completed: Bool

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id, title, completed
    }
}

and then changed the ForEach to:

ForEach(todo) { item in

That works fine here in Xcode 12 so let’s see if that makes a difference to the fact that you are using Xcode 11.7

Can you post your entire ForEach loop? IME, this kind of message pops up when there is either missing body inside a SwiftUI element or there is an error with the code that is there (said error often being that there is something that is not a View present).