从 SwiftUI 中的嵌套 JSON API 获取数据(对象在数组中的对象在另一个对象中)

Bei*_*Bei 0 api json nested object swiftui

初学者,对此有点难以理解。;)

我找到了一些示例,这些示例向我展示了如何从 JSON API feed 中获取数据(如果 feed 的结构为对象数组),但我不知道如何在以下情况下获取数据(特别是urltitle) :我正在检索的数据以更复杂的嵌套结构返回,如下所示:

{
    "races": {
        "videos": [{
            "id": 1,
            "url": "firsturl",
            "title": "1st Video Title"
        }, {
            "id": 2,
            "url": "secondurl",
            "title": "2nd Video Title"
        }]
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经成功地从另一个 API 提要中获取数据,该提要的结构为一个简单的对象数组——它类似于上面的内容,但没有额外的两个导入对象,即: { "races": { "videos":

这是我从几个适用于简单数组的示例中拼凑而成的代码:

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)")
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我花了几天的时间阅读和观看教程,但到目前为止,没有任何东西可以帮助我处理更复杂的 JSON API feed。任何提示将非常感谢!

更新:

在 Swift Playground 教程和下面评论中提到的建议结构的帮助下,我成功地检索了更复杂的数据,但仅在 Swift Playgrounds 中使用:

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 )
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,我正在努力解决如何将此 Playgrounds 片段正确集成到工作 SwiftUI 文件的 VideosViews 等中。

更新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)")
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

jn_*_*pdx 6

有一个名为 QuickType (app.quicktype.io) 的很棒的网站,您可以在其中粘贴一些 JSON 并获取为您生成的 Swift 结构。这是它为您提供的:

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
}

Run Code Online (Sandbox Code Playgroud)

他们的模板生成器中有一个错误,该错误破坏了演示行(我已经提交了一个已合并的拉取请求,但在撰写本文时尚未在网站上发布),但它应该是这样的:

let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
Run Code Online (Sandbox Code Playgroud)

使用do/try这样您可以捕获错误,您可以通过执行以下操作来解码数据并到达较低级别:

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
}
Run Code Online (Sandbox Code Playgroud)

根据您的评论和更新进行更新:您选择的路线与我最初的建议略有不同,但这很好。我唯一建议的是,您可能希望在某个时刻处理处理错误,而不是仅仅返回nil所有完成结果(假设您需要处理错误 - 也许只是不加载是可以接受的)。

这是代码的简单重构:

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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以看到现在Fetcher有一​​个@Published用于存储曲目的属性 ([VideoItem])。getJSON仍然在 fetcher 中,但现在private只是为了表明它不应该被直接调用。但是,现在有一个名为fetchData()您的视图将调用的新函数。当fetchData获取数据时,它将 @Published 属性设置为该数据。我使用??运算符告诉编译器,如果followers为 nil,则直接使用[]即可。这一切都在一个DispatchQueue.main.async块中,因为 URL 调用可能不会在主线程上返回,并且我们需要确保始终更新主线程上的 UI(如果您更新 UI,Xcode 会在运行时警告您这一点)不同的线程)。

JSONRacesView呼入,这恰好fetchDataonAppear听起来会发生的时候发生。

最后要注意的是我用的@StateObject@ObservedObject. 如果您尚未使用 iOS 14 或 macOS 11,则可以改用 @ObservedObject。有一些差异超出了这个答案的范围,但很容易通过谷歌搜索到。