I'm trying to make a second API endpoint request, and to do so, am making a second URLSession.shared.dataTask within the initial/first API endpoint request’s URLSession.shared.dataTask’s do block. However, my code doesn't execute after/within the second API endpoint request's URLSession.shared.dataTask’s line of code/scope.

I keep getting an infinite while loop that executes outside the scope of the second API endpoint request's URLSession.shared.dataTask when I run the program.

I’m using the Yelp Fusion API’s “Search” endpoint here. Documentation: https://www.yelp.com/developers/documentation/v3/business_search

The tutorial that I got the original code and format from: https://medium.com/@khansaryan/yelp-fusion-api-integration-af50dd186a6e

Code:

Venue.swift:

import Foundation

struct Venue {
    var name: String?
    var id: String?
    var rating: Float?
}

FetchData.swift:

import Foundation

extension ViewController {
    
    func retrieveVenues(latitude: Double,
                        longitude: Double,
                        category: String,
                        limit: Int,
                        sortBy: String,
                        completionHandler: @escaping ([Venue]?, Error?) -> Void) {

        //Prints
        print("Check 1")

        //Making API Call
        let apikey =
        "API key"

        let baseURL =
        "https://api.yelp.com/v3/businesses/search?latitude=\(latitude)&longitude=\(longitude)&categories=\(category)&limit=\(limit)&sort_by=\(sortBy)"

        let url = URL(string: baseURL)

        // Creating Request
        var request = URLRequest(url: url!)
        request.setValue("Bearer \(apikey)", forHTTPHeaderField: "Authorization")
        request.httpMethod = "GET"

        //Prints before "boringssl_metrics_log_metric_block_invoke" log statement.
        print("Check 2")

        //Log statement "boringssl_metrics_log_metric_block_invoke" printed after the below line of code.

        //Initialize session and task
        URLSession.shared.dataTask(with: request) { (data, response, error) in

            //Prints after "boringssl_metrics_log_metric_block_invoke" log statement.
            print("Check 3")

            if let error = error {
                completionHandler(nil, error)

               //Doesn't print. Is set to print after "boringssl_metrics_log_metric_block_invoke" log statement.
               print("Check 4")

            }
            
            //Prints after "boringssl_metrics_log_metric_block_invoke" log statement.
            print("Check 5")
            
            do {

                //Prints.
                print("Check 6")

                // Read data as JSON
                let json = try JSONSerialization.jsonObject(with: data!, options: [])

                //Prints.
                print("Check 7")

                // Main dictionary
                guard let resp = json as? NSDictionary else {return}

                //Prints.
                print("Check 8")

                guard let totalBusinesses = resp.value(forKey: "total") as? Int else {return}
                
                //Prints.
                print("totalBusinesses outisde and after guard-let statment:", totalBusinesses)

                
                // Businesses
                guard let businesses = resp.value(forKey: "businesses") as? [NSDictionary] else {return}

                //Prints.
                print("Check 9")

                var venuesList: [Venues] = []

                //Prints.
                print("Check 10")              

                //Accessing each business
                for business in businesses {
                    var venue = Venues()
                    venue.name = business.value(forKey: "name") as? String
                    venue.id = business.value(forKey: "id") as? String
                    venue.rating = business.value(forKey: "rating") as? Float
                     
                    venuesList.append(venue)

                    //Prints.
                    print("venuesList.count inside the first initial API Search endpoint request:", venuesList.count)

                }
                
                //Prints.
                print("venuesList.count outside the first initial API Search endpoint request, and its for-loop: for business in businesses { and before the while loop for extra needed API Search endpoint requests below:", venuesList.count)


                //Code for making the amount of API requests to show and add all businesses to venuesList using limit and offsset pararmeters, and totalBusinesses variable. Limit is always 50, and offsset parameter as of now is also always 50, and will be incrimented by 50 at then end of the while loop's executing code's body (within the while loop).

                //Code for an if-statement if the total number of businesses from the initial API Search enpdoint request is more than 50, and therefore, need to make more API "Search" endpoint requests.
                if totalBusinesses > 50 {

                    //Code for making more requests.

                    //Offset value counter. Will add a 50 at the end of every while loop iteration (within evey while loop iteration.)
                    var offsetValue = 50

                    //Print check for offsetValue before while loop for any extra needed requests. Should just print 50. Prints 50. 
                    print("offsetValue before while loop for any extra needed API Search endpoint requests:", offsetValue)

                    //Print Check for seeing what venuesList.count is before the while loop below for any extra needed requests. Prints.
                    print("venuesList.count before while loop for any extra needed API Search endpoint requests:", venuesList.count)

                    //While loop for making requests and adding venue to VeneusList until the total number of businesses have been added.
                    while venuesList.count != totalBusinesses {

                        let baseURL =
                        "https://api.yelp.com/v3/businesses/search?latitude=\(latitude)&longitude=\(longitude)&categories=\(category)&limit=\(limit)&offset=\(offsetValue)&sort_by=\(sortBy)"
                        
                        let url = URL(string: baseURL)

                        // Creating Request
                        var request = URLRequest(url: url!)
                        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
                        request.httpMethod = "GET"

                        //Prints, however, the "boringssl_metrics_log_metric_block_invoke" log statement isn't printed after it, when it should be. Is described more below.
                        print("Check 2")

                        //Log statement "boringssl_metrics_log_metric_block_invoke" is not printed after the below line of code.

                        //Print statements aren't being printed after the below line of code starting with "URLSession.shared.dataTask".
                        //Initialize session and task
                        URLSession.shared.dataTask(with: request) { (data, response, error) in

                            //Print check after below code doesnt print, therefore code under it isn't being executed.

                            //Doesn't print.
                            print("Check 3")

                            if let error = error {
                                completionHandler(nil, error)

                                //Doesn't print.
                                print("Check 4")
                            }
                            //Doesn't print.
                            print("Check 5")

                            do {

                                //Doesn't print.
                                print("Check 6")

                                // Read data as JSON
                                let json = try JSONSerialization.jsonObject(with: data!, options: [])

                                //Doesn't print.
                                print("Check 7")

                                // Main dictionary
                                guard let resp = json as? NSDictionary else {return}

                                //Doesn't print.
                                print("Check 8")

                                // Businesses
                                guard let businesses = resp.value(forKey: "businesses") as? [NSDictionary] else {return}

                                
                                //Print Check for businesses at start of first needed extra API Endpoint Request with offset of 50. Doesn't print.
                                print("Businesses at start of first needed extra API Endpoint Request with offset of 50:", businesses)

                                //Doesn't print.
                                print("Check 9")

                                //Accessing each business
                                for business in businesses {
                                    var venue = Venues()
                                    venue.name = business.value(forKey: "name") as? String
                                    venue.id = business.value(forKey: "id") as? String
                                    venue.rating = business.value(forKey: "rating") as? Float
                                     
                                    venuesList.append(venue)
                                }
                                

                            } catch {
                                print("Caught error")
                            }
                            }.resume()

                        offsetValue += 50

                        //Prints.
                        print("offsetValue after its incrimented by 50 at end of and still within while-loop:", offsetValue)
                        
                        //Prints.
                        print("venuesList.count after offsetValue print statement where its incrimented by 50 at the end of and still within while-loop:",  venuesList.count)


                    }
                    //While Loop closing bracket is one line above this comment.

                    //Print check for exitting while loop.
                    //Still isn't being printed yet, because am stuck in an infinite while loop.
                    print("Exited while loop for any needed extra API endpoint requests.")
                }
                //closing bracket of if-statement: "if totalBusinesses > 50 {" is one line above this comment.
                
                completionHandler(venuesList, nil)
                
            } catch {
                print("Caught error")
            }
            }.resume()

    }
}

Current Return Statement in Terminal:

Check 1
Check 2
[Date and time when project is run and project name] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
Check 3
Check 5
Check 6
Check 7
Check 8
totalBusinesses outisde and after guard-let statment: 103
Check 9
Check 10
venuesList.count inside the first initial API Search endpoint request: 1
venuesList.count inside the first initial API Search endpoint request: 2
...
venuesList.count inside the first initial API Search endpoint request: 49
venuesList.count inside the first initial API Search endpoint request: 50
venuesList.count outside the first initial API Search endpoint request, and its for-loop: for business in business { and before the while loop for extra needed API Search endpoint requests below: 50
offsetValue before while loop for extra needed requests: 50
venuesList.count before while loop for any extra needed API Search endpoint requests: 50
Check 2
offsetValue before while loop for extra needed requests: 100
venuesList.count before while loop for any extra needed API Search endpoint requests: 50
Check 2
offsetValue before while loop for extra needed requests: 150
venuesList.count before while loop for any extra needed API Search endpoint requests: 50
Check 2
offsetValue before while loop for extra needed requests: 200
venuesList.count before while loop for any extra needed API Search endpoint requests: 50
Check 2
Continues in an infinite while loop until quitting program (closing or stopping simulator).

Thanks!

--

Update:

Below is an updated FetchData.swift version using @Paulw11's solution (Doesn't include the async/await code yet, because I want to figure out how to return the value (the variable totalBusinesses's value) totalBusinesses from the makeInitialAPIRequest function that also contains/sends back a completion handler, to the retrieveVenues function, first. This is a current sticking point. Thanks for the help!):

Updated FetchData.swift version using @Paulw11's solution without async/await code:

import Foundation

extension ViewController {
    
    //Below code is actually located at top of ViewController class.
    var outerScopeRunningVenuesList: [Venue] = []
    
    func retrieveVenues(latitude: Double,
                        longitude: Double,
                        category: String,
                        limit: Int,
                        sortBy: String,
                        completionHandler: @escaping ([Venue]?, Error?) -> Void) {

        //Code for making first/Intial API request, and using outerScopeRunningVenuesList for venuesList values.
        makeInitialAPIRequest(latitude: latitude,
                              longitude: longitude,
                              category: category,
                              limit: limit,
                              sortBy: sortBy) { (response, error) in
        
            if let response = response {
                
                self.outerScopeRunningVenuesList = response
                
                //*Still need to handle the error here, do later.

            }
        }
        
        //Code for getting totalBusinesses return value from makeInitialAPIRequest function.
        var overallMakeInitialAPIRequestReturnValue = makeInitialAPIRequest(latitude: latitude,
                                                                     longitude: longitude,
                                                                     category: category,
                                                                     limit: limit,
                                                                     sortBy: sortBy) { (response, error) in
                                               
                                                   if let response = response {
                                                       
                                                       self.outerScopeRunningVenuesList = response
                                                       
                                                       //*Still need to handle the error here, do later.

                                                   }
                                               }
        
        
        //Getting totalBusinesses return value.
        var recievedTotalBusinesses = overallMakeInitialAPIRequestReturnValue.0
        

        //Code for making the amount of API requests to show and add all businesses to venuesList using limit and offsset pararmeters, and totalBusinesses variable. Limit is always 50, and offsset parameter as of now is also always 50, and will be incrimented by 50 at then end of the while loop's executing code's body (within the while loop).

        //Code for an if-statement if the total number of businesses from the initial API Search enpdoint request is more than 50, and therefore, need to make more API "Search" endpoint requests.
        if recievedTotalBusinesses > 50 {

            //Code for making more requests.

            //Offset value counter. Will add a 50 at the end of every while loop iteration (within evey while loop iteration.)
            var offsetValue = 50

            //Print check for offsetValue before while loop for any extra needed requests. Should just print 50.
            print("offsetValue before while loop for extra needed requests:", offsetValue)

            //Print Check for seeing what venuesList.count is before the while loop below.
            print("outerScopeRunningVenuesList.count before while loop for any extra needed API Search endpoint requests:", outerScopeRunningVenuesList.count)

            //While loop for making requests and adding venue to VeneusList until the total number of businesses have been added.
            while outerScopeRunningVenuesList.count != recievedTotalBusinesses {

                //Code for making extra needed API requests, and using outerScopeRunningVenuesList for venuesList values.
                makeAnyExtraNeededAPIRequest(venuesList: outerScopeRunningVenuesList,
                                             offsetValue: offsetValue,
                                             latitude: latitude,
                                             longitude: longitude,
                                             category: category,
                                             limit: limit,
                                             sortBy: sortBy) { (response, error) in
                
                    if let response = response {
                        
                        self.outerScopeRunningVenuesList = response

                        //*Still need to handle the error here, do later.
                    }


                }

                offsetValue += 50

                print("offsetValue after its incrimented by 50 at end of and still within while-loop:", offsetValue)

                print("outerScopeRunningVenuesList.count after offsetValue print statement where its incrimented by 50 at the end of and still within while-loop:",  outerScopeRunningVenuesList.count)


            }
            //While Loop closing bracket is one line above this comment.

            //Print check for exitting while loop.
            //Still isn't being printed yet, because am stuck in an infinite while loop.
            print("Exitted while loop for any needed extra API Endpoint requests.")
        }
        //Closing bracket of if-statement: "if totalBusinesses > 50 {" is one line above this comment.
                
        completionHandler(outerScopeRunningVenuesList, nil)

    }
    
    func makeInitialAPIRequest(latitude: Double,
                               longitude: Double,
                               category: String,
                               limit: Int,
                               sortBy: String,
                               completionHandler: @escaping ([Venue]?, Error?) -> Void) -> (totalBusinesses: Int) {
        
        print("Check 1")
        
        //Making API Call
        let apikey =
        "API key"

        let baseURL =
        "https://api.yelp.com/v3/businesses/search?latitude=\(latitude)&longitude=\(longitude)&categories=\(category)&limit=\(limit)&sort_by=\(sortBy)"

        let url = URL(string: baseURL)

        // Creating Request
        var request = URLRequest(url: url!)
        request.setValue("Bearer \(apikey)", forHTTPHeaderField: "Authorization")
        request.httpMethod = "GET"
        
        print("Check 2")

        //Initialize session and task
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            
            print("Check 3")

            if let error = error {
                completionHandler(nil, error)
                
                print("Check 4")

            }
            
            print("Check 5")
            
            do {
                
                print("Check 6")

                // Read data as JSON
                let json = try JSONSerialization.jsonObject(with: data!, options: [])
                
                print("Check 7")

                // Main dictionary
                guard let resp = json as? NSDictionary else {return}
                
                print("Check 8.1: Before totalBusinesses.")

                guard let totalBusinesses = resp.value(forKey: "total") as? Int else {return}
                
                print("Check 8.2: After totalBusinesses and before businesses.")

                
                // Businesses
                guard let businesses = resp.value(forKey: "businesses") as? [NSDictionary] else {return}
                
                print("Check 9")

                var venuesList: [Venues] = []

                
                //Accessing each business
                for business in businesses {
                    var venue = Venues()
                    venue.name = business.value(forKey: "name") as? String
                    venue.id = business.value(forKey: "id") as? String
                    venue.rating = business.value(forKey: "rating") as? Float
                     
                    venuesList.append(venue)
                }
                
                completionHandler(venuesList, nil)
                return totalBusinesses
                
            } catch {
                print("Caught error")
            }
            }.resume()
        
    }
    
    func makeAnyExtraNeededAPIRequests(veneusList: [Venue]?,
        offsetValue: Int,
        latitude: Double,
        longitude: Double,
        category: String,
        limit: Int,
        sortBy: String,
        completionHandler: @escaping ([Venue]?, Error?) -> Void)  {
        
        print("Check 1")
        
        //Code for making any needed extra API endpoint requests.
        let baseURL =
        "https://api.yelp.com/v3/businesses/search?latitude=\(latitude)&longitude=\(longitude)&categories=\(category)&limit=\(limit)&offset=\(offsetValue)&sort_by=\(sortBy)"
        
        let url = URL(string: baseURL)

        // Creating Request
        var request = URLRequest(url: url!)
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.httpMethod = "GET"

        print("Check 2")

        //Print statements arent being printed after below line of code starting with "URLSession.shared.dataTask".
        //Initialize session and task
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            
            print("Check 3")

            if let error = error {
                completionHandler(nil, error)

                
                print("Check 4")
            }
            
            print("Check 5")

            do {

                
                print("Check 6")

                // Read data as JSON
                let json = try JSONSerialization.jsonObject(with: data!, options: [])

                
                print("Check 7")

                // Main dictionary
                guard let resp = json as? NSDictionary else {return}

                
                print("Check 8")

                // Businesses
                guard let businesses = resp.value(forKey: "businesses") as? [NSDictionary] else {return}

                
                //Print Check for businesses at needed extra API Endpoint Request.
                print("Businesses at needed extra API Endpoint Request:", businesses)

                
                print("Check 9")

                //Accessing each business
                for business in businesses {
                    var venue = Venues()
                    venue.name = business.value(forKey: "name") as? String
                    venue.id = business.value(forKey: "id") as? String
                    venue.rating = business.value(forKey: "rating") as? Float
                     
                    venuesList.append(venue)
                }
                
                
                completionHandler(venuesList, nil)

            } catch {
                print("Caught error")
            }
            }.resume()
}
1

There are 1 answers

5
Paulw11 On BEST ANSWER

Since you are targeting iOS 15, you are making things much harder for yourself by not embracing async/await. You can also use Codable to handle the JSON parsing.

First, create Codable structs to handle the result (hint app.quicktype.io can do this for you):

// MARK: - BusinessSearchResult
struct BusinessSearchResult: Codable {
    let total: Int
    let businesses: [Business]
    let region: Region
}

// MARK: - Business
struct Business: Codable {
    let rating: Double
    let price, phone, alias: String?
    let id: String
    let isClosed: Bool?
    let categories: [Category]
    let reviewCount: Int?
    let name: String
    let url: String?
    let coordinates: Center
    let imageURL: String?
    let location: Location
    let distance: Double
    let transactions: [String]

    enum CodingKeys: String, CodingKey {
        case rating, price, phone, id, alias
        case isClosed
        case categories
        case reviewCount
        case name, url, coordinates
        case imageURL
        case location, distance, transactions
    }
}

// MARK: - Category
struct Category: Codable {
    let alias, title: String
}

// MARK: - Center
struct Center: Codable {
    let latitude, longitude: Double
}

// MARK: - Location
struct Location: Codable {
    let city, country, address2, address3: String?
    let state, address1, zipCode: String?

    enum CodingKeys: String, CodingKey {
        case city, country, address2, address3, state, address1
        case zipCode
    }
}

// MARK: - Region
struct Region: Codable {
    let center: Center
}

Then you can create an api class that uses async/await to fetch the data.

The basic strategy is:

  • Fetch the first results
  • Take a note of the total results that are expected
  • Limit the total to 1000 (This is an API limit)
  • Keep making requests, increasing the offset each time, until you have the expected results.
  • Return the results
class YelpApi {
    
    enum SortOption: String {
        case bestMatch="best_match"
        case rating="rating"
        case reviewCount="review_count"
        case distance="distance"
    }
    
    private var apiKey: String
    
    init(apiKey: String) {
        self.apiKey = apiKey
    }
    
    func searchBusiness(latitude: Double,
                        longitude: Double,
                        category: String? = nil,
                        sortBy: SortOption? = nil) async throws -> [Business] {
        
        var queryItems = [URLQueryItem]()
        queryItems.append(URLQueryItem(name:"latitude",value:"\(latitude)"))
        queryItems.append(URLQueryItem(name:"longitude",value:"\(longitude)"))
        if let category = category {
            queryItems.append(URLQueryItem(name:"categories", value:category))
        }
        if let sortOption = sortBy {
            queryItems.append(URLQueryItem(name:"sort_by",value:sortOption.rawValue))
        }
        
        var results = [Business]()
        
        var expectedCount = 0
        let countLimit = 50
        var offset = 0
        
        queryItems.append(URLQueryItem(name:"limit", value:"\(countLimit)"))
        
        repeat {
            
            var offsetQueryItems = queryItems
            
            offsetQueryItems.append(URLQueryItem(name:"offset",value: "\(offset)"))
            
            var urlComponents = URLComponents(string: "https://api.yelp.com/v3/businesses/search")
            urlComponents?.queryItems = offsetQueryItems
            
            guard let url = urlComponents?.url else {
                throw URLError(.badURL)
            }
            
            var request = URLRequest(url: url)
            request.setValue("Bearer \(self.apiKey)", forHTTPHeaderField: "Authorization")
            
            let (data, _) = try await URLSession.shared.data(for: request)
            let businessResults = try JSONDecoder().decode(BusinessSearchResult.self, from:data)

            expectedCount = min(businessResults.total,1000)
            
            results.append(contentsOf: businessResults.businesses)
            offset += businessResults.businesses.count
        } while (results.count < expectedCount)
        
        return results
    }
}

I have used URLComponents rather than string interpolation as this handles things like percent encoding where required.