为什么代码没有被执行 after/within 秒 URLSession.shared.dataTask,即在初始 URLSession.shared.dataTask 的 do 块内? Swift

Why isn't code being executed after/within a second URLSession.shared.dataTask, that is within an initial URLSession.shared.dataTask’s do block? Swift

我正在尝试发出第二个 API 端点请求,为此,我在 initial/first API 端点请求的 URLSession.shared.dataTask 的 do 块。但是,我的代码不执行 after/within 第二个 API 端点请求的 URLSession.shared.dataTask 行 code/scope.

当我 运行 程序时,我不断得到一个在第二个 API 端点请求的 URLSession.shared.dataTask 范围之外执行的无限 while 循环。

我在这里使用 Yelp Fusion API 的“搜索”端点。文档:https://www.yelp.com/developers/documentation/v3/business_search

我得到原始代码和格式的教程:https://medium.com/@khansaryan/yelp-fusion-api-integration-af50dd186a6e

代码:

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).

谢谢!

--

更新:

下面是使用@Paulw11 解决方案的更新 FetchData.swift 版本(还不包​​括 async/await 代码,因为我想弄清楚如何 return 值(变量 totalBusinesses 的值)来自 makeInitialAPIRequest 函数的 totalBusinesses 也 contains/sends 返回完成处理程序,首先是 retrieveVenues 函数。这是当前的症结所在。感谢您的帮助!):

使用@Paulw11 的解决方案更新了 FetchData.swift 版本,没有 async/await 代码:

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()
}

由于您的目标是 iOS 15,因此您不接受 async/await 会让事情变得更加困难。您还可以使用 Codable 来处理 JSON 解析。

首先,创建 Codable 结构来处理结果(提示 app.quicktype.io 可以为您完成):

// 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
}

然后您可以创建一个 api class 来使用 async/await 来获取数据。

基本策略是:

  • 获取第一个结果
  • 记下预期的总结果
  • 将总数限制为 1000(这是一个 API 限制)
  • 不断提出请求,每次增加 offset,直到获得预期结果。
  • Return 结果
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
    }
}

我使用了 URLComponents 而不是字符串插值,因为它可以在需要时处理诸如百分比编码之类的事情。