Can dataTaskWithRequest be completed before new view controller load? - swift

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):

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

call a specific View Controller when remotepush notification is clicked in swift

I want to show push notification in a specific view controller when tap on notification and also want to send data from notification to view controller. I am using swift for development
As #luzo pointed out, Notifications are the way to send to communicate to view controller(s) that an event has happened. The notification also has a userinfo parameter that accepts a dictionary of data you would like to send together with the notification to the view controller.
In Swift 3, add this to the tap button:
let center = NotificationCenter.default
center.post(name: Notification.Name(rawValue: "nameOfNotification"),
object: nil,
userInfo:["id":"data"])
And in the viewcontroller, register for the id of the notification and add a function reference:
let center = NotificationCenter.default
center.addObserver(forName:NSNotification.Name(rawValue: "nameOfNotification"), object:nil, queue:nil, using:notifDidConnect)
and add the function implementation:
func notifDidConnect(notification:Notification) -> Void {
guard let userInfo = notification.userInfo,
let id = userInfo["id"] as? String else {
print("error occured")
return
}
print("notification received")
}
You should implement router for your view controllers which will be listening to notification send from app delegate and he will decide what to do. This is how I would do it, might be a better solution.

View shown after programmatically triggered segue doesn't update immediately

I created two views and then connect one to the other by ctrl-dragging and creating a segue. Gave this segue the identifier "functionOutput1"
I then created a function and programmatically triggered the segue created above based on certain output in a function.
performSegue(withIdentifier: "functionOuput1", sender: self)
The view segues okay but the contents of the view don't appear until a minute later.
Is there something else I need to be doing here?
EDIT:
I'm performing this segue in a callback function for a URLRequest
let task = session.dataTask(with: request as URLRequest, completionHandler: {
data, response, error -> Void in
if (error != nil) {
// there is an error with the request
print("error: ", error)
} else {
// Request was successful. Check the contents of the response.
print("response: ", response)
let httpResponse = response as! HTTPURLResponse
if httpResponse.statusCode != 200 {
print("problems with server request")
} else {
print("request successful")
//go to next page
performSegue(withIdentifier: "functionOuput1", sender: self)
}
}
})
This could be a threading issue. Because the data is returned asynchronously. You need to run the view updates on the main thread. You can do this with Swift 3.0 with the following code:
DispatchQueue.main.async {
// Your view updates here
}

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
}

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.