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.