In swift, how can I wait until a server response is received before I proceed? - swift

I would like to only execute a segue if I get a certain response from the server. In swift, how can I wait until I get a response to continue?

Bottom line, you don't "wait" for the response, but rather simply specify what you want to happen when the response comes in. For example, if you want to perform a segue when some network request is done, you should employ the completion handler pattern.
The issue here is that you're probably accustomed to just hooking your UI control to a segue in Interface Builder. In our case, we don't want to do that, but rather we want to perform the network request, and then have its completion handler invoke the segue programmatically. So, we have to create a segue that can be performed programmatically and then hook your button up to an #IBAction that performs the network request and, if appropriate, performs the segue programmatically. But, note, there should be no segue hooked up to the button directly. We'll do that programmatically.
For example:
Define the segue to be between the two view controllers by control-dragging from the view controller icon in the bar above the first scene to the second scene:
Give that segue a storyboard identifier by selecting the segue and going to the "Attributes Inspector" tab:
Hook up the button (or whatever is going to trigger this segue) to an #IBAction.
Write an #IBAction that performs network request and, upon completion, programmatically invokes that segue:
#IBAction func didTapButton(_ sender: Any) {
let request = URLRequest(...). // prepare request however your app requires
let waitingView = showWaitingView() // present something so that the user knows some network request is in progress
// perform network request
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// regardless of how we exit this, now that request is done, let's
// make sure to remove visual indication that network request was underway
defer {
DispatchQueue.main.async {
waitingView.removeFromSuperview()
}
}
// make sure there wasn't an error; you'll undoubtedly have additional
// criteria to apply here, but this is a start
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
// parse and process the response however is appropriate in your case, e.g., if JSON:
//
// guard let responseObject = try? JSONSerialization.jsonObject(with data) else {
// // handle parsing error here
// return
// }
//
// // do whatever you want with the parsed JSON here
// do something with response
DispatchQueue.main.async {
performSegue(withIdentifier: "SegueToSceneTwo", sender: self)
}
}
task.resume()
}
/// Show some view so user knows network request is underway
///
/// You can do whatever you want here, but I'll blur the view and add `UIActivityIndicatorView`.
private func showWaitingView() -> UIView {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .Dark))
effectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(effectView)
NSLayoutConstraint.activateConstraints([
effectView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor),
effectView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor),
effectView.topAnchor.constraintEqualToAnchor(view.topAnchor),
effectView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
])
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
effectView.addSubview(spinner)
spinner.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints([
spinner.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor),
spinner.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor)
])
spinner.startAnimating()
return effectView
}

Related

Trigger a segue from a function (without UI element)

I'm very new with programming. Currently, I need to trigger a segue directly after a function is being executed.
This is my code:
func onlineSearch() {
let urlToGoogle = "https://www.google.com/search?q=\(resultingText)"
let urlString = urlToGoogle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
url = URL(string: urlString!)
performSegue(withIdentifier: K.goToBrowser, sender: nil)
}
}
When I run this, I get this error:
Warning: Attempt to present <MyApp.SFSafariViewController: 0x10153be70> on <MyApp.CameraViewController: 0x101708460> whose view is not in the window hierarchy!
But if I run all the same, but simply trigger the segue from a button, instead of the way I want, it actually works perfectly.
#IBAction func webViewButtonPressed(_ sender: UIButton) { // TEMP
performSegue(withIdentifier: K.goToBrowser, sender: self)
}
P.S.: Nevermind the grotesque unwrapping, it is just working as a placeholder right now.
Reposting my comment as an answer with a code snippet -
If the caller of onlinesearch() may be on anything other than the main thread, you need to wrap the performSegue call in a DispatchQueue.main.async{} block.
DispatchQueue.main.async {
performSegue(withIdentifier: K.goToBrowser, sender: nil)
}
Generally, methods which affect UI (updating controls, performing segues, etc.) need to be on the main thread. Calls from UIKit are already on the main thread (viewDidLoad, draw, events, notifications, timers etc.), but other task completion handlers (and anything run on explicitly on a different queue) may not be.
try using this to open the safari URL
if let url = URL(string: "https://www.google.com/search?q=\(resultingText)") {
UIApplication.shared.open(url)
}
Here's a function if you want to use it anywhere else.
But you should mark Chris Comas answer as the correct one.
func openURL(url: URL) {
UIApplication.shared.open(url, options: [:])
}
}
Just for the sake of knowledge:
let urlGoogle = URL(string: "https://www.google.com")
openURL(url: urlGoogle)
if you want to open the browser inside the app check Swift documentation: https://developer.apple.com/documentation/uikit/uiwebview

In swift, how do I only perform a segue after a server has given me a certain response?

In swift, how do I only perform a segue after a server has given me a certain response?
I'd like to click a submit button. That will send a request to the server. Then, if the server responds positively, I want a view to be presented modally, otherwise, I want to write a UIAlertView.
I know how to write the UIAlertView and I have the Modal View prepared, I just can't figure out how to write it such that the modal view only presents itself conditionally.
First you need to give a segue identifier on your storyboard. Then you can use:
self.performSegueWithIdentifier("yourSegueIdentifier", sender: nil)
first make a segue between the view controllers and give the segue identifier
eg: self.performSegueWithIdentifier("SegueIdentifier", sender: nil)
and when you get the response of the service request and on the basis of that check the status or the valid http status code.
eg: if status returns true {
self.performSegueWithIdentifier("SegueIdentifier", sender: nil)
}
or if statuscode == 200...299 {
self.performSegueWithIdentifier("SegueIdentifier", sender: nil)
}
Assuming you have your submit button wired up to an IBAction you could do something like this...
#IBAction func submitClicked(sender: UIButton) {
//try to hit server
let serverResponse = server.trySomeFunction()
if serverResponse {
//server response was successful!
self.performSegueWithIdentifier("SegueIdentifier", sender: self)
} else {
//server response failure, present alert
}
}

Correct asynchronous Authentication while keeping a responsive UI

What it's supposed to be
I have a username field. When the username is entered and the sendButton is clicked, the userdata is fetched with a asynchronousRequest as a JSON file.
After the sendButton is clicked, I want to display an ActivityIndicator.
The UI shall still be responsive, while the request is made.
How it is now
I click the sendButton and the UI freezes. Even the ActivityIndicator does NOT get displayed.
The code
LoginVC:
func buttonTouchedUpInside(sender: UIButton) {
toggleActivityIndicatorVisibilityOn(true)
LoginManager.sharedInstance.checkUserForCredentials(username: textFieldLogin.text, password: "")
toggleActivityIndicatorVisibilityOn(false)
}
func loginManagerDidFinishAuthenticationForUser(userData: [String:String]?){
// Delegate Method, which works with the fetched userData.
}
LoginManager
func checkUserForCredentials(#username: String ,password: String) -> Void {
let url = NSURL(string: "\(Config.checkCredentialsUrl)username=\(username)")
let request = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(request, queue: .mainQueue()) { (response, data, error) -> Void in
if error != nil {
//Display error-message
}
var error : NSError?
let json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: &error) as? [String:String]
self.delegate?.loginManagerDidFinishAuthenticationForUser(json)
}
}
In short: I want the request to be made in background, that the Activityindicator is shown and the UI stays responsive. After the asynchronous request successfully fetched the json, the delegate method shall be called
The second line of code in the buttonTouchedUpInside method, which reads LoginManager.sharedInstance.checkUserForCredentials(username: textFieldLogin.text, password: "") is calling an asynchronous function within it, which means that it is not blocking the next line of code... which is the one that (I am guessing) triggers your loading screen to become invisible again.
Basically, your loading screen is showing up, but it is immediately being hidden again. To fix at least the part with your loading screen, put the third line of code in the buttonTouchedUpInside function in the callback method loginManagerDidFinishAuthenticationForUser instead.

Can dataTaskWithRequest be completed before new view controller load?

I am new to swift and I am trying to do post request using dataTaskWithRequest. I have two view controllers, LoginViewController and SecondViewController. In the LoginViewController I have submit button to submit form login and then go to the second view. However in the button action function I called dataTaskWithRequest to get authentication.
How can dataTaskWithRequest complete his task before SecondViewController load?
The key here is that the login view controller's button should not be a segue to the second view controller. Instead, it should simply be an #IBAction which performs the authentication with the dataTaskWithRequest, and only if you determined it was successful in the completionHandler closure, would it programmatically transition to the next view controller.
So, let's pick that apart:
Hook up button in login scene to an #IBAction, which creates a request, initiates it, and in the completion block determines if it was successful and if so, tells the :
#IBAction func didTapLoginButton(sender: UIButton) {
let request = NSMutableURLRequest(URL: NSURL(string: "loginurl")!)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.HTTPBody = ...
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) -> Void in
if data == nil {
// handle error here
println("\(error)")
} else {
// parse response here and determine if successful
var loginSuccessful: Bool = ...
if loginSuccessful {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.performSegueWithIdentifier("SegueToSecond", sender: sender)
}
}
}
}
task.resume()
}
Clearly, there's a lot more you might want to do there (e.g. tell the user if the authorization failed, etc.), but this illustrates the moving parts:
create request;
in completionHandler, see if authorization succeeded; and
anything you do with the UI must be dispatched back to the main queue.
Note, the above assumes you have a segue from the login scene to the second scene. You can do this by control drag from the view controller icon above the login scene to the second scene:
When that's done, select the segue and give it a unique storyboard id (the same one you will use in the #IBAction code, above):

How should i handle Alamofire response in shouldPerformSegueWithIdentifier?

I am trying to learn Swift and what i am trying to do is to create simple login page. Whenever user click on login button, i make a request to my web service in shouldPerformSegueWithIdentifier in order to check if username and password is correct. I dont want to use IBAction (like touchupinside etc). After parsing response that comes from web service, i check if there is any error comes from web service response (like user name is not valid). If there is any error I would like to prevent segue by returning false. But i cant return false inside of Alamofire. So how should i handle it? Here is my code :
override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool {
if identifier == "segueLoginToMainPage" {
//...
//Checking if text fields are filled or not
else {
let callURL : String = CONFIG.BASE_URL + "user/login"
let parameters = [HTTPParamNames.FIELD_USER_NAME : self.txtUserName.text,
HTTPParamNames.FIELD_PASSWORD : self.txtPassword.text]
Alamofire.request(.POST, callURL, parameters: parameters)
.responseJSON { (_,_,JSON,_) in
var returnedData = JSON as NSDictionary
if returnedData["status"] as String == "error" {
//thats what i want to do
return false
}
}
}
}
return true
}
But i cant do return false because it is async. I have found some questions like this in stackoverflow but answers are not what exactly i am looking for. Thanks!
You simply should not try to use asynchronous method within shouldPerformSegueWithIdentifier. Instead you should:
Remove segue from login button, itself.
Add segue between the view controllers, themselves. Specifically, control-drag from the originating scene's view controller icon (in Xcode 6+, this icon is in the bar above the scene, in earlier versions of Xcode, this icon is in the bar below the scene) to the destination scene.
Give this new segue a "storyboard identifier".
Hook up login button to an IBAction function.
In that IBAction function, perform your asynchronous network request, and if the login was successful, then programmatically performSegueWithIdentifier, supplying the storyboard identifier that you specified in step 3.
I think that not using IBAction is a good solution in this case but anyway there is a workaround how to achieve your goal. For this workaround you need to declare a variable typed boolean to check wether user is ready to login. Your shouldPerformSegueWithIdentifier method should always return value of that variable but after you make a request. Finally after your asynchronous request finishes call the prepareForSegue method
var readyForLogin = false
override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool {
if identifier == "segueLoginToMainPage" {
//...
//Checking if text fields are filled or not
else {
if(!readyForLogin){
let callURL : String = CONFIG.BASE_URL + "user/login"
let parameters = [HTTPParamNames.FIELD_USER_NAME : self.txtUserName.text,
HTTPParamNames.FIELD_PASSWORD : self.txtPassword.text]
Alamofire.request(.POST, callURL, parameters: parameters)
.responseJSON { (_,_,JSON,_) in
var returnedData = JSON as NSDictionary
if returnedData["status"] as String != "error" {
//thats what i want to do
self.readyForLogin = true
self.performSegueWithIdentifier("segueLoginToMainPage", sender: self)
}
}
}}
}
return readyForLogin
}