how to manage a several asynchronous task before doing some action? - ios

I am beginner in programming. I actually have my own answer of this questions and the app worked as I am expected, but I am not sure if this is the correct way to to this.
This check out action will be triggered after the user click chechoutButton. but before before this chechoutButton.isEnabled , I have to make sure 3 parameters are available (not nil). before doing this check out action, I need 3 parameters :
get user's coordinate from GPS.
get user's location address from Google Place
API
Get current date time from server for verification.
method to get user location address from Google Place API will be triggered only if I get the coordinate from GPS, and as we know, fetching data from the internet (to take date and time) also takes time, it should be done asynchronously.
how do I manage this checkoutButton only enabled if those 3 parameters are not nil ? Is there a better way according to apple guideline to do this
the simplified code are below
class CheckoutTVC: UITableViewController {
#IBOutlet weak var checkOutButton: DesignableButton!
var checkinAndCheckoutData : [String:Any]? // from MainMenuVC
var dateTimeNowFromServer : String?
var userLocationAddress : String?
let locationManager = LocationManager()
var coordinateUser : Coordinate? {
didSet {
getLocationAddress()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// initial state
checkOutButton.alpha = 0.4
checkOutButton.isEnabled = false
getDateTimeFromServer()
getCoordinate()
}
#IBAction func CheckoutButtonDidPressed(_ sender: Any) {
}
}
extension CheckoutTVC {
func getDateTimeFromServer() {
activityIndicator.startAnimating()
NetworkingService.getDateTimeFromServer { (result) in
switch result {
case .failure(let error) :
self.activityIndicator.stopAnimating()
// show alert
case .success(let timeFromServer) :
let stringDateTimeServer = timeFromServer as! String
self.dateTimeNowFromServer = stringDateTimeServer
self.activityIndicator.stopAnimating()
}
}
}
func getCoordinate() {
locationManager.getPermission()
locationManager.didGetLocation = { [weak self] userCoordinate in
self?.coordinateUser = userCoordinate
self?.activateCheckOutButton()
}
}
func getLocationAddress() {
guard let coordinateTheUser = coordinateUser else {return}
let latlng = "\(coordinateTheUser.latitude),\(coordinateTheUser.longitude)"
let request = URLRequest(url: url!)
Alamofire.request(request).responseJSON { (response) in
switch response.result {
case .failure(let error) :// show alert
case .success(let value) :
let json = JSON(value)
let locationOfUser = json["results"][0]["formatted_address"].string
self.userLocationAddress = locationOfUser
self.locationAddressLabel.text = locationOfUser
self.activateNextStepButton()
}
}
}
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
}
I manage this by using this method, but I don't know if this is the correct way or not
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}

You can use DispatchGroup to know when all of your asynchronous calls are complete.
func notifyMeAfter3Calls() {
let dispatch = DispatchGroup()
dispatch.enter()
API.call1() { (data1)
API.call2(data1) { (data2)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
}
dispatch.enter()
API.call3() { (data)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
dispatch.notify(queue: DispatchQueue.main) {
finished?(dispatchSuccess)
}
}
You must have an equal amount of enter() and leave() calls. Once all of the leave() calls are made, the code in DispatchGroupd.notify will be called.

Related

DispatchGroup.wait() freezes program

I am making two asynchronous network calls and would like to use a Dispatch Group to wait until the call complete and then resume. My program is freezing.
class CommentRatingViewController: UIViewController, UITextViewDelegate {
let myDispatchGroup = DispatchGroup()
#IBAction func saveRatingComment(_ sender: Any) {
rating = ratingView.rating
if rating != 0.0 {
myDispatchGroup.enter()
saveRating(articleID: post.articleID, userID: post.userID) //Network call
self.updatedRating = true
}
if commentsTextView.text != "" {
myDispatchGroup.enter()
saveComment(articleID: post.articleID, userID: post.userID, comment: commentsTextView.text!) //Network call self.addedComment = true
}
myDispatchGroup.wait()
DispatchQueue.main.async {
self.delegate?.didCommentOrRatePost(updatedRating: self.updatedRating, addedComment: self.addedComment)
}
}
And here is one of the network calls:
func saveRating (articleID: String, userID: String) {
let userPostURLRaw = "http://www.smarttapp.com/DesktopModules/DnnSharp/DnnApiEndpoint/Api.ashx?method=UpdatePostRating"
Alamofire.request(
userPostURLRaw,
method: .post,
parameters: ["articleID": articleID,
"newRating": self.rating,
"UserID": userID]
)
.responseString { response in
guard let myString = response.result.value else { return }
DispatchQueue.main.async {
self.myDispatchGroup.leave()
}
}
}
The network calls worked until I introduced Dispatch Group code.
I've resolved this.
The problem was that myDispatchGroup.enter() and self.myDispatchGroup.leave() where being called on different threads. I moved the call to the beginning and very end of the network requests and it works fine now.

Remove the observer using the handle in Firebase in Swift

I have the following case. The root controller is UITabViewController. There is a ProfileViewController, in it I make an observer that users started to be friends (and then the screen functions change). ProfileViewController can be opened with 4 tabs out of 5, and so the current user can open the screen with the same user in four places. In previous versions, when ProfileViewController opened in one place, I deleted the observer in deinit and did the deletion just by ref.removeAllObservers(), now when the user case is such, I started using handle and delete observer in viewDidDisappear. I would like to demonstrate the code to find out whether it can be improved and whether I'm doing it right in this situation.
I call this function in viewWillAppear
fileprivate func firObserve(_ isObserve: Bool) {
guard let _user = user else { return }
FIRFriendsDatabaseManager.shared.observeSpecificUserFriendshipStart(observer: self, isObserve: isObserve, userID: _user.id, success: { [weak self] (friendModel) in
}) { (error) in
}
}
This is in the FIRFriendsDatabaseManager
fileprivate var observeSpecificUserFriendshipStartDict = [AnyHashable : UInt]()
func observeSpecificUserFriendshipStart(observer: Any, isObserve: Bool, userID: String, success: ((_ friendModel: FriendModel) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
DispatchQueue.global(qos: .background).async {
let specificUserFriendRef = Database.database().reference().child(MainGateways.friends.description).child(currentUserID).child(SubGateways.userFriends.description).queryOrdered(byChild: "friendID").queryEqual(toValue: userID)
if !isObserve {
guard let observerHashable = observer as? AnyHashable else { return }
if let handle = self.observeSpecificUserFriendshipStartDict[observerHashable] {
self.observeSpecificUserFriendshipStartDict[observerHashable] = nil
specificUserFriendRef.removeObserver(withHandle: handle)
debugPrint("removed handle", handle)
}
return
}
var handle: UInt = 0
handle = specificUserFriendRef.observe(.childAdded, with: { (snapshot) in
if snapshot.value is NSNull {
return
}
guard let dict = snapshot.value as? [String : Any] else { return }
guard let friendModel = Mapper<FriendModel>().map(JSON: dict) else { return }
if friendModel.friendID == userID {
success?(friendModel)
}
}, withCancel: { (error) in
fail?(error)
})
guard let observerHashable = observer as? AnyHashable else { return }
self.observeSpecificUserFriendshipStartDict[observerHashable] = handle
}
}
Concerning your implementation of maintaining a reference to each viewController, I would consider moving the logic to an extension of the viewController itself.
And if you'd like to avoid calling ref.removeAllObservers() like you were previously, and assuming that there is just one of these listeners per viewController. I'd make the listener ref a variable on the view controller.
This way everything is contained to just the viewController. It also is potentially a good candidate for creating a protocol if other types of viewControllers will be doing similar types of management of listeners.

Assigning a value to a variable after Firebase return result?

I've been struggling with this for 2 hours now. I'm building an app in Swift, using Firebase Database and storage.
The goal is to update User profile. The user has 2 images - Profile and header. Now, I have to first check if they've selected an image from the photo library, if not - just get the old URL from the database and submit it back to the database with the rest of the updated information. If it's a new selected image, upload the image to the Storage, get back the URL using downloadURL assign it to the var storageHeaderDownloadedURL and/or var storageProfileDownloadedURL and submit the string values with the rest of the user data to Firebase Database.
The problem is that it obviously assigns the values of an empty String (I've declared them as such) BEFORE I get back the downloaded URL. If the user doesn't update the images but the rest of the UITextFields it all works, the old URL is submitted to the Firebase Database.
My question is how do I execute the downloaded URL methods for from the storage and then assign it to var storageHeaderDownloadedURL and var storageProfileDownloadedURL first hand?
func updateUserProfile ()
{
if let userID = FIRAuth.auth()?.currentUser?.uid
{
// Note: Storage references to profile images & profile headers folder
let storageUserProfileID = Storage.storage.profile_images.child(userID)
let storageUserHeaderID = Storage.storage.profile_headers.child(userID)
guard let imageProfile = profileImage.image else { return }
guard let headerImage = headerImage.image else { return }
if let newProfileImage = UIImagePNGRepresentation(imageProfile), let newHeaderImage = UIImagePNGRepresentation(headerImage)
{
storageUserProfileID.put(newProfileImage, metadata: nil, completion: { (metadata, error) in
if error != nil
{
showAlert(title: "Oops!", msg: (error?.localizedDescription)!, actionButton: "OK", viewController: self)
return
}
// Get the URL from the storage
storageUserProfileID.downloadURL(completion: { (url, error) in
if error != nil
{
showAlert(title: "Oops!!!", msg: (error?.localizedDescription)!, actionButton: "OK", viewController: nil)
return
}
else
{
if let profileImgDownloadedURL = url?.absoluteString
{
self.storageProfileDownloadedURL = profileImgDownloadedURL
print(self.storageProfileDownloadedURL)
self.selectedProfileImage = .True
}
}
})
})
storageUserHeaderID.put(newHeaderImage, metadata: nil, completion: { (metadata, error) in
if error != nil
{
showAlert(title: "Oops!", msg: (error?.localizedDescription)!, actionButton: "OK", viewController: self)
return
}
// Get the URL from the storage
storageUserHeaderID.downloadURL(completion: { (url, error) in
if error != nil
{
showAlert(title: "Oops!!!", msg: (error?.localizedDescription)!, actionButton: "OK", viewController: self)
return
}
else
{
if let headerImgDownloadedURL = url?.absoluteString
{
self.storageHeaderDownloadedURL = headerImgDownloadedURL
print(self.storageHeaderDownloadedURL)
self.selectedHeaderImage = .True
}
}
})
})
//Note: Update the info for that user in Database
print(self.storageHeaderDownloadedURL)
print(self.storageProfileDownloadedURL)
var finalHeaderImageURL = String()
switch self.selectedHeaderImage {
case .True:
finalHeaderImageURL = self.storageHeaderDownloadedURL
break
case .False:
finalHeaderImageURL = self.oldHeaderImageInDB
break
}
print(finalHeaderImageURL)
var finalProfileImageURL = String()
switch self.selectedProfileImage {
case .True:
finalProfileImageURL = self.storageProfileDownloadedURL
break
case .False:
finalProfileImageURL = self.oldProfilePhotoImageInDB
break
}
print(finalProfileImageURL)
guard let newDisplayName = self.displayNameTextField.text else { return }
guard let newLocation = self.locationTextField.text else { return }
guard let newDescription = self.bioTextField.text else { return }
guard let newWebsite = self.websiteTextField.text else { return }
guard let newBirthday = self.birthdayTextField.text else { return }
let newUpdatedUserDictionary = ["imageProfile": finalProfileImageURL,
"imageHeader" : finalHeaderImageURL,
"description" : newDescription,
"location": newLocation,
"displayName": newDisplayName,
"website": newWebsite,
"birthday": newBirthday,
]
Database.dataService.updateUserProfile(uid: userID, user: newUpdatedUserDictionary)
showAlert(title: "Hey", msg: "Your profile was updated", actionButton: "OK", viewController: self)
} // Get new uploaded profile and header image URLs
}
}
The enums I use for the switch statements to determine if it's an old URL or a new one:
enum SelectedHeaderImage
{
case True
case False
}
enum SelectedProfileImage
{
case True
case False
}
Class outlets:
var storageProfileDownloadedURL = String()
var storageHeaderDownloadedURL = String()
var oldProfilePhotoImageInDB = String()
var oldHeaderImageInDB = String()
var selectedHeaderImage = SelectedHeaderImage.False
var selectedProfileImage = SelectedProfileImage.False`
From what I understood, your problem is with queuing. You want the code below to execute after the download is complete but it executes in its normal flow. If this is what your problem is then I would suggest you to create another enum, with three download states/count. And move that code below you want to be executed later in a function. Increment the state of new enum when download is complete. It would look something like this:
enum DownloadCount
{
case Zero
case One
case Two
}
var downloadCount = DownloadCount.Zero
and in each of the success block of your download complete change it to, I will just write one here to give you the idea of what needs to be done.
if let profileImgDownloadedURL = url?.absoluteString
{
self.storageProfileDownloadedURL = profileImgDownloadedURL
print(self.storageProfileDownloadedURL)
self.selectedProfileImage = .True
if(downloadCount == .Zero)
{
downloadCount = DownloadCount.One
}
else
{
downloadCount = DownloadCount.Two
}
self.newAssigningFunction()
}
func newAssigningFunction()
{
if(downloadCount == .Two)
{
//Do your storage/saving work here
}
}
Also if you need to execute this function again, it would be best to set downloadCount back to Zero at start of your updateUserProfile function. Let me know if somethis is unclear or you need further help. Or if this was not your case.

How do I call a function only after an async function has completed?

I have a function Admin that runs asynchronously in the background.
Is there a way to make sure that the function is completed before calling the code after it?
(I am using a flag to check the success of the async operation. If the flag is 0, the user is not an admin and should go to the NormalLogin())
#IBAction func LoginAction(sender: UIButton) {
Admin()
if(bool.flag == 0) {
NormalLogin()
}
}
func Admin() {
let userName1 = UserName.text
let userPassword = Password.text
let findTimeLineData2:PFQuery = PFQuery(className: "Admins")
findTimeLineData2.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if !(error != nil){
for object in objects as! [PFObject] {
let userName2 = object.objectForKey("AdminUserName") as! String
let userPassword2 = object.objectForKey("AdminPassword") as! String
if(userName1 == userName2 && userPassword == userPassword2) {
//hes an admin
bool.flag = 1
self.performSegueWithIdentifier("AdminPage", sender: self)
self.UserName.text = ""
self.Password.text = ""
break;
}
}
}
}
}
You need to look into completion handlers and asynchronous programming. Here's an example of an async function that you can copy into a playground:
defining the function
notice the "completion" parameter is actually a function with a type of (Bool)->(). Meaning that the function takes a boolean and returns nothing.
func getBoolValue(number : Int, completion: (result: Bool)->()) {
if number > 5 {
// when your completion function is called you pass in your boolean
completion(result: true)
} else {
completion(result: false)
}
}
calling the function
here getBoolValue runs first, when the completion handler is called (above code) your closure is run with the result you passed in above.
getBoolValue(8) { (result) -> () in
// do stuff with the result
print(result)
}
applying the concept
You could apply this concept to your code by doing this:
#IBAction func LoginAction(sender: UIButton) {
// admin() calls your code, when it hits your completion handler the
// closure {} runs w/ "result" being populated with either true or false
Admin() { (result) in
print("completion result: \(result)") //<--- add this
if result == false {
NormalLogin()
} else {
// I would recommend managing this here.
self.performSegueWithIdentifier("AdminPage", sender: self)
}
}
}
// in your method, you pass a `(Bool)->()` function in as a parameter
func Admin(completion: (result: Bool)->()) {
let userName1 = UserName.text
let userPassword = Password.text
let findTimeLineData2:PFQuery = PFQuery(className: "Admins")
findTimeLineData2.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if !(error != nil){
for object in objects as! [PFObject] {
let userName2 = object.objectForKey("AdminUserName") as! String
let userPassword2 = object.objectForKey("AdminPassword") as! String
if(userName1 == userName2 && userPassword == userPassword2) {
// you want to move this to your calling function
//self.performSegueWithIdentifier("AdminPage", sender: self)
self.UserName.text = ""
self.Password.text = ""
// when your completion handler is hit, your operation is complete
// and you are returned to your calling closure
completion(result: true) // returns true
} else {
completion(result: false) // returns false
}
}
}
}
}
Of course, I'm not able to compile your code to test it, but I think this will work fine.

how show activityindicator in swift?

in my app i have a login with a usernamaeTextfield, passwordtextfield and login button. When the login button is tapped i check the fields. I need show a activityindicator in this time, but my var activityindicator is always hidden.
This is my code.
#IBOutlet weak var activity: UIActivityIndicatorView!
override func viewDidLoad() {
self.activity.hidden = true
super.viewDidLoad()}
#IBAction func login(sender: AnyObject) {
activity.hidden = false
activity.startAnimating()
if (self.username.isEmpty || self.password.isEmpty){
self.showAlert("Asegurese de ingresar un usuario y contraseƱa!")
}else{
var user = user_function()
if !user.user_valid(self.username,password: self.password){
self.showAlert("Usuario Invalido")
}else{
}
}
activity.hidden = true
activity.stopAnimating()
}
my code of user_valid is
func user_valid(username :String, password : String)->Bool{
var resultados : Array<JSON> = []
userbase64 = self.encode_to_base64(username)
passbase64 = self.encode_to_base64(password)
var api = channels_function()
resultados = api.load_videos("https://api.cxntv.com/api/v1/videos/?type=canales&page_size=100&ordering=-id")
if errormessage.isEmpty{
api.save_LiveChannels(resultados)
saver_user(userbase64, passbase64: passbase64, username: username, password: password)
errormessage = ""
return true
}else{
errormessage = ""
return false}
}
and loads videos is:
func load_videos(url :String)->Array<JSON>{
var resultados : Array<JSON> = []
var request = Get_Data()
self.task_completed = false
request.remoteUrl = url
request.getData({data, error -> Void in
println("los datos")
//println(data)
if (data != nil){
// Fix possible error if no "results" key
if let results = data["results"].array {
resultados = results
self.task_completed = true
}
println("Data reloaded")
} else {
println("api.getData failed")
self.task_completed = true
}
})
while(!self.task_completed){}
return resultados
}
and get data is:
var remoteUrl = ""
func getData(completionHandler: ((JSON!, NSError!) -> Void)!) -> Void {
let url: NSURL = NSURL(string: remoteUrl)!
let request: NSMutableURLRequest = NSMutableURLRequest(URL: url)
let session = NSURLSession.sharedSession()
println(request.HTTPBody)
request.addValue(userbase64 ,forHTTPHeaderField: "X_CXN_USER")
request.addValue( passbase64,forHTTPHeaderField: "X_CXN_PASS")
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
if (error != nil) {
return completionHandler(nil, error)
}
var error: NSError?
let json = JSON(data : data)
if (error != nil){
return completionHandler(nil, error)
} else {
if let results = json["detail"].string {
errormessage = results
return completionHandler(nil, error)
} else {
return completionHandler(json, nil)
}
}
})
task.resume()
}
The problem is loadVideos, in which you've taken a very nice, asynchronous method and made it synchronous (blocking UI updates), and, have done so in a pretty inefficient manner with a spinning while loop. Instead, make loadVideos asynchronous:
func loadVideos(url:String, completionHandler: (Array<JSON>?) -> ()) {
var request = Get_Data()
request.remoteUrl = url
request.getData {data, error in
println("los datos")
//println(data)
if (data != nil){
// Fix possible error if no "results" key
if let results = data["results"].array {
completionHandler(results)
}
println("Data reloaded")
} else {
println("api.getData failed")
completionHandler(nil)
}
}
}
And then userValid should use the completion block parameter (employing a completionBlock pattern of its own):
func userValid(username :String, password : String, completionHandler: (Bool) -> ()) {
userbase64 = encode_to_base64(username)
passbase64 = encode_to_base64(password)
var api = channelsFunction()
api.loadVideos("https://api.cxntv.com/api/v1/videos/?type=canales&page_size=100&ordering=-id") { results in
if results != nil {
api.save_LiveChannels(results!)
saver_user(userbase64, passbase64: passbase64, username: username, password: password)
errormessage = ""
completionHandler(true)
}else{
completionHandler(false)
}
}
}
And then login would call this asynchronously, too:
#IBAction func login(sender: AnyObject) {
activity.hidden = false
activity.startAnimating()
if username.isEmpty || password.isEmpty {
showAlert("Asegurese de ingresar un usuario y contraseƱa!")
} else {
userValid() { success in
if !user.user_valid(self.username,password: self.password){
self.showAlert("Usuario Invalido")
}else{
}
dispatch_async(dispatch_get_main_queue(), {
activity.hidden = true
activity.stopAnimating()
}
}
}
}
I'm sure I don't have all of your functions and variables right, but hopefully you can see the pattern: Don't use synchronous methods and use completionHandler parameters with your asynchronous methods.
--
If you want to see my original answer, that I posted before you shared the additional code, see the revision history of this answer. But the above outlines the basic approach.
It happens because you show and hide it in the same method, not allowing the application to update the screen.
Try dispatching
// CODE***
activity.hidden = true
activity.stopAnimating()
asynchronously.
Your shown code will never work as intended. Since you are in an IBAction it is safe to assume that when this function is called you generally are operating on the main thread.
I assume that the // CODE*** section takes a long time (otherwise there would be no need for an activity indicator).
Therefore you either currently do
actually perform the time-expensive operation on the main-thread and are therefore locking it -> very bad. Since you are on the main / UI-loop no UI updates happen, therefore no acitivity indicator is shown until the login is completely which is the exact time where you hide the indicator again.
or perform the operating asynchronously in the background (recommended way). If you do this however you have to move the stopAnimating to that background-task as well.
What you should do is the following:
#IBAction func login(sender: AnyObject) {
activity.hidden = false
activity.startAnimating()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
// CODE***
dispatch_async(dispatch_get_main_queue(), {
activity.hidden = true
activity.stopAnimating()
})
})
}
Important to note is that the stopping has to be done on the main thread again.
Your code contains connection to the server so you the code between startAnimating and stopAnimating will be executed on a different thread, and that's why hide function will be executed before you notice.
What you have to do is to show the activityIndicator and hide it inside the connection code after you receive the response from the server. Also, you should write the code to hide the activityIndicator inside a dispatch like this:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
activity.hidden = true
activity.stopAnimating()
})
the answer can be more helpful if you post the rest of the code.
Edit:
You should remove the hide code from the login function and hide the activityIndicator in load_videos like this:
func load_videos(url :String)->Array<JSON>{
...
request.getData({data, error -> Void in
println("los datos")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
activity.hidden = true
activity.stopAnimating()
})
...
})
return resultados
}
you should learn to use dispatch_async
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)){
//Code take time here
//<#Code#>
dispatch_async(dispatch_get_main_queue(){
//update UI
//<#Code#>
}
}
and remember that UPDATE UI in the main queue

Resources