Present a custom UIAlert from a UINavigationController stack view - swift

I am having trouble presenting a CustomAlert, subclass of UIViewController, from a UINavigationController stack view. To be more descriptive, I have a UITabBarController that wraps a UINavigationController. navController, has a rootView and a another pushed to the stack view, leafView of type UITableViewController. From leafView, I would like to present an alertView of type UIViewController which is transparent for the most part and with a opaque view inside. The problem is that when presenting, the background of this alertView is no longer transparent, but black, and upon dismissal it does not return to the leaf controller, but to the rootView. I think I am not presenting my alert from the correct view. How should I fix this?
searchController.present(friendAlert, animated: false, completion: nil)

For alerts, it is much easier to present them from the "top view controller" instead of searching for the responsible controller in your hierarchy. A genius solution to find the controller is shown in this answer:
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
You can then show your alert from anywhere and after dismissing your alert you will get right back to the current view controller:
DispatchQueue.main.async {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in }))
UIApplication.topViewController()?.present(alert, animated: true, completion: nil)
}

Related

How to dismiss a navigation controller?

I'm presenting a navigation controller (detailview) with showDetailViewController(). I want to dismiss it when a button is pressed. How can I dismiss this viewcontroller?
Code I've attempted:
//detailviewcontroller
#objc
func cancel(_ sender: Any) {
print("cancelPressed")
//self.navigationController?.popViewController(animated: true)
//self.navigationController?.dismiss(animated: true, completion: nil)
//self.dismiss(animated: true, completion: nil)
//splitViewController?.dismiss(animated: true, completion: nil)
//splitViewController?.navigationController?.popToRootViewController(animated: true)
//splitViewController?.navigationController?.popViewController(animated: true)
//splitViewController?.navigationController?.dismiss(animated: true, completion: nil)
//navigationController?.popViewController(animated: true)
//navigationController?.dismiss(animated: true, completion: nil)
//self.navigationController?.popToRootViewController(animated: true)
//self.navigationController?.viewControllers.remove(at: 1)
//self.navigationController?.viewControllers.remove(at: 0) - this one removes to blank view
//self.presentingViewController?.dismiss(animated: true, completion: nil)
}
I've tried multiple stackoverflow solutions:
Dismissing a navigation view controller
How to dismiss a view controller in a navigation controller, without dismissing the whole stack
ios swift - dismiss root view controller of navigation controller
Can't dismiss View Controller that's embedded in a Navigation Controller
How to dismiss a navigation controller presented from another navigation controller in iOS 10 and below?
Can't dismiss navigation controller in Swift
Can't dismiss navigation controller in Swift
Dismissing View Controller Doesn't Work While Using Navigation Controller
Dismiss current navigation controller when clicked tab bar
How to dismiss a certain view controller
How I'm presenting the detailviewcontroller:
//masterviewcontroller
// delegation for passing data between controllers
weak var passDelegate: PlaceSelectionDelegate?
func insertNewObject(_ sender: Any) {
if let detailViewController = passDelegate as? DetailViewController {
if let detailNavigationController = detailViewController.navigationController {
detailViewController.delegate = self
splitViewController?.showDetailViewController(detailNavigationController, sender: nil)
}
}
}
Expected results:
dismiss detail viewController on button press.
Actual results:
No dismiss.
Ok, I've been looking for a way of dealing with this showDetailViewController for a while now and although I'm not sure this answers your question, this is how I did it:
I have two controllers, let's call it FirstController and SecondController. They both inherit from UIViewController. When I click a certain navbar button in the FirstController, I use showDetailViewController to show SecondController as a sheet moving upwards, like the "calendars" button in the Calendar app.
In FirstController I implemented 2 functions: one to show the SecondController and one to dismiss it. I call the dismiss action from within the SecondController. It looks like this:
// FirstController
#objc func showSecondView() {
let vc = SecondController()
vc.delegate = self
let nav = UINavigationController(rootViewController:vc)
self.showDetailViewController(nav, sender: self)
}
func closeSecondView() {
self.dismiss(animated: true)
}
As you can see I wrapped my SecondController in a UINavigationController, so it shows the navbar with title and all. This is how the code looks like:
// SecondController
weak var delegate: FirstController!
override func viewDidLoad() {
super.viewDidLoad()
title = "Second"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(closeMe))
}
#objc func closeMe() {
delegate.closeSecondView()
}
Notice how I made a property for the FirstController and I use it to dismiss the SecondController. When I presented SecondController I assigned self as delegate, which means I can now call the closeSecondView method from within it.
I am not sure whether it works out of the box with split-view. The documentation seems to suggest that whenever you use it, if the screen is big enough, it behaves differently because UISplitViewController overrides it. It also says there that all view controllers have this method.
Take a look: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621432-showdetailviewcontroller
Give it a go, I hope this helps.
This should work if you simply want to dismiss the navigation controller
navigationController?.dismiss(animated: true, completion: nil)

perform segue after view controller is dimissed

I am using UINavigationController. I want to show an intermediate screen eg. White and then from there I want to dimiss and segue to green.
The reason I don't create a segue from white to green is because in the case the user goes back they should go back to blue because blue is my main screen.
Here's the code:
class BlueViewController: UIViewController {
#IBAction func tapBlue(_ sender: Any) {
self.performSegue(withIdentifier: "whiteSegue", sender: self)
}
}
class WhiteViewController: UIViewController {
#IBAction func tapGreen(_ sender: Any) {
navigationController?.popViewController(animated: true)
weak var pvc = self.presentingViewController
dismiss(animated: true){
pvc?.performSegue(withIdentifier: "greenSegue", sender: self)
}
}
}
Here's the codebase
https://github.com/omenking/DismissAndSegue
No error occurs but when white is dismissed it doesn't go to green.
I know this has been asked before on StackOverflow but the other examples did not work or were out of date with latest iOS.
The main issue is that since you are using a navigation view controller and pushing view controllers on and off the stack, the self.presentingViewController variable will be nil. That is used for modal presentations, not navigation view controllers.
Try this:
class WhiteViewController: UIViewController {
#IBAction func tapGreen(_ sender: Any) {
// Get navigation stack, remove last item (White VC)
var viewControllers = navigationController?.viewControllers
viewControllers.removeLast()
// Instantiate new Green VC from storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil) //Change bundle name
let greenViewController = storyboard.instantiateViewController(withIdentifier: "GreenViewController") //Change storyboard ID
viewControllers.append(greenViewController)
// Perform the transition to Green VC with animation
navigationController?.setViewControllers(viewControllers, animated: true)
}
}
A slightly different solution is to link your view controllers blue->white->green, and then in the green view controller, just remove the white view controller from the navigation stack.
Your green view controller then becomes as simple as this.
class GreenViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let count = self.navigationController?.viewControllers.count {
self.navigationController?.viewControllers.remove(at: count - 2)
}
}
}
No special handling needed in the other view controllers.
I do this, in white controller
performSegue(withIdentifier: "showSchedule", sender: date)
if let count = self.navigationController?.viewControllers.count {
self.navigationController?.viewControllers.remove(at: count - 2)
}

Dismiss viewcontroller depends on presentation style (modal or push)

I did FoodTracker programmatically and here is the code that doesn't work for me.
func cancel(){
let isPresentingInAddMealMode = presentingViewController is UINavigationController
if isPresentingInAddMealMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController {
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The MealViewController is not inside a navigation controller.")
}
}
Here is how it looks in the storyboard:
Actually it works modally when the plus button is pressed but it doesn't work with popviewcontroller. I pushed it in the navigation stack with didSelectRow in tableViewController.

Present UIAlertView from current modal's delegate

The issue in question involves an AddItemView which is presented modally by its delegate and contains a tableView. When the user selects an item from the tableView, it triggers an action on the delegate. Depending on the response from the server, the delegate may present a either another modal view or a UIAlertView on top of the current modal.
Important Note: This UIAlertView needs to be presented while the modal is still on screen. The modally presented view containing the tableView cannot be dismissed after user selection because the user needs to be able to select multiple items from the table and, one by one, send them back to the delegate for processing.
Currently, the UIAlerView is not being displayed and I suspect it is because the already-presented modal is preventing that. Is there a workaround to present the UIAlertView from the delegate when the delegate is sitting underneath a modal and without dismissing that modal?
The UIAlertView is currently displayed like so by the delegate, while the delegate is sitting under a modal:
var alert = UIAlertController(title: "Error", message: "Error message from server", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "actionOne", style: .Default, handler: { action in
// perform some action
}))
alert.addAction(UIAlertAction(title: "actionTwo", style: .Destructive, handler: { action in
// perform some action
}))
self.presentViewController(alert, animated: true, completion: nil)
Here is the error that is returned when the UIAlertView is presented by the delegate:
Warning: Attempt to present <UIAlertController: 0x156da6300> on <productionLINK_Scanner.ContainerContents: 0x156e65b20> whose view is not in the window hierarchy!
If possible, please provide answer using Swift.
Solved:
Used the following extension, thanks to yonat on GitHub:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
Within the delegate in question, it was implemented like so:
var alert = UIAlertController(title: "Alert Title", message: "Message Body", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in
}))
if let topController = UIApplication.topViewController(base: self) {
topController.presentViewController(alert, animated: true, completion: nil)
} else {
// If all else fails, attempt to present the alert from this controller.
self.presentViewController(alert, animated: true, completion: nil)
}
This now allows the following process:
ContainerView is loaded as a delegate of ItemTableView
User clicks searchButton
ContainerView modally presents a list of items ItemTableView
Each time a use selects a row in ItemTableView, the didSelectItem function is called in the instance of ContainerView that presented the ItemTableView. The ItemTableView does NOT get dismissed - the user can continue selecting items.
ContainerView submits a request to the server
Depending on the response, the ContainerView may present a UIAlertView.
The alertView is properly displayed using the above-mention code on top of whatever view is top-most in the hierarchy.
"When the user selects an item, it triggers and action on the delegate"
set a break point at start of the delegate method which is triggered by selecting an item. check whether that delegate method is being called or not ?
and test this too. (ACTION :UIAlertAction!)in

Dismiss Popover after touch

I've created a popover inside my MainViewController when some button its touched using the UIPopoverPresentationController and set like it's delegate like it's showed in the WWDC 2014, in the following way :
MainViewController.swift
class MainViewController : UIViewController, UIPopoverPresentationControllerDelegate {
#IBAction func showPopover(sender: AnyObject) {
var popoverContent = self.storyboard?.instantiateViewControllerWithIdentifier("PopOverViewController") as UIViewController
popoverContent.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = popoverContent.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(250, 419)
popover!.delegate = self
popover!.sourceView = self.view
popover!.sourceRect = CGRectMake(180,85,0,0)
self.presentViewController(popoverContent, animated: true, completion: nil)
}
}
The popover have a View inside it and when the View it's clicked with a Tap Gesture Recognizer I show LastViewController using a modal segue, the modal segue is created through the Interface Builder, not in code using an action to present the another LastViewController
Once the LastViewController is dismissed and I'm back in the MainViewController the popover remains open.
Inside the PopOverController I only have the default code nothing more.
LastViewController.swift
#IBAction func dismissVIew(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
The above code is used to dismiss the LastViewController once the button inside is touched.
Storyboard
How can I dismiss the popover once the another LastViewController it's visible, or before the another LastViewController should be opened?
Thanks in advance
I have already answer same problem over here.
There scenario is different but solution is same
You have to write code for dismiss presented view controller on completion of current view controller.
Write below code on your dismissVIew method of LastViewController.swift
var tmpController :UIViewController! = self.presentingViewController;
self.dismissViewControllerAnimated(false, completion: {()->Void in
println("done");
tmpController.dismissViewControllerAnimated(false, completion: nil);
});
Download link
In your button action on the FinalViewController, have you tried:
#IBAction func dismissMe() {
//this should tell the popover to tell the main view controller to dismiss it.
self.presentingViewController!.presentingViewController!.dismissViewControllerAnimated(false, completion: nil)
}
here is how I would do it.
I usually use lazy initialization for the PopoverViewController and it's ContentViewController
lazy var popoverVC: UIPopoverController = {
let vc = UIPopoverController(contentViewController: self.contentVC)
vc.delegate = self
return vc
}()
lazy var contentVC: UIViewController = {
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("ContentViewController") as UIViewController
vc.modalInPopover = true
return vc
}()
inside my contentViewController I hold a reference to the UIPopoverController.
var popoverVC: UIPopoverController!
then when I show the popover i just assign the popoverController to the contentViewController
#IBAction func showPopover(sender: UIButton) {
contentVC.popoverVC = self.popoverVC
let viewCenterRect = self.view.convertRect(self.view.bounds, toView: self.view)
popoverVC.presentPopoverFromRect(CGRectMake(CGRectGetMidX(viewCenterRect), CGRectGetMidY(viewCenterRect), 1, 1), inView: self.view, permittedArrowDirections: UIPopoverArrowDirection.allZeros, animated: true)
}
finally I dismiss the Popover programmatically inside an #IBAction
#IBAction func dismissPopover(sender: AnyObject) {
popoverVC.dismissPopoverAnimated(true)
}
Inside the viewcontroller you can override viewWillAppear()
Inside of this block dimiss it
override public func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
_viewToDismiss.removeFromSuperView()
}
But the above code assumes you have a reference to the PopOver object, which I don't think is good practice based on how you described the problem.
Rather, why not have the viewcontroller that created the PopOver be responsible for destroying it. Put this in the class that listens for the button touch (which I also assumes creates the PopOver as well)
- (void)viewWillDisappear:(BOOL)animated
{
_popOver.removeFromSuperView()
}
The popover have a View inside it and when the View it's clicked with a Tap Gesture Recognizer I show another ViewController using a modal segue.
As far as I understand from what you say, you should be able to call dismissViewControllerAnimated(_:completion:) from the action associated to your tap gesture recogniser. This will dismiss the popover you presented calling:
self.presentViewController(popoverContent, animated: true, completion: nil)
You can call this method on the popover view controller itself, depending on what is more convenient for you:
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, it automatically forwards the message to the presenting view controller.
Based on #Jageen answer
Swift 4.2
let tmpController :UIViewController! = self.presentingViewController;
self.dismiss(animated: false, completion: {()->Void in
print("done");
tmpController.dismiss(animated: false, completion: nil);
});