How to work with async functions swift? Completion handlers [duplicate] - swift

This question already has answers here:
Run code only after asynchronous function finishes executing
(2 answers)
Closed 5 years ago.
Im trying to wait for the function to process in order to show my image. I have try many things but none of this worked. I know this is an async function and basically i have to wait in order to get the right values but I dont know how to fix this function right here. I hope you can help me out. Thank you!
func createListProductsGood(Finished() -> void) {
refProducts.child("Products").queryOrderedByKey().observe(.childAdded, with: { snapshot in
let prod = snapshot.value as! NSDictionary
let active = snapshot.key
let rejected = prod["NotInterested"] as! String
let photoURL = prod["photoURL"] as! String
var findit = false
// print(rejected)
if (rejected != self.userUID){
//print(active)
if rejected.contains(","){
var pointsArr = rejected.components(separatedBy: ",")
for x in pointsArr{
if x.trimmingCharacters(in: NSCharacterSet.whitespaces) == self.userUID {
// print("dont show")
findit = true
return
}
}
if (findit == false){
if let url = NSURL(string: photoURL) {
if let data = NSData(contentsOf: url as URL) {
self.ProductId = active
self.productPhoto.image = UIImage(data: data as Data)
}}
}
}else{
print(active)
if let url = NSURL(string: photoURL) {
if let data = NSData(contentsOf: url as URL) {
self.ProductId = active
self.productPhoto.image = UIImage(data: data as Data)
}}
}
}
})
finished()
}
Edited:
This is how my viewDidLoad looks like:
override func viewDidLoad() {
super.viewDidLoad()
setAcceptedOrRejected()
createListProductsGood{_ in
}
}
func createListProductsGood(finished: #escaping (_ imageData: Data) -> Void) {
refProducts.child("Products").queryOrderedByKey().observe(.childAdded, with: { snapshot in
let prod = snapshot.value as! NSDictionary
let active = snapshot.key
let rejected = prod["NotInterested"] as! String
let photoURL = prod["photoURL"] as! String
var findit = false
// print(rejected)
if (rejected != self.userUID){
//print(active)
if rejected.contains(","){
var pointsArr = rejected.components(separatedBy: ",")
for x in pointsArr{
if x.trimmingCharacters(in: NSCharacterSet.whitespaces) == self.userUID {
// print("dont show")
findit = true
return
}
}
if (findit == false){
if let url = NSURL(string: photoURL) {
if let data = NSData(contentsOf: url as URL) {
self.ProductId = active
DispatchQueue.main.async {
self.productPhoto.image = UIImage(data: data as Data)
}
}}
}
}else{
print(active)
if let url = NSURL(string: photoURL) {
if let data = NSData(contentsOf: url as URL) {
self.ProductId = active
DispatchQueue.main.async {
self.productPhoto.image = UIImage(data: data as Data)
}
}}
}
}
})
}
This is my second method:
func setAcceptedOrRejected() {
refProducts.child("Products").queryOrderedByKey().observe(.childAdded, with: { snapshot in
let prod = snapshot.value as! NSDictionary
if self.ProductId == snapshot.key{
self.texto = prod["NotInterested"] as! String
self.refProducts.child("Products").child(self.ProductId).updateChildValues(["NotInterested": self.texto + ", " + self.userUID])
} })
}

You should change:
func createListProductsGood(Finished() -> void) {
to:
func createListProductsGood(finished: #escaping (_ something: SomeType) -> Void) {
or to be more specific:
func createListProductsGood(finished: #escaping (_ imageData: Data) -> Void) {
then wherever in your function you get the image, you call
finished(imageData)
so you can pass the imageData through a closure to where its needed.
then you call this function like this:
createListProductsGood{ imageData in
...
let image = UIImage(data: imageData)
// update UI from main Thread:
DispatchQueue.main.async {
self.productPhoto.image = image
}
}
Also:
it's not convention to use Finished(), you should use finished()
using void is wrong. You must use Void or ()
If you're having problems with closures and completionHandlers, I recommend you first try getting your hands dirty with a simple UIAlertController. See here. Try creating an action with a closure, e.g. see here
EDIT :
Thanks to Leo's comments:
func createListProductsGood(finished: #escaping(_ imageData: Data?, MyError?) -> Void) {
let value: Data?
let error = MyError.someError("The error message")
refProducts.child("Products").queryOrderedByKey().observe(.childAdded, with: { snapshot in
let prod = snapshot.value as! NSDictionary
let active = snapshot.key
let rejected = prod["NotInterested"] as! String
let photoURL = prod["photoURL"] as! String
var findit = false
// print(rejected)
if (rejected != self.userUID){
//print(active)
if rejected.contains(","){
var pointsArr = rejected.components(separatedBy: ",")
for x in pointsArr{
if x.trimmingCharacters(in: NSCharacterSet.whitespaces) == self.userUID {
// print("dont show")
findit = true
return
}
}
if (findit == false){
if let url = NSURL(string: photoURL) {
if let data = NSData(contentsOf: url as URL) {
self.ProductId = active // REMOVE
self.productPhoto.image = UIImage(data: data as Data) // REMOVE
finished(data, nil) //ADD
}else{
finished(nil,error) //ADD
}
}
}
}else{
print(active)
if let url = NSURL(string: photoURL) {
if let data = NSData(contentsOf: url as URL) {
self.ProductId = active // REMOVE
self.productPhoto.image = UIImage(data: data as Data) // REMOVE
finished(data,nil) //ADD
}else{
finished(nil,error) //ADD
}
}
}
}
})
}
And then you call it like:
createListProductsGood { imageData, error in guard let value = imageData, error == nil else { // present an alert and pass the error message return }
...
let image = UIImage(data: imageData)
// update UI from main Thread:
DispatchQueue.main.async {
self.ProductId = active
self.productPhoto.image = image } }
Basically this way the createListProductsGood takes in 2 closures, one for if the image is present, another for if an error was returned.

Related

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.

Swift: listen to url property then download photo

I run this code in viewDidLoad, but Profile.currentProfile does not have the photoUrl yet, so it is nil and this code never runs
private func getProfilePicture() {
if let photoURLString = Profile.currentProfile?.photoUrl {
if let photoURL = URL(string: photoURLString) {
if let photoData = try? Data(contentsOf: photoURL) {
self.profilePhotoView.image = UIImage(data: photoData)
self.letsGoButton.isEnabled = true
}
}
} else {
self.profilePhotoView.image = UIImage(named: "default-profile-icon")
}
}
How can I wait until the photoUrl is not nil, then run this code? Thanks
Rik
(edit) this is how profile is set. This is called before the viewController is instantiated.
func copyProfileFieldsFromFB(completionHandler: #escaping ((Error?) -> Void)) {
guard AccessToken.current != nil else { return }
let request = GraphRequest(graphPath: "me",
parameters: ["fields": "email,first_name,last_name,gender, picture.width(480).height(480)"])
request.start(completionHandler: { (_, result, error) in
if let data = result as? [String: Any] {
if let firstName = data["first_name"] {
Profile.currentProfile?.firstName = firstName as? String
}
if let lastName = data["last_name"] {
Profile.currentProfile?.lastName = lastName as? String
}
if let email = data["email"] {
Profile.currentProfile?.email = email as? String
}
if let picture = data["picture"] as? [String: Any] {
if let imageData = picture["data"] as? [String: Any] {
if let url = imageData["url"] as? String {
Profile.currentProfile?.photoUrl = url
}
}
}
}
completionHandler(error)
})
}
Normally, you'll want to use completion handlers to keep track of asynchronous activities. So in your viewDidLoad() you could call something like
Profile.currentProfile?.getPhotoURL { urlString in
if let photoURL = URL(string: photoURLString) {
if let photoData = try? Data(contentsOf: photoURL) {
self.profilePhotoView.image = UIImage(data: photoData)
self.letsGoButton.isEnabled = true
}
}
}
And on your Profile class it would look something like this:
func getPhotoURL(completion: #escaping (urlString) -> Void) {
// get urlString
completion(urlString)
}
You can add private var profileUrl and use didSet observing with it:
... //e.g. your controller
private var profileUrl: URL? {
didSet {
if let url = profileUrl {
getProfilePicture(from: url)// update your func
}
}
}

Converting Swift ios Networking to use Alamofire

I got a source code from a github page written in swift and implementing GoogleMaps. I now want to refactor the codes to use Alamofire and SwiftyJSON so that I can improve the code but I got confused because through my learning of swift I used Alamofire and swiftyJSON for every networking process so I am confused currently. the code below
typealias PlacesCompletion = ([GooglePlace]) -> Void
typealias PhotoCompletion = (UIImage?) -> Void
class GoogleDataProvider {
private var photoCache: [String: UIImage] = [:]
private var placesTask: URLSessionDataTask?
private var session: URLSession {
return URLSession.shared
}
let appDelegate = UIApplication.shared.delegate as! AppDelegate
func fetchPlacesNearCoordinate(_ coordinate: CLLocationCoordinate2D, radius: Double, types:[String], completion: #escaping PlacesCompletion) -> Void {
var urlString = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(coordinate.latitude),\(coordinate.longitude)&radius=\(radius)&rankby=prominence&sensor=true&key=\(appDelegate.APP_ID)"
let typesString = types.count > 0 ? types.joined(separator: "|") : "food"
urlString += "&types=\(typesString)"
urlString = urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? urlString
guard let url = URL(string: urlString) else {
completion([])
return
}
if let task = placesTask, task.taskIdentifier > 0 && task.state == .running {
task.cancel()
}
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
placesTask = session.dataTask(with: url) { data, response, error in
var placesArray: [GooglePlace] = []
defer {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
completion(placesArray)
}
}
guard let data = data,
let json = try? JSON(data: data, options: .mutableContainers),
let results = json["results"].arrayObject as? [[String: Any]] else {
return
}
results.forEach {
let place = GooglePlace(dictionary: $0, acceptedTypes: types)
placesArray.append(place)
if let reference = place.photoReference {
self.fetchPhotoFromReference(reference) { image in
place.photo = image
}
}
}
}
placesTask?.resume()
}
func fetchPhotoFromReference(_ reference: String, completion: #escaping PhotoCompletion) -> Void {
if let photo = photoCache[reference] {
completion(photo)
} else {
let urlString = "https://maps.googleapis.com/maps/api/place/photo?maxwidth=200&photoreference=\(reference)&key=\(appDelegate.APP_ID)"
guard let url = URL(string: urlString) else {
completion(nil)
return
}
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
session.downloadTask(with: url) { url, response, error in
var downloadedPhoto: UIImage? = nil
defer {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
completion(downloadedPhoto)
}
}
guard let url = url else {
return
}
guard let imageData = try? Data(contentsOf: url) else {
return
}
downloadedPhoto = UIImage(data: imageData)
self.photoCache[reference] = downloadedPhoto
}
.resume()
}
}
}
any help to refactor the codes to use Alamofire and swiftyJSON would be appreciated.
Both Alamofire and SwiftyJSON have pretty decent instructions, and there are plenty of examples online to look for. However, this would be a decent starting point - you need to replace your session.dataTask and session.downloadTask with Alamofire methods. For example, instead of:
session.downloadTask(with: url) { url, response, error in
var downloadedPhoto: UIImage? = nil
defer {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
completion(downloadedPhoto)
}
}
guard let url = url else {
return
}
guard let imageData = try? Data(contentsOf: url) else {
return
}
downloadedPhoto = UIImage(data: imageData)
self.photoCache[reference] = downloadedPhoto
}
.resume()
use this skeleton and implement your models and logic:
Alamofire
.request(url)
.responseJSON { dataResponse in
switch dataResponse.result {
case .success:
guard let json = JSON(dataResponse.data) else {
return
}
// Continue parsing
case .failure(let error):
// Handle error
print("\(error)")
}
}

Mulitple calls to meetup api swift

I am trying to get a list of events from the meetup's api. Its crashes when request mulitple times as it states can only request once and is limited to 200 requests. I am getting groups then using the "urlName" api call to get the event coming up for that group. The result would be an array of events from a bunch of meetup groups. Here is my code.
func getEventsFromMeetup(complete: (groups: [Meetup], succes: Bool) -> Void) {
var currentUserInterests = [String]()
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let recommendedBaseUrl = "https://api.meetup.com/recommended/groups?key=\(meetupAPIKey)"
let url = NSURL(string: recommendedBaseUrl)
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "GET"
session.dataTaskWithRequest(request) { (data, response, error) in
guard error == nil else {
print(error)
complete(groups: [Meetup](), succes: false)
return
}
guard let data = data else {
print("Error with data")
complete(groups: [Meetup](), succes: false)
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments) as! [NSDictionary]
var groups = [Meetup]()
CurrentUserFM.sharedInstance.getCurrentUserInterests { (interests) in
currentUserInterests = interests
var ints = [String]()
print(json.count)
for j in json {
let m = Meetup(data: j)
ints.append(m.name!)
if let i = m.interestName {
if currentUserInterests.contains(i) {
groups.append(m)
}
}
print("ints: \(ints.count)")
print("json: \(json.count)")
if Int(ints.count) >= Int(json.count) {
dispatch_async(dispatch_get_main_queue(), {
complete(groups: groups, succes: true)
return
})
}
}
}
}catch {
print(error)
}
}.resume()
}
func getEventsForGroups(completionHandler: (meetupEvents: [MeetupEvent]) -> ()) {
self.getEventsFromMeetup { (groups, success) in
var meetupEvents1 = [MeetupEvent]()
var ints = [String]()
for group in groups {
let eventBaseUrl = "https://api.meetup.com/\(group.urlname!)?key=\(meetupAPIKey)"
let url = NSURL(string: eventBaseUrl)
let request = NSMutableURLRequest(URL: url!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
session.dataTaskWithRequest(request, completionHandler: { (data, reponse, error) in
guard error == nil else {
print(error)
return
}
guard let data = data else {
print("Error with data")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
let m = MeetupEvent()
if let name = json["name"] as? String {
m.name = name
}
if let link = json["link"] as? String {
m.link = link
}
if let interestName = group.name {
m.interestName = interestName
}
if let image = json["key_photo"]??["thumb_link"] as? String {
m.image = image
}
meetupEvents1.append(m)
ints.append("1")
if Int(ints.count) >= Int(groups.count) {
dispatch_async(dispatch_get_main_queue(), {
completionHandler(meetupEvents: meetupEvents1)
return
})
}
}catch {
print(error)
}
}).resume()
}
}
}
Any suggestions on how to get the events from the groups so that the app wont crash from meetup calls?

Extra argument 'error' in call in swift

I am new to swift so please treat me as beginner.
I am following tutorial, this is pretty old tutorial and it has used GoogleMap framework whereas I am doing it with pod. In func geocodeAddress in MapTasks.swift file I am getting error called
Extra argument 'error' in call
func geocodeAddress(address: String!, withCompletionHandler completionHandler: ((status: String, success: Bool) -> Void)) {
if let lookupAddress = address {
var geocodeURLString = baseURLGeocode + "address=" + lookupAddress
geocodeURLString = geocodeURLString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
let geocodeURL = NSURL(string: geocodeURLString)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let geocodingResultsData = NSData(contentsOfURL: geocodeURL!)
let request = NSMutableURLRequest(URL: geocodingResultsData)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(let data, let response, let error) in
if let _ = response as? NSHTTPURLResponse {
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? NSDictionary
if error != nil {
print("error=\(error!)")
return
}
if let parseJSON = json {
}
} catch {
print(error)
}
}
}
task.resume()
else {
// Get the response status.
let status = dictionary["status"] as! String
if status == "OK" {
let allResults = dictionary["results"] as! Array<Dictionary<NSObject, AnyObject>>
self.lookupAddressResults = allResults[0]
// Keep the most important values.
self.fetchedFormattedAddress = self.lookupAddressResults["formatted_address"] as! String
let geometry = self.lookupAddressResults["geometry"] as! Dictionary<NSObject, AnyObject>
self.fetchedAddressLongitude = ((geometry["location"] as! Dictionary<NSObject, AnyObject>)["lng"] as! NSNumber).doubleValue
self.fetchedAddressLatitude = ((geometry["location"] as! Dictionary<NSObject, AnyObject>)["lat"] as! NSNumber).doubleValue
completionHandler(status: status, success: true)
}
else {
completionHandler(status: status, success: false)
}
}
})
}
else {
completionHandler(status: "No valid address.", success: false)
}
}
So far I know is I am getting this error because of the diffrent version of swift. Tutorial I am following is written in old version of swift and I am doing it in new
In Swift 2.0, you cannot add 'error' argument in NSJSONSerialization method, you need to use try-catch statement as follows:
func geocodeAddress(address: String!, withCompletionHandler completionHandler: ((status: String, success: Bool) -> Void)) {
if let lookupAddress = address {
var geocodeURLString = baseURLGeocode + "address=" + lookupAddress
geocodeURLString = geocodeURLString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
let geocodeURL = NSURL(string: geocodeURLString)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let geocodingResultsData = NSData(contentsOfURL: geocodeURL!)
let request = NSMutableURLRequest(URL: geocodeURL!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(let data, let response, let error) in
if let _ = response as? NSHTTPURLResponse {
do {
let dictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? NSDictionary
if error != nil {
print("error=\(error!)")
return
}
if let parseJSON = dictionary {
let status = dictionary["status"] as! String
if status == "OK" {
let allResults = dictionary["results"] as! Array<Dictionary<NSObject, AnyObject>>
self.lookupAddressResults = allResults[0]
// Keep the most important values.
self.fetchedFormattedAddress = self.lookupAddressResults["formatted_address"] as! String
let geometry = self.lookupAddressResults["geometry"] as! Dictionary<NSObject, AnyObject>
self.fetchedAddressLongitude = ((geometry["location"] as! Dictionary<NSObject, AnyObject>)["lng"] as! NSNumber).doubleValue
self.fetchedAddressLatitude = ((geometry["location"] as! Dictionary<NSObject, AnyObject>)["lat"] as! NSNumber).doubleValue
completionHandler(status: status, success: true)
}
else {
completionHandler(status: status, success: false)
}
}
} catch {
print(error)
}
}
}
task.resume()
})
}
}