I have some custom modal presentation and custom controller to present (subclass of UIViewController). It is it's own transitioning delegate and returns some animated transitioning object and presentation controller. I use animated transitioning object to add presented view to container view when presenting and to remove it when dismissing, and of course to animate it. I use presentation controller to add some helper subview.
public final class PopoverPresentationController: UIPresentationController {
private let touchForwardingView = TouchForwardingView()
override public func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
self.containerView?.insertSubview(touchForwardingView, atIndex: 0)
}
}
public final class PopoverAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
func setupView(containerView: UIView, presentedView: UIView) {
//adds presented view to container view
}
public func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
//1. setup views
//2. animate presentation or dismissal
}
}
public class PopoverViewController: UIViewController, UIViewControllerTransitioningDelegate {
init(...) {
...
modalPresentationStyle = .Custom
transitioningDelegate = self
}
public func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PopoverAnimatedTransitioning(forPresenting: true, position: position, fromView: fromView)
}
public func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PopoverAnimatedTransitioning(forPresenting: false, position: position, fromView: fromView)
}
public func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController?, sourceViewController source: UIViewController) -> UIPresentationController? {
return PopoverPresentationController(presentedViewController: presented, presentingViewController: presenting, position: position, fromView: fromView)
}
}
Everything works fine when I present the controller with presentViewController and pass true in animated property. But when I want to present it without animation and pass false, UIKit only calls presentationControllerForPresentedViewController method, and does not call animationControllerForPresentedController at all. And as far as presented view is added to views hierarchy and positioned in it in animation transitioning object, which is never created, nothing is presented.
What I'm doing is I'm checking in presentation controller if transition is animated and if not I create animated transitioning object manually and make it to setup views.
override public func presentationTransitionWillBegin() {
...
if let transitionCoordinator = presentedViewController.transitionCoordinator() where !transitionCoordinator.isAnimated() {
let transition = PopoverAnimatedTransitioning(forPresenting: true, position: position, fromView: fromView)
transition.setupView(containerView!, presentedView: presentedView()!)
}
}
It works but I'm not sure if that's the best approach.
Documentation says that presentation controller should be responsible only for doing any additional setup or animation during transition and the main work for presentation should be done in animated transitioning object.
Is it ok to always setup views in presentation controller instead and only animate them in animated transitioning object?
Is there any better way to solve that problem?
Solved that by moving all the logic of views setup from animated transitioning to presentation controller.
Related
https://i.stack.imgur.com/kqKLf.gif
Problem:
When implementing the Present transition using UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning, if modalPresentationStyle is not .fullScreen, the return of the view(forKey: .from) method of UIViewControllerContextTransitioning is nil.
In the case of Dismiss, on the contrary, the return of view(forKey: .to) is nil. So, if I use the viewController view returned by viewController(forKey: .to) for animation, when the transition is complete, nothing remains in the view layer, and a black screen is displayed.
SomePresentingViewController.swift
let somePresentedViewController = SomePresentedViewController()
somePresentedViewController.transitioningDelegate = somePresentedViewController.transitionController
self.present(somePresentedViewController, animated: true, completion: nil)
SomePresentedViewController.swift
class SomePresentedViewController: UIViewController {
var transitionController = TransitionController()
#IBAction func closeButtonTapped(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
...
TransitionController.swift
class TransitionController: NSObject, UIViewControllerTransitioningDelegate {
let animator: SlideAnimator
override init() {
animator = SlideAnimator()
super.init()
}
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
animator.isPresenting = true
return animator
}
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
animator.isPresenting = false
return animator
}
}
SlideAnimator.swift
class SlideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting: Bool = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
print(transitionContext.containerView)
if isPresenting {
animateSlideUpTransition(using: transitionContext)
} else {
animateSlideDownTransition(using: transitionContext)
}
}
func animateSlideDownTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
let container = transitionContext.containerView
let screenOffDown = CGAffineTransform(translationX: 0, y: container.frame.height)
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0.0,
options: .curveEaseInOut,
animations: {
fromVC.view.transform = screenOffDown
}) { (success) in
fromVC.view.transform = .identity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
...
}
Information gathering:
iOS13 bug? In the link below, you can see that the radar for the issue is open to this day.
http://www.openradar.me/radar?id=4999313432248320
There have been many comments that this is a bug in iOS 13 on stackoverflow. But there have been some more compelling comments that this is Apple's intention and why:
https://stackoverflow.com/a/25901154/5705503
A possible cause:
If NO is returned from -shouldRemovePresentersView, the view associated
with UITransitionContextFromViewKey is nil during presentation. This
intended to be a hint that your animator should NOT be manipulating the
presenting view controller's view. For a dismissal, the -presentedView
is returned.
Why not allow the animator manipulate the presenting view controller's
view at all times? First of all, if the presenting view controller's
view is going to stay visible after the animation finishes during the
whole presentation life cycle there is no need to animate it at all — it
just stays where it is. Second, if the ownership for that view
controller is transferred to the presentation controller, the
presentation controller will most likely not know how to layout that
view controller's view when needed, for example when the orientation
changes, but the original owner of the presenting view controller does.
-Apple Documentation Archive-
https://developer.apple.com/library/archive/samplecode/CustomTransitions/Listings/CustomTransitions_Custom_Presentation_AAPLCustomPresentationController_m.html
Attempts to solve:
Set modalPresentationStyle to .fullScreen.
I don't pick this solve because the background of the present view controller must be translucent and the view controller behind it must be visible.
Create a new project to see if the problem recurs
Problem reproduced!
Build on iOS12
Problem reproduced
Subclass UIPresentationController to override shouldRemovePresentersView property to return false and adopt it as presentationController.
I tried the solution mentioned in the link below, but the results were not different.
https://stackoverflow.com/a/41314396/5705503
The role of shouldRemovePresentersView as I know it:
Indicates whether the view of the view controller being switched is removed from the window at the end of the presentation transition.
Substitute the view of the view controller returned by viewController(forKey:) to use it for animation and add the view to the key window.
I was able to achieve the desired result, but it was a bad approach and I did not adopt it as a workaround as it could cause various problems in the future.
Environments:
iOS 13.5 simulator, iOS 13.5.1 iPhoneXS
Xcode 11.5 (11E608c)
Addition)
I found an old Sample where the same problem occurs (black screen is visible when dismiss transition is complete) in the same situation as me.
Please, I hope someone can clone this Sample and run it and let me know how to fix this.
https://www.thorntech.com/2016/03/ios-tutorial-make-interactive-slide-menu-swift/
It is possible in some cases (iPhone X, iOS 13) to dismiss presented view controllers with a gesture, by pulling from the top.
In that case, I can't seem to find a way to notify the presenting view controller. Did I miss something?
The only I found would be to add a delegate method to the viewDidDisappear of the presented view controller.
Something like:
class Presenting: UIViewController, PresentedDelegate {
func someAction() {
let presented = Presented()
presented.delegate = self
present(presented, animated: true, completion: nil)
}
func presentedDidDismiss(_ presented: Presented) {
// Presented was dismissed
}
}
protocol PresentedDelegate: AnyObject {
func presentedDidDismiss(_ presented: Presented)
}
class Presented: UIViewController {
weak var delegate: PresentedDelegate?
override func viewDidDisappear(animated: Bool) {
...
delegate?.presentedDidDismiss(self)
}
}
It is also possible to manage this via notifications, using a vc subclass but it is still not satisfactory.
extension Notification.Name {
static let viewControllerDidDisappear = Notification.Name("UIViewController.viewControllerDidDisappear")
}
open class NotifyingViewController: UIViewController {
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .viewControllerDidDisappear, object: self)
}
}
There must be a better way to do this?
From iOS 13 Apple has introduced a new way for the users to dismiss the presented view controller by pulling it down from the top. This event can be captured by implementing the UIAdaptivePresentationControllerDelegate to the UIViewController you're presenting on, in this case, the Presenting controller. And then you can get notified about this event in the method presentationControllerDidDismiss. Here is the code example :-
class Presenting: UIViewController, UIAdaptivePresentationControllerDelegate {
func someAction() {
let presented = Presented()
presented.presentationController?.delegate = self
present(presented, animated: true, completion: nil)
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Note:
This method only gets triggered for dismissing by swiping from the top and not for the programmatic dismiss(animated:,completion:) method.
You don't need any custom delegate or Notification observer for getting the event where the user dismisses the controller by swiping down, so you can remove them.
Adopt UIAdaptivePresentationControllerDelegate and implement presentationControllerDidAttemptToDismiss (iOS 13+)
extension Presenting : UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentingViewController.presentedDidDismiss(self)
}
}
UIPresentationController has a property presentingViewController. The name is self-explanatory. You don't need the explicit delegate protocol.
The method is actually called to be able to show a dialog for example to save changes before dismissing the controller. You can also implement presentationControllerDidDismiss()
And do not post notifications to controllers which are related to each other. That's bad practice.
I've a TabBarController in which presents a view controller with custom animations with UIViewControllerAnimatedTransitioning;
The animations run normally without issues, but after the animationController(forPresented) function runs, the Presenting view controller disappears.
I've found a question around here with people having the same issues but none of those tries solved my issue.
I've read that there is a bug in iOS and we should had again the 'vanished' view controller to the stack, but adding this with UIApplication.shared.keyWindow?.addSubview(presentingView) makes the view added on top of the presentedView and I don't know it adding it again, adds another one to the stack, because it could only be a graphical bug and the view is still part of the container.
Here's some code:
// Global var
var transition = Animator()
// Present a VC modally using tab bar
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController is NewPostVC {
if let newVC = tabBarController.storyboard?.instantiateViewController(withIdentifier: "newPostVC") as? NewPostVC {
newVC.transitioningDelegate = self
newVC.interactor = interactor // new
tabBarController.present(newVC, animated: true)
return false
}
}
return true
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitioningMode = .Present
return transition
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitioningMode = .Dismiss
return transition
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
//*****************************
/// On the Animator class:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
// Animates to present
if transitioningMode == .Present {
// Get views
guard
let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to),
let presentingView = transitionContext.view(forKey: UITransitionContextViewKey.from)
else {
print("returning")
return
}
// Add the presenting view controller to the container view
containerView.addSubview(presentingView)
// Add the presented view controller to the container view
containerView.addSubview(presentedView)
// Animate
UIView.animate(withDuration: presentDuration, animations: {
presentingView.transform = CGAffineTransform.identity.scaledBy(x: 0.85, y: 0.85);
presentingView.layer.cornerRadius = 7.0
presentingView.layer.masksToBounds = true
presentedView.transform = CGAffineTransform.identity.scaledBy(x: 0.6, y: 0.6);
presentedView.layer.masksToBounds = true
}, completion: { _ in
// On completion, complete the transition
transitionContext.completeTransition(true)
//UIApplication.shared.keyWindow?.addSubview(presentingView)
})
}
// Animates to dismiss
else {
// TODO: - Implement reverse animation
}
}
Note that the animations itself are just tests I'm doing, just scaling them around.
Thx.
The idea is that you don't need to add subview .from during presentation and .to during dismissal to the contatiner's view hierarchy. And if you want to see background underneath, just set the contatiner's view background color to .clear.
After reading Apple's related documentation here I found that it's not a bug that the presentingViewController disappears from the screen, it's just how the API works.
Anyone using transmission animation read the documentation, which was updated and you'll find really interesting and solid explanations there.
I've been dabbling with some very basic custom transitions recently and I'm starting to get the hang of it. Up until this point, however, I had only made Modal transitions.
I wrote the following code for a basic "Fade" transition from my "Master View" to my "Settings View" (both of which are part of the NavigationController Stack):
import UIKit
import QuartzCore
///Transition manager for transitioning between the loginVC and the content that follows.
class FadeTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let destinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let sourceView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let destinationView = transitionContext.viewForKey(UITransitionContextToViewKey)
transitionContext.containerView().addSubview(destinationView!)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { () -> Void in
sourceView!.alpha = 0.0
destinationView!.alpha = 1.0
}) { (didComplete) -> Void in
println("Fade Transition: \(didComplete)")
transitionContext.completeTransition(didComplete)
}
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.5
}
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
I was excited. I rejoiced. All appeared to be well until I then went to a different View Controller that also has a segue from the "Master View"...and it had the same transition. Of course this is to be expected, I just hadn't thought of it.
How can I limit this transition to only occur between two specific UIViewControllers?
You need to look at the UINavigationControllerDelegate method navigationController(_:animationControllerForOperation:fromViewController:toViewController). You have implemented it in your custom transition class above (I would suggest making a more general class your nav controller delegate instead, such as the view controller that contains it or the app delegate), but you return self indisciminately, indicating that every transition for that navigation controller should use your fade transition.
Instead, look at the from/toViewController parameters to decide whether you want to use a custom transition between those particular view controllers. If you want to use the default animated transition, just return nil.
From the UINavigationControllerDelegate Protocol Reference:
Return Value
The animator object responsible for managing the transition animations,
or nil if you want to use the standard navigation controller
transitions. The object you return must conform to the
UIViewControllerAnimatorTransitioning protocol.
I am currently working on a project using custom transition between view controllers. In my storyboard, I control drag a button to another view controller to do a present modally transition, and in the presenting view controller's file I override the prepareForSegue method, the code is:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.destinationViewController is RightEyeViewController {
let rightEyeVC = segue.destinationViewController as RightEyeViewController
rightEyeVC.modalPresentationStyle = .Custom
rightEyeVC.transitioningDelegate = RightEyeTransitioningDelegate()
}
}
And in the UIViewControllerTransitioningDelegate I return the UIViewControllerAnimatedTransitioning that I created, the code is:
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animationController = RightEyeAnimatedTransitioning()
animationController.isPresentation = true
return animationController
}
The problem is every time the app crashes at the line return animationController, but after I add a break point before this line, the transition works without any problems, I don't know what caused this problem, can anyone offer some help!