【问题标题】:UITableView with sections from a local JSON file带有来自本地 JSON 文件的部分的 UITableView
【发布时间】:2018-07-03 01:59:52
【问题描述】:

我仍然被困住了,为此我已经把头撞到墙上好几个星期了。所以在这里我再问一次,这次以更完整的方式。我只想完成这项工作。我一直在尝试尽可能多地使用 Swift 4(因为我正在学习坚持一套规则/语法似乎更容易,但在这一点上,我不在乎使用什么语言,只要它有效这样我就可以继续我需要对应用执行的其他操作了。

目标:查看 JSON 的本地版本,并将其与托管版本进行比较。如果托管较新,请将本地版本替换为较新的版本。然后解析本地JSON文件创建UITableView,并按状态分节。

问题:它使用了一种从网站实时解析它的旧方法,但显示重复和错误的部分计数。现在似乎将本地与托管进行了正确比较,但 UITableView 现在根本没有被填充。我怀疑我所有的问题都在 tableView 部分,但我已经尝试了 10 万亿种不同的方法,但都没有奏效。我假设我没有正确地将它指向本地 JSON 文件。

代码: 这是我的整个 ViewController:

import UIKit
import os.log
import Foundation

class BonusListViewController: UITableViewController {

    var bonuses = [JsonFile.JsonBonuses]()

    let defaults = UserDefaults.standard

    override func viewDidLoad() {
        super.viewDidLoad()

        // MARK: Data Structures
        // Settings Struct
        struct Constants {
            struct RiderData {
                let riderNumToH = "riderNumToH"
                let pillionNumToH = "pillionNumToH"
            }
            struct RallyData {
                let emailDestinationToH = "emailDestinationToH"
            }
        }

        //MARK: Check for updated JSON file
        checkJSON()

        //MARK: Trigger JSON Download
        /*
        downloadJSON {
            print("downloadJSON Method Called")
        }
        */
    }
    // MARK: - Table View Configuration
    // MARK: Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        print("Found \(bonuses.count) sections.")
        return bonuses.count
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print("Found \(bonuses.count) rows in section.")
        return bonuses.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
        cell.textLabel?.text = bonuses[indexPath.section].name.capitalized
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        performSegue(withIdentifier: "showDetail", sender: self)
    }
    // MARK: - Table View Header
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 30
    }
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return bonuses[section].state
    }
    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 3
    }

    // MARK: Functions
    // MARK: - Download JSON from ToH webserver

    func downloadJSON(completed: @escaping () -> ()) {
        let url = URL(string: "http://tourofhonor.com/BonusData.json")
        URLSession.shared.dataTask(with: url!) { [weak self] (data, response, error) in
            if error == nil {
                do {
                    let posts = try JSONDecoder().decode(JsonFile.self, from: data!)
                    DispatchQueue.main.async {
                        completed()
                    }
                    print("Downloading Updated JSON (Version \(posts.meta.version))")
                    print(posts.bonuses.map {$0.bonusCode})
                    print(posts.bonuses.map {$0.state})
                    self?.bonuses = posts.bonuses
                    self?.defaults.set("downloadJSON", forKey: "jsonVersion") //Set version of JSON for comparison later
                    DispatchQueue.main.async {
                        //reload table in the main queue
                        self?.tableView.reloadData()
                    }
                } catch {
                    print("JSON Download Failed")
                }
            }
        }.resume()
    }


    func checkJSON() {
        //MARK: Check for updated JSON file
        let defaults = UserDefaults.standard
        let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
        let jsonURL = URL(string: hostedJSONFile)
        var hostedJSONVersion = ""
        let jsonData = try! Data(contentsOf: jsonURL!)
        let jsonFile = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String : Any]
        let metaData = jsonFile["meta"] as! [String : Any]
        hostedJSONVersion = metaData["version"] as! String
        let localJSONVersion = defaults.string(forKey: "jsonVersion")
        if localJSONVersion != hostedJSONVersion {
            print("L:\(localJSONVersion!) / H:\(hostedJSONVersion)")
            print("Version Mismatch: Retrieving lastest JSON from server.")
            updateJSONFile()
        } else {
            //Retrieve the existing JSON from documents directory
            print("L:\(localJSONVersion!) / H:\(hostedJSONVersion)")
            print("Version Match: Using local file.")
            let fileURL = defaults.url(forKey: "pathForJSON")
            do {
                let localJSONFileData = try Data(contentsOf: fileURL!, options: [])
                let myJson = try JSONSerialization.jsonObject(with: localJSONFileData, options: .mutableContainers) as! [String : Any]
                //Use my downloaded JSON file to do stuff
                print(myJson)
                DispatchQueue.main.async {
                    //reload table in the main queue
                    self.tableView.reloadData()
                }
            } catch {
                print(error)
            }
        }
    }

    func updateJSONFile() {
        print("updateJSONFile Method Called")
        let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
        let jsonURL = URL(string: hostedJSONFile)
        let itemName = "BonusData.json"
        let defaults = UserDefaults.standard
        do {
            let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
            let fileURL = directory.appendingPathComponent(itemName)
            let jsonData = try Data(contentsOf: jsonURL!)
            let jsonFile = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String : Any]
            let metaData = jsonFile!["meta"] as! [String : Any]
            let jsonVersion = metaData["version"]
            print("JSON VERSION ", jsonVersion!)
            try jsonData.write(to: fileURL, options: .atomic)
            defaults.set(fileURL, forKey: "pathForJSON") //Save the location of your JSON file to UserDefaults
            defaults.set(jsonVersion, forKey: "jsonVersion") //Save the version of your JSON file to UserDefaults
            DispatchQueue.main.async {
                //reload table in the main queue
                self.tableView.reloadData()
            }
        } catch {
            print(error)
        }
    }

    // MARK: - Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let destination = segue.destination as? BonusDetailViewController {
            destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
        }
    }
}

这里是 JsonFile.swift,它提供了 JSON 解析的结构:

import Foundation

struct JsonFile: Codable {
    struct Meta: Codable {
        let fileName: String
        let version: String
    }
    struct JsonBonuses: Codable {
        let bonusCode: String
        let category: String
        let name: String
        let value: Int
        let city: String
        let state: String
        let flavor: String
        let imageName: String
    }
    let meta: Meta
    let bonuses: [JsonBonuses]
}

我需要有人像我 5 岁一样解释它。我觉得我理解我的函数在做什么,但我终其一生都无法弄清楚为什么它不起作用,也不知道为什么它什么时候起作用工作(使用旧方法),这些部分完全不合时宜。如果您看到我提出的问题,我很抱歉,我只是想学习如何做到这一点,这样我就可以自给自足,但这一篇对我来说没有意义。

【问题讨论】:

  • 如果您必须从服务器下载整个 json 文件以检查版本,那么进行版本检查有什么意义?始终使用您下载的版本。这将大大简化您目前无处不在的逻辑。
  • 没有显示的原因是因为您为bonuses 属性设置值的唯一位置是您从未调用过的downloadJSON
  • 我不认为它是在下载托管的 JSON 来比较版本。我同意如果它无论如何都要下载它,那么在打开时尝试下载就可以了。这里的意图是应用程序可以在没有数据连接的情况下运行,因此如果检查失败,仍然有一个本地文件可以依赖。
  • 至于bonuses 属性,它只能从downloadJSON 调用,因为这是我使用的旧方法。我尝试将其更改为其他各种东西以使用更新的方法,但都失败了。我把那个代码留在了那里,所以我可以恢复使用downloadJSON 进行比较。一旦新方法正常工作,它将被删除。
  • 缓存 json 以供离线使用是有效的,但您当前的实现只会在没有连接时崩溃。你的downloadJSON 方法比新的尝试更接近你想要的。

标签: ios swift xcode uitableview


【解决方案1】:

将您的目标拆分为单独的任务,并为每个任务编写一个函数。

你需要能够:

  • 从服务器下载您的奖金
  • 将您的奖金保存到本地文件
  • 从本地文件加载您的奖金

您当前的 downloadJSON 函数与您想要的第一个函数接近,但我对其进行了轻微修改,因此它不会直接处理控制器的其他部分,而不仅仅是将奖金发送回完成处理程序:

func downloadJSON(completed: @escaping ([JsonFile.JsonBonuses]?) -> ()) {
    let url = URL(string: "http://tourofhonor.com/BonusData.json")!

    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if error == nil, let data = data {
            do {
                let posts = try JSONDecoder().decode(JsonFile.self, from: data)
                completed(posts.bonuses)
            } catch {
                print("JSON Download Failed")
            }
        } else {
            completed(nil)
        }
    }.resume()
}

将您的 json 保存到文件很简单,因为您的对象实现了 Codable:

func saveBonuses(_ bonuses: [JsonFile.JsonBonuses], to url: URL) {
    try? FileManager.default.removeItem(at: url)

    do {
        let data = try JSONEncoder().encode(bonuses)
        try data.write(to: url)
    } catch {
        print("Error saving bonuses to file:", error)
    }
}

与从文件加载类似:

func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
    do {
        let data = try Data(contentsOf: url)
        let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
        return bonuses
    } catch {
        print("Error loading bonuses from file:", error)
        return nil
    }
}

这些部分都是独立的,所以现在您需要另一个具有逻辑的函数将它们联系在一起。我们想尝试从服务器获取 json 并将其保存到文件中,或者如果失败,则加载之前保存到文件中的任何 json 并使用它:

func loadBonuses(completion: @escaping ([JsonFile.JsonBonuses]?) -> Void) {
    let localBonusesURL = try! FileManager.default
        .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("Bonuses.json")

    downloadJSON { bonuses in
        if let bonuses = bonuses {
            completion(bonuses)
            saveBonuses(bonuses, to: localBonusesURL)
        } else {
            completion(loadBonusesFromFile(localBonusesURL))
        }
    }
}

现在您可以在加载视图控制器时使用这个新的loadBonuses 函数:

override func viewDidLoad() {
    super.viewDidLoad()

    loadBonuses { [weak self] bonuses in
        self?.bonuses = bonuses ?? []
        self?.tableView.reloadData()
    }
}

【讨论】:

    【解决方案2】:

    在深入研究 iOS UITableView 的工作原理之前,先弄清楚这一点:

    • 您有表格视图来指定多个数据项。
    • 这些项目按表格视图行排列。

    现在:

    • 如果可以以某种方式对它们进行分类,它们将被分组到部分中(通过部分标题可见)。
    • 如果它们无法分类,它们都会显示在单个(通常是不可见的)部分中。

    首先,想想你是如何展示奖金的。它们是平面列表(数组),还是分组为更大的块?

    如果没有分类:

    • 你有一个部分,你的numberOfSections方法必须返回1。
    • 您的numberOfRowsInSection 必须返回bonuses.count
    • 最重要的是,您的cellForRowAt 应该是这样的(注意,bonus 数组是按行索引而不是按节索引):

      override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
          let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
          cell.textLabel?.text = bonuses[indexPath.row].name.capitalized
          return cell
       }
      

    如果它有分类,那么您必须将bonuses 视为数组数组。

    • 您的numberOfSections 将返回bonuses.count - 数组的数量。
    • 您的numberOfRowsInSection 将从 bonuses 数组中获取一个数组(假设 x[] - 注意 x 本身就是一个数组)元素,并返回 x.count
    • 您的cellForRowAt 将再次从奖金中获取一个数组元素(比如说x[])。然后,它将从 x 中的 with 获取行项,例如:x[indexPath.row],您的最终代码将如下所示(我省略了解包等,因为编译器会告诉您):

      override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
          let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
          let x = bonuses[indexPath.section]
          let bonusItem = x[indexPath.row]
          cell.textLabel?.text = bonusItem.name.capitalized
          return cell
       }
      

    【讨论】:

    • 因此,如果我对您的理解正确,那么 JSON 结构本身就是我的问题的一部分,因为我想按州划分,但没有与该分类匹配的数组。所以我应该调整 JSON 以将 Bones 数组分解为包含相关数据数组的状态,这是正确的理解吗?
    • 首先尝试查看奖金数组包含哪些奖金,并使用单节方法检查您的表格视图是否显示所有奖金(即使未分类)。验证后,如果需要,请尝试按状态将奖金分成更小的数组,然后尝试第二种方法。
    • 我可能会用这个问题打开潘多拉的盒子,但无论如何我都会这样做。实现搜索并让用户搜索他们的状态,而不是将数据划分为部分会更容易吗?我计划在这部分工作后实现搜索功能,所以也许我根本不需要担心这些部分?
    • 当然,这取决于你。请参阅其他地方的数组过滤代码,并有奖金阵列只有平坦的奖金项目。然后只使用 numberOfSections 返回 1 的单个部分并显示项目。
    猜你喜欢
    • 2011-12-14
    • 1970-01-01
    • 2021-11-16
    • 1970-01-01
    • 2022-01-23
    • 2020-02-24
    • 2023-03-25
    • 2021-11-30
    • 2017-07-16
    相关资源
    最近更新 更多