I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}
I'm having an issue developing my app. The thing is this, when I enter a certain code, a JSON file starts downloading and got saved in an array. That is fine, the problem is ones it finishes downloading I tried to automatically save the information in CoreData, by running this code below.
var i:Int = 0
while(i < itemsDescargados.count){
let invitacionItems = Invitem(context: self.mangedObjectContext)
invitacionItems.titulo = itemsDescargados[i].titulo
invitacionItems.modo = itemsDescargados[i].modo
invitacionItems.id = itemsDescargados[i].id
invitacionItems.descripcion = itemsDescargados[i].descripcion
invitacionItems.category = itemsDescargados[i].category
invitacionItems.rojo = itemsDescargados[i].rojo
invitacionItems.verde = itemsDescargados[i].verde
invitacionItems.azul = itemsDescargados[i].azul
invitacionItems.imagen = itemsDescargados[i].imagen
do {
try self.mangedObjectContext.save()
print("Downloaded and Saved!")
}
catch{
print("errorCoreData: \(error)")
}
i = i + 1
}
When I put this code into a button and tap it myself it works perfectly, it saves the data and everything is fine, but the problem is that if I run this code into the network manager class class NetworkManagerInv: ObservableObject { after the download it's finished then when the app runs try self.mangedObjectContext.save() automatically prints in the Console log "errorCoreData:nilError" and doesn't save anything.
I did a lot of research but I didn't find anything helpful so I hope you guys can help me. Thanks in advance, sorry if I misspelled something English is not my main language.
Here the NetworkManagerCode
class NetworkManagerInv: ObservableObject {
#Environment(\.managedObjectContext) var mangedObjectContext
#FetchRequest(fetchRequest: Invitem.getAllItems()) var invitem:FetchedResults<Invitem>
var didChange = PassthroughSubject<NetworkManagerInv, Never>()
var jsoninvItem = [JsoninvItem]() {
didSet {
didChange.send(self)
}
}
init(){
let configuration = URLSessionConfiguration.ephemeral
let session = URLSession(configuration: configuration)
print("Network")
guard let url = URL(string: "http://192.168.1.42:" + codigo + "/items.json") else { return }
session.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
let items2 = try! JSONDecoder().decode([JsoninvItem].self, from: data)
DispatchQueue.main.async {
self.jsoninvItem = items2
itemsDescargados = items2
if(imgDescargadas.count>0){
//Here i need to put the code, this runs ones it finished
}
}
print("Compleated fetching json items")
}.resume()
}
I would like to simulate async and await request from Javascript to Swift 4. I searched a lot on how to do it, and I thought I found the answer with DispatchQueue, but I don't understand how it works.
I want to do a simple stuff:
if let items = result.value {
var availableBornes = [MGLPointFeature]()
for item in items {
guard let id = item.id else { continue }
let coordinate = CLLocationCoordinate2D(latitude: Double(coor.x), longitude: Double(coor.y))
// ...
// This is an asynchronous request I want to wait
await _ = directions.calculate(options) { (waypoints, routes, error) in
guard error == nil else {
print("Error calculating directions: \(error!)")
return
}
// ...
if let route = routes?.first {
let distanceFormatter = LengthFormatter()
let formattedDistance = distanceFormatter.string(fromMeters: route.distance)
item.distance = formattedDistance
// Save feature
let feature = MGLPointFeature()
feature.attributes = [
"id": id,
"distance": formattedDistance
]
availableBornes.append(feature)
}
}
}
// This should be called after waiting for the async requests
self.addItemsToMap(availableBornes: availableBornes)
}
What should I do?
Thanks to vadian's comment, I found what I expected, and it's pretty easy. I use DispatchGroup(), group.enter(), group.leave() and group.notify(queue: .main){}.
func myFunction() {
let array = [Object]()
let group = DispatchGroup() // initialize
array.forEach { obj in
// Here is an example of an asynchronous request which use a callback
group.enter() // wait
LogoRequest.init().downloadImage(url: obj.url) { (data) in
if (data) {
group.leave() // continue the loop
}
}
}
group.notify(queue: .main) {
// do something here when loop finished
}
}
We have to await!
The async-await Swift Evolution proposal SE-0296 async/await was accepted after 2 pitches and revision modifications recently on December 24th 2020. This means that we will be able to use the feature in Swift 5.5. The reason for the delay is due to backwards-compatibility issues with Objective-C, see SE-0297 Concurrency Interoperability with Objective-C. There are many side-effects and dependencies of introducing such a major language feature, so we can only use the experimental toolchain for now. Because SE-0296 had 2 revisions, SE-0297 actually got accepted before SE-0296.
General Use
We can define an asynchronous function with the following syntax:
private func raiseHand() async -> Bool {
sleep(3)
return true
}
The idea here is to include the async keyword alongside the return type since the call site will return (BOOL here) when complete if we use the new await keyword.
To wait for the function to complete, we can use await:
let result = await raiseHand()
Synchronous/Asynchronous
Defining synchronous functions as asynchronous is ONLY forward-compatible - we cannot declare asynchronous functions as synchronous. These rules apply for function variable semantics, and also for closures when passed as parameters or as properties themselves.
var syncNonThrowing: () -> Void
var asyncNonThrowing: () async -> Void
...
asyncNonThrowing = syncNonThrowing // This is OK.
Throwing functions
The same consistency constraints are applied to throwing functions with throws in their method signature, and we can use #autoclosures as long as the function itself is async.
We can also use try variants such as try? or try! whenever we await a throwing async function, as standard Swift syntax.
rethrows unfortunately still needs to go through Proposal Review before it can be incorporated because of radical ABI differences between the async method implementation and the thinner rethrows ABI (Apple wants to delay the integration until the inefficiencies get ironed out with a separate proposal).
Networking callbacks
This is the classic use-case for async/await and is also where you would need to modify your code:
// This is an asynchronous request I want to wait
await _ = directions.calculate(options) { (waypoints, routes, error) in
Change to this:
func calculate(options: [String: Any]) async throws -> ([Waypoint], Route) {
let (data, response) = try await session.data(from: newURL)
// Parse waypoints, and route from data and response.
// If we get an error, we throw.
return (waypoints, route)
}
....
let (waypoints, routes) = try await directions.calculate(options)
// You can now essentially move the completion handler logic out of the closure and into the same scope as `.calculate(:)`
The asynchronous networking methods such as NSURLSession.dataTask now has asynchronous alternatives for async/await. However, rather than passing an error in the completion block, the async function will throw an error. Thus, we have to use try await to enable throwing behaviour. These changes are made possible because of SE-0297 since NSURLSession belongs to Foundation which is still largely Objective-C.
Code impacts
This feature really cleans up a codebase, goodbye Pyramid of Doom 👋!
As well as cleaning up the codebase, we improve error handling for nested networking callbacks since the error and result are separated.
We can use multiple await statements in succession to reduce the dependency on DispatchGroup. 👋 to Threading Deadlocks when synchronising DispatchGroups across different DispatchQueues.
Less error-prone because the API is clearer to read. Not considering all exit paths from a completions handler, and conditional branching means subtle bugs can build up that are not caught at compile time.
async / await is not back-deployable to devices running < iOS 13, so we have to add if #available(iOS 13, *) checks where supporting old devices. We still need to use GCD for older OS versions.
(Note: Swift 5 may support await as you’d expect it in ES6!)
What you want to look into is Swift's concept of "closures". These were previously known as "blocks" in Objective-C, or completion handlers.
Where the similarity in JavaScript and Swift come into play, is that both allow you to pass a "callback" function to another function, and have it execute when the long-running operation is complete. For example, this in Swift:
func longRunningOp(searchString: String, completion: (result: String) -> Void) {
// call the completion handler/callback function
completion(searchOp.result)
}
longRunningOp(searchString) {(result: String) in
// do something with result
}
would look like this in JavaScript:
var longRunningOp = function (searchString, callback) {
// call the callback
callback(err, result)
}
longRunningOp(searchString, function(err, result) {
// Do something with the result
})
There's also a few libraries out there, notably a new one by Google that translates closures into promises: https://github.com/google/promises. These might give you a little closer parity with await and async.
You can use this framework for Swift coroutines - https://github.com/belozierov/SwiftCoroutine
Unlike DispatchSemaphore, when you call await it doesn’t block the thread but only suspends coroutine, so you can use it in the main thread as well.
func awaitAPICall(_ url: URL) throws -> String? {
let future = URLSession.shared.dataTaskFuture(for: url)
let data = try future.await().data
return String(data: data, encoding: .utf8)
}
func load(url: URL) {
DispatchQueue.main.startCoroutine {
let result1 = try self.awaitAPICall(url)
let result2 = try self.awaitAPICall2(result1)
let result3 = try self.awaitAPICall3(result2)
print(result3)
}
}
In iOS 13 and up, you can now do this using Combine. Future is analogous to async and the flatMap operator on publishers (Future is a publisher) is like await. Here's an example, loosely based on your code:
Future<Feature, Error> { promise in
directions.calculate(options) { (waypoints, routes, error) in
if let error = error {
promise(.failure(error))
}
promise(.success(routes))
}
}
.flatMap { routes in
// extract feature from routes here...
feature
}
.receiveOn(DispatchQueue.main) // UI updates should run on the main queue
.sink(receiveCompletion: { completion in
// completion is either a .failure or it's a .success holding
// the extracted feature; if the process above was successful,
// you can now add feature to the map
}, receiveValue: { _ in })
.store(in: &self.cancellables)
Edit: I went into more detail in this blog post.
You can use semaphores to simulate async/await.
func makeAPICall() -> Result <String?, NetworkError> {
let path = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: path) else {
return .failure(.url)
}
var result: Result <String?, NetworkError>!
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
result = .success(String(data: data, encoding: .utf8))
} else {
result = .failure(.server)
}
semaphore.signal()
}.resume()
_ = semaphore.wait(wallTimeout: .distantFuture)
return result
}
And here is example how it works with consecutive API calls:
func load() {
DispatchQueue.global(qos: .utility).async {
let result = self.makeAPICall()
.flatMap { self.anotherAPICall($0) }
.flatMap { self.andAnotherAPICall($0) }
DispatchQueue.main.async {
switch result {
case let .success(data):
print(data)
case let .failure(error):
print(error)
}
}
}
}
Here is the article describing it in details.
And you can also use promises with PromiseKit and similar libraries
Async/await is now officially supported in Swift.
It would yield be something like this
func myFunction() async throws {
let array: [Object] = getObjects()
let images = try await withThrowingTaskGroup(of: Data.self, returning: [Data].self) { group in
array.forEach { object in
group.async {
try await LogoRequest().downloadImage(url: object.url)
}
}
return try await group.reduce([], {$0 + [$1]})
}
// at this point all `downloadImage` are done, and `images` is populated
_ = images
}
Use async/ await below like this,
enum DownloadError: Error {
case statusNotOk
case decoderError
}
Method Call
override func viewDidLoad() {
super.viewDidLoad()
async{
do {
let data = try await fetchData()
do{
let json = try JSONSerialization.jsonObject(with: data, options:.mutableLeaves)
print(json)
}catch{
print(error.localizedDescription)
}
}catch{
print(error.localizedDescription)
}
}
}
func fetchData() async throws -> Data{
let url = URL(string: "https://www.gov.uk/bank-holidays.json")!
let request = URLRequest(url:url)
let (data,response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else{
throw DownloadError.statusNotOk
}
return data
}
Here you can see a basic difference, how the callBack replacing by async/await
Basically, scene A and scene B = Call API by the closure.
Scene C and scene D= Calling API by Async/Await.
Scene E = Serial API call by nested closure.
Scene F= Serial API call by Async/Await.
Scene G = Parallel API call by Async/Await.
Parallel API call
user
29384092840923
chatRoomsJoined
chatRoom1
chatroom5
chatrooms
chatRoom1
users
29384092840923
298340982039490
I'm trying to load a tableview with information about the chat rooms a user has joined into. In the case above, user "29384092840923" has joined into chatRoom1, and I need the count of children of the users node in chatRoom1
My initial strat was to get an array of the joinedChatRooms from the "user" node and then do a for loop through and do a getDocument on each of the items in the array.
static func loadFavoriteRooms(forUID uid: String, completedFetch: #escaping (_ favoritedRoomsArray : [String]?, _ error : Error?)->()) {
let userFavoritesRef = database.collection("users").document(uid).collection("favoritedRooms")
userFavoritesRef.getDocuments { (snapshot, error) in
if error != nil {
completedFetch(nil, error!)
print("There was an error", error!.localizedDescription)
} else {
var roomArray = [String]()
for document in snapshot!.documents {
//Create a roomRef with the documentID, do a getDocument with it, and create an object with it?
let roomName = document.documentID
roomArray.append(roomName)
}
completedFetch(roomArray, nil)
}
}
}
My problem with what happened above was once I started sending off additional getDocument requests within the for-loop for the individual roomRefs, my completedFetch completion call was returning before the for loop was done asynchronously, and I wasn't getting a filled array back.
What's the cleanest way to do this? Do I need to do a dispatch group here or is there a better way to accomplish this? Using dispatch groups with firestore seems wrong here to me for some reason.
One possible option could be to use a DispatchGroup. Something like -
var roomArray = [String]()
let dispatchGroup = DispatchGroup()
for document in snapshot!.documents {
let roomId = document.documentID
let roomRef = database.collection("rooms").document(roomId)
dispatchGroup.enter()
roomRef.getDocument { (roomSnapshot, error) in
// Create the room from the snapshot here
roomArray.append(roomName)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main, execute: {
completedFetch(roomArray, nil)
})
Just make sure you got your .enter() and .leave() calls correct otherwise you'll get some very strange crashes.
I am attempting to build a video merging app that allows users to select several short clips from a collection view and then generates a preview of the videos all merged into one. I am using the Photos framework (PHCachingImageManager) to populate the collection view and am passing an array of the selected PHAssets to the function below in order to request low quality AVAssets (for merging & generating the preview).
The problem is, I need to keep the AVAssets in the order in which the user selected them, but the "requestAVAsset" function is asynchronous and the completion handler is often called multiple times. I've never used Dispatch Groups before, but attempted to use them below...and the AVAssets are still out of order sometimes.
func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray: [AVAsset] = []
let dispatchGroup = DispatchGroup()
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for asset in assets {
dispatchGroup.enter()
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (video, audioMix, info) in
guard video != nil else { return }
videoArray.append(video!)
dispatchGroup.leave()
})
}
dispatchGroup.wait()
return videoArray
}
I'm guessing I've either misplaced some code or am approaching this in entirely the wrong way! Any suggestions are appreciated.
If you capture the current index while you're iterating the AVAssets, you can insert rather than append. That's how I do it, at least.
func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray = [AVAsset?](repeating: nil, count: assets.count)
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for (i, asset) in assets.enumerated() {
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (video, audioMix, info) in
guard let video = video else { return }
videoArray.remove(at: i)
videoArray.insert(video, at: i)
})
}
return videoArray.flatMap { $0 }
}
Giving the array the desired number of items as nil will stop it from erroring when inserting items, and then when the download is complete, remove the existing nil value and replace it with the actual AVAsset.
Finally, flatMap the resulting array to unpack the optionals (and optionally check that you have the desired number of items by comparing it with the incoming assets array).
Dodging the dispatch question entirely because it's late and I've had a bad day, but what if you kept the "correct" index associated with the video, and then sorted on that? I think something like this would work.
struct SelectedVideo {
let index: Int
let asset: AVAsset
}
func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray: [SelectedVideo] = []
let dispatchGroup = DispatchGroup()
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for (index, asset) in assets.enumerated() {
dispatchGroup.enter()
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (videoMb, audioMixMb, infoMb) in
guard let video = videoMb else return
videoArray.append(SelectedVideo(index, video))
dispatchGroup.leave()
})
}
dispatchGroup.wait()
return videoArray.sort { $0.index < $1.index}.map({$0.video})
}
This is kind of a hack (haven't even tried compiling it), but like I said, it's been a bad day.
A couple minor changes to note: I changed the params to the closure to say "Mb" which means "maybe" and is a nice convention I've seen for naming optionals passed to closures. Also, instead of "guard video != nil" followed by force-unwrapping, it's much preferred to do a "guard let video = videoMb", and then video is non-optional.