Home > Mobile >  How to call an instance of a class that makes an API call, and a function within that class that mak
How to call an instance of a class that makes an API call, and a function within that class that mak

Time:05-27

This is a follow up to the following question: 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

I’m trying to assign the current instance of a variable using self.variable to a function call from an instance of a class.

This can be seen at line of code starting with: “self.venues = “ in ViewController.swift in the attached code.

I believe Task has something to do with this. I’ve read the documentation about Task, and have read more about it online, but haven’t found the solution yet.

Also: I get the error message “Cannot assign value of type 'Task<(), Never>' to type '[Venue]'" at line of code starting with: “self.venues = “ in ViewController.swift.

Code:

ViewController.swift:

import UIKit
import CoreLocation

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    @IBOutlet var tableView: UITableView!
    
    var venues: [Venue] = []
    
    override func viewDidLoad() async {
        super.viewDidLoad()
        
        tableView.register(UINib(nibName: "CustomTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomTableViewCell")
        tableView.delegate = self
        tableView.dataSource = self
        
        let yelpApi = YelpApi(apiKey: "Api key")
        
        self.venues = Task {
            do { try await yelpApi.searchBusiness(latitude: selectedLatitude, longitude: selectedLongitude, category: "category quary goes here", sortBy: "sort by quary goes here", openAt: )

            }
            catch {
            //Handle error here.
            print("Error")
            }
        }
        
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
        
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return venues.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath) as! CustomTableViewCell
        
        //Details for custom table view cell go here.
    }
        
    //Rest of table view protocol functions.
}

Venue.swift:

import Foundation

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

// MARK: - Business
struct Venue: 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
}

FetchData.swift:

import UIKit
import Foundation
import CoreLocation

class YelpApi {
    
    private var apiKey: String
    
    init(apiKey: String) {
        self.apiKey = apiKey
    }
    
    func searchBusiness(latitude: Double,
                        longitude: Double,
                        category: String,
                        sortBy: String) async throws -> [Venue] {
        
        var queryItems = [URLQueryItem]()
        queryItems.append(URLQueryItem(name:"latitude",value:"\(latitude)"))
        queryItems.append(URLQueryItem(name:"longitude",value:"\(longitude)"))
        queryItems.append(URLQueryItem(name:"categories", value:category))
        queryItems.append(URLQueryItem(name:"sort_by",value:sortBy))
       
        var results = [Venue]()
        
        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
    }
}

Thanks!

CodePudding user response:

You have an asynchronous operation - searchBusinesses. When you call this function, it takes time to get a result. You are using await to handle this.

You can't use await outside of an asynchronous context, which viewDidLoad is not. You are using a Task to create an asynchronous context. So far so good.

Where you have gone wrong is trying to assign the result to venues. You can only perform this assignment once the await completes. You don't get this result from a Task, you get it in the Task:

Task {
    do { 
        self.venues = try await yelpApi.searchBusiness(latitude: selectedLatitude, longitude: selectedLongitude, category: "category quary goes here", sortBy: "sort by quary goes here", openAt: )
        self.tableView.reloadData()
    } catch {
            //Handle error here.
            print("Error")
    }
}

Note that you should not combine GCD dispatch queues and async/await and in this case, you don't need to worry about the main queue.

UIViewController is tagged @MainActor. This means that Tasks are already executed on the main actor unless you specifically create a detached task.

  • Related