Nested completion handlers that do not return items to be sent to TableViewController - swift

I want to get some data from firebaseFirestore and download an image URL from firebaseStorage, while preparing for my segue that will bring the user to the TableViewController, where they will be displayed. Even when using some nested completion handlers (perhaps I made the code too longs), I'm still not able to perform my asyncronous tasks in order, thus rushing too early to the segue. For simplicity I'm using the single segue (no identifiers). In the ViewControllerForTable I have stated a variable var cells : [Cella] = [] globally.
let firestoreUsersReference = Firestore.firestore().collection("users")
let storageReference = Storage.storage()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! ViewControllerForTable
prepareDataForSegue(firestoreReference: firestoreUsersReference) { (cella) in
destinationVC.cells = cella
print(destinationVC.cells)
}
}
func getImagesDownloaded(reference: StorageReference, completion: #escaping (UIImage?,Error?)->()) {
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard error == nil, let data = data else {
completion(nil,error)
return
}
guard let image = UIImage(data: data) else {
completion(nil, FirebaseErrors.expectedImage)
return
}
completion(image,nil)
}
}
enum FirebaseErrors: Error {
case expectedImage
}
func prepareDataForSegue (firestoreReference: CollectionReference, completion : #escaping ([Cella])->()) {
var cellaArray : [Cella] = []
firestoreUsersReference.getDocuments { (querySnapshot, err) in
if err != nil {
print("There has been an error \(String(describing: err?.localizedDescription))")
}
else {
self.getDocumentsFromFirestore(querySnapshot: querySnapshot, completion: { (title, description, image) in
let newCell = Cella(image: image, title: title, bodyMessage: description)
print("NEW CELL : \(newCell)")
cellaArray.append(newCell)
})
}
}
completion(cellaArray)
}
func getDocumentsFromFirestore (querySnapshot: QuerySnapshot?, completion: #escaping (String,String,UIImage)->()) {
for documents in querySnapshot!.documents {
print("\(documents.documentID) => \(documents.data())")
let data = documents.data()
let title = data["userTitle"] as? String
let description = data["userDescription"] as? String
let imageURL = data["userImageURL"] as! String
print("Title: \(String(describing: title)), Description: \(String(describing: description)), imageURL: \(imageURL)")
let storagePath = Storage.storage().reference(forURL: imageURL)
self.getImagesDownloaded(reference: storagePath, completion: { (image, error) in
guard let image = image, error == nil else {
print(String(describing : error?.localizedDescription))
return
}
print("TITLE: \(String(describing: title)), IMAGE: \(image)")
completion(title!, description!, image)
})
}
}

If I understand your question correctly, here what you need to do:
Disconnect the segue for the button in your storyboard.
In the IBAction function for the button, do your prepareDataForSegue work
Once the completion handler is called, call performSegue, which will call the prepareSegue, where you can assign the downloaded cella.
This should load the tableVC only when the data is available.

Related

I can't understand why posts are not loading in firebase

firebase does not write, although it seems to be syntactically correct. The application compiles, but no writes take place.
The documentation also seems to be correct.
the logic is simple. The user uploads a file from the phone, the file is sent to the repository
#IBAction func addButtonTouchUpInside(_ sender: Any) {
ProgressHUD.show("Waiting...", interaction: false)
if let profileImg = self.selectedImage, let imageData = image?.jpegData(compressionQuality: 0.4){
let photoIdString = NSUUID().uuidString
let storageRef = Storage.storage().reference(forURL: https://example.com).child("posts").child(photoIdString)
storageRef.putData(imageData, metadata: nil, completion: {(metadata, error) in
if error != nil {
ProgressHUD.showError(error!.localizedDescription)
return
}
let photoUrl = storageRef.downloadURL(completion: {url, error in
if error != nil {
print("Failed to download url:", error!)
return
} else {
let url = url?.absoluteString
self.sendDataToDatabase(photoUrl: url!)
}
})
})
}
}
func sendDataToDatabase(photoUrl: String){
let ref = Database.database().reference()
let postsReference = ref.child("posts")
let newPostId = postsReference.childByAutoId().key
let newPostReference = postsReference.child(newPostId!)
newPostReference.setValue(["photoUrl": photoUrl], withCompletionBlock: {
(Error, ref) in
if Error != nil {
ProgressHUD.showError(Error!.localizedDescription)
return
}
ProgressHUD.showSuccess("Success")
})
}
}
extension PerformViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]){
print("Did finish picking media")
if let image = info[UIImagePickerController.InfoKey.originalImage ] as? UIImage{
selectedImage = image
photo.image = image
}
dismiss(animated: true, completion: nil)
}
}

How to load image from Firebase into users avatar

I have a problem with loading images from firebase. I have two functions. One of them collect info about user, second one load users avatar image. Unfortunately images load after function creates new user. I know it will be problem with asynchronous of Firebase but I don't know how to set up DispatchQueue to work properly. Can you help me with that?
// function that load user image in user manager class
func loadUserImage(contactUserID: String, completion: #escaping (UIImage) -> Void) {
let userID = Auth.auth().currentUser!.uid
var userImageRef = self.storage.child("\(userID)/userImage.jpg")
var image = UIImage()
if contactUserID != "" {
userImageRef = self.storage.child("\(contactUserID)/userImage.jpg")
}
userImageRef.getData(maxSize: 5 * 1024 * 1024) { (data, error) in
if let error = error {
print("Error with retrieving data: \(error.localizedDescription)")
} else {
if data?.count != 0 {
image = UIImage(data: data!)!
} else {
image = UIImage(systemName: "person.circle.fill")!
}
completion(image)
}
}
}
// function that load user in contact manager class
func loadContactList(completion: #escaping ([User]) -> Void) {
let currentUserID = Auth.auth().currentUser!.uid
db.collection("contacts")
.document(currentUserID)
.collection("userContacts")
.addSnapshotListener { (querySnapshot, error) in
var contactList = [User]()
if let error = error {
print("Error with retrieving data from DB: \(error.localizedDescription)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for document in snapshotDocuments {
let data = document.data()
let uid = data["uid"] as! String
let name = data["name"] as! String
let email = data["email"] as! String
var contact = User(email: email, name: name, userID: uid)
DispatchQueue.global().sync {
self.userService.loadUserImage(contactUserID: uid) { (image) in
contact.photoURL = image
}
}
contactList.append(contact)
contactList.sort {
$0.name < $1.name
}
completion(contactList)
}
}
}
}
}
// Function implementation in viewController
func loadContactList() {
self.contactService.loadContactList { (contactArray) in
self.contactList = contactArray
self.tableView.reloadData()
}
}
What you can do is to store the image url in the firebase database and after that create this extension:
import UIKit
let imageCache: NSCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(urlString: String) {
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
let url = URL(string: urlString)
if let data = try? Data(contentsOf: url!) {
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data) {
imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
self.image = downloadedImage
}
})
}
}
}
And call:
if let url = data["imgUrl"] as? String {
self.myImageView.loadImageUsingCacheWithUrlString(urlString: url)
}
For that what you need to do is to create and initialize an UIImage object. If you are working with cell classes you need to create this object in the cell.

Cannot share with UICloudSharingController; vanishes with "uploading" message

while presenting the UICloudSharingController on top of a view, it presents the screen and when I select the messages option to send a message to a person whom I want to share with, it gives a spinning wheel with "uploading" message and vanishes - attached.
However when I go to cloudkit dashboard the root record has been shared. But I cannot share it with specific person. Is it because it has shared global? How can I fix it?
self.shareInfraRecord(zoneID: appDelegate.privateContactZoneID, completion: { (status) in
if ( status == false) {
return
}
})
func shareInfraRecord(zoneID: CKRecordZone.ID, completion: #escaping(Bool) -> Void) {
if let rootRecord = self.rootRecord {
if self.rootRecord?.share == nil {
let sharingController = UICloudSharingController { (controller, preparationHandler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let shareID = CKRecord.ID(recordName: UUID().uuidString, zoneID: zoneID)
var share = CKShare(rootRecord: rootRecord, shareID: shareID)
share[CKShare.SystemFieldKey.title] = Cloud.ShareInfrastructure.ContactShareTitleKey as CKRecordValue?
share[CKShare.SystemFieldKey.shareType] = Cloud.ShareInfrastructure.ContactShareTypeKey as CKRecordValue?
let modifyRecZoneOp = CKModifyRecordsOperation(recordsToSave:[rootRecord, share], recordIDsToDelete: nil)
modifyRecZoneOp.modifyRecordsCompletionBlock = { (records, recordID, error) in
if error != nil {
if let ckerror = error as? CKError {
if let serverVersion = ckerror.serverRecord as? CKShare {
share = serverVersion
}
completion(false)
}
}
preparationHandler(share, self.defaultContainer, error)
}
self.privateDB?.add(modifyRecZoneOp)
}
sharingController.availablePermissions = [.allowReadOnly, .allowPrivate]
sharingController.delegate = self
sharingController.popoverPresentationController?.sourceView = self.view
self.present(sharingController, animated:true, completion:nil)
} else {
let shareRecordID = rootRecord.share!.recordID
let fetchRecordsOp = CKFetchRecordsOperation(recordIDs: [shareRecordID])
fetchRecordsOp.fetchRecordsCompletionBlock = { recordsByRecordID, error in
guard error == nil, let share = recordsByRecordID?[shareRecordID] as? CKShare else {
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
//self.saveToCloudKitStatus(recordName: myRecordName, success: false)
}
completion(false)
return
}
DispatchQueue.main.async {
let sharingController = UICloudSharingController(share: share, container: self.defaultContainer!)
completion(true)
//completionHandler(sharingController)
}
}
self.privateDB?.add(fetchRecordsOp)
}
}
}
This might be a bit late but I was running into this issue too, while using NSPersistentCloudKitContainer and it seems the issue was just making sure that my iCloud container name in the Capabilities section of the settings matched my app bundle name ie iCloud.com.goddamnyouryan.MyApp

Return response as object in swift

I have a function that connects to an API to retrieve data. The API takes two parameters accessCode (provided by user in a text box) and then UDID (UDID of their device). I can parse the data from within the function, but only locally. I need to store the values that are returned but am unsure on how to return them properly. Essentially I need this to return the json object as a dictionary (I think...) so it can be parsed outside of the async task. I've read through the swift documentation and that's where I found out how to do the requests, but I can't find a way to store the returned values in memory for access outside of the function.
func getResponse(accessCode:String, UDID:String, _ completion: #escaping (NSDictionary) -> ()) {
let urlPath = "https://apihosthere.com/api/validate?accessCode=" + accessCode + "&UDID=" + UDID
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult as? NSDictionary
print(results)
completion(results!)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
First of all don't use NSDictionary in Swift, use native [String:Any] and declare the type as optional to return nil if an error occurs.
And never use .mutableContainers in Swift, the option is useless.
func getResponse(accessCode:String, UDID:String, completion: #escaping ([String:Any]?) -> Void)) {
let urlPath = "https://apihosthere.com/api/validate?accessCode=" + accessCode + "&UDID=" + UDID
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error else {
print(error)
completion(nil)
return
}
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
print(jsonResult)
completion(jsonResult)
} else {
completion(nil)
}
} catch {
print(error)
completion(nil)
}
}
task.resume()
}
Your mistake is that you don't consider the closure, you have to execute the entire code inside the completion handler
#IBAction func StartWizard(_ sender: UIButton) {
//Store entered access code
let accessCode = AccessCodeField.text!
//Call API to validate Access Code
getResponse(accessCode:accessCode, UDID:myDeviceUDID) { [weak self] result in
if let accessCodeFound = result?["Found"] as? Bool {
print("Value of Found during function:")
//If access code is valid, go to License view
print(accessCodeFound)
if accessCodeFound {
//Load License View
DispatchQueue.main.async {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let licenseController = storyboard.instantiateViewController(identifier: "LicenseViewPanel")
self?.show(licenseController, sender: self)
}
}
}
}
}
Your completion closure should handle the obtained data. You would call the function like this:
getResponse(accessCode: "code", UDID: "udid", completion: { result in
// Do whatever you need to do with the dictionary result
}
Also, I'd recommend you to change your NSDictionary with a swift Dictionary.
This is what the API returns as a response
{
AccessCode = 00000000;
Client = "0000 - My Company Name";
EmailAddress = "brandon#brandonthomas.me";
FirstName = Brandon;
Found = 1;
LastName = Thomas;
Status = A;
UDIDregistered = 1;
}
And this is what calls the function. I am calling at after clicking a button after an access code is being entered in a text field.
#IBAction func StartWizard(_ sender: UIButton) {
//Store entered access code
let accessCode = AccessCodeField.text!
var accessCodeFound: Bool? = nil
//Call API to validate Access Code
getResponse(accessCode:accessCode, UDID:myDeviceUDID) { result in
accessCodeFound = result["Found"] as! Bool
print("Value of Found during function:")
print(accessCodeFound)
//accessCodeFound = true
}
//If access code is valid, go to License view
print("Value of Found after function:")
print(accessCodeFound)
//accessCodeFound = nil ???
//it seems the value is getting reset after the function completes
if accessCodeFound == true{
//Load License View
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let licenseController = storyboard.instantiateViewController(identifier: "LicenseViewPanel")
self.show(licenseController, sender: Any?.self)
}
}

Is there a way to use my array of type Music, in another scope?

I'm attempting to print/dump and array of type Music outside of a function it's created in. I can successfully dump the musicItems array inside of the getMusicData function but when I set the musicItems array outside of the scope, it won't print anything. What am I doing wrong with the scope here? I have a feeling it's super simple but I just can't figure it out. Thanks in advance for taking the time to read this.
edit: It's giving me "0 elements" in the console when I attempt to dump the musicItems array in the ViewController class. Well, the function is in the same class as well so I guess I don't know what to call the first array. The parent array?
struct MusicResults: Decodable {
let results: [Music]?
}
struct Music: Decodable {
let trackName: String?
let collectionName: String?
let artworkUrl30: String?
}
class ViewController: UITableViewController, UISearchBarDelegate {
var musicItems: [Music] = []
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData()
dump(musicItems)
}
Here is the function.
func getMusicData() {
var musicItems: [Music] = []
guard let searchTerm = searchString else {return}
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
// print(results.trackName!)
musicItems.append(results)
}
//dump(musicItems)
self.musicItems = musicItems
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
Fixed Code
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData {
music in
dump(music)
}
function:
func getMusicData(completionHandler: #escaping (_ music: [Music]) -> ()) {
...
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
musicItems.append(results)
}
completionHandler(musicItems)
...
Your 'getMusicData' function is asynchronous which means that when it executes, it queues data task in a background queue and proceeds the execution and since there are no more institutions it simply returns control to its calling site - 'musicButton()' action, which in its turn executes the next instruction - prints the 'musicItems' array which might (and most likely, is) still not populated as the network call haven’t yet completed. One of the options that you have here is to pass a completion block to your 'getMusicData' function, that runs it after data task gets the results.
Another option is to use Property Observers
var musicItems: [Music] = [] {
didSet {
dump(self.musicItems)
/// This is where I would do the...
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
}
}
and then
func getMusicData() {
guard let searchTerm = searchString else { print("no search"); return }
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { print("url error"); return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { print(err ?? "unknown"); return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
if let results = music.results {
self.musicItems.append(contentsOf: results)
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}