【问题标题】:Parsing a JSON file in Swift在 Swift 中解析 JSON 文件
【发布时间】:2020-05-04 05:34:17
【问题描述】:

我是 Swift 新手,我正在开发的应用程序遇到问题。我正在尝试解析 API 提供的一些 JSON,但遇到了程序无法将 JSON 解码为结构提供的 Swift 类型的问题。

Recipe_API_Caller.swift

 import Foundation
 import UIKit

class call_Edamam_API{

func fetch(matching query: [String: String], completion: @escaping ([Recipe]?)-> Void){
        let baseURL = URL(string: "https://api.edamam.com/search")!

        guard let url = baseURL.withQueries(query)
            else{
                completion(nil)
                print("Cannot build URL")
                return
    }
    print(url)


        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

            let Recipe_Decoder = JSONDecoder()

            if let data = data,

                let Recipes = try?
                    Recipe_Decoder.decode(Hit.self, from: data){

                completion(Recipes.hits)
            }
            else{
                print("JSON Decoding Error")
                completion(nil)
                return
            }
        }

        task.resume()


}
}

Recipe.swift

import Foundation
struct Hit: Codable{
    let hits: [Recipe]
}

struct Recipe: Codable{
    var name: String
    var image: URL
    var Ingredient_List: Array<String>
    var See_More_URL: URL

    enum Recipe_Coding_Keys: String, CodingKey{
        case name = "label"
        case image = "image"
        case Ingredient_List = "ingredientLines"
        case See_More_URL = "url"
    }

    init(from decoder: Decoder) throws {
        let recipe_Info = try decoder.container(keyedBy: Recipe_Coding_Keys.self)

        name = try recipe_Info.decode(String.self, forKey: Recipe_Coding_Keys.name)
        image = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.image)
        Ingredient_List = try recipe_Info.decode(Array.self, forKey: Recipe_Coding_Keys.Ingredient_List)
        See_More_URL = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.See_More_URL)

        print(name)
        print(See_More_URL)
    }
}

RecipeTableViewController.swift

import UIKit

class RecipeTableViewController: UITableViewController {

    @IBAction func downloadButtonTouched(_ sender: Any) {
    }




    let recipe_API_Call = call_Edamam_API()

    var returned_Recipes = [Recipe]()



    override func viewDidLoad() {
        super.viewDidLoad()
        print(searchTermImport)
        fetchRecipes()
    }

    func fetchRecipes(){
        self.returned_Recipes = []
        self.tableView.reloadData()

        let query: [String: String] = [
            "q": "Chicken",
            "app_id": "xxx",
            "app_key": "xxx"
        ]

        recipe_API_Call.fetch(matching: query, completion: {(returned_Recipes) in
            DispatchQueue.main.async {
                if let returned_Recipes = returned_Recipes{
                    self.returned_Recipes = returned_Recipes
                    self.tableView.reloadData()
                }
                else{
                    print("Fetch Error")
                }
            }
        })
    }

    func configureTable(cell: UITableViewCell, forItemAt indexPath: IndexPath){
        let returned_Recipe = returned_Recipes[indexPath.row]

        cell.textLabel?.text = returned_Recipe.name

        let network_task = URLSession.shared.dataTask(with: returned_Recipe.image){ (data, response, error)
            in
            guard let Image_dest = data else{
                return
            }
            DispatchQueue.main.async {
                let image = UIImage(data: Image_dest)
                cell.imageView?.image = image
            }
        }
        network_task.resume()
    }

    // MARK: - Table view data source


    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return returned_Recipes.count
    }


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)

        // Configure the cell...
        configureTable(cell: cell, forItemAt: indexPath)
        return cell
    }

从 URL 检索到的部分 JSON

{
    "q" : "Chicken",
    "from" : 0,
    "to" : 10,
    "more" : true,
    "count" : 168106,
    "hits" : [ {
        "recipe" : {
            "uri" : "http://www.edamam.com/ontologies/edamam.owl#recipe_b79327d05b8e5b838ad6cfd9576b30b6",
            "label" : "Chicken Vesuvio",
            "image" : "https://www.edamam.com/web-img/e42/xxx.jpg",
            "source" : "Serious Eats",
            "url" : "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html",
            "shareAs" : "http://www.edamam.com/recipe/chicken-vesuvio-b79327d05b8e5b838ad6cfd9576b30b6/chicken",
            "yield" : 4.0,
            "dietLabels" : [ "Low-Carb" ],
            "healthLabels" : [ "Sugar-Conscious", "Peanut-Free", "Tree-Nut-Free" ],
            "cautions" : [ "Sulfites" ],
            "ingredientLines" : [ "1/2 cup olive oil", "5 cloves garlic, peeled", "2 large russet potatoes, peeled and cut into chunks", "1 3-4 pound chicken, cut into 8 pieces (or 3 pound chicken legs)", "3/4 cup white wine", "3/4 cup chicken stock", "3 tablespoons chopped parsley", "1 tablespoon dried oregano", "Salt and pepper", "1 cup frozen peas, thawed" ],

在运行应用程序时,程序从 API 调用接收数据,但无法将其解码为我在 Recipe.swift 中创建的 Swift 类型。我收到了我在 Recipe_API_Caller.swift 中创建的“JSON 解码错误”。我认为我的问题可能是 JSON 文件的“命中”部分没有另一个结构,但我添加了它,它仍然给我错误。

我可以打印创建的 URL 并将其输入到 Web 浏览器并手动浏览 JSON 文件,这样我就知道我的 URL 和 API 密钥工作正常。 Xcode 没有向我提供任何关于正在发生的事情的官方错误,所以我不确定如何从这里开始。我将不胜感激你们都可以提供的任何建议!

【问题讨论】:

  • JSON 解码错误通常附带对导致问题的特定键的解释。您可以查看它以获取有关错误的更多信息,也可以将其添加到问题中,以便其他人更好地了解您的情况。此外, ViewController 和 fetch 实现可能与问题无关。相反,请考虑将您的空间与您返回的更多 JSON 一起使用。

标签: swift


【解决方案1】:

您的代码的真正问题是解码时不存在错误处理,您需要重写该部分以便捕获并打印任何错误

if let data = data {
    do {
        let Recipes = try Recipe_Decoder.decode(Hit.self, from: data)
        completion(Recipes.hits)
    }
    else {
         print(error)
         completion(nil)
    }
}

如果您更改为这种类型的错误处理,您将从错误中获得大量信息,在这种情况下是

keyNotFound(Recipe_Coding_Keys(stringValue: "label", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "hits", intValue: nil), _JSONKey(stringValue: "Index 0", intValue : 0)], debugDescription: "No value associated with key Recipe_Coding_Keys(stringValue: \"label\", intValue: nil) (\"label\").", underlyingError: nil))

这告诉我们解码器在“hits”中找不到键“label”,这是因为您的数据结构中缺少键“recipe”的一层

【讨论】:

  • 好的。我已经实施了您的解决方案,现在我正在正确接收错误。谢谢你!我还有另一个问题,特别是关于解码和完成。我也实现了PGDev的Codable Structs,那么decode和completion需要怎么改呢?它是否需要在结构的“顶部”(根结构)或完全其他的地方开始解码?
  • @Gamerprime 解码必须从顶层开始,因此 Root.self 应该用作解码器的参数,但对完成处理程序的调用是相同的。
  • 好的,我对解码器进行了更改,并将参数更改为 Root.self,这又给了我一个错误。 “无法将 '[Hit]' 类型的值转换为预期的参数类型 '[Recipe]?'” fetch 函数的 @escaping 参数是否也需要更改?
  • @Gamerprime 是的,您需要将类型更改为 [Hit],然后您需要在完成处理程序中处理新类型。
【解决方案2】:

问题在于 JSON 和您的结构之间的结构差异。 JSON 中的 hits 键包含一个对象数组,这些对象(在给定的 sn-p 中)只有一个 recipe 键,然后包含您的 Recipe 对象。不过,您尝试直接在hits 数组中解码Recipe

以下模型文件应该可以工作:

struct Hit: Codable{
    let hits: [RecipeHit]
}

struct RecipeHit: Codable {
    let recipe: Recipe
}

struct Recipe: Codable{
    var name: String
    var image: URL
    var Ingredient_List: Array<String>
    var See_More_URL: URL

    enum Recipe_Coding_Keys: String, CodingKey{
        case name = "label"
        case image = "image"
        case Ingredient_List = "ingredientLines"
        case See_More_URL = "url"
    }

    init(from decoder: Decoder) throws {
        let recipe_Info = try decoder.container(keyedBy: Recipe_Coding_Keys.self)

        name = try recipe_Info.decode(String.self, forKey: Recipe_Coding_Keys.name)
        image = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.image)
        Ingredient_List = try recipe_Info.decode(Array.self, forKey: Recipe_Coding_Keys.Ingredient_List)
        See_More_URL = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.See_More_URL)

        print(name)
        print(See_More_URL)
    }
}



注意:以下部分只是一个小提示,上面的代码不需要运行!
您可以通过使用 Swift 的编码/命名约定并让编译器为您生成初始化程序来节省一些代码行... :-)

struct Recipe: Codable {
    private enum CodingKeys: String, CodingKey {
        case name = "label"
        case image
        case ingredientList = "ingredientLines"
        case seeMoreURL = "url"
    }

    var name: String
    var image: URL
    var ingredientList: Array<String>
    var seeMoreURL: URL
}

【讨论】:

    【解决方案3】:

    您的 Codable 类型来解析上述 JSON 格式应该是,

    struct Root: Decodable {
        let hits: [Hit]
    }
    
    struct Hit: Decodable {
        let recipe: Recipe
    }
    
    struct Recipe: Decodable {
        let name: String
        let image: URL
        let ingredientList: [String]
        let seeMoreUrl: URL
    
        enum CodingKeys: String, CodingKey {
            case name = "label"
            case image
            case ingredientList = "ingredientLines"
            case seeMoreUrl = "url"
        }
    }
    

    使用Codable进行解析时要记住的一些关键点,

    1. 使用enum CodingKeys 代替任何其他自定义名称。 init(from:) 将自动从 enum CodingKeys 中选择密钥并在解析时使用。
    2. 如果您没有任何特定的解析实现,则无需显式实现init(from:)
    3. 在 Swift 中定义变量时使用 camel-case。示例:使用ingredientList 而不是Ingredient_List
    4. 在您希望 API 返回 null 的任何地方使用 optional 类型。
    5. 如果您有解码/编码要求之一(不确定您的要求),请使用 Decodable/Encodable

    注意:使代码尽可能短。

    【讨论】:

    • 谢谢!我没有意识到在这种情况下不需要 init(from:) 。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 2023-03-29
    • 2020-06-22
    • 1970-01-01
    • 1970-01-01
    • 2015-01-21
    • 1970-01-01
    • 1970-01-01
    • 2017-06-15
    相关资源
    最近更新 更多