Attempt to present ViewController which is already presenting (null) - swift

I am creating a user and logging in the user after using the Facebook SDK. All is as it should be with correct inserting the user into the database. I then want to present an alert and upon dismissal of the alert, I want to load a different storyboard (essentially the main menu).
Now my understanding is the error is an issue with threading. BUT.. I am passing the loading of the storyboard etc in as a block to the alert. So shouldn't it then get called only after the user dismisses the alert? I also tried making it run on the main thread (which was mentioned here) but I am still getting the error.
What am I missing here?
The originating call:
if DBUser.insertDictionary(user.getModelAsStringDictionary()) {
if DBUser.loginUserWithFacebookId(facebookId!){
self.accountCreatedAlert()
}else{
//log error, user could not be logged in.
}
}else{
// log error user account could not be inserted into the database
}
}
The async bundling of the alert text and the switchToMainStoryboard() method:
private func accountCreatedAlert(fn:()){
let asyncMoveToMain = {dispatch_async(dispatch_get_main_queue()) {
self.switchToMainStoryboard()
}}
self.showAlert("You will now be logged in", title: "Account created", fn: asyncMoveToMain)
}
The Alert:
func showAlert(text : NSString, title : NSString, fn:()->Void){
var alert = UIAlertController(title: title, message: text, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
UIApplication.sharedApplication().delegate?.window!?.rootViewController?.presentViewController(alert, animated: true, completion: fn)
}
And where the storyboard gets loaded:
func switchToMainStoryboard() ->Void{
let main = UIStoryboard(name: "Main", bundle: nil)
var viewController = main.instantiateInitialViewController() as UIViewController
viewController.modalTransitionStyle = UIModalTransitionStyle.CoverVertical
self.presentViewController(viewController, animated: true, completion: nil)
}

So I tried a lot of messing about with the dispatch_async and dispatch_sync methods and each time, no matter what I did I was getting a lock condition that froze the UI.
Upon further inspection of the documentation (and my code), I provided the block instead to the AlertControllerAction instead of the presentViewController. Thanks to this post for providing the syntax. The resulting changes are as follows:
// MARK: Alert Related Methods
func showAlert(text : NSString, title : NSString, fn:()->Void){
var alert = UIAlertController(title: title, message: text, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: {(alert: UIAlertAction!) in fn()}))
UIApplication.sharedApplication().delegate?.window!?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
And then went about calling it like so:
private func showAccountCreatedAlertThenLoadMainStoryboard(){
self.showAlert("You will now be logged in", title: "Account created", fn: {self.switchToMainStoryboard()})
}
The required sequence is now happening without issue. Hope this helps someone else.

Alternative way is that you can ask GCD to delay with your next presentation, like so:
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
// next present
}

Related

Can't move from a VC to another one, can't figure out why

I have a register screen in my app, after the user writes done a phone number and agrees to the terms, and clicking "register" the app should move the user to the next vc. The problem is, although everything looks to be ok, the app won't move to the next screen.
This is my code:
func handleRegister() {
if UserDefaults.standard.object(forKey: USER_AGREED) == nil {
let alert = UIAlertController(title: "Terms and Conditions", message: "Please agree to the terms and conditions to continue", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (alert) in
self.showTermsAndConditions()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .destructive, handler: nil))
self.present(alert, animated: true)
}else{
guard let userPhoneNumber = self.mView.textField.text else { return }
UserDefaults.standard.set(userPhoneNumber, forKey: USER_PHONE_NUMBER)
let vc = Tabbar()
self.navigationController?.pushViewController(vc, animated: true)
}
}
I tried to put a breakpoint, and it does gets to the else block when it should, and recognizes the Tabbar, but just won't move there.
Any suggestions?
There are two possible sources of trouble here.
You are saying self.navigationController?. The question mark means: "If I have no navigationController, do nothing." If you have no navigationController, you do nothing. Perhaps what you meant to say is self.present(vc, animated:true).
You are saying let vc = Tabbar(). That is not an existing view controller in the interface or storyboard. So when you move there, it might be empty (that depends on how Tabbar view controller is constructed).

UIAlertController is complete and presented but does not appear during viewDidLoad

I have this alert controller setup to appear as soon as the view controller is loaded. However, it does not appear.
I believe I have all the facets covered - title, message, alert style, action button and present... but still does not appear.
Unsure what I'm missing.
let array = quoteBank()
print(array.sarcasticQuotes[0].quote)
let title = "Message"
let message = array.sarcasticQuotes[0].quote
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(.init(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
Attempting to show an alert in viewDidLoad is too soon. The view controller isn't displayed yet. Use viewDidAppear.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Use this if statement to only show the alert once
if self.isBeingPresented || self.isMovingToParentViewController {
// show your alert here
}
}
You need to perform that on the main thread:
DispatchQueue.main.async {
present(alert, animated: true, completion: nil)
}
The reason behind that is that the view controller's hierarchy will be set after viewDidLoad is finished. So by doing this, you're scheduling the presentation of the alert on the main thread to the time the main thread is finished from executing viewDidLoad.

ViewController segue and login

I'm new to Swift (and programming in general). I'm having some difficulties in Xcode with the login feature. What I want is that the user should be logged in if no errors was returned and the user should be sent to another controller. I've read some of the documentation from Apple and the performSegueWithIdentifier (and of course some questions asked here), but it still does not work when I use a segue with push or modal that are given an identifier. Either the app crashes, or the user is sent to my UITabBarController even if the credentials was incorrect.
This is my LogInViewController.swift:
#IBAction func loginAction(sender: AnyObject)
{
if self.emailField.text == "" || self.passwordField.text == ""
{
let alertController = UIAlertController(title: "Oops!", message: "Please enter an email and password.", preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Cancel, handler: nil)
alertController.addAction(defaultAction)
self.presentViewController(alertController, animated: true, completion: nil)
}
else
{
FIRAuth.auth()?.signInWithEmail(self.emailField.text!, password: self.passwordField.text!) { (user, error) in
if error == nil
{
self.emailField.text = ""
self.passwordField.text = ""
}
else
{
let alertController = UIAlertController(title: "Oops!", message: error?.localizedDescription, preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Cancel, handler: nil)
alertController.addAction(defaultAction)
self.presentViewController(alertController, animated: true, completion: nil)
}
// User Logged in
self.performSegueWithIdentifier("LoggedIn", sender: self)
}
}
}
The error I get in the console:
2016-09-04 14:55:30.019 DrinkApp[37777:1006336] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Could not find a navigation controller for segue 'LoggedIn'. Push segues can only be used when the source controller is managed by an instance of UINavigationController.'
There are two main ways to send a user to another View Controller. The first is performSegueWithIdentifier. Here's how you would go about using that:
First of all, control-drag from the yellow circle of the original VC to the target VC in the interface builder.
Next, click on the 'segue arrow' that appears between the two View Controllers.
In the identifier field, type in mySegue.
Next, go back to your code. When you reach the sign in section, add the following code:
self.performSegueWithIdentifier("mySegue", sender: nil)
The second method is a bit more advanced. Here's how that would work:
First, click on the yellow circle of the destination VC. Click on the newspaper, then call the storyboard ID myVC.
Make sure to click on Storyboard ID
Go back to the same part of the code and copy and paste this code:
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let initViewController: UIViewController = storyboard.instantiateViewControllerWithIdentifier("myVC") as UIViewController
self.presentViewController(initViewController, animated: true, completion: nil)
Comment down below if you have any questions. Good luck!
Here is a Firebase login example:
func login() {
FIRAuth.auth()?.signInWithEmail(textFieldLoginEmail.text!, password: textFieldLoginPassword.text!) { (user, error) in
if error != nil {
let alert = UIAlertController(title: "User Authentication error",
message: error?.localizedDescription,
preferredStyle: .Alert)
let OKAction = UIAlertAction(title: "OK",
style: .Default) { (action: UIAlertAction!) -> Void in
}
let resetPasswordAction = UIAlertAction(title: "OK",
style: .Default) { (action: UIAlertAction!) -> Void in
}
alert.addAction(OKAction)
guard let errorCode = FIRAuthErrorCode(rawValue: (error?.code)!) else { return }
switch errorCode {
case .ErrorCodeWrongPassword:
alert.addAction(resetPasswordAction)
case .ErrorCodeNetworkError:
print("Network error")
default:
print("Other error\n")
break
}
self.presentViewController(alert, animated: true, completion: nil)
} else { // User Logged in
self.performSegueWithIdentifier("MySegue", sender: self)
}
}
}
I only handled two errors for this example.
If you use push segue, you need to have navigation controller, and your root segue should lead to the login view controller, then another segue to the next view controller. This last segue must have an identifier (in this case it's MySegue)
Edit:
Since it seems that your problem is not with Firebase, but with setting up the storyboard. Here is an example:
Set up one UINavigationController two UIViewControllers
Control Drag between the UINavigationController to the middle UIViewControllers and choose root view controller from the popup menu
Control Drag between the UIViewController to the last UIViewControllers and choose show
select the segue added in the previous step and change its identifier (in the attributes inspector) to MySegue.
5.Choose the Middle UIViewControllers and change the class name (in the identity inspector) to your login view controller class.
Here is the full setup:
control drag menu (step 2):
Segue attributes inspector (step 4):
I found this answer mentioned here to be super elegant.
https://fluffy.es/how-to-transition-from-login-screen-to-tab-bar-controller/
it works with tab bars, and nav bars. you create a function in the scene delegate, that switches the root view controller. you access the scene delgates functions and windows with (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?

How to show one UIAlertController after the other in Swift

I want to show user a message once.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let defaultsDatafirstTrue = NSUserDefaults.standardUserDefaults()
if let _ = defaultsDatafirstTrue.stringForKey("firstTrue") {
} else {
let alertController = UIAlertController(title: "You can add road markers just do long press on the Map", message: "", preferredStyle: .Alert)
let OKAction = UIAlertAction(title: "OK", style: .Default) { (action:UIAlertAction) in
}
alertController.addAction(OKAction)
self.presentViewController(alertController, animated: true, completion:nil)
defaultsDatafirstTrue.setObject("true", forKey: "firstTrue")
}
}
But I have error Warning: Attempt to present <UIAlertController: 0x7c2a8000> on <****: 0x7d193800> whose view is not in the window hierarchy! Because at the first run the iOS app displays a warning to the user that will use the location determination.
How can I see my message after the system message?
Run the code from viewDidAppear: not from viewWillAppear:.
The problem with viewWillAppear: is that it is called before the view is actually visible, so you cannot present another view from it yet. Also, viewWillAppear: can be actually called multiple times and then cancelled (e.g. for interactive transitions).

Swift code : Is there a way to make the alert view disappear automatically

Is there a way to make the alert view disappear automatically.. after some seconds, without user action. Currently I have my code as follow, and it require user to press ok to disappear the alert dialog. I would like to show the alert and not have user intervention, and just have the alert disappear in few seconds. Thanks for any comments.
My code as below:
func showAlertController (message: String) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .Alert)
alertController.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
presentViewController(alertController, animated: true, completion: nil)
}
You can delay anything with dispatch_after. For example, this would dismiss the alert view after 3 seconds.
let delayTime = dispatch_time(DISPATCH_TIME_NOW,
Int64(3 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
presentedViewController.dismissViewControllerAnimated(true, completion: nil);
}
You can also use #matt's awesome delay function.
Try the dismissViewControllerAnimated:completion: method