Cannot pop back in Navigation controller from Alert completion: - ios

Swift 4, XCode 9.4
I cannot pop back to the root of the stack in the completion of (or even near) a UIAlertController.
The requirement is to show the user a confirmation and then directly transition back to the 'home' page. When I try I always get something like :
popToViewController:transition: called on <UINavigationController 0x7fc4fe85f000> while an existing transition or presentation is occurring; the navigation stack will not be updated.
I understand the concept I suppose. The UIAlertController is in control, or the nav controller is not sufficiently in control to pop back. Ok makes some sense since I am in the completion. But I don't see how to trigger my transition without being in there.
#IBAction func doneAction(_ sender: Any) {
if verifyDoneProperly() {
let alert = UIAlertController(title: "Complete", message: "Good Job", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: {
if let nav = self.navigationController {
nav.popToViewController(select, animated: true)
}
})
}
}
FWIW, It doesn't work when I specify the view controller either.
I have read carefully through this answer but nothing there is really going to help me (AFAIK).
The only thing I can really think to do is to send a message to the root controller and have it display the alert after having transitioned back (Perhaps checking for a flag in viewWillAppear)
There must surely be a correct way to do this. Can anyone suggest something?
Important Note: I cannot use segues here although I do have a storyboard. The transition to the 'worker' view controller is done programmatically and chosen based on the current state.

I'm not sure it's the best idea to pop a view controller while it's still presenting something else. And the completion of present is not executed after the view controller is dismissed, only after it has finished presentation (i.e., become visible on the screen).
You can pop your view controller in the handler of your OK action — which will be called after your view controller is no longer presenting the alert.
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
self.navigationController?.popToViewController(select, animated: true)
}))

Make sure to perform the pop animation in the next run loop by embedding it in a dispatch block:
DispatchQueue.main.async {
self.navigationController?.popToViewController(select, animated: true)
}

Related

How to disable touch outside screen dismiss view using MDCAlertController Material Design - swift

I am new iOS programming and now am fascinated in using MaterialComponents which provide by google. Now i facing one problem in component named Dialog.
When the view has been pop up on screen when i touch outside that pop up view and then that view has been dismiss. I don't want that to happen in my app.
I don't want user to click outside popup view to dismiss that popup view. What i want i just want user to click on action button that i provide for user's choice then the view should be dismiss when click on that action button only.
Really glade that you help.
MDCAlertController is inherited from UIViewController.
So, in order to restrict user to click outside MDCAlertController you have to access its property named view and then superview?.subviews[0].isUserInteractionEnabled = false
I have completed one example using MDCAlertController
let alert = MDCAlertController(title: title, message: message)
alert.buttonTitleColor = UIColor(red:0.03, green:0.62, blue:0.09, alpha:1.0)
//MDCAlertControllerThemer.applyScheme(alertScheme, to: alert)
let okayAction = MDCAlertAction(title: "Okay") { (action) in
print("User click okay")
}
let cancelAction = MDCAlertAction(title: "Cancel", handler: nil)
alert.addAction(okayAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: {
// When the Dialog view has pop up on screen then just put this line of code when Dialog view has completed pop up.
alert.view.superview?.subviews[0].isUserInteractionEnabled = false
})
use this.
let alert = MDCAlertController(title: title, message: message)
alert.mdc_dialogPresentationController.dismissOnBackgroundTap = false
https://material.io/develop/ios/components/dialogs/api-docs/Categories/UIViewController_28MaterialDialogs_29.html
https://material.io/develop/ios/components/dialogs/api-docs/Classes/MDCDialogPresentationController.html#/c:objc(cs)MDCDialogPresentationController(py)dismissOnBackgroundTap

Segue performing on separate button click

I'm following a course online, the app I'm building is basically a cut down version of Instagram as you can see on the left hand side I have a view for signing in, on the right hand side I have a view for signing up.
Pretty straight forward right, now I have created a Seque from the signin page from the button Register which when pressed takes you to the registration view (one on the right), this works as expected however if I'm on the signin page and click sign in and it errors for whatever reason I display the error message but straight after is performs the Seque to the registration screen even though I never pressed register
This is my storyboard:
This is my code behind the Sign In button located on the left view
#IBAction func btnSignIn(sender: AnyObject) {
if txtUsername.text == "" || txtPassword.text == "" {
displayAlert("Error", message: "Username and Password required!")
}
}
I have no other code inside this controller which would cause the Seque to initialise.
If someone can shed some light into how I can stop this Seque from happening on Sign In click and only happen when I press Register I'd appreciate it.
Update
After further investigation this seems to be an issue with my alert box I'm displaying. If I comment out the alert box and press signin and let it error yet not display anything then click register which takes me to the view on the right, click sign in and get taken back to view on the left and then press Sign In display the alert box and when it closes it seems like it thinks the view controller that's being displayed in the one previous to the current one on the page, which is odd. This is my alert function:
func displayAlert(title: String, message: String) { // Display alert message to user. Passing in title and message which will be displayed.
if #available(iOS 8.0, *) {
let a = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.Alert)
a.addAction((UIAlertAction(title: "OK", style: .Default, handler: { (action) -> Void in
self.dismissViewControllerAnimated(true, completion: nil)
})))
self.presentViewController(a, animated: true, completion: nil)
} else {
// Fallback on earlier versions
}
}
You don't need to dismiss the alert view controller in the action. When the user selects an action, the alert controller is automatically dismissed.
Since you are calling dismissViewControllerAnimated you are going back to the previous view controller.
Right click on Sign-in button and check Connections Inspector, may be you have copy-pasted these two buttons so the action of both sign-in and register button would be same.

iOS: Which function to override when I need to do something after the view is visible to the user?

So the first thing my app does is get a list of movies from an API. I'm trying to handle if there's a network issue. Currently, in my viewDidLoad method, I call "updateApiInfo", which contains the following code:
if self.movies == nil {
print("stuff went wrong")
let alert = UIAlertController(title: "Network Error", message: "There was a nework error.\nYou must be connected to the internet to use Flicks.", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Exit", style: UIAlertActionStyle.Default, handler: {(UIAlertAction) in
UIControl().sendAction(Selector("suspend"), to: UIApplication.sharedApplication(), forEvent: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
When viewDidLoad calls updateApiInfo, I get this message:
Warning: Attempt to present <UIAlertController: 0x7fad10e3ad80> on <Flicks.MoviesViewController: 0x7fad10e35cc0> whose view is not in the window hierarchy!
When I call updateApiInfo just from the user refreshing, the error pops up as expected.
I'm assuming that viewDidLoad only gets called before the view gets displayed to the user, which I guess is causing this error. Is there a method I can stick this code in for after the view is displayed to the user, so the problem can presumably get fixed?
You need to use viewDidAppear: to make sure the view is already in the window hierarchy.
There's a pretty good explanation of the order in which the methods are called here:
What is the process of a UIViewController birth (which method follows which)?

Unable to choose order of buttons in UIAlertController

I was under the impression that if the normal action is a destructive action and the other is a cancel action in their UIAlertController that the destructive one should be on the left and the cancel should be on the right.
If the normal action is not destructive, then the normal action should be on the right and the cancel should be on the left.
That said, I have the following:
var confirmLeaveAlert = UIAlertController(title: "Leave", message: "Are you sure you want to leave?", preferredStyle: .Alert)
let leaveAction = UIAlertAction(title: "Leave", style: .Destructive, handler: {
(alert: UIAlertAction!) in
//Handle leave
}
)
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
confirmLeaveAlert.addAction(leaveAction)
confirmLeaveAlert.addAction(cancelAction)
self.presentViewController(confirmLeaveAlert, animated: true, completion: nil)
I was under the impression that if I add the leaveAction first, then the cancelAction that the leaveAction would be the button on the left. This was not the case. I tried adding the buttons in the opposite order as well and it also resulted in the buttons being added in the same order.
Am I wrong? Is there no way to achieve this?
My solution to this was to use the .Default style instead of .Cancel for the cancelAction.
Since iOS9 there is a preferredAction property on UIAlertController. It places action on right side. From docs:
When you specify a preferred action, the alert controller highlights the text of that action to give it emphasis. (If the alert also contains a cancel button, the preferred action receives the highlighting instead of the cancel button.)
The action object you assign to this property must have already been added to the alert controller’s list of actions. Assigning an object to this property before adding it with the addAction: method is a programmer error.

UIAlertController memory leak/issues - Swift

Instruments shows a memory leak from simply opening and closing the alert controller.
#IBAction func delBtnAc(sender: AnyObject) {
let deleteAlert = UIAlertController(title: "Delete Image?", message: "", preferredStyle: .Alert)
let cancelIt = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
deleteAlert.addAction(cancelIt)
presentViewController(deleteAlert, animated: true, completion: nil)
}
I have reduced the alert to only a cancel button for testing.
Edited: Removed deleteAlert.dismissViewController in closure. Fixed retain cycle, but still shows a memory leak. Perhaps a bug.
Your alert action's completion handler has a strong reference to your alert controller.
Your alert action has a strong reference to its completion handler.
Your alert controller has a strong reference to the alert action.
So here we have a classic retain cycle.
The problem is the strong reference from the completion handler to the alert controller itself, which in this case, happens to be completely unnecessary. The alert controller dismisses itself after running the appropriate completion handler.
We can completely eliminate the line.
If we were doing something non-redundant in the completion handler, we would need to create a weak reference to the completion handler so that we could use that in the completion handler.
I found the same problem.
I solved it by setting the alert to null after button action:
deleteAlert = null inside your cancel button action

Resources