async let does not compile because of the "Reference to captured var 'imageDescriptors' in concurrently-executing code" - swift

I am trying to use withThrowingTaskGroup to get some data from the web. I found that my code runs a bit slow, so I tried to use actors with async/await. I use Descriptors to get the
func getAllStocksList() async throws -> [SingleStockViewModel] {
var stockViewModels = [SingleStockViewModel]()
let urlString = URLBuilder.getAllStocks.makeString()
let (data, response) = try await URLSession.shared.data(from: URL(string: urlString)!)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
throw "Invalid HttpResponseCode"
}
let dataResponse = try JSONDecoder().decode([StockDetails].self, from: data)
let stocksDetailsList = dataResponse[..<25]
let stockSymbolsList = stocksDetailsList.map {
$0.title
}
let dataResponseDict = dataResponse.toDictionary {
$0.title
}
var stockPrices: [String: Double] = [:]
let imageUrlStringsDict = try await imageService.makeStockImageUrlStringsList(for: stockSymbolsList)
let nonEmptyImageUrlStringsList = imageUrlStringsDict.filter {
!$0.value.isEmpty
}
var imageDescriptors = [Descriptor]()
var chartDescriptors = [Descriptor]()
for item in nonEmptyImageUrlStringsList {
imageDescriptors.append(
Descriptor(
stockSymbol: item.key,
stockImageUrlString: nonEmptyImageUrlStringsList[item.key]!,
type: .image)
)
chartDescriptors.append(
Descriptor(stockSymbol: item.key,
stockImageUrlString: "",
type: .marketData)
)
let price = try await fetchStockPrice(for: item.key)
stockPrices[item.key] = price
}
print(chartDescriptors)
async let taskResults = try fetchGroupedStocksInfo(descriptors: imageDescriptors)
async let marketDataTaskResult = try fetchGroupedStocksInfo(descriptors: chartDescriptors)
let (taskResultsDict, marketDataTaskResultDict) = try await (taskResults, marketDataTaskResult)
}
and the stock descriptor is passed to
func fetchGroupedStocksInfo(descriptors: [Descriptor]) async throws -> [String: TaskResult] {
try await withThrowingTaskGroup(of: (String, TaskResult).self, returning: [String: TaskResult].self) { group in
for descriptor in descriptors {
group.addTask { [self] in
switch descriptor.type {
case .image:
let (_, image) = try await self.imageService.makeStockImageTuple(descriptor.stockImageUrlString)
let (symbol, marketResponse) = try await merketInfoSerice.fetchMarketInfo(descriptor.stockSymbol, numberOfDays: 3)
print(image)
return (descriptor.stockSymbol, TaskResult.image(image))
case .marketData:
let (symbol, marketResponse) = try await merketInfoSerice.fetchMarketInfo(descriptor.stockSymbol, numberOfDays: 3)
print(marketResponse.close)
return (symbol, TaskResult.marketData(marketResponse))
}
}
}
return try await group.reduce(into: [:]) {
$0[$1.0] = $1.1
}
}
}
and the errors found in compile time are
(63, 73) Reference to captured var 'imageDescriptors' in concurrently-executing code
(64, 82) Reference to captured var 'chartDescriptors' in concurrently-executing code
How can I make these error disappear, though I sense that my code can cause some problems at runtime.

You received this error:
Reference to captured var 'imageDescriptors' in concurrently-executing code
The issue is that you are supplying a mutable array to async let, an expression that will be await’ed later. As SE-0317 async let bindings says:
The right-hand side of a async let expression can be thought of as an implicit #Sendable closure ... the closure is #Sendable and nonisolated, meaning that it cannot access non-sendable state of the enclosing context.
Bottom line, the problem goes away if you replace it with an immutable array.
You can initialize them to constants directly:
let imageDescriptors = items.map { item in
Descriptor(
stockSymbol: item.key,
stockImageUrlString: nonEmptyImageUrlStringsList[item.key]!,
type: .image
)
}
let chartDescriptors = items.map { item in
Descriptor(
stockSymbol: item.key,
stockImageUrlString: "",
type: .marketData
)
}
You can make your own copy:
let descriptors = imageDescriptors
async let taskResults = try fetchGroupedStocksInfo(descriptors: descriptors)
Instead of async let, you can make your own Task with an explicit capture list:
let subtask1 = Task { [imageDescriptors] in
await self.fetchGroupedStocksInfo(descriptors: imageDescriptors)
}
let subtask2 = Task { [chartDescriptors] in
await try fetchGroupedStocksInfo(descriptors: chartDescriptors)
}
let (taskResultsDict, marketDataTaskResultDict) = try await (subtask1.value, subtask2.value)
NB: This opts out of structured concurrency.

Related

How to ignore async let throws error when save response in tuple?

i have a code like this:
Task {
async let configsReq = self.interactor.getConfigs()
async let optionsReq = self.interactor.getOptions()
async let updateStateReq = self.interactor.getAppUpdateState()
async let contactsReq = self.interactor.getContactOptions()
var config: Config?
var options: AvailableOptions?
var updateState: UpdateType?
var contacts: ContactOptions?
do {
config = try await configsReq
} catch {
config = nil
}
do {
options = try await optionsReq
} catch {
options = nil
}
do {
updateState = try await updateStateReq
} catch {
updateState = nil
}
do {
contacts = try await contactsReq
} catch {
contacts = nil
}
self.goToNextPage()
}
in this case it does not matter for me that the requests get correct response or throws error. i don't want to block user to get correct response.
And also I want to make sure that all my requests are answered (correct or error response) to take the user to the next page
how can i write these codes cleaner and better with new swift concurrency?
i tried like this (but i could not get match error to each related request):
Task {
async let configs = self.interactor.getConfigs()
async let options = self.interactor.getOptions()
async let updateState = self.interactor.getAppUpdateState()
async let contacts = self.interactor.getContactOptions()
do {
let array = try await [configs, options, updateState, contacts]
} catch {
print(error)
}
}
If I understand the question correctly, you want to:
“match error to each related request”, but that
you want to proceed regardless of success or failure, as it “does not matter for me that the requests get correct response or throws error”.
If that is the pattern you are looking for, I might suggest using Task result:
async let configsReq = Task { try await interactor.getConfigs() }
async let optionsReq = Task { try await interactor.getOptions() }
async let stateReq = Task { try await interactor.getAppUpdateState() }
async let contactsReq = Task { try await interactor.getContactOptions() }
let config = await configsReq.result
let options = await optionsReq.result
let state = await stateReq.result
let contacts = await contactsReq.result
goToNextPage(config: config, options: options, state: state, contacts: contacts)
Or, more concisely:
async let configs = Task { try await interactor.getConfigs() }
async let options = Task { try await interactor.getOptions() }
async let state = Task { try await interactor.getAppUpdateState() }
async let contacts = Task { try await interactor.getContactOptions() }
await goToNextPage(config: configs.result, options: options.result, state: state.result, contacts: contacts.result)
Where goToNextPage might be defined as:
func goToNextPage(
config: Result<Config, Error>,
options: Result<AvailableOptions, Error>,
state: Result<UpdateType, Error>,
contacts: Result<ContactOptions, Error>
) { … }
That way, goToNextPage can look at the .success or .failure for each, to retrieve either the value or error associated with each of the four requests.
Needless to say, you also could have four properties for these four requests, and then goToNextPage could refer to those, rather than taking them as parameters to the method. It’s functionally the same thing, but you have to decide either local vars that are passed to the next method or update properties that are accessed by the next method.
You asked:
… if we don't want to use Result anymore, how can do that?
Yes, we do not use Result very much, anymore, as that was historically a pattern for returning either value or error in traditional asynchronous patterns, and nowadays we try a series of tasks, catch thrown errors, but generally early exit once one of them fails.
But if you really want to capture the success and failure for each of the four concurrent requests, then Result encapsulates that quite well.
I would make a little helper that helps wrap the error into a Result:
extension Result {
init(asyncCatching block: () async throws -> Success) async where Failure == Error {
do {
self = .success(try await block())
} catch {
self = .failure(error)
}
}
}
In case of errors, you even get the Error object for each getXXX method, rather than just a nil. Of course, if you really just want a nil, you can write a helper that returns optionals instead.
// this is essentially like refactoring out the repeated parts of your first code
func asyncCatchWithNil<Result>(function: () async throws -> Result) async -> Result? {
do {
return try await function()
} catch {
return nil
}
}
Then you could do:
Task {
async let configs = Result(asyncCatching: self.interactor.getConfigs)
async let options = Result(asyncCatching: self.interactor.getOptions)
async let updateState = Result(asyncCatching: self.interactor.getAppUpdateState)
async let contacts = Result(asyncCatching: self.interactor.getContactOptions)
/* or
async let configs = asyncCatchWithNil(function: self.interactor.getConfigs)
async let options = asyncCatchWithNil(function: self.interactor.getOptions)
async let updateState = asyncCatchWithNil(function: self.interactor.getAppUpdateState)
async let contacts = asyncCatchWithNil(function: self.interactor.getContactOptions)
*/
let (configsResult, optionsResult, updateStateResult, contactsResult)
= await (configs, options, updateState, contacts)
// you can inspect each result here if you'd like
self.goToNextPage()
}
The idea here is that you get a type that can contain both the response and error at the point of async let, rather than catching the error later.

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:))

I am building an app with Swift and SwiftUI. In MainViewModel I have a function who call Api for fetching JSON from url and deserialize it. this is made under async/await protocol.
the problem is the next, I have received from xcode the next comment : "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates." in this part of de code :
func getCountries() async throws{
countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}
who calls this one:
func fetchCountries() async throws -> [Country]? {
guard let url = URL(string: CountryUrl.countriesJSON.rawValue ) else {
print("Invalid URL")
return nil
}
let urlRequest = URLRequest(url: url)
do {
let (json, _) = try await URLSession.shared.data(for: urlRequest)
if let decodedResponse = try? JSONDecoder().decode([Country].self, from: json) {
debugPrint("return decodeResponse")
return decodedResponse
}
} catch {
debugPrint("error data")
}
return nil
}
I would like to know if somebody knows how I can fix it
First fetch the data asynchronously and then assign the result to the property on the main thread
func getCountries() async throws{
let fetchedData = try await MainViewModel.countriesApi.fetchCountries()
await MainActor.run {
countries = fetchedData ?? []
}
}
Off topic perhaps but I would change fetchCountries() to return an empty array rather than nil on an error or even better to actually throw the errors since it is declared as throwing.
Something like
func fetchCountries() async throws -> [Country] {
guard let url = URL(string: CountryUrl.countriesJSON.rawValue ) else {
return [] // or throw custom error
}
let urlRequest = URLRequest(url: url)
let (json, _) = try await URLSession.shared.data(for: urlRequest)
return try JSONDecoder().decode([Country].self, from: json)
}
There are two ways to fix this. One, you can add the #MainActor attribute to your functions - this ensures they will run on the main thread. Docs: https://developer.apple.com/documentation/swift/mainactor. However, this could cause delays and freezing as the entire block will run on the main thread. You could also set the variables using DispatchQueue.main.async{} - see this article from Hacking With Swift. Examples here:
#MainActor func getCountries() async throws{
///Set above - this will prevent the error
///This can also cause a lag
countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}
Second option:
func getCountries() async throws{
DispatchQueue.main.async{
countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}
}

Cannot convert return expression of type 'Task<[String], Error>' to return type '[String]' when using Task.init{}

I'm learning how to iterate with async and await in swift.
My current stage is on:
import Foundation
import SwiftUI
import Darwin
enum MyError: Error {
case genError
}
let myString : String = """
https://httpbin.org/anything
https://httpbin.org/ip
https://httpbin.org/user-agent
https://httpbin.org/headers
https://httpbin.org/get
https://httpbin.org/post
https://httpbin.org/put
https://httpbin.org/delete
https://httpbin.org/gzip
https://httpbin.org/status/:code
https://httpbin.org/response-headers?key=val
https://httpbin.org/redirect/:n
https://httpbin.org/relative-redirect/:n
https://httpbin.org/cookies
https://httpbin.org/cookies/set/:name/:value
https://httpbin.org/basic-auth/:user/:passwd
https://httpbin.org/hidden-basic-auth/:user/:passwd
https://httpbin.org/digest-auth/:qop/:user/:passwd
https://httpbin.org/stream/:n
https://httpbin.org/delay/:n
"""
func fetchInfo(for url: String, with index:Int) async throws -> String {
let request = URLRequest(url: URL(string: url)!)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
print("error found\n" + String(index) + "\n" + (url))
throw MyError.genError }
let thisOutput = String(data: data, encoding: .utf8)!
return thisOutput
}
func fetchOnebyOne(urls: String) async throws -> [String] {
var count : Int = 0
var finalArray : [String] = []
for item in myString.components(separatedBy: "\n") {
count += 1
do {
let thisThis: String = try await fetchInfo(for: item, with: count)
finalArray.append(thisThis)
}
catch {
print("\(count) ---------------------------- error found\n\n\n\n")
}
} // : for
return finalArray
}
Task{
let finalOutput = try await fetchOnebyOne(urls: myString)
print(finalOutput)
}
For the fetchOneByOne(), on a webpage, I know I can use async to get the same results, so I rewrite this function:
func fetchOnebyOne(urls: String) {
async {
var count : Int = 0
var finalArray : [String] = []
for item in myString.components(separatedBy: "\n") {
count += 1
do {
let thisThis: String = try await fetchInfo(for: item, with: count)
finalArray.append(thisThis)
}
catch {
print("\(count) ---------------------------- error found\n\n\n\n")
}
} // : for
} //: async
}
fetchOnebyOne(urls: myString)
I'm successful to get the same output. But I got a yellow warning in Xcode, the async should be replaced with Task.init. So I change the async to Task.init. The output still same.
But actually you can see, the fetchOnebyOne() doesn't return a [String] anymore. Because I cannot solve the warnings if I make it return [String]. I tried the below code:
func fetchOnebyOne(urls: String) -> [String] {
Task.init {
var count : Int = 0
var finalArray : [String] = []
for item in myString.components(separatedBy: "\n") {
count += 1
do {
let thisThis: String = try await fetchInfo(for: item, with: count)
finalArray.append(thisThis)
}
catch {
print("\(count) ---------------------------- error found\n\n\n\n")
}
} // : for
return finalArray
} // : Task
}
the warning is:
No 'init' candidates produce the expected contextual result type
'[String]'
and I did some research and change the first line to
func fetchOnebyOne(urls: String) -> [String] {
Task.init {() async throws -> [String] in
var count : Int = 0
var finalArray : [String] = []
for item in myString.components(separatedBy: "\n") {
count += 1
do {
let thisThis: String = try await fetchInfo(for: item, with: count)
finalArray.append(thisThis)
}
catch {
print("\(count) ---------------------------- error found\n\n\n\n")
}
} // : for
return finalArray
} // : Task
}
I got warning:
Cannot convert return expression of type 'Task<[String], Error>' to
return type '[String]'
I stuck here and cannot find useful information about Task.init, especially about the error - 'Task<[String], Error>' on internet.
I did all of this for knowledge, for learning swift. No practical use. Hope people here could help. Thanks.

Swift Async let with loop

I want to get data in parallel. I found an example to call API in parallel but I want to store async let variables with loop.
Async let example. However, this example doesn't use a loop.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
I want to do something like the following.
let items = photoNames.map({ photo in
async let item = downloadPhoto(named: photo)
return item
})
let photos = await items
show(photos)
You can use a task group. See Tasks and Task Groups section of the The Swift Programming Language: Concurrency (which would appear to be where you got your example).
One can use withTaskGroup(of:returning:body:) to create a task group to run tasks in parallel, but then collate all the results together at the end.
E.g. here is an example that creates child tasks that return a tuple of “name” and ”image”, and the group returns a combined dictionary of those name strings with their associated image values:
func downloadImages(names: [String]) async -> [String: UIImage] {
await withTaskGroup(
of: (String, UIImage).self,
returning: [String: UIImage].self
) { [self] group in
for name in names {
group.addTask { await (name, downloadPhoto(named: name)) }
}
var images: [String: UIImage] = [:]
for await result in group {
images[result.0] = result.1
}
return images
}
}
Or, more concisely:
func downloadImages(names: [String]) async -> [String: UIImage] {
await withTaskGroup(of: (String, UIImage).self) { [self] group in
for name in names {
group.addTask { await (name, downloadPhoto(named: name)) }
}
return await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
}
}
They run in parallel:
But you can extract them from the dictionary of results:
let stooges = ["moe", "larry", "curly"]
let images = await downloadImages(names: stooges)
imageView1.image = images["moe"]
imageView2.image = images["larry"]
imageView3.image = images["curly"]
Or if you want an array sorted in the original order, just build an array from the dictionary:
func downloadImages(names: [String]) async -> [UIImage] {
await withTaskGroup(of: (String, UIImage).self) { [self] group in
for name in names {
group.addTask { await (name, downloadPhoto(named: name)) }
}
let dictionary = await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
return names.compactMap { dictionary[$0] }
}
}
Rob's answer is good. You can use an Array instead of a Dictionary too, to preserve order.
let photos = await photoNames.map(downloadPhoto)
public extension Sequence where Element: Sendable {
func mapWithTaskGroup<Transformed: Sendable>(
priority: TaskPriority? = nil,
_ transform: #escaping #Sendable (Element) async throws -> Transformed
) async rethrows -> [Transformed] {
try await withThrowingTaskGroup(
of: EnumeratedSequence<[Transformed]>.Element.self
) { group in
for (offset, element) in enumerated() {
group.addTask(priority: priority) {
(offset, try await transform(element))
}
}
return try await group.reduce(
into: map { _ in nil } as [Transformed?]
) {
$0[$1.offset] = $1.element
} as! [Transformed]
}
}
}
If the order of result doesn't matter here, use a TaskGroup instead.

Swift Completion & Loop Issue

Trying to download a PKG file from one of three urls. The logic basically finds the latency of a download from each download url and sets the final download url to the host with the lowest latency.
import Cocoa
import Alamofire
// Create my object via Struct
struct Package {
var latency: Double?
var name: String?
var statuscode: Int?
var download: Bool?
var downloadUrl: String?
}
// Download the package from the provided download url and return the object
func getPKG(pkgName: String, dpUrl: String, completion: #escaping (Package) -> (Package)) {
let url = URL(string: "\(dpUrl)\(pkgName)")
let parameters: Parameters = ["foo":"bar"]
Alamofire.download(url, method: .get, parameters: parameters, encoding: JSONEncoding.default, to: destination)
.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
debugPrint("Download Progress...: \(progress.fractionCompleted)")
}
.validate(statusCode: 200..<399)
.response { response in
debugPrint(response.response!)
debugPrint(response.response!.statusCode)
debugPrint(response.timeline.latency)
let dlObject = Package(latency: response.timeline.latency, name: pkgName, statuscode: response.response?.statusCode, download: true, downloadUrl: dpUrl)
completion(dlObject)
}
}
var share_response = [String: Double]()
var package_sources: NSArray! = ["https://www.jss1.com/Share", "https://www.jss2.com/Share", "https://www.jss3.com/Share"]
let package_names: String = ["Dummy1.pkg", "Dummy2.pkg", "Dummy3.pkg"]
// Loop through the package sources and find the one with
// the lowest latency.
for share_url in package_sources {
getPKG(pkgName: "Dummy.pkg", dpUrl: share_url, completion: {
dlObject in
if dlObject.latency != nil {
share_response[share_url] = dlObject.latency
} else {
debugPrint("nothing yet")
}
return dlObject
})
}
let final_download_url = share_response.min { a, b in a.value < b.value }
// Here is where it breaks and responds with nil
for package in package_names {
let download_url = URL(string: final_download_url + package)
Download commands here...
}
This is done by looping through each download url and populating a dictionary with the key as the url and the value as the latency. When the script moves on to download from the "fastest" download url, it fails with nil.
I'm assuming that's because the script is moving on while the completion handler is still running and nothing is in the dictionary yet, but how would I address this?
Based on the answer from #vadian at Synchronous request using Alamofire
...
let group = DispatchGroup()
var share_response = [String: Double]()
var package_sources: NSArray! = ["https://www.jss1.com/Share", "https://www.jss2.com/Share", "https://www.jss3.com/Share"]
let package_names: String = ["Dummy1.pkg", "Dummy2.pkg", "Dummy3.pkg"]
// Loop through the package sources and find the one with
// the lowest latency.
for share_url in package_sources {
group.enter()
getPKG(pkgName: "Dummy.pkg", dpUrl: share_url, completion: {
group.leave()
dlObject in
if dlObject.latency != nil {
share_response[share_url] = dlObject.latency
} else {
debugPrint("nothing yet")
}
return dlObject
})
}
group.notify(queue: DispatchQueue.main) {
let final_download_url = share_response.min { a, b in a.value < b.value }
// Here is where it breaks and responds with nil
for package in package_names {
let download_url = URL(string: final_download_url + package)
Download commands here...
}
}