My iOS App starts with UIViewController A which is embedded as first element in a UINavigationController. When the app is started or when returning to it after some time in background I would like to show a password prompt. In this case UIViewController A should present UIViewController B which shows the password prompt.
The user should immediately see UIViewController B, not A and then B sliding in, etc. Thus, I have presented UIViewController B in viewWillAppear in UIViewController A:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: nil)
}
}
This works fine, but an error message is logged:
Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x7fe9af01c200>.
It is obvious that presenting UIViewController B from UIViewController A before it became visible causes this problem. Moving from viewWillAppear to viewDidAppear would solve the error message. However, than the user would first see A then B...
Is it even possible to overlay a ViewControler A with ViewController B without A becoming visible first?
I know that there might be other solutions like adding the view of the password ViewController manually to the view hierachy, etc. However, I would prefer a clean way where A is in complete control. Is this possible?
Or is it save to simple ignore the warning?
It sounds a bit tricky, might do the job though.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
self.view.alpha = 0
// Maybe (or not?)
self.navigationController?.view.backgroundColor = .white
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: { [weak self] in
self?.view.alpha = 1
})
}
}
I am presenting a UIActivityViewController to share an .mp4 video from a URL:
let viewController: UIViewController = ... // the presenting view controller
let url: URL = ... // local file
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
viewController.present(activityController, animated: false, completion: nil)
When the "save video" option is selected, the video is saved, but the presenting UIViewController disappears (and I can see that .viewWillDisappear() is called on it.)
How do I make the presenting UIViewController not disappear?
Note that all of the other share options that I've tried do not have this problem (messages, airdrop, instagram).
I have tried to set the sourceView and sourceRect, but it does not seem to help.
activityController.popoverPresentationController?.sourceView = viewController.view!
activityController.popoverPresentationController?.sourceRect = viewController.view!.frame
I've looked for errors, but didn't find any:
activityController.completionWithItemsHandler = { (a: UIActivity.ActivityType?, b: Bool, c: [Any]?, d: Error?) in
if let error = d {
print(error)
}
}
Also, all of my UIViewController lifecycle overrides call their super, i.e.:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
This is what it looks like:
It's bringing down my entire view with it!
For what it's worth, viewController is setup by calling a segue that is setup in a storyboard:
class LaunchController : UIViewController {
var performedSegue = false
override func viewDidLayoutSubviews() {
if !performedSegue {
self.performSegue(withIdentifier: "main", sender: self)
performedSegue = true
}
}
}
It looks like this "feature" was introduced by Apple in iOS 13. I tested
this on iOS 12 and the presenting ViewController does not disappear.
I traced the call stack and it appear that UIActivityViewController is calling dismiss on presenting view controller (or it's UINavigationController) when saving to camera roll succeeds.
I don't know the way to prevent this since it's Apple private API and there's nothing in documentation about this. The only way I found, is to to set some kind of flag savingToCameraRoll and set it to true when presenting UIActivityViewController, override dismiss method on presenting ViewController and then check this flag inside dismiss.
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if !savingToCameraRoll {
// Dismiss view controller if UIActivityViewController not in use
super.dismiss(animated: animated, completion: completion)
}
// Handle UIActivityViewController dismiss attempt.
// If you do nothing, the presenting ViewController should not be dismissed.
}
You should also remember to set savingToCameraRoll to false in completionWithItemsHandler.
In the storyboard, I marked the presenting ViewController as "Is Initial View Controller" instead of presenting it via a segue. That solved the issue for me (though it's a workaround that would not work in general).
When I present a UIAlertController over a view controller, the controls are dimmed as expected.
However, if the same view controller is itself modally presented, the display of the alert controller does not dim the controls (the two buttons remain blue).
How do I make a presented view controller itself handle presentations correctly, and dim its controls?
Here is a small example project. The relevant code is in MainViewController.swift.
The best workaround I have so far is to use a customized UIAlertController subclass to set the tintAdjustmentMode alongside its appear/disappear animations, using the transitionCoordinator:
/// A `UIAlertController` that can udpates its presenting view controller's `tintAdjustmentMode` code as it appears and disappears
class TintAdjustingAlertController: UIAlertController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animatePresentingViewTintAdjustmentMode(tintAdjustmentMode: .dimmed, forViewControllerAtKey: .from)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
animatePresentingViewTintAdjustmentMode(tintAdjustmentMode: .automatic, forViewControllerAtKey: .to)
}
private func animatePresentingViewTintAdjustmentMode(tintAdjustmentMode mode: UIView.TintAdjustmentMode, forViewControllerAtKey key: UITransitionContextViewControllerKey) {
transitionCoordinator?.animate(alongsideTransition: { context in
if let presentingNavigationController = context.viewController(forKey: key) as? UINavigationController {
presentingNavigationController.navigationBar.tintAdjustmentMode = mode
presentingNavigationController.viewControllers.forEach { $0.view.tintAdjustmentMode = mode }
} else if let presentingViewController = context.viewController(forKey: key) {
presentingViewController.view.tintAdjustmentMode = mode
}
}, completion: nil)
}
}
This works, but I hope not to have to pepper it throughout my code. Would still love to know a) if there is a simple way to make this work as expected, or b) if this is indeed an iOS bug, is there a more elegant workaround?
I have also submitted a radar for this: http://www.openradar.me/radar?id=6113750608248832
I have the code to go to the next page and show the tabbar when I click the button.
#IBAction func moveBtn(_ sender: Any) {
guard let next = self.storyboard?.instantiateViewController(withIdentifier: "nextPage") as? nextPageCon else {
return
}
self.definesPresentationContext = true
next.modalPresentationStyle = .overCurrentContext
self.present(next, animated: false)
}
My question is, when I go to another page using the tab bar on the next page and then return, I want to show the main page.
Currently, when I move using the tab bar and then come back, the next page is displayed as it is.
How can I fix it?
Add this to your nextPageCon view controller:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.presentingViewController?.dismiss(animated: false, completion: nil)
}
When you present nextPageCon and then navigate away by selecting a different tab, this func will trigger and dismiss itself.
This is my first application for iOS.
So I have a UIVIewController with a UITableView where I have integrated a UISearchBar and a UISearchController in order to filter TableCells to display
override func viewDidLoad() {
menuBar.delegate = self
table.dataSource = self
table.delegate = self
let nib = UINib(nibName: "ItemCellTableViewCell", bundle: nil)
table.registerNib(nib, forCellReuseIdentifier: "Cell")
let searchButton = UIBarButtonItem(barButtonSystemItem: .Search, target: self, action: "search:")
menuBar.topItem?.leftBarButtonItem = searchButton
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
return controller
})()
self.table.reloadData()
}
I am using also a modal segue in order to open the element's ViewController where I will display details of the element.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.index = indexPath.row
self.performSegueWithIdentifier("ItemDetailFromHome", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "ItemDetailFromHome") {
let settingsVC = segue.destinationViewController as! ItemDetailViewController
settingsVC.parent = self
if self.isSearching == true && self.searchText != nil && self.searchText != "" {
settingsVC.item = self.filteredItems[self.index!]
} else {
settingsVC.item = self.items[self.index!]
}
}
}
That works fine until I try to display the ItemDetailViewController for a filtered element (through the UISearchController).
I have the following message :
Warning: Attempt to present <ItemDetailViewController: *> on <HomeViewController: *> which is already presenting (null)
At every time I am going to the ItemDetailViewController.viewDidLoad() function but after that when the search is activated I have the previous error.
Any idea ? I have tried to use the following async dispatch but without success
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.index = indexPath.row
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("ItemDetailFromHome", sender: self)
})
}
I have found out a solution.
I have add the following code in HomeViewController.viewDidLoad and that works !
definesPresentationContext = true
In my case, I found my code to present the new viewController (a UIAlertController) was being called twice.
Check this before messing about with definesPresentationContext.
In my case, I tried too early to show the new UIViewController before closing the previous one. The problem was solved through a call with a slight delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.callMethod()
}
The problem for me is that I was presenting two modals and I have to dismiss both and then execute some code in the parent window ... and then I have this error... I solved it seting this code in the dismiss o the last modal presented:
self.dismiss(animated: true, completion: {
self.delegate?.callingDelegate()
})
in other words instead of just dismiss two times .. in the completion block of the first dismiss call delegate that will execute the second dismiss.
What worked for me was to add the presentation of the alert to the main thread.
DispatchQueue.main.async {
self.present(alert, animated: true)
}
The presentation of the current viewController was not complete. By adding the alert to the main thread it can wait for the viewController's presentation to complete before attempting to present.
I got the same issue when i tried to present a VC which called inside the SideMenu(jonkykong).
first i tried inside the SideMenu and i called it from the delegate to the MainVC both had the same issue.
Solution: dismiss the SideMenu first and present the new VC after will works perfectly!.
This happened with me on our project. I was presenting our log in/log out ViewController as a pop-over. But whenever I tried to log back out again and display the pop-over again, I was getting this logged out in my console:
Warning: Attempt to present UIViewController on <MY_HOME_VIEW_CONTROLLER> which is already presenting (null)
My guess is that the pop-over was still being held by my ViewController even though it was not visible.
However you are attempting to display the new ViewController, the following code I used to solve the issue should work for you:
func showLoginForm() {
// Dismiss the Old
if let presented = self.presentedViewController {
presented.removeFromParentViewController()
}
// Present the New
let storyboard = UIStoryboard(name: "MPTLogin", bundle: Bundle(for: MPTLogin.self))
let loginVC = storyboard.instantiateViewController(withIdentifier: "LogInViewController") as? MPTLogInViewController
let loginNav = MPTLoginNav(rootViewController: loginVC!)
loginNav.modalPresentationStyle = .pageSheet;
self.present(loginNav, animated: true, completion: nil)
}
I faced the same kind of problem
What I did is from Interface builder selected my segue
Its kind was "Present Modally"
and its presentation was "Over current context"
i changed the presentation to "Default", and then it worked for me.
In my case I was trying to present a UIAlertController at some point in the app's lifetime after using a UISearchController in the same UINavigationController.
I wasn't using the UISearchController correctly and forgot to set searchController.isActive = false before dismissing. Later on in the app I tried to present the alert but the search controller, though not visible at the time, was still controlling the presentation context.
My problem was that (in my coordinator) i had presented a VC on a VC and then when i wanted to present the next VC(third one), presented the third VC from the first one which obviously makes the problem which is already presenting.
make sure you are presenting the third one from the second VC.
secondVC.present(thirdVC, animated: true, completion: nil)
Building on Mehrdad's answer: I had to first check if the search controller is active (if the user is currently searching):
if self.searchController.isActive {
self.searchController.present(alert, animated: true, completion: nil)
} else {
self.present(alert, animated: true, completion: nil)
}
where alert is the view controller to present modally.
This is what finally worked for me, as my project didn't exactly have a NavigationVC but instead, individual detached VC's. as xib files
This code produced the bug:
present(alertVC, animated: true, completion: nil)
This code fixed the bug:
if presentedViewController == nil{
navigationController?.present(alertVC, animated: true, completion: nil)
}
For me it was an alert that was interfering with the new VC that I was about to present.
So I moved the new VC present code into the OK part of my alert, Like this :
func showSuccessfullSignupAndGoToMainView(){
let alert = UIAlertController(title: "Alert", message: "Sign up was successfull.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
switch action.style{
case .default:
// Goto Main Page to show businesses
let mainStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc : MainViewController = mainStoryboard.instantiateViewController(withIdentifier: "MainViewController") as! MainViewController
self.present(vc, animated: false, completion: nil)
case .cancel:
print("cancel")
case .destructive:
print("destructive")
}}))
self.present(alert, animated: true, completion: nil)
}
My issue was that I was trying to present an alert from a view that wasn't on top. Make sure you present from the top-most viewController.
In my case this was an issue of a button which was duplicated in Interface Builder. The original button had a touch-up handler attached, which also presented a modal view. When I then attached a touch-up handler on the copied button, I forgot to remove the copied handler from the original, causing both handlers to be fired and thus creating the warning.
More than likely you have your Search button wired directly to the other view controller with a segue and you are calling performSegueWithIdentifier. So you are opening it twice, which generates the error that tells you "is already presenting."
So don't call performSegueWithIdentifier, and that should do the trick.
Make sure you Dismiss previous one before presenting new one!