2

Here is the JSON I need to decode, a picture of it is available in the link below as well as the text (formatting wont work so it looks ugly). I am pretty sure I represented it correctly with my structs.

enter image description here

{ "success": { "total": 1 }, "contents": { "quotes": [ { "quote": "The last time doesn't exist. It's only this time. And everything is going to be different this time. There's only now.", "author": "Bill Murray", "length": "118", "tags": [ "inspire", "present" ], "category": "inspire", "title": "Inspiring Quote of the day", "date": "2019-01-16", "id": null } ], "copyright": "2017-19 theysaidso.com" } }

Whenever I run my code, which attempts to take the fields from the JSON and store them into variables in order to display them in a UITableView, it fails. I test it by trying to make a label appear as with the author's name as its title. "author" is a field in the JSON. These are the important parts of the code:

Class ViewController: UITableViewController  {

...
var quoteArray = [Quote]()
//quoteArray Stores the quote objects that contain the fields I need
.....
//STRUCTS TO REPRESENT THE JSON
struct Quote: Decodable {
    let quote: String?
    let author: String?
    let length: String?
    let tags: [String]?
    let category: String?
    let title: String?
    let date: String?
}

struct WebsiteObjectStruct: Decodable {
    let success: SuccessStruct
    let contents: ContentsStruct
}

struct SuccessStruct: Decodable{
    let total: Int?
}

struct ContentsStruct: Decodable{
    let quotes: [Quote]?
    let copyright:  String?
}

.....

//FUNCTION WHERE DECODING HAPPENS

fileprivate func fetchJSON(){
        ...
        self.websiteObject = try decoder.decode(WebsiteObjectStruct.self, from: data)
        self.tableView.reloadData()
        ...
}
...

//CELL FOR ROW AT FUNCTION FOR TABLEVIEW

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cellId")
    let authorText = quoteArray[0].author
    cell.textLabel?.text = author.text
    //For quoteArray we are looking at zero index because in my JSON there
    // is ONLY EVER ONE element, located at index 0, in quoteArray
    return cell
}

}

The app runs and the the tableView is empty, it doesnt have the author's name (In this case bill murray). Anyway, here is the error message:

Failed to decode: typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))

It says it expected to decode an array but instead found a dictionary. Well I changed it one time to decode not an array but a struct, and had a variable declared in the class that was of type struct (the purpose of the struct mirrors the purpose of the array).

Short story is, I changed the code slightly to accomodate for the struct, and it could print the author's name to the console ONLY if the print statement was within the same coding brackets as the decoding statement. Still, it couldn't store it into a variable for use.

I don't think the problem is Array vs Dictionary but with the "underlyingError" the console talks about, that the Array is nil. No matter what type the variable is, whether Array or a Struct, the variable getting placed into the textField is always nil.

I get this error:

Fatal Error: unexpected found nil while unwrapping optional value

Perhaps an issue with threading or asynchrosity?

Update: this code works:

class MainNetworkManager{ //Request JSON Format from the wep API

static func fetchJSON(fetchUrl: String, quoteViewController: QuoteViewController) {
    let urlString = fetchUrl
    guard let url = URL(string: urlString) else { return }
    URLSession.shared.dataTask(with: url) { (data, _, err) in
        DispatchQueue.main.async {
            if let err = err {
                print("Failed to get data from url:", err)
                return
            }

            guard let data = data else { return }

            do {
                // link in description for video on JSONDecoder
                let decoder = JSONDecoder()
                // Swift 4.1
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                //self.webStruct = try decoder.decode(WebsiteObjectStruct.self, from: data)
                //                        self?.quoteArray = quotesArray
                //                        self?.reloadInputViews()
                let tempStruct = try decoder.decode(WebsiteObjectStruct.self, from: data)

                //print(tempStruct.contents.quotes[0].length)
                quoteViewController.webStruct = tempStruct

                //quoteViewController.setupLabels(array: (tempStruct.contents.quotes))
                quoteViewController.setupLabels(obj: tempStruct)


            } catch let jsonErr {
                print("Failed to decode:", jsonErr)
            }
        }
        }.resume()
}
orbek1815
  • 43
  • 3
  • 2
    Welcome to stackoverflow. Please read : https://stackoverflow.com/help/on-topic, https://stackoverflow.com/help/how-to-ask and https://stackoverflow.com/help/dont-ask. You may need to edit your question based on these guidelines. – Soumya Kanti Jan 16 '19 at 06:04
  • Possible duplicate of [What does "fatal error: unexpectedly found nil while unwrapping an Optional value" mean?](https://stackoverflow.com/questions/32170456/what-does-fatal-error-unexpectedly-found-nil-while-unwrapping-an-optional-valu) – Cristik Jan 16 '19 at 07:13
  • From error it looks like your top level object in the response JSON is array. Did you try printing the response data in string? can you add that to question? – Mukesh Jan 16 '19 at 07:21
  • Maybe you need to do `DispatchQueue.main.async { self.tableView.reloadData() }` instead when reloading the table view. – Joakim Danielson Jan 16 '19 at 08:00
  • Post your JSON as text, not images. we cannot copy and paste it and try to decode it from an image. – Scriptable Jan 16 '19 at 08:11
  • Could you assert this: You do a try/catch and print the error. Could you also print `let dataStr = String(data: data, encoding: .utf8)` and print it when it fails because of that error? – Larme Jan 16 '19 at 09:39

2 Answers2

0

It's a little difficult to figure out the mistake with the code you posted. I think the problem could be with the type definition of the variable websiteObject. I created a a small playground to test it and your structs are fine.

I create a small project that works ok using your structs. You can check it out here: https://github.com/acyrman/StackOverflow54211226.

The relevant change is in the fetchJSON function. Instead of using a self.websiteObject, which I didn't know how you defined it, I used a local variable like this: let websiteObject = try decoder.decode(WebsiteObjectStruct.self, from: data), then proceed to get the quotes and assign to your quoteArray variable.

fileprivate func fetchJSON() {
    let urlString = "http://quotes.rest/qod.json?category=inspire"
    guard let url = URL(string: urlString) else { return }
    URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
        if error != nil {
            self?.displayAlert("Error fetching data: \(String(describing: error?.localizedDescription))")
        }

        let decoder = JSONDecoder()
        do {
            guard let data = data else { throw NSError(domain: "this.app", code: -1, userInfo: nil) }

            let websiteObject = try decoder.decode(WebsiteObjectStruct.self, from: data)
            if let quotesArray = websiteObject.contents.quotes {
                DispatchQueue.main.async {
                    self?.quoteArray = quotesArray
                    self?.tableView.reloadData()
                }
            }
        } catch let error {
            self?.displayAlert("Error decoding json data: \(String(describing: error.localizedDescription))")
        }
    }.resume()
}

For the app I'm fetching the quote from: http://quotes.rest/qod.json?category=inspire. Also in the info.plist is important to enable the ATS Settings to enable fetching data from the non https url.

enter image description here

The code is just to test your structs, don't expect a clean code project ;)

The app calls fetchJSON in viewDidLoad, using the subtitle cell style the UI looks like this:

enter image description here

Aaron Cyrman
  • 556
  • 3
  • 13
  • Thanks. your fetchJSON looks very similar to mine. But why do you have an extra tidbit here: ". . .URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in . . . " Why use "weak self"? Also, I originally wanted to save pop the quote and author's name on a separate ViewController, but it seems like I can't because I need to use reloadData() and that works only on UITableViewControllers. So is there a way to handle the JSON fetching in one class and then successfully pass on the info I need to a separate view controller? – orbek1815 Jan 16 '19 at 19:55
  • I used `weak self` because in the closure there is a reference to `quoteArray` which is defined in the `ViewController`, I wanted to avoid a retain cycle. – Aaron Cyrman Jan 17 '19 at 03:13
  • I think it will be cleaner to have a different class that manages the data and network calls so your ViewController doesn't get too big and with mix of responsibilities. In a talk by Marcus Zarra (https://academy.realm.io/posts/slug-marcus-zarra-exploring-mvcn-swift/) he suggest to have a DataController/NetworkController class which manages the network calls and saves the data to Core Data. You set the data display using a `NSFetchedResultsController` which reacts when the data is updated. – Aaron Cyrman Jan 17 '19 at 03:31
  • If you don't want to use Core Data and you can create a `DataManager` class that fetches data. In that class you define a function `func fetchJSON(success: (([Quote]) ->Void))` that receives a completion block to be executed when the data is correctly fetch and parse into a `Quote` array. You pass a completion block to get the authors in one ViewController and another block to get the quote from the other ViewController. The function `fetchJSON` is just like the one posted, in the then case of `if let quotesArray = websiteObject.contents.quotes` you call `completion(quotesArray)`. – Aaron Cyrman Jan 17 '19 at 04:05
0

Two issues.

  1. To use quoteArray you have to copy the array containing the quotes

    self.websiteObject = try decoder.decode(WebsiteObjectStruct.self, from: data)
    self.quoteArray = self.websiteObject.contents.quotes ?? []
    DispatchQueue.main.async {
        self.tableView.reloadData()
    }
    
  2. In cellForRow you have to get the item by the given index path. And dequeue the cell, set the style in Interface Builder.

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
        let cell = tableview.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath)
        let quote = quoteArray[indexPath.row]
        cell.textLabel?.text = quote.author
        cell.detailTextLabel?.text = quote.quote
        return cell
    }
    
vadian
  • 274,689
  • 30
  • 353
  • 361