2

I assume this question has been around many times, but I can't get my head around all the GCD and completion handling stuff. A quick fix with maybe some links to useful articles would be much appreciated.

I have a function that gets and processes data from a network request:

  func getTracklist(album id: String) -> String {

        //create a GET request

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in
            guard let data = data else { return }
            let decoder = JSONDecoder()
            if let jsonTracks = try? decoder.decode(TrackRoot.self, from: data) {
                tracks = jsonTracks.items!
            }
            //append the tracklist string
            for track in tracks {
                combinedTracks += "\(track.id)%2C"
            }
        }
            task.resume()

        return String(combinedTracks.dropLast(3))

    }

The returned value is used in another function:

formattedAlbum.trackList = self.getTracklist(album: album.id!)

As you might have guessed, the first function doesn't manage to get the data in time, and an empty string is assigned to the tracklist.

Using Alexey's answer:

for album in self.albums {
                let formattedAlbum = AlbumFormatted(context: self.persistenceManager.context)

                formattedAlbum.albumName = album.name
                self.getTracklist(album: album.id!) { (data) in
                    formattedAlbum.trackList = data
                }
                self.formattedAlbums.append(formattedAlbum)
                print(formattedAlbum)
            }
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(5)) {
                print(self.formattedAlbums[1])
            }
func getTracklist(album id: String, completion: @escaping (String?)->()) {
   //URLSession stuff

   let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard let data = data else { return }
            let decoder = JSONDecoder()
            if let jsonTracks = try? decoder.decode(TrackRoot.self, from: data) {
                tracks = jsonTracks.items!
                for track in tracks {
                    combinedTracks += "\(track.id),"
                }
                completion(String(combinedTracks.dropLast()))
            }
        }
            task.resume()
    }

prints:

(entity: AlbumFormatted; id: 0x6000018fb400 <x-coredata:///AlbumFormatted/tFD0D9588-2820-451B-9A00-99175026ED043>; data: {
    albumName = "Everyday Life";
    trackList = "6Tb7Zfo4PcSiS4TqQ4NnTh,1e8D1BCD2afT56Km7UahpB,45PqOIkZ9PdCjsCJQYzx9G,1cXXhzPnbrXjNQYbLdUJdy,3pcPPhPAiurm2Ior11SHrz,7jib2tJjQ82kTIZZATMvAK,0ZlVUhjO8c0bOx1D2Btznf,0UvUivL70eDwhTWBd8S38I,6VzRvCbolqcUswaS";
})
Danylo Kurylo
  • 235
  • 2
  • 11
  • As you suspect this is a dupe, but to know which one to dupe to, is the caller of this method on the main queue (i.e. is it part of the UI code)? If it is, then you can't do it this way (you need a different approach). If it isn't, then there's a technique. The easiest search for the dupes is here: https://stackoverflow.com/search?q=%5Bswift%5D+async+return – Rob Napier Nov 26 '19 at 15:52
  • @RobNapier, this is all used to populate the model, so no UI stuff is involved. Thanks for the link, I'll keep searching. – Danylo Kurylo Nov 26 '19 at 15:57
  • Problem is `dataTask(with:completionHandler:)` is an async function meaning, that the function returns immediately however the completion handler will get executed at a later point in time once the response from the server is receive. It would be better to understand async functions and why completion handlers are needed. In your function `getTracklist` would need a completion handler as a parameter. – user1046037 Nov 26 '19 at 16:12
  • To clarify: 1. Does `jsonTracks ` contain correctly decoded json? 2. Did you print out the `tracks`? Does that contain right parameters? 3. Does `combinedTracks` contain wanted result? – Aleksey Potapov Nov 26 '19 at 16:43
  • @AlekseyPotapov, yes, the print(data) statement from the last code snippet prints out the tracks correctly, but nothing is assigned to the formattedAlbum.trackList. – Danylo Kurylo Nov 26 '19 at 17:00
  • What is a type of `trackList` ? And where do you check its value? (if outside of the closure - then that is an error) – Aleksey Potapov Nov 26 '19 at 17:02
  • I'm calling the function with the handler in a for-loop where AlbumFormatted is a CoreData entity: for album in self.albums { let formattedAlbum = AlbumFormatted(context: self.persistenceManager.context) formattedAlbum.albumName = album.name self.getTracklist(album: album.id!) { (data) in print(data) formattedAlbum.trackList = data } – Danylo Kurylo Nov 26 '19 at 17:03
  • trackList is an optional String, and a CoreData attribute. At the end of the for-loop I'm just printing out the whole formattedAlbum to check the results – Danylo Kurylo Nov 26 '19 at 17:06
  • @AlekseyPotapov, when I modify the `combinedTracks` inside the URLSession datatask, it gets printed out by `print(data)`, and the `formattedAlbum.tracklist` is nil. When I do it outside the datatask, both values are empty strings (I guess because of the declaration of `var combinedTracks = ""`) – Danylo Kurylo Nov 26 '19 at 17:21
  • Tell me one more thing: does formattedAlbum.albumName contain the value that you provide from album.name ? – Aleksey Potapov Nov 26 '19 at 17:32
  • Yes, the albumName is present and different in every formattedAlbum when I print it out – Danylo Kurylo Nov 26 '19 at 17:39

2 Answers2

3

I see the solution using closures: Just test in your ViewController for a reference.

override func viewDidLoad() {
    super.viewDidLoad()
    getTracklist { data in
        print(data)
    }
}

func getTracklist(completion: @escaping (Data?)->()) {
    let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        // process errors here
        completion(data) // here you return your decoded data (I omit your json, as we don't have a model)
    }
    task.resume()
}

UPD After reading your comments:

for album in self.albums { 
    let formattedAlbum = AlbumFormatted(context: self.persistenceManager.context) 
    formattedAlbum.albumName = album.name 
    self.getTracklist(album: album.id!) { data in 
        print(data) 
        formattedAlbum.trackList = data 
    }
    print(formattedAlbum.trackList) // will return you nil
    // here you will not have results in formattedAlbum.trackList, because getTracklist method processes the data asynchronously   
}

Check this, that and this SO threads.

Aleksey Potapov
  • 3,683
  • 5
  • 42
  • 65
  • Hi! Thanks for the answer. I tried implementing that and the results have not changed. Updated my question. Am I doing something wrong? – Danylo Kurylo Nov 26 '19 at 16:33
  • Thanks for your time, I'll dig into it! – Danylo Kurylo Nov 26 '19 at 17:23
  • I tried printing out an album from formattedAlbums after a deadline of a couple of seconds, and it contained a correct value in the "trackList". Thanks for making it clear for me! – Danylo Kurylo Nov 26 '19 at 17:43
  • @DanilKurilo deadline... Could you show up your working solution? Because it is bad practice to use deadlines, and timers while async operations performed... – Aleksey Potapov Nov 26 '19 at 17:50
  • @Rob yep, I totally agree with you. I will edit the answer, even it was already approved and seems like was useful. – Aleksey Potapov Nov 26 '19 at 17:50
  • “I tried printing out an album from formattedAlbums after a deadline of a couple of seconds...” - While that may have been illuminating, you never just check after a few seconds. You'd generally use dispatch group and have it `notify` you when it's done, e.g. https://gist.github.com/robertmryan/4e4f52dcb4222db4b5ec777d824db549 – Rob Nov 26 '19 at 17:51
  • Thanks @Rob, I'll try that! Aleksey, I updated the question once again with what I have now – Danylo Kurylo Nov 26 '19 at 18:39
1

This is one legit way of handling call backs:

func getTrackList(album id: String, completion: @escaping(String)->()){
    //create a GET request
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in
        guard let data = data else { return }
        let decoder = JSONDecoder()
        if let jsonTracks = try? decoder.decode(TrackRoot.self, from: data) {
            tracks = jsonTracks.items!
        }
        //append the tracklist string
        for track in tracks {
            combinedTracks += "\(track.id)%2C"
        }
        completion(combinedTracks)
    }
    task.resume()
}

While calling this function:

getTrackList(album: album.id) { [weak self] combinedTrack in
    guard let self = self else {return}
    self.formattedAlbum.trackList = combinedTrack
}

Note: You might also want to handle the cases if request if failed or you receive empty response/string

Mithra Singam
  • 1,905
  • 20
  • 26