“ From View Controller”使用 UIViewController 上下文转换消失

我有一个问题,我在下面描述了它。

我使用 UIViewControllerContextTransitioning自定义过渡。

我有两个视图控制器,第一个视图控制器和第二个视图控制器。

现在我想在第一个视图控制器上添加第二个视图控制器和一个动画。我已经做到了,现在第二视图控制器是透明的,所以我们可以看到第一视图控制器下面的第二视图控制器。

但是我看不到第一个视图控制器,我只能看到第二个视图控制器下面的黑色屏幕。

-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
self.transitionContext = transitionContext;
if(self.isPresenting){
[self executePresentationAnimation:transitionContext];
}
else{
[self executeDismissalAnimation:transitionContext];
}
}


-(void)executePresentationAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
UIView* inView = [transitionContext containerView];
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];


UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];


CGRect offScreenFrame = inView.frame;
offScreenFrame.origin.y = inView.frame.size.height;
toViewController.view.frame = offScreenFrame;


toViewController.view.backgroundColor = [UIColor clearColor];
fromViewController.view.backgroundColor = [UIColor clearColor];
inView.backgroundColor = [UIColor  clearColor];
[inView insertSubview:toViewController.view aboveSubview:fromViewController.view];
// [inView addSubview:toViewController.view];
CFTimeInterval duration = self.presentationDuration;
CFTimeInterval halfDuration = duration/2;


CATransform3D t1 = [self firstTransform];
CATransform3D t2 = [self secondTransformWithView:fromViewController.view];


[UIView animateKeyframesWithDuration:halfDuration delay:0.0 options:UIViewKeyframeAnimationOptionCalculationModeLinear animations:^{


[UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.5f animations:^{
fromViewController.view.layer.transform = t1;
}];


[UIView addKeyframeWithRelativeStartTime:0.5f relativeDuration:0.5f animations:^{
fromViewController.view.layer.transform = t2;
}];
} completion:^(BOOL finished) {
}];




[UIView animateWithDuration:duration delay:(halfDuration - (0.3*halfDuration)) usingSpringWithDamping:0.7f initialSpringVelocity:6.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
toViewController.view.frame = inView.frame;
} completion:^(BOOL finished) {
[self.transitionContext completeTransition:YES];
}];
}

[self.transitionContext completeTransition:YES];调用时,第一个视图控制器突然消失,黑屏显示在第二个视图控制器下面。

有人知道吗? 谢谢。

33600 次浏览

adds a view controller as a child of another view controller.

[self addChildViewController:childViewController];

check and let me know.

I was having the same problem here – looks like a bug in iOS 8. I've filed a radar.

I used Reveal to inspect the view hierarchy after the screen goes black. The key UIWindow is completely empty – no view hierarchy at all!

Reveal'd

I played around a bit and it looks like there is an easy workaround, for simple cases. You can just re-add the toViewController's view as a subview of the key window's:

transitionContext.completeTransition(true)
UIApplication.sharedApplication().keyWindow!.addSubview(toViewController.view)

I've checked and the key window's rootViewController is still correctly set, so that's fine. I'm not sure what would happen if you presented your controller from within an already presented modal controller, so for more complex cases, you'll have to experiment around.

instead of [inView insertSubview:toViewController.view aboveSubview:fromViewController.view]; just add: [inView addSubview:toViewController.view];

if (self.presenting) {


[transitionContext.containerView addSubview:toViewController.view];
// your code


} else {
// your code
}

You can see an example here: link and it works on iOS 7 and iOS 8

In iOS 8, you must manipulate the views returned by viewForKey: instead of the .view property of the view controllers returned by viewControllerForKey:. This isn't particularly clear from the beta documentation, but if you look into the source for UIViewControllerTransitioning.h you'll see this comment above viewControllerForKey::

// Currently only two keys are defined by the
// system - UITransitionContextToViewControllerKey, and
// UITransitionContextFromViewControllerKey.
// Animators should not directly manipulate a view controller's views and should
// use viewForKey: to get views instead.
- (UIViewController *)viewControllerForKey:(NSString *)key;

So instead of adjusting frames etc of toViewController.view, use the return value of [transitionContext viewForKey:UITransitionContextToViewKey].

If your app needs to support iOS7 and/or Xcode 5, then you can use a simple category method on UIViewController like the following:

- (UIView *)viewForTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
NSString *key = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey] == self ? UITransitionContextFromViewKey : UITransitionContextToViewKey;
return [transitionContext viewForKey:key];
} else {
return self.view;
}
#else
return self.view;
#endif
}

Then, get your toViewController and fromViewController as usual, but get the views using [toViewController viewForTransitionContext:transitionContext].

Edit: There appears to be a bug, where the presenting view controller's view is nil when returned from viewForKey, which prevents you from making modal transitions that animate the presenting view at all (such as sliding off, or flip-horizontal). I filed a bug for iOS8 at rdar://17961976 (http://openradar.appspot.com/radar?id=5210815787433984). Also see the sample project at http://github.com/bcherry/TransitionBug

Edit 2: Thanks to graveley for the suggestion, using UIModalPresentationFullScreen fixes the issue. Perhaps this is not a bug. Apple may intend that UIModalPresentationCustom only modifies the view of the incoming modal. If you want to modify the outgoing view, you need to guarantee full screen presentation of the new view? In any case, you should use viewForKey and UIModalPresentationFullScreen.

Not setting modalPresentationStyle to UIModalPresentationCustom fixed the issue for me.

In other words, leaving at the default of UIModalPresentationFullScreen instead of specifying UIModalPresentationCustom fixed the disappearing view issue. Note the UIViewControllerTransitioningDelegate protocol still seems to be followed even when leaving this at the default. If i recall correctly, once upon a time UIModalPresentationCustom was a requirement.

Works so far, have only tried this for non-interactive animations.

In iOS 8, you need create a UIPresentationController and implement the method below, in the UIViewControllerTransitioningDelegate.

- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source;

Asks your delegate for the custom presentation controller to use for managing the view hierarchy when presenting a view controller.

Return Value:

The custom presentation controller for managing the modal presentation.

Discussion:

When you present a view controller using the UIModalPresentationCustom presentation style, the system calls this method and asks for the presentation controller that manages your custom style. If you implement this method, use it to create and return the custom presentation controller object that you want to use to manage the presentation process.

If you do not implement this method, or if your implementation of this method returns nil, the system uses a default presentation controller object. The default presentation controller does not add any views or content to the view hierarchy.

Availability Available in iOS 8.0 and later.

For more information watch the WWDC 2014 video:

https://developer.apple.com/videos/wwdc/2014/?include=228

There's also a sample code from the WWDC called "LookInside: Presentation Controllers Adaptivity and Custom Animator Objects", which you can download from the WWDC 2014 sample code page.

You may need to change the sample code a little bit. The UIPresentationController init method changed to:

initWithPresentedViewController:presented presentingViewController:presenting

Before it was presenting and then presented. Just swap them and it should work.

Here is an Objective C version of Ash's fix.

// my attempt at obj-c version of Ash's fix
UIView *theToView = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
[[[UIApplication sharedApplication] keyWindow] addSubview:theToView];
[transitionContext completeTransition:YES]

I had to swap the order and call the [transitionContext completeTransition:] method after adding the view back in to get presenting a new view controller from the dismissal completion block of another view controller to work right.

I don't know that this'll fix it for everyone but it works in my app. Cheers!

I found this worked fine for Obj-C:

    [transitionContext completeTransition:YES];
if(![[UIApplication sharedApplication].keyWindow.subviews containsObject:toViewController.view]) {
[[UIApplication sharedApplication].keyWindow addSubview:toViewController.view];
}

Seems to work fine on both ios7 and ios8.

I feel like the reasoning behind this should be explained better.

The view disappears because you take out the presenting view controller's view out of its original location (view hierarchy), put it inside the containerView that your animator provides but never returns it back after the animation has finished. So that view controller's view is removed with its superview (containerView) from the window completely.

In iOS 7 the system always returned view controllers' views that are involved in the presentation (presenting and presented) to their original places after the transition has finished animating automatically. That no longer happens for some presentation styles in iOS 8.

The rule is very simple: the animator should only manipulate the presenting view controller's view if that view controller's view is going to be hidden (removed from the view hierarchy) completely by the end of transition. In other words it means that after the initial presentation animation finishes only the presented view controller's view will be visible and not the presenting view controller's view. For example if you set presented view controller's view's opacity to 50% and use UIModalPresentationFullScreen you will not be able to see presenting view controller's view underneath the presented but if you use UIModalPresentationOverFullscreen - you will (UIPresentationController's shouldRemovePresentersView method is responsible for specifying that).

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.

In iOS 8 viewForKey: method was introduced to get views that the animator manipulates. First, it helps to follow the rule described above by returning nil whenever the animator should not touch the view. Second, it may return a different view for the animator to animate. Imagine that you are implementing a presentation similar to form sheet. In this case you would want to add some shadow or decoration around the presented view controller's view. The animator will animate that decoration instead and the presented view controller's view will be a child of the decoration.

viewControllerForKey: doesn't go away, it can still be used if a direct access to view controllers is needed but the animator should not make any assumptions about the views it needs to animate.

There are several things you can do to correctly fix an issue with a disappearing presenting view controller's view when you explicitly place it inside the animator's container view:

  1. If do not need to animate the presenting view controller's view, use viewForKey: to get views to animate instead of reaching out to view controller's views directly. viewForKey: may return nil or even completely different views.

  2. If you want to animate the presenting view controllers's view you should consider using UIModalPresentationFullScreen style or continue using UIModalPresentationCustom and implement your own subclass of UIPresentationController with shouldRemovePresentersView returning YES. In fact, the implementation of this method is the main difference between internal presentation controllers defined by UIModalPresentationFullScreen and UIModalPresentationCustom styles apart from the fact that the latter allows you to use custom presentation controllers.

  3. In all other rare cases you will have to return the presenting view controller's view to its original location as other answers suggested.

Using the new UIModalPresentationOverCurrentContext fixed it for me. My original transition on iOS 7 was just to have a blurred background of the view underneath the modal.

I found that viewForKey:UITransitionContextToViewKey returns nil on ios8. So if it's nil, I grab the view from the 'to' view controller.

However, this seems to result in the 'to' view not being moved from the container to the window when completeTransition:YES is called. So if viewForKey:UITransitionContextToViewKey returns nil, I fall over to toVC.view, and keep track of the fact that it returned nil, and after the completion I move it to the container's initial superview (which happens to be the window).

So this code works on iOS7 as well as iOS8, and should work on iOS9 too even if they fix it or not.

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
// Get the 'from' and 'to' views/controllers.
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
BOOL hasViewForKey = [transitionContext respondsToSelector:@selector(viewForKey:)]; // viewForKey is iOS8+.
UIView *fromView = hasViewForKey ?
[transitionContext viewForKey:UITransitionContextFromViewKey] :
fromVC.view;
UIView *toView = hasViewForKey ?
[transitionContext viewForKey:UITransitionContextToViewKey] :
toVC.view;


// iOS8 has a bug where viewForKey:to is nil: http://stackoverflow.com/a/24589312/59198
// The workaround is: A) get the 'toView' from 'toVC'; B) manually add the 'toView' to the container's
// superview (eg the root window) after the completeTransition call.
BOOL toViewNilBug = !toView;
if (!toView) { // Workaround by getting it from the view.
toView = toVC.view;
}
UIView *container = [transitionContext containerView];
UIView *containerSuper = container.superview; // Used for the iOS8 bug workaround.


// Perform the transition.
toView.frame = container.bounds;
[container insertSubview:toView belowSubview:fromView];
[UIView animateWithDuration:kDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
fromView.frame = CGRectOffset(container.bounds, 0, CGRectGetHeight(container.bounds));
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];


if (toViewNilBug) {
[containerSuper addSubview:toView];
}
}];
}

I found that this bug (and many more!) vanishes if you set modalPresentationStyle = UIModalPresentationFullScreen. You of course still get your custom transition animation.

I've found this extremely useful answer in a related thread by Lefteris: https://stackoverflow.com/a/27165723/3709173

To sum it up:

  1. set modalPresentationStyle to .Custom
  2. subclass UIPresentationController, override shouldRemovePresentersView (with NO)
  3. override presentationControllerForPresentedViewController in your TransitionDelegate class and return your custom UIPresentationController

+1 in your custom transition, don't add toView when the dismissal animation is happening.

Demonstrated here:

https://www.dropbox.com/s/7rpkyamv9k9j18v/CustomModalTransition.zip?dl=0 without any hacks! it's like magic! :)

I got stuck on this issue too. I was looking to create a custom transition with a semi-transparent background where I could still see the view controller I was coming from but I only got a black background. I found Mark Aron's answer in this thread helped me but it is written in Objective C so here is a Swift 3 version of that answer which I have tested for iOS 9 and iOS 10:

  1. Create a subclass of UIPresentationController. Override the shouldRemovePresentersView to false as follows:

    class ModalPresentationController: UIPresentationController {
    
    
    override var shouldRemovePresentersView: Bool {
    return false
    }
    
    
    override func containerViewWillLayoutSubviews() {
    presentedView?.frame = frameOfPresentedViewInContainerView
    }
    }
    
  2. In the place you are instantiating the new view controller and setting its transition delegate, indicate that you want it to show a custom modal presentation style as follows:

    let newVC = mainStoryboard.instantiateViewController(withIdentifier: "newVC") as! NewViewController
    
    
    newVC.transitioningDelegate = self
    
    
    newVC.modalPresentationStyle = UIModalPresentationStyle.custom
    
    
    newVC.modalPresentationCapturesStatusBarAppearance = true //optional
    
    
    present(newVC, animated: true, completion: nil)
    
  3. Now override the presentationController method of your UIViewControllerTransitioningDelegate and return your custom UIPresentationController. I had mine as an extension to my current class:

    extension CurrentViewController: UIViewControllerTransitioningDelegate {
    
    
    //this is where you implement animationController(forPresented) and animationController(forDismissed) methods
    
    
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
    
    
    return ModalPresentationController(presentedViewController: presented, presenting: source)
    
    
    }
    }
    

One other thing to note is you should not try and reference your fromView in your presentAnimator class. This will be nil and you will get an error at runtime. Other than that if you implement things like things you will get your custom transition with its animation and a semi-transparent background if you make one.

After encountering this issue, I was very confused, because I'd written something almost identical not too long ago that worked fine. Came here looking for answers to find fixes that look pretty hacky, and don't seem to understand the root cause... it's actually very easy to fix.

Some answers mention changing modalPresentationStyle to .overFullScreen. This is correct, .overCurrentContext would work too. This is expected, and the behaviour Apple documents. But why isn't this working for everyone? Why all the hacky code, and combinations of this with something else, and crazy stuff that you shouldn't be doing?

Turns out, you need to set the presentation style BEFORE THE VIEW LOADS. Not after. Do it in init, or do it from the previous controller, or however you'd like - as long as it is before the view loads.

Ok, guys, I think that I solve one case that 'a working animator' stops working properly when you build app in iOS 13 and above.

Env Xcode 11.1, iOS 13.1

Problem

What I want to do is very simple: I have a collection view, when a cell is tapped, it will segue to a detail view. Instead of using the boring default style of 'present modally', I want to make it more interesting, so I wrote an animator for view controller transition.

I set up the segue in IB by drag-and-drop from my collection VC to the detail VC. The style of segue is 'Present modally' and presentation is set to 'Full Screen'.

When it shows the detail view, everything works as expected. However, when I dismiss the detail view and return to the collection view, I can only see the animated detail view, the collection view is just gone. I poked here and there and I have a few discoveries

1.Right after the following line is called from function 'animateTransition()', the collection view resumes and shows up

transitionContext.completeTransition(true)

2.As long as the detail view is not fully cover the collection view, the collection view will not disappear when it segues back from detail view

Solution

To be honest, I know little about how the animated transition works. So I can only follow this post and the other one, try each of the answers. Unfortunately, none of them works for me. Finally, I came to a point where the only thing I can tweak is the presentation style of segue in IB (which I should have done at very very beginning). When I set the presentation to 'Over Full Screen', miracle happens and my problem is solved. The detail view could show in full screen with animation and when it dismissed, I can see both collection view as background and animated detail view.

Then one more discovery along the road

3.To refer to 'toView' and 'fromView', both of the following methods work

Indirectly way:

transitionContext.viewController(forKey: .to)?.view
transitionContext.viewController(forKey: .from)?.view

Directly way:

transitionContext.view(forKey: .to)
transitionContext.view(forKey: .from)

But when I switched the segue style to 'Over Full Screen', the directly way return 'nil' for both 'toView' and 'fromView' and only indirectly way works, this problem is also mentioned in another post, so I think it's worth to post my little discovery here.

Hope this will be helpful to someone in the future.

I was having the same problem when dismissing a content view controller.

My app has this parent view controller showing a child view controller (presenting vc) modally. Then when a subview in the childVC is tapped, it shows another vc (which I am calling the content view controller (presented vc))

My problem is that, when Dismissing the contentVC (now the presenting vc), it should go to child VC (now the presented VC) but as soon as my custom transition finishes, childVC suddenly disappears, showing the parent VC.

What I did to solve this issue is to

  1. change the .modalPresentationStyle of the childVC presented by parentVC from the default .automatic to .fullscreen.
  2. Then changed the .modalPresentationStyle of contentVC to .fullscreen as well.

This solves the issue. but it won't show your child VC as a card-style sheet on top of parentVC (when using .overCurrentContext or automatic) which is new in iOS 13.

Would love to know if there are any solution that will retain the card-style sheet for the childVC when presented by parent.