Search code examples
swiftfirebasensurlsessiongoogle-books

How to wait for Swift's URLSession to finish before running again?


Probably a stupid question, but I'm a beginner at this.

The below code is supposed to get book information from Google Books from a keyword search. It then goes through the results and checks if I have a matching ISBN in a Firebase database. It works, but currently can only search 40 books as that's the Google Books API maximum per search.

Fortunately, I can specify where to start the index and get the next 40 books to search as well. Unfortunately, I've been trying for hours to understand how the URLSession works. All the methods I've tried have shown me that the code after the URLSession block doesn't necessarily wait for the session to complete. So if I check if I've found any matches afterward, it might not even be done searching.

I suspect the answer is in completion handling, but my attempts so far have been unsuccessful. Below is my code with a URL setup to take various starting index values.

var startingIndex = 0

        //encode keyword(s) to be appended to URL
        let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"

        URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
            if error != nil {
                print(error!.localizedDescription)
            }else{

                let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]

                if let items = json["items"] as? [[String: AnyObject]] {

                    //for each result make a book and add title
                    for item in items {
                        if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
                            let book = Book()
                            //default values
                            book.isbn13 = "isbn13"
                            book.isbn10 = "isbn10"
                            book.title = volumeInfo["title"] as? String

                            //putting all authors into one string
                            if let temp = volumeInfo["authors"] as? [String] {
                                var authors = ""
                                for i in 0..<temp.count {
                                    authors = authors + temp[i]
                                }
                                book.author = authors
                            }

                            if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
                                book.imageURL = imageLinks["thumbnail"]
                            }

                            //assign isbns
                            if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {

                                for i in 0..<isbns.count {

                                    let firstIsbn = isbns[i]
                                    if firstIsbn["type"] == "ISBN_10" {
                                        book.isbn10 = firstIsbn["identifier"]
                                    }else{
                                        book.isbn13 = firstIsbn["identifier"]
                                    }
                                }
                            }

                            //adding book to an array of books
                            myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
                                if snapshot.exists() {
                                    if listings.contains(book) == false{
                                        listings.append(book)
                                    }
                                    DispatchQueue.main.async { self.tableView.reloadData() }
                                }
                            })
                            myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
                                if snapshot.exists() {
                                    if listings.contains(book) == false{
                                        listings.append(book)
                                    }
                                    DispatchQueue.main.async { self.tableView.reloadData() }
                                }
                            })
                        }
                    }
                }
            }

            SVProgressHUD.dismiss()
            }.resume()

Below is my revised code:

 func searchForSale(query: String, startingIndex: Int) {

        directionsTextLabel.isHidden = true
        tableView.isHidden = false
        listings.removeAll()
        DispatchQueue.main.async { self.tableView.reloadData() }
        SVProgressHUD.show(withStatus: "Searching")

        //clear previous caches of textbook images
        cache.clearMemoryCache()
        cache.clearDiskCache()
        cache.cleanExpiredDiskCache()


        let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"

        URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
            if error != nil {
                print(error!.localizedDescription)
            }else{

                var needToContinueSearch = true

                let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]

                if json["error"] == nil {

                    let totalItems = json["totalItems"] as? Int
                    if totalItems == 0 {
                        SVProgressHUD.showError(withStatus: "No matches found")
                        return
                    }

                    if let items = json["items"] as? [[String: AnyObject]] {

                        //for each result make a book and add title
                        for item in items {

                            if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {

                                let book = Book()
                                //default values
                                book.isbn13 = "isbn13"
                                book.isbn10 = "isbn10"
                                book.title = volumeInfo["title"] as? String

                                //putting all authors into one string
                                if let temp = volumeInfo["authors"] as? [String] {
                                    var authors = ""
                                    for i in 0..<temp.count {
                                        authors = authors + temp[i]
                                    }
                                    book.author = authors
                                }

                                if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
                                    book.imageURL = imageLinks["thumbnail"]
                                }

                                //assign isbns
                                if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {

                                    for i in 0..<isbns.count {

                                        let firstIsbn = isbns[i]
                                        //checks if isbns have invalid characters
                                        let isImproperlyFormatted = firstIsbn["identifier"]!.contains {".$#[]/".contains($0)}

                                        if isImproperlyFormatted == false {
                                            if firstIsbn["type"] == "ISBN_10" {
                                                book.isbn10 = firstIsbn["identifier"]
                                            }else{
                                                book.isbn13 = firstIsbn["identifier"]
                                            }
                                        }
                                    }
                                }

                                //adding book to an array of books
                                myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
                                    if snapshot.exists() {
                                        if listings.contains(book) == false{
                                            listings.append(book)
                                            needToContinueSearch = false
                                        }
                                        DispatchQueue.main.async { self.tableView.reloadData() }
                                    }
                                })
                                myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
                                    if snapshot.exists() {
                                        if listings.contains(book) == false{
                                            listings.append(book)
                                            needToContinueSearch = false
                                        }
                                        DispatchQueue.main.async { self.tableView.reloadData() }
                                        return
                                    }
                                    if startingIndex < 500 {
                                        if needToContinueSearch {
                                            let nextIndex = startingIndex + 40
                                            self.searchForSale(query: query, startingIndex: nextIndex)
                                        }
                                    }
                                })
                            }
                        }
                    }
                }else{
                    return
                }
            }

            SVProgressHUD.dismiss()
            }.resume()

        //hide keyboard
        self.searchBar.endEditing(true)
    }

Solution

  • In your completion handler if any results have been returned you end with:

    DispatchQueue.main.async { self.tableView.reloadData() }
    

    to trigger reloading of your table with the updated information. At this same point is where you could determine of there may be more results and initiate the next asynchronous URL task. In outline your code might be:

    let needToContinueSearch : Bool = ...;
    
    DispatchQueue.main.async { self.tableView.reloadData() }
    
    if needToContinueSearch
    {  // call routine it initiate next async URL task
    }
    

    (If there is any reason to start the task from the main thread the if would be in the block.)

    By not initiating the next search until after you've processed the results of the first you avoid having to deal with any issues of a subsequent callback trying to update your data at the same time as a previous one.

    However if you find delaying the second search in this way is too slow you can investigate ways to overlap the operations, e.g. you might have the callback just pass the processing of the results to an async task on a serial queue (so that only one set of results is being processed at once) and initiate the next async URL task.

    HTH