I've implemented a navigation mediator singleton in my application. The mediator keeps a reference to the primary view controller to launch view controller transitions from. So to present a view controller, the consumer would call the navigation mediator to do this. Same with dismissal so that it can reset the primary view controller.
The problem I am running into is with presenting a UIAlertController. I cannot dismiss this controller through my navigation mediator because it is dismissed automatically when a button is tapped, therefore losing the ability to reset the primary view controller when it is dismissed.
The only solution I've come up with is the create an extension on UIAlertController to override viewWillDisappear which resets the primary view controller like below. Is there a way to cancel the default dismissal behaviour?
extension UIAlertController {
public override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NavigationMediator.resetPrimaryViewController(self.presentingViewController)
}
}
No, but you can respond to the event before it will finish. UIALertViewDelegate. alertView:willDismissWithButtonIndex:
Else build your own AlertView.
Related
I have a View and a popover that appears on top of it, which alters data. I am trying to update the view/run a function on the main view (that is under the popover) once the popover is dismissed. I have tried numerous things including viewwillappear, but it isn't being registered as technically the view doesn't disappear since the popover is just above (And you can see part of the view from behind). If anyone can suggest how to call a function on the parent view when dismissing the popover (without crashing the app, as most of my attempts have), I would be very grateful! Thanks.
Update: I am attempting to do this with a modally presented vc now, and have attempted to use protocol callbacks but to no avail. Below is the code.
protocol MainVCDelegate: class {
func pushIt()
}
in the modally pushed view:
weak var delegate: MainVCDelegate?
#IBAction func changePartnerButton(_ sender: Any) {
delegate?.pushIt()
dismiss(animated: false)
}
in the main VC I implement the protocol and create the function to be run, but nothing happens.
In iOS 13 and iOS 14, you set yourself as the popover's presentation controller's delegate and implement presentationControllerDidDismiss. In iOS 12 and before, you set yourself as the popover's popover presentation controller's delegate and implement popoverPresentationControllerDidDismissPopover.
I have two view controllers. On the first one I have a button, which shows the second one modally. Then, I close the second one by tapping a button on it (it goes down). For dismissing transition I have created a custom class which conforms to UIViewControllerAnimatedTransitioning, so I use a custom transition animation for view controllers (I needed a custom behaviour during the dismissing transition).
My question is the following: because of a custom transition that I use, is my view controller still get removed after the transition finishes or is it still there but off the screen? And if it is, will it affect the memory and how bad?
You said:
is my view controller still get removed after the transition finishes or is it still there but off the screen?
There are two completely separate issues here.
First, there is a question of the view controller hierarchy. When you present a new view controller, the old view controller is always kept in the view controller hierarchy so that when you dismiss back to it, it will still be there. However, when you dismiss, the dismissed view controller will be removed from the view controller hierarchy and (unless you do something unusual, like keeping your own strong reference to it somewhere) it will be deallocated.
Second, there is a separate question of the view hierarchy. When presenting, the UIPresentationController dictates whether the presenting view controller's view remains in the view hierarchy or not. By default, it keeps it in the view hierarchy, but generally if doing a modal, full-screen "present", you'd specify a UIPresentationController subclass that tells it to remove the presenting view controller's view when the transition is done.
For example, when doing a custom modal "present" transition where the presented view controller's view is opaque and covers the whole screen, then your UIViewControllerTransitioningDelegate would not only supply the animation controllers, but also specify a presentation controller:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return YourAnimationController(...)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return YourAnimationController(...)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
And that presentation controller might be fairly minimal, only telling it to remove the presenter's view:
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
The ViewController remains in the hierarchy.
You can confirm this by verifying the value of self.parentViewController property in the modal view controller.
Edit:
Will that view controller affect the memory?
Yes it will continue to perform all operations or activities (e.g: streaming audio, playing animation, running timers etc) in the parentViewController.
And if so, how can this situation be handled?
There are methods available in UIViewController class to detect when a ViewController will or is appeared / disappeared.
More specifically, following methods are at your disposal:
viewWillAppear (Notifies the view controller that its view is about to be added to a view hierarchy)
viewDidAppear (Notifies the view controller that its view was added to a view hierarchy)
viewWillDisappear (Notifies the view controller that its view is about to be removed from a view hierarchy)
viewDidDisappear (Notifies the view controller that its view was removed from a view hierarchy)
You can use viewWillAppear and viewDidAppear in parentViewController to start or resume such activities / operations.
And viewWillDisappear and viewDidDisappear to suspend or stop such activities / operations.
Hope this helps.
I'm going assume that you are speaking about the view of your modal viewController staying in the window hierarchy - since you are asking if it is still there but off the screen, I believe you are talking about the view, and the controller, since controller is never on the screen.
And if you are asking about the view (which I assume that you are), calling completeTransition on transitionContext will remove it from the window hierarchy (so there is no need to call fromVC.view.removeFromSuperview()) - and by contract you are required to call completeTransition when the transition finishes in the UIViewControllerAnimatedTransitioning implementation.
You can confirm this behavior by checking the value of fromVC.view.superview before and after calling transitionContext.completeTransition:
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
fromVC.view.frame = endFrame
}) { (_) in
print(">>> \(fromVC.view.superview)")
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
print(">>> \(fromVC.view.superview)")
}
Yes, the previous UIViewController remains there.
As described in Apple docs
viewControllerToPresent
The view controller to display over the
current view controller’s content.
I have a storyboard set up with a UITabBarController which contains a UINavigationController for each tab. For one of the UINavigationControllers there are no transition animations when pushing or presenting a view controller.
There are, at least, two different cases when this happens
1. I have a storyboard segue set up to push the child view controller. The segue triggers when selecting a cell in a table view. The "Animates"-box is checked.
Attempting to programatically push the child view controller yields the same result.
self.navigationController?.pushViewController(nextController, animated: true)
2. There is also no animation when attempting to modally present another view controller from the root view controller of the navigation controller.
modalViewController.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal
self.present(modalViewController, animated: true, completion: nil)
If I present the modalViewController from another view controller the transition is animated which leads me to believe that there is something wrong in the root view controller that is presenting.
Is there a way to disable animations on a UIViewController that I might accidentally have triggered? I have checked and verified that there are no UIView.setAnimationsEnabled(false)
Use self.navigationController?.pushViewController( instead of self.present(
You set up animation in UINavigationController. But you called the function self.present( which is provided by UIViewController. UIViewController of course cannot provide the animation.
In my overridden viewWillDisappear(_ animated: Bool) of the parent view controller I had some code that reset the state of a custom view. The reset did in turn disable actions via CATransaction.setDisableActions(true), thus disabling the transition animations.
Moving the reset to viewDidDisappear(_ animated: Bool) resolved the issue.
My target include a lot view need to present different view modally base on each user action. Here what I want to do to get cleaner view hierarchy and better user experience.
Root View Controller present First View Controller modally
When I clicked button on the First View Controller, then the Second View Controller appear modally over it.
As soon as the Second View Controller did appear, I want to dismiss or remove the first one from view hierarchy.
Can I do that? If so, how should i do it?
If not, what is the right way to solve this out cause I will present many modally presented view controllers over each view. I think even if I want to dismiss current view, the previous one will still remain appear when current one dismiss.
UPDATE :
VC1 (Root) > VC 2 (which was present modally) > VC 3 (which was
present modally over VC 2)
When i dismiss VC3, the VC2 is still on view memory. So, I don't want to appear VC2 as soon as I dismiss VC3 and instead I want to see VC1 by removing or dismissing VC2 from view hierarchy.
WANT : At the image, when I dismiss the blue,I don't want see the pink in my view memory and I want to remove it as soon as the blue one appear.
That's what i want to do.
Any Help?Thanks.
So, let's assume that you have a storyboard similar to:
What should happens is:
Presenting the the second ViewController (from the first ViewController).
Presenting the the third ViewController (from the second ViewController).
dismissing to the first ViewController (from the third ViewController).
In the third ViewController button's action:
#IBAction func tapped(_ sender: Any) {
presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
As you can see, by accessing the presentingViewController of the current ViewController, you can dismiss the previous hierarchy of the view controllers:
The view controller that presented this view controller.
By implementing presentingViewController?.presentingViewController? that means that: the presented of the presented current ViewController :)
It might seem a little bit confusing, but it is pretty simple.
So the output should be like (I added background colors to the viewControllers -as vc1: orange, vc2: black and vc3: light orange- to make it appears clearly):
EDIT:
If you are asking to remove the ViewController(s) in the middle (which in this example the second ViewController), dismiss(animated:completion:) does this automatically:
If you present several view controllers in succession, thus building a
stack of presented view controllers, calling this method on a view
controller lower in the stack dismisses its immediate child view
controller and all view controllers above that child on the stack.
When this happens, only the top-most view is dismissed in an animated
fashion; any intermediate view controllers are simply removed from the
stack. The top-most view is dismissed using its modal transition
style, which may differ from the styles used by other view controllers
lower in the stack.
Referring to what are you asking:
I think even if I want to dismiss current view, the previous one will
still remain appear when current one dismiss.
I think that appears clearly on the UI (and I find it ok), but as mentioned in the dismiss documentation discussion, both the third and the second will be removed from the stack. That's the right way.
Here is my opinion in different perspective,
Root View Controller present Second View Controller
Add FirstView onto Second View
Dismiss FirstView Controller when button pressed.
Second View Controller,
class ViewController: UIViewController, FirstViewControllerProtocol {
weak var firstViewController: FirstViewController?
override func viewDidLoad() {
super.viewDidLoad()
print("Not initiated: \(firstViewController)")
firstViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "FirstViewController") as? FirstViewController
addChildViewController(firstVC!)
firstViewController?.delegate = self
view.addSubview((firstViewController?.view)!)
print("Initiated: \(firstViewController)")
}
func dismiss() {
firstViewController?.view.removeFromSuperview()
firstViewController?.removeFromParentViewController()
}
}
FirstViewController,
protocol FirstViewControllerProtocol {
// Use protocol/delegate to communicate within two view controllers
func dismiss()
}
class FirstViewController: UIViewController {
var delegate: FirstViewControllerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismiss(_ sender: Any) {
delegate?.dismiss()
}
deinit {
print("BYE")
}
}
What you want is an "unwind segue":
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html#//apple_ref/doc/uid/TP40007457-CH15-SW8
https://developer.apple.com/library/archive/technotes/tn2298/_index.html
It allows you to dismiss multiple view controllers at the same time, without having to know how many there are in the stack.
In VC1 you would implement an IBAction called (for instance) unwindToRoot. Then in the storyboard for VC3, you wire up your Done button to the Exit object and choose the unwindToRoot action.
When that button is pressed, the system will dismiss all the view controllers it needs to bring you back to VC1.
This is better than calling presentingViewController?.presentingViewController?.dismiss(), because VC3 doesn't need to know anything about the view controller hierarchy underneath it.
When a user taps outside the popover, the dismissal is animated. Is there a way to set that dismissal animation to NO? I have googled and searched on Stack extensively.
The docs for UIPopover state:
When displayed, taps outside of the popover window cause the popover
to be dismissed automatically. To allow the user to interact with the
specified views and not dismiss the popover, you can assign one or
more views to the passthroughViews property. Taps inside the popover
window do not automatically cause the popover to be dismissed. Your
view and view controller code must handle actions and events inside
the popover explicitly and call the dismissPopoverAnimated: method as
needed.
I have implemented the dismissPopoverAnimated: method with NO and that works great for all the cases when I call that method.
The problem is when a user taps outside the popover to dismiss, dismissPopoverAnimated: is not called.
taps outside of the popover window cause the popover
to be dismissed automatically.
And that dismissal is animated. There seems to be no way to control that dismissal. I am using the popover to present a color picker for a drawing app. Taps to draw are not registered until the popover has finished animating out. This creates a noticeable delay as you are not able to draw immediately but must wait for the animation to complete.
I thought that - (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController could work but there is no way AFAIK to set the animation property in this method. Just return yes or no.
Is there a different method I can implement to be able to set the animation to NO?
In the view controller that presents your UIPopoverController, conform to the UIPopoverControllerDelegate protocol and implement the following delegate method. I just tested this and it does dismiss the popover without animation.
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
[self.myPopoverController dismissPopoverAnimated:NO];
return YES;
}
Just make sure that you have set the delegate of your popover controller to the view controller that implements this.
Swift 5
This will disable the animation, when we close the popOver by tapping outside.
extension YourViewController: UIPopoverPresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
// to prevent animation, we need to dismiss it manuallly with animated: false
presentationController.presentingViewController.dismiss(animated: false, completion: nil)
return true
}
}
On iOS 9+ as by default modalPresentationStyle = .Popover you can implement this method to prevent dismiss clicking out
public func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool {
return false
}