Swift Completion Handler For Loop to be performed once instead of 10 times due to the loop - swift

I I have a loop with a firestore query in it that is repeated 10 times. I need to call the (completion: block) after all the 10 queries completed; Here I have my code so that it performs the (completion: block) per query but this would be too heavy on the server and the user's phone. How can I change below to accomplish what I just described?
static func getSearchedProducts(fetchingNumberToStart: Int, sortedProducts: [Int : [String : Int]], handler: #escaping (_ products: [Product], _ lastFetchedNumber: Int?) -> Void) {
var lastFetchedNumber:Int = 0
var searchedProducts:[Product] = []
let db = Firestore.firestore()
let block : FIRQuerySnapshotBlock = ({ (snap, error) in
guard error == nil, let snapshot = snap else {
debugPrint(error?.localizedDescription)
return
}
var products = snapshot.documents.map { Product(data: $0.data()) }
if !UserService.current.isGuest {
db.collection(DatabaseRef.Users).document(Auth.auth().currentUser?.uid ?? "").collection(DatabaseRef.Cart).getDocuments { (cartSnapshot, error) in
guard error == nil, let cartSnapshot = cartSnapshot else {
return
}
cartSnapshot.documents.forEach { document in
var product = Product(data: document.data())
guard let index = products.firstIndex(of: product) else { return }
let cartCount: Int = document.exists ? document.get(DatabaseRef.cartCount) as? Int ?? 0 : 0
product.cartCount = cartCount
products[index] = product
}
handler(products, lastFetchedNumber)
}
}
else {
handler(products, lastFetchedNumber)
}
})
if lastFetchedNumber == fetchingNumberToStart {
for _ in 0 ..< 10 {
//change the fetching number each time in the loop
lastFetchedNumber = lastFetchedNumber + 1
let productId = sortedProducts[lastFetchedNumber]?.keys.first ?? ""
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
}
}
}
as you can see at the very end I am looping 10 times for this query because of for _ in 0 ..< 10 :
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
So I need to make the completion: block handler to be called only once instead of 10 times here.

Use a DispatchGroup. You can enter the dispatch group each time you call the async code and then leave each time it's done. Then when everything is finished it will call the notify block and you can call your handler. Here's a quick example of what that would look like:
let dispatchGroup = DispatchGroup()
let array = []
for i in array {
dispatchGroup.enter()
somethingAsync() {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
handler()
}

Related

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 for loop not waiting for firestore call to complete

I know firestore calls are async which means this will not work:
private func removeUserSavedMomentFromAllUsers(moment: StoryMoment, completion: #escaping () -> Void) {
guard let savedByUIDs = moment.savedByUIDs else { return }
guard let momentID = moment.id else { return }
for id in savedByUIDs {
self.userInfoCollection.document(id).collection("savedMedias").document(momentID).delete { error in
if let error = error {
print("Error removing user saved moment from UID: \(error)")
}
}
}
}
Since the loop will continue before the delete call completes (same with get requests). I have used dispatch groups in the past to solve this issue. Heres a working example:
private func removeUserSavedMomentFromAllUsers(moment: StoryMoment, completion: #escaping () -> Void) {
guard let savedByUIDs = moment.savedByUIDs else { return }
guard let momentID = moment.id else { return }
let disSemaphore = DispatchSemaphore(value: 0)
let dispatchQueue = DispatchQueue(label: "group 1")
dispatchQueue.async {
for id in savedByUIDs {
self.userInfoCollection.document(id).collection("savedMedias").document(momentID).delete { error in
if let error = error {
print("Error removing user saved moment from UID: \(error)")
} else {
disSemaphore.signal()
}
}
disSemaphore.wait()
}
}
}
But those do all the work on the background thread.
My question is: How can I use async/await in a for loop where you call firebase docs?
The code in the first part of the question does work - and works fine for small group of data. However, in general it's recommended to not call Firebase functions in tight loops.
While the question mentions DispatchQueues, we use DispatchGroups with .enter and .leave as it's pretty clean.
Given a Firebase structure
sample_data
a
key: "value"
b
key: "value"
c
key: "value"
d
key: "value"
e
key: "value"
f
key: "value"
and suppose we want to delete the d, e, and f documents. Here's the code
func dispatchGroupDelete() {
let documentsToDelete = ["d", "e", "f"]
let collection = self.db.collection("sample_data") //self.db points to my Firestore
let group = DispatchGroup()
for docId in documentsToDelete {
group.enter()
let docToDelete = collection.document(docId)
docToDelete.delete()
group.leave()
}
}
While this answer doesn't use async/await, those may not be needed for this use case
If you want to use async/await you try do this
let documentsToDelete = ["d", "e", "f"]
let collection = self.db.collection("sample_data")
for docId in documentsToDelete {
let docToDelete = collection.document(docId)
Task {
do {
try await docToDelete.delete()
} catch {
print("oops")
}
}
}

Index out of Range exception when using firebase database and storage to download data

Here is my method to retrieve an array of user and post objects from the database.
func getRecentPost(start timestamp: Int? = nil, limit: UInt, completionHandler: #escaping ([(Post, UserObject)]) -> Void) {
var feedQuery = REF_POSTS.queryOrdered(byChild: "timestamp")
if let latestPostTimestamp = timestamp, latestPostTimestamp > 0 {
feedQuery = feedQuery.queryStarting(atValue: latestPostTimestamp + 1, childKey: "timestamp").queryLimited(toLast: limit)
} else {
feedQuery = feedQuery.queryLimited(toLast: limit)
}
// Call Firebase API to retrieve the latest records
feedQuery.observeSingleEvent(of: .value, with: { (snapshot) in
let items = snapshot.children.allObjects
let myGroup = DispatchGroup()
var results: [(post: Post, user: UserObject)] = []
for (index, item) in (items as! [DataSnapshot]).enumerated() {
myGroup.enter()
Api.Post.observePost(withId: item.key, completion: { (post) in
Api.User.observeUser(withId: post.uid!, completion: { (user) in
results.insert((post, user), at: index) //here is where I get my error -> Array index is out of range
myGroup.leave()
})
})
}
myGroup.notify(queue: .main) {
results.sort(by: {$0.0.timestamp! > $1.0.timestamp! })
completionHandler(results)
}
})
}
Here is the call to the method from my view controller. I am currently using texture UI to help with a faster smoother UI.
var firstFetch = true
func fetchNewBatchWithContext(_ context: ASBatchContext?) {
if firstFetch {
firstFetch = false
isLoadingPost = true
print("Begin First Fetch")
Api.Post.getRecentPost(start: posts.first?.timestamp, limit: 8 ) { (results) in
if results.count > 0 {
results.forEach({ (result) in
posts.append(result.0)
users.append(result.1)
})
}
self.addRowsIntoTableNode(newPhotoCount: results.count)
print("First Batch Fetched")
context?.completeBatchFetching(true)
isLoadingPost = false
print("First Batch", isLoadingPost)
}
} else {
guard !isLoadingPost else {
context?.completeBatchFetching(true)
return
}
isLoadingPost = true
guard let lastPostTimestamp = posts.last?.timestamp else {
isLoadingPost = false
return
}
Api.Post.getOldPost(start: lastPostTimestamp, limit: 9) { (results) in
if results.count == 0 {
return
}
for result in results {
posts.append(result.0)
users.append(result.1)
}
self.addRowsIntoTableNode(newPhotoCount: results.count)
context?.completeBatchFetching(true)
isLoadingPost = false
print("Next Batch", isLoadingPost)
}
}
}
In the first section of code, I have debugged to see if I could figure out what is happening. Currently, firebase is returning the correct number of objects that I have limited my query to (8). But, where I have highlighted the error occurring at, the index jumps when it is about to insert the fifth object, index[3] -> 4th object is in array, to index[7]-> 5th object about to be parsed and inserted, when parsing the 5th object.
So instead of going from index[3] to index[4] it jumps to index[7].
Can someone help me understand what is happening and how to fix it?
The for loop has continued on its thread while the observeUser & observePost callbacks are on other threads. Looking at your code, you can probably just get away with appending the object to the results array instead of inserting. This makes sense because you are sorting after the for loop anyway, so why does the order matter?

Swift pthread read/write lock taking a while to release the lock

I am trying to implement a read/write lock in Swift with the pthread API's and I have come across a strange issue.
My implementation is largely based on the following with the addition of a timeout for attempted read locks.
http://swiftweb.johnholdsworth.com/Deferred/html/ReadWriteLock.html
Here is my implementation:
public final class ReadWriteLock {
private var lock = pthread_rwlock_t()
public init() {
let status = pthread_rwlock_init(&lock, nil)
assert(status == 0)
}
deinit {
let status = pthread_rwlock_destroy(&lock)
assert(status == 0)
}
#discardableResult
public func withReadLock<Result>(_ body: () throws -> Result) rethrows -> Result {
pthread_rwlock_rdlock(&lock)
defer { pthread_rwlock_unlock(&lock) }
return try body()
}
#discardableResult
public func withAttemptedReadLock<Result>(_ body: () throws -> Result) rethrows -> Result? {
guard pthread_rwlock_tryrdlock(&lock) == 0 else { return nil }
defer { pthread_rwlock_unlock(&lock) }
return try body()
}
#discardableResult
public func withAttemptedReadLock<Result>(_ timeout: Timeout = .now, body: () throws -> Result) rethrows -> Result? {
guard timeout != .now else { return try withAttemptedReadLock(body) }
let expiry = DispatchTime.now().uptimeNanoseconds + timeout.rawValue.uptimeNanoseconds
var ts = Timeout.interval(1).timespec
var result: Int32
repeat {
result = pthread_rwlock_tryrdlock(&lock)
guard result != 0 else { break }
nanosleep(&ts, nil)
} while DispatchTime.now().uptimeNanoseconds < expiry
// If the lock was not acquired
if result != 0 {
// Try to grab the lock once more
result = pthread_rwlock_tryrdlock(&lock)
}
guard result == 0 else { return nil }
defer { pthread_rwlock_unlock(&lock) }
return try body()
}
#discardableResult
public func withWriteLock<Return>(_ body: () throws -> Return) rethrows -> Return {
pthread_rwlock_wrlock(&lock)
defer { pthread_rwlock_unlock(&lock) }
return try body()
}
}
/// An amount of time to wait for an event.
public enum Timeout {
/// Do not wait at all.
case now
/// Wait indefinitely.
case forever
/// Wait for a given number of seconds.
case interval(UInt64)
}
public extension Timeout {
public var timespec: timespec {
let nano = rawValue.uptimeNanoseconds
return Darwin.timespec(tv_sec: Int(nano / NSEC_PER_SEC), tv_nsec: Int(nano % NSEC_PER_SEC))
}
public var rawValue: DispatchTime {
switch self {
case .now:
return DispatchTime.now()
case .forever:
return DispatchTime.distantFuture
case .interval(let milliseconds):
return DispatchTime(uptimeNanoseconds: milliseconds * NSEC_PER_MSEC)
}
}
}
extension Timeout : Equatable { }
public func ==(lhs: Timeout, rhs: Timeout) -> Bool {
switch (lhs, rhs) {
case (.now, .now):
return true
case (.forever, .forever):
return true
case (let .interval(ms1), let .interval(ms2)):
return ms1 == ms2
default:
return false
}
}
Here is my unit test:
func testReadWrite() {
let rwLock = PThreadReadWriteLock()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInteractive
queue.isSuspended = true
var enterWrite: Double = 0
var exitWrite: Double = 0
let writeWait: UInt64 = 500
// Get write lock
queue.addOperation {
enterWrite = Double(Timeout.now.rawValue.uptimeNanoseconds) / Double(NSEC_PER_MSEC)
rwLock.withWriteLock {
// Sleep for 1 second
var ts = Timeout.interval(writeWait).timespec
var result: Int32
repeat { result = nanosleep(&ts, &ts) } while result == -1
}
exitWrite = Double(Timeout.now.rawValue.uptimeNanoseconds) / Double(NSEC_PER_MSEC)
}
var entered = false
var enterRead: Double = 0
var exitRead: Double = 0
let readWait = writeWait + 50
// Get read lock
queue.addOperation {
enterRead = Double(Timeout.now.rawValue.uptimeNanoseconds) / Double(NSEC_PER_MSEC)
rwLock.withAttemptedReadLock(.interval(readWait)) {
print("**** Entered! ****")
entered = true
}
exitRead = Double(Timeout.now.rawValue.uptimeNanoseconds) / Double(NSEC_PER_MSEC)
}
queue.isSuspended = false
queue.waitUntilAllOperationsAreFinished()
let startDifference = abs(enterWrite - enterRead)
let totalWriteTime = abs(exitWrite - enterWrite)
let totalReadTime = abs(exitRead - enterRead)
print("Start Difference: \(startDifference)")
print("Total Write Time: \(totalWriteTime)")
print("Total Read Time: \(totalReadTime)")
XCTAssert(totalWriteTime >= Double(writeWait))
XCTAssert(totalReadTime >= Double(readWait))
XCTAssert(totalReadTime >= totalWriteTime)
XCTAssert(entered)
}
Finally, the output of my unit test is the following:
Start Difference: 0.00136399269104004
Total Write Time: 571.76081609726
Total Read Time: 554.105705976486
Of course, the test is failing because the write lock is not released in time. Given that my wait time is only half a second (500ms), why is it taking roughly 570ms for the write lock to execute and release?
I have tried executing with optimizations both on and off to no avail.
I was under the impression that nanosleep is high resolution sleep timer I would expect to have a resolution of at least 5-10 milliseconds here for the lock timeout.
Can anyone shed some light here?
Turns out foundation was performing some kind of optimization with the OperationQueue due to the long sleep in my unit test.
Replacing the sleep function with usleep and iterating with a 1ms sleep until the total time is exceed seems to have fixed the problem.
// Get write lock
queue.addOperation {
enterWrite = Double(Timeout.now.rawValue.uptimeNanoseconds) / Double(NSEC_PER_MSEC)
rwLock.withWriteLock {
let expiry = DispatchTime.now().uptimeNanoseconds + Timeout.interval(writeWait).rawValue.uptimeNanoseconds
let interval = Timeout.interval(1)
repeat {
interval.sleep()
} while DispatchTime.now().uptimeNanoseconds < expiry
}
exitWrite = Double(Timeout.now.rawValue.uptimeNanoseconds) / Double(NSEC_PER_MSEC)
}

Swift: Async method into while loop

I want to use a async function inside a while loop but the function don't get enough time to finish and the while loop starts again and never ends.
I should implement this problem with increment variable , but what is the solution? thanks a lot.
output loops between "Into repeat" - "Into function"
var condition = true
var userId = Int.random(1...1000)
repeat {
print("Into repeat")
checkId(userId, completionHandler: { (success:Bool) -> () in
if success {
condition = false
} else {
userId = Int.random(1...1000)
}
}) } while condition
func checkId(userId:Int,completionHandler: (success:Bool) -> ()) -> () {
print("Into function")
let query = PFUser.query()
query!.whereKey("userId", equalTo: userId)
query!.findObjectsInBackgroundWithBlock({ (object:[PFObject]?, error:NSError?) -> Void in
if object!.isEmpty {
completionHandler(success:false)
} else {
completionHandler(success:true)
}
})
}
You can do this with a recursive function. I haven't tested this code but I think it could look a bit like this
func asyncRepeater(userId:Int, foundIdCompletion: (userId:Int)->()){
checkId(userId, completionHandler: { (success:Bool) -> () in
if success {
foundIdCompletion(userId:userId)
} else {
asyncRepeater(userId:Int.random(1...1000), completionHandler: completionHandler)
}
})
}
You should use dispatch_group
repeat {
// define a dispatch_group
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup) // enter group
print("Into repeat")
checkId(userId, completionHandler: { (success:Bool) -> () in
if success {
condition = false
} else {
userId = Int.random(1...1000)
}
// leave group
dispatch_group_leave(dispatchGroup)
})
// this line block while loop until the async task above completed
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)
} while condition
See more at Apple document