0

Beginner here, in a bit over my head with this. ;)

I've found examples that have shown me how to get data from a JSON API feed if the feed is structured as an array of objects, but I don't know how to approach getting the data (specifically, url and title) if the data I'm retrieving comes back in a more complex nested structure like this one:

{
    "races": {
        "videos": [{
            "id": 1,
            "url": "firsturl",
            "title": "1st Video Title"
        }, {
            "id": 2,
            "url": "secondurl",
            "title": "2nd Video Title"
        }]
    }
}

I've succeeded at get data from another API feed that's structured as a simple array of objects--it's like what's above but without the extra two lead-in objects, namely this: { "races": { "videos":

Here's the code I pieced together from a few examples that worked for the simple array:

import SwiftUI

struct Video: Codable, Identifiable {
    public var id: Int
    public var url: String
    public var title: String
}

class Videos: ObservableObject {
  @Published var videos = [Video]()
     
    init() {
        let url = URL(string: "https://exampledomain.com/jsonapi")!
        URLSession.shared.dataTask(with: url) {(data, response, error) in
            do {
                if let videoData = data {
                    let decodedData = try JSONDecoder().decode([Video].self, from: videoData)
                    DispatchQueue.main.async {
                        self.videos = decodedData
                    }
                } else {
                    print("no data found")
                }
            } catch {
                print("an error occurred")
            }
        }.resume()
    }
}

struct VideosView: View {
    @ObservedObject var fetch = Videos()
    var body: some View {
        VStack {
            List(fetch.videos) { video in
                VStack(alignment: .leading) {
                    Text(video.title)
                    Text("\(video.url)")
                }
            }
        }
    }
}

I've spent several hours over a few days reading and watching tutorials, but so far nothing is sinking in to help me tackle the more complex JSON API feed. Any tips would be greatly appreciated!

UPDATE:

With the help of a Swift Playground tutorial and the suggested structs mentioned in the comments below, I've succeeded at retrieving the more complex data, but only in Swift Playgrounds, using this:

import SwiftUI

struct Welcome: Codable {
    let races: Races
}

struct Races: Codable {
    let videos: [Video]
}

struct Video: Codable {
    let id: Int
    let url, title: String
}

func getJSON<T: Decodable>(urlString: String, completion: @escaping (T?) -> Void) {
    guard let url = URL(string: urlString) else {
        return
    }
    let request = URLRequest(url: url)
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        if let error = error {
            print(error.localizedDescription)
            completion(nil)
            return
        }
        guard let data = data else {
            completion(nil)
            return
        }
        let decoder = JSONDecoder()
        guard let decodedData = try? decoder.decode(T.self, from: data) else {
            completion(nil)
            return
        }
        completion(decodedData)
    }.resume()
}

getJSON(urlString: "https://not-the-real-domain.123/api/") { (followers:Welcome?) in
    if let followers = followers {
        for result in followers.races.videos {
            print(result.title )
        }
    }
}

Now, I'm struggling with how to properly integrate this Playgrounds snippet in to the working SwiftUI file's VideosViews, etc.

UPDATE 2:

import SwiftUI

struct Welcome: Codable {
    let races: RaceItem
}

struct RaceItem: Codable {
    let videos: [VideoItem]
}

struct VideoItem: Codable {
    let id: Int
    let url: String
    let title: String
}

class Fetcher: ObservableObject {
    func getJSON<T: Decodable>(urlString: String, completion: @escaping (T?) -> Void) {
        guard let url = URL(string: urlString) else {
            return
        }
        let request = URLRequest(url: url)
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                print(error.localizedDescription)
                completion(nil)
                return
            }
            guard let data = data else {
                completion(nil)
                return
            }
            let decoder = JSONDecoder()
            guard let decodedData = try? decoder.decode(T.self, from: data) else {
                completion(nil)
                return
            }
            completion(decodedData)
        }.resume()
    }
}

struct JSONRacesView: View {
    
    @ObservedObject var fetch = Fetcher()
    
    getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
        if let followers = followers {
            for result in followers.races.videos {
                print(result.title )
            }
        }
    }
    
    var body: some View {
        VStack {
            List(fetch.tracks) { track in
                VStack(alignment: .leading) {
                    Text(track.title)
                    Text("\(track.url)")
                }
            }
        }
    }

1 Answer 1

6

There's a great site called QuickType (app.quicktype.io) where you can paste in some JSON and get the Swift structs generated for you. Here's what it gives you:

import Foundation

// MARK: - Welcome
struct Welcome: Codable {
    let races: Races
}

// MARK: - Races
struct Races: Codable {
    let videos: [Video]
}

// MARK: - Video
struct Video: Codable {
    let id: Int
    let url, title: String
}

They have a bug in their template generator that mangles the demo line (I've submitted a pull request that is merged but isn't live on the site at the time of this writing), but here's what it should look like:

let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)

Using do/try so you can catch the errors, you can decode the data and reach the lower levels by doing:

do {
  let welcome = try JSONDecoder().decode(Welcome.self, from: jsonData)
  let videos = welcome.races.videos //<-- this ends up being your [Video] array
} catch {
  //handle any errors
}

Update, based on your comments and updates: You chose to go a little bit of a different route than my initial suggestion, but that's fine. The only thing that I would suggest is that you might want to deal with handling errors at some point rather than just returning nil in all of the completions (assuming you need to handle errors -- maybe it just not loading is acceptable).

Here's a light refactor of your code:

class Fetcher: ObservableObject {
    @Published var tracks : [VideoItem] = []
    
    private func getJSON<T: Decodable>(urlString: String, completion: @escaping (T?) -> Void) {
        guard let url = URL(string: urlString) else {
            return
        }
        let request = URLRequest(url: url)
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                print(error.localizedDescription)
                completion(nil)
                return
            }
            guard let data = data else {
                completion(nil)
                return
            }
            let decoder = JSONDecoder()
            guard let decodedData = try? decoder.decode(T.self, from: data) else {
                completion(nil)
                return
            }
            completion(decodedData)
        }.resume()
    }
    
    func fetchData() {
        getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
            DispatchQueue.main.async {
                self.tracks = followers?.races.videos ?? []
            }
        }
    }
}

struct JSONRacesView: View {
    @StateObject var fetch = Fetcher()
    
    var body: some View {
        VStack {
            List(fetch.tracks, id: \.id) { track in
                VStack(alignment: .leading) {
                    Text(track.title)
                    Text("\(track.url)")
                }
            }
        }.onAppear {
            fetch.fetchData()
        }
    }
}

You can see that now Fetcher has a @Published property that will store the tracks ([VideoItem]). getJSON is still in fetcher, but now it's private just to show that it isn't meant to be called directly. But, now there's a new function called fetchData() that your view will call. When fetchData gets data back, it sets the @Published property to that data. I used the ?? operator to tell the compiler that if followers is nil, then just use [] instead. This is all in a DispatchQueue.main.async block because the URL call is probably not going to return on the main thread and we need to make sure to always update the UI on the main thread (Xcode will warn you about this at runtime if you update the UI on a different thread).

JSONRacesView calls fetchData in onAppear, which happens exactly when it sounds like it will.

Last thing to note is I used @StateObject instead of @ObservedObject. If you're not on iOS 14 or macOS 11 yet, you could use @ObservedObject instead. There are some differences outside the scope of this answer, but that are easily Google-able.

Sign up to request clarification or add additional context in comments.

8 Comments

Thanks for recommending QuickType. I had tried that site previously but had a hard time figuring out how to put all the pieces together into a real SwiftUI project. I ended up getting something to work by combining your QuickType struct suggestions with a Swift Playground tutorial (youtube.com/watch?v=ivFtzG8Bqkk&t=0s) that used a different approach, but I'm struggling with how to turn the playground into a proper SwiftUI file. Going to read some more...
Okay, I updated my question with more data to show some additional progress (if I can call it that). ;)
Not sure I understand what the problem is. You know how to get the data and decode it. Can't you just put your getJSON func in your ObservableObject and then set self.videos to the result once you've decoded it: self.videos = followers.races.videos?
I did manage to figure out that I needed to embed the getJSON function into the ObservableObject, but in trying to put the function call "getJSON(urlString..." into the SwiftUI View, I'm getting all kinds of errors, so I imagine either it's in the wrong place or there's some other things I'm not comprehending. As I said, I think I'm in a bit over my head, having just started learning to program in January. I'll post what I've assembled above as "Update 2".
You can't call getJSON outside of a function like that. I'll post a modification of your code momentarily
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.