foreword
This article is a little note I made after learning onevcat's introduction to this transition.
Today, let's implement a simple custom transition. Let's take a look at a rendering that this article will implement:
Detailed process
warm up
We first create a project, first use storyboard to quickly create two controllers, one as the main controller, called ViewController
, and the other as the presented controller, called PresentViewController
, and use autoLayout to quickly build the interface. like this:
We first do the logic of clicking the button above ViewController
, presenting it PresentViewController
, clicking PresentViewController
the button above, and dismissing it. PresentViewController
There are two points to note here:
Because I use it here
segue
, when theViewController
button is clicked, we only need to call it like this.#pragma mark - 点我弹出 -(IBAction)presentBtnClick:(UIButton *)sender { [self performSegueWithIdentifier:@"PresentSegue" sender:nil]; }
When we usually write dismiss, we usually send
dismissViewController
related methods directly to self in the second controller. In the current SDK, if the current VC is displayed, the message will be forwarded directly to the VC that displayed it. But this is not a good implementation, it violates the philosophy of programming, and it is easy to fall into the pit. So we do it in a standarddelegate
waydismiss
.
First we PresentViewController
declare a proxy method in the controller.
#import <UIKit/UIKit.h>
@class PresentViewController;
@protocol PresentViewControllerDelegate <NSObject>
- (void)dismissViewController:(PresentViewController *)viewController;
@end
@interface PresentViewController : UIViewController
@property (nonatomic, weak) id<PresentViewControllerDelegate> delegate;
@end
In the button's click event, let the delegate do the work of closing the current controller.
#pragma mark - 点击关闭
- (IBAction)closeBtnClick:(UIButton *)sender {
if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) {
[self.delegate dismissViewController:self];
}
}
At the same time, ViewController
you need to set PresentViewController
the proxy in and implement the proxy method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"PresentSegue"]) {
PresentViewController *presetVC = segue.destinationViewController;
presetVC.delegate = self;
}
}
#pragma mark - PresentViewControllerDelegate
- (void)dismissViewController:(PresentViewController *)viewController {
[self dismissViewControllerAnimated:YES completion:nil];
}
OK, here, we have completed a basic transition (this is also an effect that comes with the system). like this:
main content
Next, to touch the main content we are going to talk about today, we use a new class in iOS7 UIViewControllerTransitioning
to implement custom transitions.
UIViewControllerAnimatedTransitioning
First we need an UIViewControllerAnimatedTransitioning
object that implements the protocol named. Creating a class is called PresentAnimation
inheriting from NSObject
and implementing the UIViewControllerAnimatedTransitioning
protocol. ( Note: UIKit framework needs to be imported )
@interface PresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>
This agreement is responsible for the specific content of the transition. When developers make custom switching effects, most of the code will be used to implement this protocol. There are only two methods that must be implemented in this protocol:
// 返回动画的时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// 在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
implement these two methods
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
return 0.8f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
// 1.我们需要得到参与切换的两个ViewController的信息,使用context的方法拿到它们的参照;
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 2.对于要呈现的VC,我们希望它从屏幕下方出现,因此将初始位置设置到屏幕下边缘;
CGRect finaRect = [transitionContext finalFrameForViewController:toVC];
toVC.view.frame = CGRectOffset(finaRect, 0, [UIScreen mainScreen].bounds.size.height);
// 3.将view添加到containerView中;
[[transitionContext containerView] addSubview:toVC.view];
// 4.开始动画。这里的动画时间长度和切换时间长度一致。usingSpringWithDamping的UIView动画API是iOS7新加入的,描述了一个模拟弹簧动作的动画曲线;
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
toVC.view.frame = finaRect;
} completion:^(BOOL finished) {
// 5.在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。
[transitionContext completeTransition:YES];
}];
}
important point
UITransitionContextToViewControllerKey
AndUITransitionContextFromViewControllerKey
For example, if B is presented from A, then A isFromViewController
, B isToViewController
If from B dismiss to A, then A isToViewController
, B isFromViewController
UIViewControllerTransitioningDelegate
The function of this interface is relatively simple. When VC switching is required, the system will ask the object that implements this interface whether to use a custom transition effect.
So, a better place is ViewController
to implement this protocol directly in the main controller.
ViewController
Complete the following code in :
@interface ViewController ()<PresentViewControllerDelegate,UIViewControllerTransitioningDelegate>
@property (nonatomic, strong) PresentAnimation *presentAnimation;
@end
@implementation ViewController
#pragma mark - 懒加载
- (PresentAnimation *)presentAnimation {
if (!_presentAnimation) {
_presentAnimation = [[PresentAnimation alloc] init];
}
return _presentAnimation;
}
#pragma mark - UIViewControllerTransitioningDelegate
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return self.presentAnimation;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"PresentSegue"]) {
PresentViewController *presetVC = segue.destinationViewController;
presetVC.delegate = self;
presetVC.transitioningDelegate = self;
}
}
Now let's take a look at our effect:
Compared with the effect of the above system, when we present the second controller, there is a spring effect.
Gesture-driven percentage toggle
Now we add a function, which is to use gesture swipe to dismiss. In layman's terms, it is to let the presented controller use gesture to dismiss.
Create a class that inherits from
UIPercentDrivenInteractiveTransition
#import <UIKit/UIKit.h> @interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController; @end
- We write a method for external class calls. Entry to the VC that allows external classes to see the incoming gesture dismiss.
Since the VC that needs gesture dismiss is passed in, we need to save it so that the current class can be used elsewhere, so we create a new property to save the incoming VC.
#import "PanInteractiveTransition.h" @interface PanInteractiveTransition () @property (nonatomic, strong) UIViewController *presentVC; @end @implementation PanInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController { self.presentVC = viewController; UIPanGestureRecognizer *panGestR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)]; [self.presentVC.view addGestureRecognizer:panGestR]; } #pragma mark - panGestureAction -(void)panGestureAction:(UIPanGestureRecognizer *)pan { CGPoint transition = [pan translationInView:self.presentVC.view]; NSLog(@"%.2f",transition.y); switch (pan.state) { case UIGestureRecognizerStateBegan:{ [self.presentVC dismissViewControllerAnimated:YES completion:nil]; } break; case UIGestureRecognizerStateChanged:{ // CGFloat percent = MIN(1.0, transition.y/300); [self updateInteractiveTransition:percent]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ if (pan.state == UIGestureRecognizerStateCancelled) { // 手势取消 [self cancelInteractiveTransition]; }else{ [self finishInteractiveTransition]; } } break; default: break; } }
As with creation
PresentAnimation
, we create aDismissAnimation
class@interface DismissAnimation : NSObject<UIViewControllerAnimatedTransitioning> @end @implementation DismissAnimation -(NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.4f; } -(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initRect = [transitionContext initialFrameForViewController:fromVC]; CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height); UIView *contrainerView = [transitionContext containerView]; [contrainerView addSubview:toVC.view]; [contrainerView sendSubviewToBack:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = finalRect; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } @end
Finally, we add a gesture-driven object to the main controller, a dismiss transition object, and lazy load.
-(PanInteractiveTransition *)paninterTransition { if (!_paninterTransition) { _paninterTransition = [[PanInteractiveTransition alloc] init]; } return _paninterTransition; } -(DismissAnimation *)dismissAnimation { if (!_dismissAnimation) { _dismissAnimation = [[DismissAnimation alloc] init]; } return _dismissAnimation; } #pragma mark - UIViewControllerTransitioningDelegate -(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return self.dismissAnimation; } -(id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator { return self.paninterTransition; } -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"PresentSegue"]) { // ... [self.paninterTransition panToDismiss:presetVC]; } }
Complete
At this point, when we run the program, we will find that although the above code can be gesture-driven, the function of clicking the button to dismiss cannot be used. This is because if you just return self.paninterTransition, the animation of clicking the button dismiss will be invalid; if you just return nil, then the effect of gesture sliding will be invalid . In summary, we consider the score situation.
Next we will improve it.
Add an attribute to
PanInteractiveTransition
indicate whether it is in the switching process (used to determine whether to click the button to dismiss or gesture-driven to dismiss)// 是否处于切换过程中 @property (nonatomic, assign, getter=isInteracting) BOOL interacting;
Add an attribute to
PanInteractiveTransition
indicate whether dismiss is required (used to dismiss when the gesture exceeds the specified height, if not, it will be restored)@property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
Modified
PanInteractiveTransition
methodpanGestureAction:
:-(void)panGestureAction:(UIPanGestureRecognizer *)pan { CGPoint transition = [pan translationInView:pan.view]; switch (pan.state) { case UIGestureRecognizerStateBegan:{ self.interacting = YES; [self.presentVC dismissViewControllerAnimated:YES completion:nil]; } break; case UIGestureRecognizerStateChanged:{ // CGFloat percent = fmin(fmax(transition.y/300.0, 0.0), 1.0); self.shouldComplete = (percent > 0.5); [self updateInteractiveTransition:percent]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ self.interacting = NO; // 如果下移的距离小于300或者取消都当做取消 if (!self.isShouldComplete || pan.state == UIGestureRecognizerStateCancelled) { // 手势取消 [self cancelInteractiveTransition]; }else{ [self finishInteractiveTransition]; } } break; default: break; } }
Another point is that you need to modify
DismissAnimation
a piece of code:-(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initRect = [transitionContext initialFrameForViewController:fromVC]; CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height); UIView *contrainerView = [transitionContext containerView]; [contrainerView addSubview:toVC.view]; [contrainerView sendSubviewToBack:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = finalRect; } completion:^(BOOL finished) { // 此处做了修改,由之前的[transitionContext completeTransition:YES]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; }
ok, so far, one of our custom transition animations is done.