UIAlertController removes tabbar on segue - ios

I have an alertController:
let alertController = UIAlertController(title: "Success", message: "Your book has been uploaded", preferredStyle: .alert)
let PostBook = UIAlertAction(title: "OK", style: .cancel, handler: { action in self.performSegue(withIdentifier: "PostBook", sender: nil)})
alertController.addAction(PostBook)
self.present(alertController, animated: true, completion: nil)
print("Posted to Firebase. ")
In the UIAlertAction, I have an action to segue, and every time I segue, it whites out the tab bar. The tab bar is still there, it justs whites it out, which I don't want. The segue is a show segue.
before segue:
after segue:

Embed in a navigationController to your view so that all the segues goes through the navigationController instead.
Second view controller:

Related

How to prevent a UIViewController from being popped out?

I have a childViewController which is pushed from parentViewController. In childViewController I want to block pop action in a particular condition.
I wrote this code in viewWillDisappear: But I guess need to do this somewhere else.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
}
Actually, there is a very simple solution: navigationItem.hidesBackButton = true - this will hide "BACK" button and disable swipe-to-back feature. 🤓
There are two cases here:
User can pop using back button.
User can pop using interactive pop gesture of navigation controller.
I think you should use a custom back button and name it Done and write your logic of showing alert on press of this button.
Using the custom back button would disable the interactive pop gesture by default and you will be spared from playing the dance of enabling/disabling interactivePopGesture on navigation controller.
block pop action till your changes are not saved like this
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
self.navigationController?.popViewController(animated: true)
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
Update - Add this below custom button and its action in child View controller which is being pushed from parent View Controller
so, without satisfying your condition user can not move from child to parent again
For customising action of navigation backButton you need to manually add a back Button using below line , you can Customise barButton being added here in DidLoad
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.backToInitial(sender:)))
It will perform required Action
#objc func backToInitial(sender: AnyObject) {
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
self.navigationController?.popViewController(animated: true)
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
}
and I do not think you can stop default back button action task of navigation Controller as it is designed the way to perform it
But yes you can manage it in ViewWillDisappear as :
override func viewWillDisappear(_ animated: Bool) {
if self.isMovingFromParentViewController || self.isBeingDismissed {
self.navigationController?.popViewController(animated: false) //here task but will not result as Expected output
}
}
-----------------Re-Update ---------------
in swift I used a objective-C class to get output as expected now, childViewController pop action is being controller from a alert using default back button that we get from navigation controller
You can customise you pop action to perform or not until your condition is not satisfied
Github Link - https://github.com/RockinGarg/NavigationBackButton.git
I found a much more elegant solution (in my opinion).
It works no matter how the user triggers the pop (accessibility escape, swipe back gesture, tapping back) since it overrides the built in pop methods that the system uses.
Swift 5
public class DiscardSafeNavigationController:UINavigationController {
/// Should the pop be prevented? Set this to `true` when you have changes which need to be protected
public var hasUnsavedChanges:Bool = false
/// Show a prompt on the top most screen asking the user if they wish to proceed with the pop
/// - Parameter discardCallback: The callback to use if the user opts to discard
private func confirmDiscardChanges(discardCallback:#escaping (()->())) {
let alertController = UIAlertController.init(title: "Discard changes", message: "Are you sure you want to discard any unsaved changes?", preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction.init(title: "Discard", style: UIAlertAction.Style.destructive, handler: { (_) in
discardCallback()
//User elected to discard and so, at this point, they no longer have changes to save
self.hasUnsavedChanges = false
}))
alertController.addAction(UIAlertAction.init(title: "Cancel", style: UIAlertAction.Style.cancel, handler: nil))
self.topViewController?.present(alertController, animated: true, completion: nil)
}
override func popViewController(animated: Bool) -> UIViewController? {
//If there aren't unsaved changes, popping is safe
if !hasUnsavedChanges {
return super.popViewController(animated: animated)
}else {
//Changes have been made. Block the pop and first check with the user before continuing
confirmDiscardChanges {
super.popViewController(animated: animated)
}
return nil
}
}
}
and when you want to enable discard protection from any child view controllers, simply use:
(self.navigationController as? DiscardSafeNavigationController)?.hasUnsavedChanges = true
and then any time the navigation controller is asked to pop it, the navigation controller will ask the user first.

Forcing user to select a row in tableview before navigating

I want to ensure user selects a row from a list of rows (row of schedules) displayed in a tableview controller before navigating to another controller. So in the didSelectRow method, I set a boolean variable scheduleSelected to true. In my viewWillDisappear, I check on scheduleSelected and it it is false then I raise an alert and reload the tableview so I stay on the same tableview instead of navigating. It is not working it navigates to another controller anyways but does raise an alert which is too late.
How can I force the user to select a row before navigating out of the current tableview controller?
May be there is easier way instead of this cumbersome procedure. Please let me know.
override func viewWillDisappear(_ animated: Bool) {
if (scheduleSelected == false ) {
let alertController = UIAlertController(title: "UIAlertController", message: "Select Row", preferredStyle: .alert)
alertController.message = "Choose a Schedule"
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertController, animated: true, completion: nil)
self.tableView.reloadData()
}else {
let viewController = storyboard?.instantiateViewController(withIdentifier: "Profiles" )
UIApplication.shared.keyWindow?.rootViewController?.navigationController
let tabController = UIApplication.shared.keyWindow?.rootViewController as! ViewTabBarController
let navController = tabController.selectedViewController as! UINavigationController
navController.popToViewController(viewController!, animated: true)
}
}
How you are navigating out of the current tableview controller using button action or using segue.
If you are using segue, then you have to handle this code like below:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (scheduleSelected == false ) {
let alertController = UIAlertController(title: "UIAlertController", message: "Select Row", preferredStyle: .alert)
alertController.message = "Choose a Schedule"
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertController, animated: true, completion: nil)
self.tableView.reloadData()
}
}

Error with popover and UIAlertController

I have an app with a popover. As I'm coming out of the popover. I am dismissing the popover via a UIAlertController (user answers Yes). Before dismissing the popover, though, I am calling a function on the delegate. Within that function is another UIAlertController. The second UIAlertController is not displaying because of the following error:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior.
To demo this here, I created a quick project that shows the problem. It is just a view controller with a button that calls the popover and a button on the popover that closes it and calls a delegate function containing another UIAlertController.
This is the code for the view controller that calls the popover:
//Delegate function called from popover
func doSomeStuff() {
let alert = UIAlertController(title: "Some Stuff", message: "Do you want to do some stuff", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "Yes", style: .Default, handler: {action in
print("We did something here.")
}))
alert.addAction(UIAlertAction(title: "No", style: .Cancel, handler: nil))
presentViewController(alert, animated: true, completion: nil)
}
#IBAction func callPopover(sender: UIButton) {
let popoverVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopoverView") as! PopoverController
popoverVC.modalPresentationStyle = UIModalPresentationStyle.Popover
popoverVC.preferredContentSize = CGSizeMake(200, 200)
if let popoverController = popoverVC.popoverPresentationController {
popoverController.backgroundColor = UIColor.lightGrayColor()
popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
popoverController.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds),0,0)
popoverController.sourceView = callPopoverButton
popoverController.delegate = self
popoverVC.delegate = self
self.presentViewController(popoverVC, animated: true, completion: nil)
}
}
//Allows popover to present on devices besides iPad.
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle{
return UIModalPresentationStyle.None
}
The callPopover function is the action for the button on the first screen.
This is the code for the popover screen:
var delegate: ViewController!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func returnToMainView(sender: UIButton) {
let alert = UIAlertController(title: "Dismiss Popover", message: "Do you want to dismiss this popover?", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "Yes", style: .Default, handler: {action in
self.delegate.doSomeStuff()
self.dismissViewControllerAnimated(true, completion: nil)
}))
alert.addAction(UIAlertAction(title: "No", style: .Cancel, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
When tapping the popover button on the first screen, the popover displays correctly:
Tapping the return button displays the alert:
Clicking Yes returns from the alert and should display the second alert, but that's where I get the error.
I think that the second alert is not displaying because the popover has not finished being dismissed, but have no idea how to get around it.
I see that the you are presenting the second popup view controller from the first popup view controller which is being dismissed. That is causing the issue.
Instead why can't you present the second view controller from the navigation controller instead of first popup view controller.
Change this line of presentViewController(alert, animated: true, completion: nil) to self.navigartionController.presentViewController(alert, animated: true, completion: nil) in yourdoSomeStuff() method so that the first popup can dismiss freely and the navigation controller actually presents your second popup. Hope this helps!

How to Show a UIAlertController and Perform an Action when User clicks OK?

I am stuck with a UIAlertController.
I have this alert view:
#IBAction func AddPatientButton(sender: AnyObject) {
let alert = UIAlertController(title:"Registro Paciente", message: "Paciente Registrado", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title:"Ok", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
And it shows me an alert view correctly. But what I want is that when the user press the button Ok, this button performs a segue and send the user to another ViewController.
This sample code does what you want.
#IBAction func AddPatientButton(sender: AnyObject) {
let alert = UIAlertController(title:"Registro Paciente", message: "Paciente Registrado", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title:"Ok", style: UIAlertActionStyle.Default, handler: { alertAction in
// Code to segue/change VC !
self.performSegueWithIdentifier("segue_name", animated: true)
//
}))
Just make sure to create a segue to the view controller you want to go to AND to give that segue an identifier in the inspector (click on the segue and check the inspector). Then copy paste that identifier where "segue_name" is.
Let me show it in images:
1) Add a new VC (unless you already have one you want to go to obviously). Then Ctrl-drag from the yellow icon of the VC you come from to the VC you want to go to:
2) Select Show or any option you are looking for:
3) Select the segue (the arrow that appeared), click on the Attributes Inspector (Top Right), and fill in the identifier:
Then copy paste that identifier where "segue_name" is in the code I gave you.
You need to add a handler to the button. You are almost there. Add a manual segue to the view controller you want to go to. Make sure to add an identifier to that segue.
alert.addAction(UIAlertAction(title:"Ok", style: UIAlertActionStyle.Default, handler: {
self.performSegueWithIdentifier("my_segue_name", animated: true)
}))
let alertController = UIAlertController(title: titleAlert, message: msg, preferredStyle: UIAlertControllerStyle.Alert)
let go = UIAlertAction(title: titleBtn, style: UIAlertActionStyle.Default, handler: { (action: UIAlertAction!) in
//Your Action here
})
alertController.addAction(go)
alertController.addAction(UIAlertAction(title: "CANCEL", style: UIAlertActionStyle.Destructive, handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)

UIAlertController Detached View Controller

Sometimes when I create a UIAlertController and present it in an if-statement, I get the the warning Presenting view controllers on detached view controllers is discouraged. How do I circumvent that warning? Is there something different I can do in calling & presenting the UIAlertController? Should I use something different then putting the UIAlertController in a if-statement?
if(!defaults.boolForKey("hasLaunchedOnce")) {
updateSettingsLabel.hidden = false
var alert = UIAlertController(title: "Instructions", message: "Sample Text", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default) {
(action: UIAlertAction!) -> Void in
}
alert.addAction(okAction)
presentViewController(alert, animated: true,completion: nil)
}
You should present the UIAlertController in viewDidAppear() or somewhere after the view has shown. The view hierarchy is not ready before viewDidAppear(), so presenting another view before that may be problematical.
try doing your presentViewController like this:
self.view.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)

Resources