0

I am working with an API (Youtube API to be more precise...) to get a title of video.

So I basically created a function to call the API and get the answer of what I'm looking for. After that I want to be able to take that result and return it in a tableCellView.

Getting the info is fine, everything works. But when I want to take the info and return it, there is a problem.

So here is my function:

func fetchData(userCompletionHandler: @escaping (String?, Error?) -> Void){
    var yUrl = URL(string: youtubeUrlForRequest)
    let task = URLSession.shared.dataTask(with: yUrl!) { (data, yresponse, yerror) in
        do {
            if let jsonResult = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as? [String : AnyObject] {

                if var items = jsonResult["items"] as? [AnyObject]? {
                    var snippetDict = items?[0]["snippet"] as! [String: AnyObject]
                    var titleVideo = snippetDict["title"] as! String
                    userCompletionHandler(titleVideo, nil)
                }
            }
        }
        catch {
            print("json error: \(error)")
        }
    }
    task.resume()

}

So that part above is fine.

Here is how I call my function:

fetchData ( userCompletionHandler: { (ytitle, yerror) in
        //var untitre = ""

        guard let untitle = ytitle else{
            print("Ok I'm in!!!")
            //untitle = ytitle
            //print(untitle)
            return
        }
        print("OK HERE!!!!!!!!")
        print(untitle)
    })
    //var test_titre = "Le titre est: " + bontitre
    print(untitle)
    return ()

So the print(untitle) under the print("OK HERE!!!!!!!") is fine, I do get the data I want.

But the print(untitle) over the return() won't print the same thing. It will print the value I gave to untitle when I created it above:

var untitle: String = "a"

My goal is to be able to have return(untitle) with the title of the video where the return() is right now.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Francis Dolbec
  • 175
  • 1
  • 11
  • Think asynchronously and do the things you have to do **in** the completion handler where *`print("OK HERE!!!!!!!")` is fine*. You can't *return a value* from a method containing an asynchronous task. – vadian Jun 01 '19 at 14:45
  • I also tried `if let` before trying `guard let` without success. :( – Francis Dolbec Jun 01 '19 at 14:46
  • @vadian Hmm... Not sure to understand what you mean... When I print the title under OK HERE!!!, I have at that state all the info I need to get from the API... – Francis Dolbec Jun 01 '19 at 14:47
  • I mean for example if you want to assign the `title` to a label do it in the completion handler where the value is available. Detach yourself from the imagination that the code is executed in order of the lines. – vadian Jun 01 '19 at 14:51
  • Ah ok! In my case, I call my function that fetch the data in the reloadData() function of my table view to populate each cell, so basically, only the return below needs the info... – Francis Dolbec Jun 01 '19 at 14:59
  • 1
    Once again, *Think asynchronously*, learn to understand how asynchronous data processing works. Assign the data to the data source array and reload the table view **inside** the completion handler. You cannot return something from that method unless you add another completion handler. – vadian Jun 01 '19 at 15:09
  • Why don’t you add a few breakpoints to see what’s happening inside the function? You’ll see the handler will be called after the last return. – Renzo Tissoni Jun 01 '19 at 15:20
  • 1
    I bet if you use Ctrl-I to fix the indentation your code will look different and make your mistake very obvious. – gnasher729 Jun 01 '19 at 15:45
  • Just to clarify everything, I going to explain more in general what my program do. So for example, a user add a youtube link or open a file with multiple youtube links. When he adds it to the current list, the program first, append the list and then call the `reloaddata()`method of the tablecellview. In the `reloaddata()`method for each elements of my list I'm calling the Youtube API the get the title of the video and then display that title in a cell to the user. So basically, I need to have the title when I'm about to return the result for the cell, before going to the next item of the list. – Francis Dolbec Jun 01 '19 at 16:39
  • Looking on youtube right now for a solution... Could DispatchGroup be a better alternative to what I have right now? – Francis Dolbec Jun 01 '19 at 17:03
  • http://www.programmingios.net/you-cant-use-a-value-after-it-has-been-set-by-asyncronous-code/ – matt Jun 01 '19 at 21:44

1 Answers1

0

I finally manage how to get the information I wanted, with the help of DispatchGroup to make sure every pieces of info were there before doing some actions.

So main difference between the code above the following one, it's the order of the actions that changed everything. So in my old code, I used to ask for a refresh of my tableView and in that refresh function, then call the Youtube API. That setup brought me into a bottleneck. So after some advices (thank you guys!) I decided to first call the API and store result in a new array. Once everything was there (with the help of DispatchGroup), I would then refresh the tableView with the data of the fresh new array.

So here is (most of) the new code of my different functions (some are linked to buttons or other stuff on the UI):

let youtubeUrl = "https://www.googleapis.com/youtube/v3/videos?part=contentDetails%2C+snippet%2C+statistics&id="
//"AKiiekaEHhI&key="
var youtubeUrlForRequest:String = ""
let dispatchGroup = DispatchGroup()
var test_title: Array<String> = []


//func

@IBAction func plus(_ sender: NSButton) {
    if urlInput.stringValue == "" {

    } else {
        if valeurEndroit.indexOfSelectedItem == -1{
            print("coucou!")
            test_text = urlInput.stringValue
            test_data.append(test_text)
            dispatchGroup.enter()
            fetchData2(lien: test_data.last!, userCompletionHandler: {data_y, user_y, error_y in
                if let user_y = user_y{
                    self.test_title.append(user_y)
                    self.dispatchGroup.leave()
                }
            })
            dispatchGroup.notify(queue: .main) {
                //print(untitle)
                print("J'ai fini la liste de titre. Voici la liste: ")
                //print(self.test_title)
                self.urlInput.stringValue = ""
                print(self.test_title)
                self.tableView.reloadData()
            }


            //tableView.reloadData()
        }else{
            if valeurEndroit.indexOfSelectedItem >= test_data.count{
                test_text = urlInput.stringValue
                test_data.append(test_text)
                dispatchGroup.enter()
                fetchData2(lien: test_data.last!, userCompletionHandler: {data_y, user_y, error_y in
                    if let user_y = user_y{
                        self.test_title.append(user_y)
                        self.dispatchGroup.leave()
                    }
                })
                dispatchGroup.notify(queue: .main) {
                    //print(untitle)
                    print("J'ai fini la liste de titre. Voici la liste: ")
                    //print(self.test_title)
                    self.urlInput.stringValue = ""
                    print(self.test_title)
                    self.tableView.reloadData()
                }
            }else{
                var y_index = (valeurEndroit.indexOfSelectedItem)
                test_text = urlInput.stringValue
                test_data.insert(test_text, at: valeurEndroit.indexOfSelectedItem)
                dispatchGroup.enter()
                fetchData2(lien: test_data[y_index], userCompletionHandler: {data_y, user_y, error_y in
                    if let user_y = user_y{
                        self.test_title.insert(user_y, at : y_index)
                        self.dispatchGroup.leave()
                    }
                })
                dispatchGroup.notify(queue: .main) {
                    //print(untitle)
                    print("J'ai fini la liste de titre. Voici la liste: ")
                    //print(self.test_title)
                    self.urlInput.stringValue = ""
                    print(self.test_title)
                    self.tableView.reloadData()
                }
            }
        }

    // fonction du bouton +
    }
}

@IBAction func nextLien(_ sender: NSButton) {
    if test_data == [] {

    } else {
    nextUrl=test_data[0]
    var monUrl = URL(string: nextUrl)
    var maRequete = URLRequest(url: monUrl!)
    view_web.load(maRequete)
    test_data.remove(at: 0)
    test_title.remove(at: 0)
    tableView.reloadData()
    //fonction du bouton pour le prochain lien
    }
}

@IBAction func openUnFichier(_ sender: NSMenuItem) {
    let fichierPanel: NSOpenPanel = NSOpenPanel()
    fichierPanel.allowsMultipleSelection = false
    fichierPanel.canChooseFiles = true
    fichierPanel.canChooseDirectories = false
    fichierPanel.allowedFileTypes = ["txt"]
    let response = fichierPanel.runModal()
    if response == NSApplication.ModalResponse.OK{
        guard let selectedURL = fichierPanel.url else{return}
        do{
            var fullDocument = try String(contentsOf: selectedURL, encoding: String.Encoding.utf8)
            var lines : [String] = fullDocument.components(separatedBy: "\n" as String)
            for line in lines {
                test_data.append(line)
                dispatchGroup.enter()
                fetchData2(lien: test_data.last!, userCompletionHandler: {data_y, user_y, error_y in
                    if let user_y = user_y{
                        self.test_title.append(user_y)
                        self.dispatchGroup.leave()
                    }
                })
                dispatchGroup.notify(queue: .main) {
                    //print(untitle)
                    print("J'ai fini la liste de titre. Voici la liste: ")
                    //print(self.test_title)
                    self.urlInput.stringValue = ""
                    print(self.test_title)
                    self.tableView.reloadData()
                }
            }
        } catch let error as NSError{
            print("Erreur!!!!!!! \(error)")
        }
        //tableView.reloadData()
    }else {

    }
}

func numberOfRows(in tableView: NSTableView) -> Int {
    return test_title.count
}

func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
    return (test_title[row])

}

func fetchData2(lien: String, userCompletionHandler: @escaping (String?, String?, Error?) -> Void){
    var titleLien: String = ""
    var urlToExtract_y = lien
    var videoID_y = urlToExtract_y.components(separatedBy: "=")[1]
    var youtubeUrlForRequest_y = youtubeUrl + videoID_y + "&key=" + youtubeApiKey
    print(youtubeUrlForRequest_y)
    var Url_y = URL(string: youtubeUrlForRequest_y)
    var titleVideo_y = ""
    let task_y = URLSession.shared.dataTask(with: Url_y!) { (data_y, response, error_y) in
        do {
            if let jsonResult_y = try JSONSerialization.jsonObject(with: data_y!, options: JSONSerialization.ReadingOptions.allowFragments) as? [String : AnyObject] {

                if var items_y = jsonResult_y["items"] as? [AnyObject]? {
                    var snippetDict_y = items_y?[0]["snippet"] as! [String: AnyObject]
                    var titleVideo_y = snippetDict_y["title"] as! String
                    userCompletionHandler(titleVideo_y, titleVideo_y, nil)
                }
            }
        }
        catch {
            print("json error: \(error_y)")
        }
    }
    task_y.resume()
}




//var Outlet

@IBOutlet weak var urlInput: NSTextField!

@IBOutlet weak var view_web: WKWebView!

@IBOutlet weak var tableView: NSTableView!

@IBOutlet weak var valeurEndroit: NSComboBox!

Some parts has not been paste here because it's not important for the comprehension or it's sensitive (Youtube API Key for example).

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Francis Dolbec
  • 175
  • 1
  • 11
  • You still don't understand how asynchronous data processing works because you still try to make an asynchronous task synchronous by misusing `DispatchGroup`. Why don't you put the lines `self.urlInput.stringValue = ""`and `self.tableView.reloadData()` **into** the completion handler?? And the `DispatchGroup` functionally will break anyway if the check `if let user_y = user_y` fails. The only place where `DispatchGroup` is useful is in `openUnFichier` but the `notify` block must be outside the repeat loop to notify when **all** asynchronous tasks are completed. – vadian Jun 02 '19 at 04:24
  • Something like this link? https://stackoverflow.com/questions/30401439/how-could-i-create-a-function-with-a-completion-handler-in-swift – Francis Dolbec Jun 02 '19 at 08:50
  • Actually you got a completion handler. In the `plus` method remove the code related to `DispatchGroup` and move the mentioned two lines wrapped in `DispatchQueue.main.async { .. }` into the completion handler. And the check `if let user_y = user_y{` is pointless as you never call `userCompletionHandler` with `nil` values – vadian Jun 02 '19 at 08:54