Search code examples
iosswiftuitableviewstructurlsession

Swift 5: Why is my data within struct suddenly empty / nil?


Update:

The main thing I wonder about, is, that I'm able to log self.schedule (.monday) after the download JSON function successfully, but instantly gets nil when the tables should be refreshed.

The tiny Xcode window also shows that <code>schedule</code> is <code>nil</code>.

Problem

There are currently two separate UITableViews in my ScheduleViewController, where the tables should be filled differently with the downloaded data by the view controller it's downloadSchedule() function.

After a successful URLSession task, the received data will be stored in new variable called json and is printed in the console afterwards. Everything works well.

Now, when the downloadSchedule() function finished it's request and the next function to reload the tables is called, I want to count the the data for numberOfRowsInSection, but I'm getting a Thread 1: Fatal error: Unexpectedly found nil while unwrapping an optional value.

Although the variable was previously filled with the downloaded data, it is now suddenly empty when I want to update the tables. How can this happen?

Code

I declare my variable in my ScheduleViewController like so:

var schedule: Schedule?

The downloadSchedule() function gets executed after viewDidLoad():

func downloadSchedule(completed: @escaping () -> Void) {
    let url = URL(string: "example.com/meta/schedule")

    URLSession.shared.dataTask(with: url!) { data, _, error in
        if error == nil {
            do {
                self.schedule = try JSONDecoder().decode(Schedule.self, from: data!)
                DispatchQueue.main.async {
                    print("executed")
                    completed()
                }
            } catch {
                print("JSON Error")
            }
        }

    }.resume()
}

downloadSchedule {
    self.mondayTableView.reloadData()
    self.tuesdayTableView.reloadData()
}

An example what happens first to update the tables:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // Return the number of items in the sample data structure.

    var count:Int?

    if tableView == self.mondayTableView {
        count = schedule?.monday.count
    }

    if tableView == self.tuesdayTableView {
        count =  schedule?.tuesday.count
    }

    return count!

}

These are my structs for Schedule and Day:

// MARK: - Schedule
struct Schedule: Codable {
    let monday: [Day]
    let tuesday: [Day]
    let wednesday: [Day]
    let thursday: [Day]
    let friday: [Day]
    let saturday: [Day]
    let sunday: [Day]
    let nextmonday: [Day]
    let nexttuesday: [Day]
    let nextwednesday: [Day]
    let nextthursday: [Day]
    let nextfriday: [Day]
    let nextsaturday: [Day]
    let nextsunday: [Day]
    let airtimeAPIVersion: String

    enum CodingKeys: String, CodingKey {
        case monday, tuesday, wednesday, thursday, friday, saturday, sunday, nextmonday, nexttuesday, nextwednesday, nextthursday, nextfriday, nextsaturday, nextsunday
        case airtimeAPIVersion = "AIRTIME_API_VERSION"
    }
}

// MARK: - Day
struct Day: Codable {
    let startTimestamp: String?
    let endTimestamp: String?
    let name: String?
    let dayDescription: String?
    let id: Int?
    let instanceID: Int?
    let instanceDescription: String?
    let record: Int?
    let url: String?
    let imagePath: String?
    let starts: String?
    let ends: String?

    enum CodingKeys: String, CodingKey {
        case startTimestamp = "start_timestamp"
        case endTimestamp = "end_timestamp"
        case name = "name"
        case dayDescription = "description"
        case id = "id"
        case instanceID = "instance_id"
        case instanceDescription = "instance_description"
        case record = "record"
        case url = "url"
        case imagePath = "image_path"
        case starts = "starts"
        case ends = "ends"
    }
}

JSON

{
  "monday": [
    {
      "start_timestamp": "2020-06-01 01:35:00",
      "end_timestamp": "2020-06-01 01:37:37",
      "name": "TEST",
      "description": "",
      "id": 174,
      "instance_id": 950,
      "instance_description": "",
      "record": 0,
      "url": "",
      "image_path": "example.com/api/show-logo?id=174",
      "starts": "2020-06-01 01:35:00",
      "ends": "2020-06-01 01:37:37"
    }
  ],
  "tuesday": [
    ...
  ]
}

Solution

  • You shouldn't to return potential nil in the tableView:numberOfRowsInSection method. Because it should be called at initial tableview load data and 100% you will get crash. You may return 0.

    So:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Return the number of items in the sample data structure.
    
        guard let schedule = schedule else { return 0 }
    
        var count:Int = 0
    
        if tableView == self.mondayTableView {
            count = schedule.monday.count
        }
    
        if tableView == self.tuesdayTableView {
            count = schedule.tuesday.count
        }
    
        return count
    
    }
    

    Also, are you sure that

    • schedule.monday or . tuesday is not nil after loadData?
    • self.schedule is nil in the tableView:numberOfRowsInSection?
    • you don't have more than 2 tableViews with this UITableViewDelegate (in this viewcontroller)?

    Also completion handler in your loadData will be newer called if you get an error of url request or parsing json. Add completed() to this places or move your one to the bottom:

    func downloadSchedule(completed: @escaping () -> Void) {
        let url = URL(string: "https://api/....")
    
        URLSession.shared.dataTask(with: url!) { data, _, error in
            if error == nil,
                let data = data {
                do {
                    self.schedule = try JSONDecoder().decode(Schedule.self, from: data)
                    print("JSON \(self.schedule?.monday)")
                } catch {
                    print("JSON Error")
                }
            }
            DispatchQueue.main.async {
                completed()
            }
        }.resume()
    }