How should i handle Alamofire response in shouldPerformSegueWithIdentifier? - swift

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
}

Related

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
}
}

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

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
}

Reactive Cocoa Validation

How can I validate a form after a RAC Command or a button click? I have this somewhere in my View Model
self.executeLoginRacCommand = RACCommand(enabled: activateButtonSignal, signalBlock: { (any : AnyObject!) -> RACSignal! in
return self.executeLoginAPI()
})
//My question is here, how can I display an alert to my view controller saying may email is invalid.
//MARK: - Execute Login API
private func executeLoginAPI() -> RACSignal {
if self.isEmailValid(self.email) {
return self.signInServices.signInAPIService.signInAPISignal(self.email, password: self.password)
} else {
self.errorMessage = "Please insert a valid email"
return RACSignal.empty()
}
}
//MARK: - Is Email Valid
func isEmailValid(email: String) -> Bool {
let emailRegEx = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+#[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
return emailTest.evaluateWithObject(email)
}
I access it in my View Controller like this:
self.signInButton.rac_command = self.signInViewModel.executeLoginRacCommand
Can you please suggest a good way of validating an invalid email and display alert after a button click? Thanks in advance!
In your else branch, you are returning an empty RACSignal.
Instead, return the a RACSignal that sends the error (RACSignal.error(<the error>)).
In your ViewController, you can subscribe to the errors property of self.executeLoginRacCommand and display the error to the user.

How to use an NSAlert with storyboards

I'm teaching myself Swift (currently using Xcode 7.3) and I'm working with storyboards for the first time. I'm writing an OS X-based app and I want to display an alert when the user attempts to load data when data already exists. I've read the following thread, Add completion handler to presentViewControllerAsSheet but I'm having trouble wrapping my head around closures/completion handlers. I understand them "in theory" but not yet well enough to write one.
In the thread above, a Struct is being returned. I just need to return an Int or Bool to indicate whether the user wants to overwrite the data or not.
You don't need to create a second view controller. Just configure and display an NSAlert object:
#IBAction func loadData(sender : AnyObject) {
let dataAlreadyExists = true // assume this is always true
if dataAlreadyExists {
let alert = NSAlert()
alert.messageText = "Do you want to reload data?"
alert.addButtonWithTitle("Reload")
alert.addButtonWithTitle("Do not reload")
alert.beginSheetModalForWindow(self.view.window!) { response in
if response == NSAlertFirstButtonReturn {
// reload data
}
}
}
}

swift + OS X sandboxing: treat 'NSVBOpenPanel' as a 'NSOpenPanel' :: because I need to get the sender in the delegate method

Im using swift and I show a NSOpenPanel. In the delegate I need to look at the sender's prompt to distinguish which action to take:
e.g.
func show() {
...
panel.delegate = self
panel.prompt = "xy"
panel.run ....
}
func show2() {
...
panel.delegate = self
panel.prompt = "abc"
panel.run ....
}
//delegate
func panel(sender: AnyObject, shouldEnableURL url: NSURL) -> Bool {
let panelPrompt = (sender as! NSOpenPanel).prompt ...
}
without sandbox = WORKS fine
the sender of the delegate is a NSOpenPanel indeed
with sandbox = Cast fails, crash
the sender of the delegate is NOT a NSOpenPanel but a NSVBOpenPanel. Apple's private class that remotely speaks to the outside world and allows the user to choose files NORMALLY not in your sandbox. (for details I refer to apple's sandboxing guide)
So the question is how do I do use this in swift without crashing?
Is there a nice way or is it just a bug/ugly idk behavior
Do I have to revert to use performSelector?
===
Addition: extensions to NSOpenPanel don't work either!
Instead of casting the sender to NSOpenPanel (which fails because the
sender is an instance of the private NSVBOpenPanel class),
or some performSelector magic, you can use the fact that
arbitrary methods and properties can be accessed on AnyObject
without casting, and the call behaves like an implicitly
unwrapped optional:
func panel(sender: AnyObject, shouldEnableURL url: NSURL) -> Bool {
let panelPrompt = sender.prompt ?? ""
// ...
return true
}
This gives the prompt for any sender object which has a prompt
property, and the empty string as a fallback. In my test it worked well
in a sandboxed environment.
See The strange behaviour of Swift's AnyObject for more details, examples, and references to the
documentation.
This is how it would work with performSelector. It is quite ugly though:
let panelPromptUnmanaged = (sender as! NSObject).performSelector(NSSelectorFromString("prompt"))
let panelPrompt = panelPromptUnmanaged != nil ? panelPromptUnmanaged.takeRetainedValue() as! String : ""