-1

Attempting to update a menu item to return all fixtures from api.

I've got a list of fixtures being returned.

How do I go about updating the fixtureMenuItem in the MenuController with all fixtures returned from the JSON? I thought I might be able to do something along the lines of fixtureMenuItem.title = fixtures.description , but I'm getting "Thread 1: Fatal error: Index out of range."

Model

struct LiveScores: Codable {
    let success: Bool
    let fixturesData: FixturesData?
    enum CodingKeys: String, CodingKey {
        case fixturesData = "data"
        case success
    }
}

struct FixturesData: Codable {
    let fixtures: [Fixture]
    let nextPage, prevPage: Bool

    enum CodingKeys: String, CodingKey {
        case fixtures
        case nextPage = "next_page"
        case prevPage = "prev_page"
    }
}

struct Fixture: Codable, CustomStringConvertible {
    let id, date, time, round: String
    let homeName, awayName, location, leagueID: String
    let homeID, awayID: Int?

enum CodingKeys: String, CodingKey {
    case id, date, time, round
    case homeName = "home_name"
    case awayName = "away_name"
    case location
    case leagueID = "league_id"
    case homeID = "home_id"
    case awayID = "away_id"
}

var description: String {
    return "\(time): \(homeName) vs. \(awayName)"
    }
}

// MARK: Convenience initializers

extension LiveScores {
    init(data: Data) throws {
        self = try JSONDecoder().decode(LiveScores.self, from: data)
    }
}

Menu Controller - this is where I want to update the fixture menu item, to include the time, home and away team names. "Here is where all the fixtures will be populated!" - this is the hardcoded text I wish to replace with the fixture data.

var fixtures = [Fixture]()

func updateScores() {
    liveScoreApi.fetchFixtures()
    if let fixtureMenuItem = self.Menu.item(withTitle: "Fixtures") {
        fixtureMenuItem.title = "Here is where all the fixtures will be populated!"
        // TODO - populate the UI with fixtures returned from JSON response
    }
}  

Fetch Fixtures - here's where the fixtures are retrieved.

func fetchFixtures() {
    let session = URLSession.shared
    let url = URL(string: "\(baseUrl)fixtures/matches.json?key=\ 
(apiKey)&secret=\(apiSecret)&date=2018-06-02")
    let task = session.dataTask(with: url!) { data, response, err in
        // check for a hard error
        if let error = err {
            NSLog("Live Scores Api Error: \(error)")
        }

        // check the response code
        if let httpResponse = response as? HTTPURLResponse {
            switch httpResponse.statusCode {
            case 200: // perfecto!
                if let liveScores  = try? LiveScores.init(data: data!),
                    let fixture = liveScores.fixturesData
                    {
                    NSLog("\(fixture)")
                    }
            case 401: // unauthorised
                NSLog("Live Score Api returned an 'unauthorised' response.")
            default:
                NSLog("Live Scores Api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
            }
        }
    }
    task.resume()
}

In this example fixture data there are 26 fixtures and I want to show all of these.

baze
  • 23
  • 5
  • If you already have your `Menu` it should be a simple matter to first remove all existing `MenuItem`s and re-add suitable new ones according to your `fixtures` data, so I guess that is not the question. You might want to pass in the fixtures to your `updateScores` function, but you probably thought of that already. You already managed to get the difficult parts done and now you struggle with an easy one? Pleas amend your question with what is not working in your eyes. – Patru Jun 02 '18 at 14:13
  • Yep, I've got a Menu - with a MenuItem of 'Fixtures'. Essentially I want to replace the line: `fixtureMenuItem.title = "Here is where all the fixtures will be populated!"` with something like `fixtureMenuItem.title = fixtures.description` - But this doesn't work. – baze Jun 02 '18 at 14:27
  • "It doesn't work" is one of the least helpful things you can say in an SO post. Don't say "it doesn't work." Explain what happens, and how it fails t meet your needs. – Duncan C Jun 02 '18 at 14:32
  • @DuncanC - Yep my mistake trying to edit quickly! - I've updated it with the error I receive when running the project. – baze Jun 02 '18 at 14:36

2 Answers2

1

Variations of this question come up constantly on SO.

Async functions don't wait for their results to be available. You give them a callback, which is a closure (a block of code you provide) that gets executed once the operation is complete.

You should rewrite your fetchFixtures() function to take a completion handler, and then refactor your updateScores() function to pass the code that updates your menu item into the completion handler for FetchFixtures.

See my answer to the question in the thread below for a simple example of this approach:

Swift: Wait for Firebase to load before return a function

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • Ok, brilliant thanks. I'll take a look at your answer and give it a go! – baze Jun 02 '18 at 14:35
  • Took me a while but I got this working based on others and your approach - thanks again. I adjusted the `fetchFixtures()` function to take a completion handler and refactored the `updateScores()` as you've said and all is good! – baze Jun 03 '18 at 12:59
  • Good! You should either accept my answer, or post your own and accept that. And it would be ideal if you posted the refactored `fetchFixtures()` function and explained how it works. That way others can learn from your question. – Duncan C Jun 03 '18 at 13:20
  • Cool thanks. I've posted an answer with my updates (although, this is the first time I've used a completion handler so probably not the best explanation!) – baze Jun 03 '18 at 13:54
1

As Duncan said in his answer, the issue was that the results weren't actually available.

I've implemented a completion handler of handleCompletion: on the fetchFixtures() function, which takes a true/false value plus the fixtures data. This is then returned in each http response case as shown below:

func fetchFixtures(handleCompletion:@escaping (_ isOK:Bool,_ param: 
    FixturesData?)->()) {
        let session = URLSession.shared
        let url = URL(string: "\(baseUrl)fixtures/matches.json?key=\ 
                     (apiKey)&secret=\(apiSecret)&date=2018-06-04")
        let task = session.dataTask(with: url!) { data, response, err in
        // check for a hard error
        if let error = err {
            NSLog("Live Scores Api Error: \(error)")
        }

        // check the response code
        if let httpResponse = response as? HTTPURLResponse {
            switch httpResponse.statusCode {
            case 200: // perfecto!
                if let liveScores  = try? LiveScores.init(data: data!),
                    let fixture = liveScores.fixturesData
                    {
                    //NSLog("\(fixture)")
                    handleCompletion(true, fixture)
                    }
            case 401: // unauthorised
                NSLog("Live Score Api returned an 'unauthorised' response.")
                handleCompletion(false, nil) 
            default:
                NSLog("Live Scores Api returned response: %d %@", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
                handleCompletion(false, nil)
            }
        }
    }
    task.resume()
}

After implementing the above, I refactored the updateScores() to use this completion handler.

    func updateScores() {
    liveScoreApi.fetchFixtures() { (
        isOK, fixture) in
        if isOK == true {
            if let fixtureMenuItem = self.Menu.item(withTitle: "Fixtures") {
                fixtureMenuItem.title = (fixture?.fixtures.description)!
            }
        }
        else {
            NSLog("error fetching!")
        }
    }
}

The fixtureMenuItem now successfully displays the data if available.

baze
  • 23
  • 5
  • Keep in mind, that your `CompletionHandler` will be run in the context of your data-thread. UI-changes should be performed in the context of the `main` thread. Use `DispatchQueue.main.async { ... }` to achieve this. – Patru Jun 06 '18 at 17:22
  • Good catch Patru. @baze, some classes' methods call their completion handlers on a background thread. URLSession's dataTask completion handler is such a case. You might want to refactor your calls to `handleCompletion()` in your `fetchFixtures()` function to wrap the call to `handleCompletion()` in a call to `DispatchQueue.main.async()`, as mentioned by Patru in his comment. – Duncan C Jun 06 '18 at 17:38
  • You either need to do that to make sure that every time you call `fetchFixtures()` you pass in a completion handler that wraps all UIKit calls in `DispatchQueue.main.async()` – Duncan C Jun 06 '18 at 17:39
  • Thanks both. I currently now have a `update()` function in the `updateScores()`. This `update()` function is where I update the UI label/s. These updates are wrapped in `DispatchQueue.main.async()`. From what you're saying, it would be best to handle this in the `CompletionHandler`? – baze Jun 06 '18 at 19:14
  • There is no strictly "best" solution in the most general case, it will depend on your application. If your UI-changes are "cheap" (as they seem to be) they probably should be done in a single call to `DispatchQueue.main.async()`, which would be most easily achieved by wrapping it around the call to your `CompletionHandler`. If your changes are "expensive" then it might be better to distribute it among several calls to keep the application responsive. Since every call involves a (fairly expensive) context switch it will incur more work in total though. – Patru Jun 13 '18 at 16:47