This question already has answers here:
Completion gets called soon
(2 answers)
Closed 2 years ago.
I have a function that should have a completion-handler which should only be called if everything inside of it is actually completed. That is my function:
static func getWishes(dataSourceArray: [Wishlist], completion: #escaping (_ success: Bool, _ dataArray: [Wishlist]) -> Void){
var dataSourceArrayWithWishes = dataSourceArray
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
for list in dataSourceArray {
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wΓΌnsche").order(by: "wishCounter").getDocuments() { ( querySnapshot, error) in
if let error = error {
print(error.localizedDescription)
completion(false, dataSourceArrayWithWishes)
} else {
// append every Wish to array at wishIDX
for document in querySnapshot!.documents {
let documentData = document.data()
let imageUrlString = document["imageUrl"] as? String ?? ""
let imageView = UIImageView()
imageView.image = UIImage()
if let imageUrl = URL(string: imageUrlString) {
let resource = ImageResource(downloadURL: imageUrl)
imageView.kf.setImage(with: resource) { (result) in
switch result {
case .success(_):
dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(name: name, link: link, price: price, note: note, image: imageView.image!, checkedStatus: false))
completion(true, dataSourceArrayWithWishes)
print("success")
case .failure(_):
print("fail")
}
}
} else {
dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(name: name, link: link, price: price, note: note, image: imageView.image!, checkedStatus: false))
}
}
}
}
}
}
The problem lies in imageView.kf.setImage...
Right now I am calling completion after the first .success but the function should only be completed if the for-loop and all the setImages are being finished. I've tried a couple of things now but I can not make it work. So I wonder what's best practice for this case?
Here is how you use DispatchGroup for this... You need DispatchGroup to get notified when the asynchronous loop is finished
static func getWishes(dataSourceArray: [Wishlist], completion: #escaping (_ success: Bool, _ dataArray: [Wishlist]) -> Void){
var dataSourceArrayWithWishes = dataSourceArray
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
let group = DispatchGroup()
for list in dataSourceArray {
group.enter()
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wΓΌnsche").order(by: "wishCounter").getDocuments() { ( querySnapshot, error) in
defer{ group.leave() }
if let error = error {
print(error.localizedDescription)
completion(false, dataSourceArrayWithWishes)
} else {
// append every Wish to array at wishIDX
for document in querySnapshot!.documents {
group.enter()
let documentData = document.data()
let imageUrlString = document["imageUrl"] as? String ?? ""
let imageView = UIImageView()
imageView.image = UIImage()
if let imageUrl = URL(string: imageUrlString) {
let resource = ImageResource(downloadURL: imageUrl)
imageView.kf.setImage(with: resource) { (result) in
defer{ group.leave() }
switch result {
case .success(_):
dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(name: name, link: link, price: price, note: note, image: imageView.image!, checkedStatus: false))
case .failure(_):
print("fail")
}
}
} else {
dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(name: name, link: link, price: price, note: note, image: imageView.image!, checkedStatus: false))
}
}
}
}
}
group.notify(queue: DispatchQueue.main) {
completion(true, dataSourceArrayWithWishes)
print("success")
}
}
Related
So I'm trying to store data retrieved from my Firestore database into an object. My database has a collection of users, and each user has a collection of classes. I want to be able to get the logged in users collection of classes and store them in an array of objects. Most of what I've tried so far can pull data but it won't save it into anything because its able access the data from within the completion handler. Any help would be great, here's the code I'm working with rn:
db.collection("users").whereField("uid", isEqualTo: uid).addSnapshotListener { (querySnapshot, error) in
if error == nil && querySnapshot != nil {
let docId = querySnapshot?.documents[0].documentID
db.collection("users").document(docId!).collection("classes").addSnapshotListener { (querySnap, error) in
guard let documents = querySnap?.documents else{print("No Classes");return}
var imageData:UIImage?
retrievedClasses = documents.map { (querySnap) -> UserClass in
let data = querySnap.data()
if let decodedData = Data(base64Encoded: data["class_img"] as! String, options: .ignoreUnknownCharacters){
imageData = UIImage(data: decodedData)
}
return UserClass.init(name: data["class_name"] as! String, desc: data["class_desc"] as! String, img: imageData!, color: data["class_color"] as! String, link: data["class_link"] as! String, location: data["class_location"] as! GeoPoint, meetingTime: data["meeting_time"] as! Dictionary<String,String>)
}
print(retrievedClasses[0].printClass())
}
}
}
As I understand you need to do something like this:
func getUserClasses(for userID: String, completion: #escaping (Result<[UserClass], Error>) -> Void) {
db.collection("users").whereField("uid", isEqualTo: userID).addSnapshotListener { (querySnapshot, error) in
if error == nil && querySnapshot != nil {
let docId = querySnapshot?.documents[0].documentID
db.collection("users").document(docId!).collection("classes").addSnapshotListener { (querySnap, error) in
guard let documents = querySnap?.documents else {
print("No Classes")
completion(.failure("No Classes"))
return
}
let retrievedClasses = documents.map { (querySnap) -> UserClass in
let data = querySnap.data()
var imageData: UIImage?
if let decodedData = Data(base64Encoded: data["class_img"] as! String,
options: .ignoreUnknownCharacters) {
imageData = UIImage(data: decodedData)
}
let user = UserClass(name: data["class_name"] as! String,
desc: data["class_desc"] as! String,
img: imageData!,
color: data["class_color"] as! String,
link: data["class_link"] as! String,
location: data["class_location"] as! GeoPoint,
meetingTime: data["meeting_time"] as! Dictionary<String,String>)
return user
}
completion(.success(retrievedClasses))
}
} else {
completion(.failure("Request Error"))
}
}
}
Then you will able to use data after request completion:
getUserClasses(for: "123") { result in
switch result {
case .success(let allClasses):
retrievedClasses = allClasses // retrievedClasses is a property in your class which you are going to use
if !retrievedClasses.isEmpty() {
print(retrievedClasses[0].printClass())
}
case .failure(let error):
print(error)
}
}
I have a ShareExtension in which I like need to get the current URL. This is my function for it:
var html: String?
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if (url as? URL) != nil {
html = (self.getHTMLfromURL(url: url as? URL))
}
}
}
My problem is that I need the html but when using that variable right after that function html is still empty. I think I need some sort of completion handler but I tried different things now and can not get it right...
This is how my whole function looks like at the moment (not working, as html becomes an empty String)
#objc func actionButtonTapped(){
do {
var html: String?
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if (url as? URL) != nil {
html = (self.getHTMLfromURL(url: url as? URL))
}
}
}
let doc: Document = try SwiftSoup.parse(html ?? "")
let priceClasses: Elements = try doc.select("[class~=(?i)price]")
for priceClass: Element in priceClasses.array() {
let priceText : String = try priceClass.text()
print(try priceClass.className())
print("pricetext: \(priceText)")
}
let srcs: Elements = try doc.select("img[src]")
let srcsStringArray: [String?] = srcs.array().map { try? $0.attr("src").description }
for imageName in srcsStringArray {
print(imageName!)
}
} catch Exception.Error( _, let message) {
print(message)
} catch {
print("error")
}
}
The Goal is to have a extra function to get the url (1st code example) with a completion handler in which I can work with the created html.
the problem was that I didn't realize that I already had a completionHandler with loadItems. So what I did now was to put the whole do & catch block in another method and called it in the completion handler like this:
#objc func actionButtonTapped(){
var html: String?
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if (url as? URL) != nil {
html = (self.getHTMLfromURL(url: url as? URL))
print("bruh")
self.doStuff(html: html)
}
}
}
}
func doStuff(html: String?){
do {
let doc: Document = try SwiftSoup.parse(html ?? "")
let priceClasses: Elements? = try doc.select("[class~=(?i)price]")
for priceClass: Element in priceClasses!.array() {
let priceText : String = try priceClass.text()
print(try priceClass.className())
print("pricetext: \(priceText)")
}
let srcs: Elements = try doc.select("img[src]")
let srcsStringArray: [String?] = srcs.array().map { try? $0.attr("src").description }
for imageName in srcsStringArray {
print(imageName!)
}
} catch Exception.Error( _, let message) {
print(message)
} catch {
print("error")
}
}
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("πππππππIn closure function to update articlesπππππππ")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
I am getting base64 images in an API call and I need to store these as files locally.
I was trying to write this data without having to convert the string to UIImage and then UIImage to JPEGRepresentation.
I don't want the overhead of creating UIImages first and theen changing to JPEGs if I can avoid it, but I don't know if this is possible?
I can see in my app Container, that all the files are there and the correct filesize but when I try to open them, they won't and I am told they may be corrupt.
extension NSData {
func writeToURL2(named:URL, completion: #escaping (_ result: Bool, _ url:NSURL?) -> Void) {
let tmpURL = named as NSURL
DispatchQueue.global(qos: .background).async { [weak self] () -> Void in
guard let strongSelf = self else { completion (false, tmpURL); return }
strongSelf.write(to: tmpURL as URL, atomically: true)
var error:NSError?
if tmpURL.checkResourceIsReachableAndReturnError(&error) {
print("We have it")
completion(true, tmpURL)
} else {
print("We Don't have it\(error?.localizedDescription)")
completion (false, tmpURL)
}
}
}
}
and it is used like:
for employee in syncReponse
{
autoreleasepool{
if let employeeJsonStr = employee["data"] as? String{
if let employeeDataDict = try? JSONSerializer.toDictionary(employeeJsonStr), let proPic = employeeDataDict["profilepicture"] as? String, proPic.removingWhitespaces() != "", let idStr = employeeDataDict["employeeId"] as? String, let proPicData = (proPic.data(using: .utf8)) {
let empPicDir = mainDir.appendingPathComponent(util_Constants.DIR_EMP_PICS)
let filename = empPicDir.appendingPathComponent(idStr+".jpg")
(proPicData as NSData).writeToURL2(named: filename, completion: { (result, url) -> Void in
})
}
let Emp = DATA_EP_employee(employeeJsonStr : employeeJsonStr)
dataList.append(Emp)
reqCount += 1
}
}
}
I want to run 2 pieces of asynchronous code in one function and escape them. I want first to download the Reciter information and then download with these information the images that is associated with the Reciter. I'm using Firestore. I tried to work with DispatchQueue and DispatchGroup but I couldn't figure something out. I hope someone can help me :)
func getReciters(completion: #escaping (Bool) -> Void) {
var reciters = [Reciter]()
self.BASE_URL.collection(REF_RECITERS).getDocuments { (snapchot, error) in
if let error = error {
debugPrint(error)
completion(false)
// TODO ADD UIALTERCONTROLLER MESSAGE
return
}
guard let snapchot = snapchot else { debugPrint("NO SNAPSHOT"); completion(false); return }
for reciter in snapchot.documents {
let data = reciter.data()
let reciterName = data[REF_RECITER_NAME] as? String ?? "ERROR"
let numberOfSurahs = data[REF_NUMBER_OF_SURAHS] as? Int ?? 0
// **HERE I WANT TO DOWNLOAD THE IMAGES**
self.downloadImage(forDocumentID: reciter.documentID, completion: { (image) in
let reciter = Reciter(name: reciterName, image: nil, surahCount: numberOfSurahs, documentID: reciter.documentID)
reciters.append(reciter)
})
}
}
UserDefaults.standard.saveReciters(reciters)
completion(true)
}
You need DispatchGroup.
In the scope of the function declare an instance of DispatchGroup.
In the loop before the asynchronous block call enter.
In the loop inside the completion handler of the asynchronous block call leave.
After the loop call notify, the closure will be executed after all asynchronous tasks have finished.
func getReciters(completion: #escaping (Bool) -> Void) {
var reciters = [Reciter]()
self.BASE_URL.collection(REF_RECITERS).getDocuments { (snapchot, error) in
if let error = error {
debugPrint(error)
completion(false)
// TODO ADD UIALTERCONTROLLER MESSAGE
return
}
guard let snapchot = snapchot else { debugPrint("NO SNAPSHOT"); completion(false); return }
let group = DispatchGroup()
for reciter in snapchot.documents {
let data = reciter.data()
let reciterName = data[REF_RECITER_NAME] as? String ?? "ERROR"
let numberOfSurahs = data[REF_NUMBER_OF_SURAHS] as? Int ?? 0
group.enter()
self.downloadImage(forDocumentID: reciter.documentID, completion: { (image) in
let reciter = Reciter(name: reciterName, image: nil, surahCount: numberOfSurahs, documentID: reciter.documentID)
reciters.append(reciter)
group.leave()
})
}
group.notify(queue: .main) {
UserDefaults.standard.saveReciters(reciters)
completion(true)
}
}
}