Disclaimer: I wrote four articles for this framework:
[iOS]UINavigationController Full screen POP for each controller custom UINavigationBar
[iOS]UINavigationController full screen POP adds bottom linkage view for each controller
[iOS]UINavigationController full screen POP for controller add left slide push
[iOS] and pop gestures cause AVPlayer to stall
The framework characteristics
✅ Full screen pop gesture support
✅ Full screen push to bound controller support
✅ Customize UINavigationBar support for each controller (including setting colors, transparency, etc.)
✅ Add bottom linkage view support for each controller
✅ Custom POP gesture range support (width calculated from the far left of the screen)
✅ turns off pop gesture support for a single controller
✅ turn off pop gesture support for all controllers
❤️ When the current controller uses AVPlayer to play video, use custom pop animation to ensure smooth playback of AVPlayer.
This is the third article in the “UINavigationController Full Screen POP” series, this time showing how to implement a left slide push into a bound controller with a push animation. If you haven’t read my previous two articles, I suggest you start with the first one. Or you can just go to my GithubJPNavigationControllerThe source code.
01, probe,
Friends who have used news software should know that netease News, for example, if you swipe left on its news details page, it will appear a push animation to open the comment page. This time we will discuss how to implement this functionality based on the previous encapsulation.
02. How are existing apps implemented in the world?
With the help of Reveal observation, the left-slide push to the next page can be roughly divided into two categories:
- The first category is apps represented by mainstream news such as netease News, Tencent News and Phoenix News. They all bind the function of left-swipe gesture push to the comment page on the news details page.
- The second category, foreign social apps represented by Instagram and Snapchat, are in the
UITabBarController
Some of the branches of the left swipe and right swipe gesture bindings have the ability to switch to different controllers.
According to Reveal observation, the function of left-swipe gesture of the first type is integrated into the UINavigationController corresponding to the current controller. The second type is to integrate a UICollectionView on the root controller of the Window, and then add the view of each controller to the UICollectionViewCell. In this way, the effect of swipe left and swipe right to switch to different controllers can be realized. The second type and I common news page sub column switch is the same truth, I believe we will achieve. Now we’re going to show you how to integrate the left swipe gesture into the UINavigationController that corresponds to the current controller.
The mainstream framework structure of iOS is as shown in the figure above. If you want to achieve the left-sliding function of the second type of APP, it is bound to need to rebuild the project, which is quite heavy for many mature apps. Therefore, it is worth trying to implement the left slide push function without changing the existing project architecture. That is, bind the swipe left gesture to the corresponding navigation controller.
03. AOP faceted programming idea
IOS engineers know about runtime, the runtime, and thanks to the nature of The Objective-C Runtime, we can dynamically add methods to classes and replace system implementations. If this behavior is abstracted into a more advanced idea, it is known as AOP (AOP is an acronym for Aspect Oriented Program), or section-oriented programming. See the explanation on Wikipedia or answer on Zhihu for details on AOP. The framework is also aOP-based, so it can implement the above features without intruding on the user’s project.
04, General idea
- 01. First we need to get the user to swipe left
left-slip
This event - 02. Next, ask the user if you want to bind the left swipe gesture to the corresponding push event
- 03. If the user is bound to events, the controller should be created to push to
- 04. Track user gestures and drive transition animations
- 05. Complete the Push gesture
05, concrete implementation
5.1. First, we need to get the event of user left-slip
As I mentioned in my second article, the framework uses UIPanGestureRecognizer instead of the system’s gestures, so we can check the UIPanGestureRecognizer’s proxy method to see if the user swiped left.
-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer{
// System pop action.
SEL action = NSSelectorFromString(@"handleNavigationTransition:");
CGPoint translation = [gestureRecognizer velocityInView:gestureRecognizer.view];
if (translation.x<0) {
// left-slip --> push.
UIViewController *rootVc = [UIApplication sharedApplication].keyWindow.rootViewController;
UIImage *snapImage = [JPSnapTool snapShotWithView:rootVc.view];
NSDictionary *dict = @{
@"snapImage" : snapImage,
@"navigationController" : self.navigationController
};
[[NSNotificationCenter defaultCenter]postNotificationName:@"NavigationDidSrolledLeft" object:dict userInfo:nil];
[gestureRecognizer removeTarget:_target action:action];
return YES;
}
else{
// right-slip --> pop.
[[NSNotificationCenter defaultCenter]postNotificationName:@"NavigationDidSrolledRight"object:self.navigationController userInfo:nil]; [gestureRecognizer addTarget:_target action:action]; }}Copy the code
5.2. Next, ask the user if they need to bind the corresponding push event to the swipe left gesture
The first step is to create a protocol so that each controller can have push functionality as long as the protocol is followed and the protocol methods are implemented.
/ *! * \~english * Just follow the JPNavigationControllerDelegate protocol and override the delegate-methodin this protocol use [self.navigationController pushViewController:aVc animated:YES] if need push gesture transition animation when left-slip.
* You should preload the data of next viewController need to display for* * \~ Chinese * If you want to implement a left swipe gesture animation for push in an interface, just follow this protocol and implement the following protocol methods, In agreement method using [self navigationController pushViewController: aVc animated: YES], it can have left slide push animation. * about data preloading, in order to obtain a good user experience, Advice before you push the push to page request to the local data, push can directly display the data in the past. * / @ protocol JPNavigationControllerDelegate < NSObject > @ required / *! * \~english * The delegate method need to overrideifNeed push gesture transition animation when left-slip. * * \~ Chinese * Need push gesture animation when left-slip. */ -(void)jp_navigationControllerDidPushLeft; @endCopy the code
Because we want to have a left-sliding push binding on every page, we can bind the agent that asks the user if they want a push to the navigationController of each controller.
/ *! * \~english * The delegatefor functionOf left-slip to push next viewController. * * \~ Chinese * Implements left-slip to push next controller proxy. */ @property(nonatomic)id<JPNavigationControllerDelegate> jp_delegate;Copy the code
5.3. If the user is bound to events, the controller should be created to push to
Because you’ve already added a portal for each controller to check if a push animation is needed. So, when a user push is detected, you should start checking that the user complies with the protocol and implements the protocol methods to determine if you need to create a push animation.
-(void)didPushLeft:(JPNavigationInteractiveTransition *)navInTr{ // Find the displaying warp navigation controller first now when left-slip, check this navigation is overrided protocol method or not after,ifYes, call this method. // When left pushing, first find the navigation controller currently in the window for packaging, and then check whether the controller complies with the left push protocol, see if the interface implements the left push proxy method, if it does, The proxy method is executed. NSArray *childs = self.childViewControllers; JPWarpViewController *warp = (JPWarpViewController *)childs.lastObject; JPWarpNavigationController *nav = (JPWarpNavigationController *)warp.childViewControllers.firstObject;if (nav) {
if([nav.jp_delegate respondsToSelector:@selector(jp_navigationControllerDidPushLeft)]) { [nav.jp_delegate jp_navigationControllerDidPushLeft]; }}}Copy the code
When we detect that the user needs a push animation, we start preparing the push animation. When we give the pop animation to the system, we need to set the JPNavigationController’s delegate to nil, and we need to add the target for our custom UIPanGestureRecognizer, I talked about this in the first article. Since the pop has been handed over to the system, only the push animation is handled here. There is no push animation, so we have to do it ourselves. To delegate the push animation of the system, we need to be a delegate to the root navigation Controller (JPNavigationController), follow the protocol, and implement both require’s delegate methods.
In the first method, we check if it is a push operation, and if it is, we return our custom push animation object. At the same time, we need gestures to drive the animation process, so we need to create a gesture monitor to update the animation as the user swipes, which is the second method.
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { // If the animation operation now is push,returnCustom Transition. // Returns our custom Push animation object if the current operation is a Push.if (self.isGesturePush && operation == UINavigationControllerOperationPush) {
self.transitioning.snapImage = self.snapImage;
return self.transitioning;
}
return nil;
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
// If the animationController is custom push instance, returnInteractivePopTransition to manage Transition Progress. // The animationController is our custom Push animation object, Return the interactivePopTransition to monitor animation completion.if (self.isGesturePush && [animationController isKindOfClass:[JPPushnimatedTransitioning class]]) {
return self.interactivePopTransition;
}
return nil;
}
Copy the code
The code for creating a gesture monitor is as follows:
- (void)handleControllerPop:(UIPanGestureRecognizer *)recognizer {
// This method be called when pan gesture start, because entrust system handle pop, so only handle push here.
// Calculate the percent of the point origin-X / screen width, alloc UIPercentDrivenInteractiveTransition instance when push start, and check user is overrided the protocol method or not, if overrided, then start push and, setstart percent = 0. // Refresh the slip percent when pan gesture changed. // Judge the slip percent is more than the JPPushBorderlineDelta when pan gesture end Create a percentage-gesture-driven transition animation when the user starts pushing, check if the user has set push on the screen, if so, start pushing and set the starting percentage to 0. Determine whether the stop point has reached the agreed range that requires a POP. CGFloat progress = [recognizer translationInView:recognizer.view].x / recognizer.view.bounds.size.width; CGPoint translation = [recognizer velocityInView:recognizer.view];if (recognizer.state == UIGestureRecognizerStateBegan) {
self.isGesturePush = translation.x<0 ? YES : NO;
}
if (self.isGesturePush) {
progress = -progress;
}
progress = MIN(1.0, MAX(0.0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan) {
if (self.isGesturePush) {
if([self.delegate respondsToSelector:@selector(didPushLeft:)]) { self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init]; self.interactivePopTransition.completionCurve = UIViewAnimationCurveEaseOut; [self.delegate didPushLeft:self]; [self.interactivePopTransition updateInteractiveTransition:0]; }}}else if (recognizer.state == UIGestureRecognizerStateChanged) {
[self.interactivePopTransition updateInteractiveTransition:progress];
}
else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
if (progress > JPPushBorderlineDelta) {
[self.interactivePopTransition finishInteractiveTransition];
}
else {
[self.interactivePopTransition cancelInteractiveTransition];
}
self.interactivePopTransition = nil;
self.isGesturePush = NO;
// Set root navigation controller's delegate be nil for follow user'S gesture. // Empty the navigation controller agent, waiting for the next slide. Self.nav. delegate = nil; }}Copy the code
5.4. Track user gestures and drive transition animations
Remember that left push animation above? You might think it would be a push animation to reverse the system’s pop animation, compared to the default pop animation. If you think so, congratulations, your intuition is right! In fact, most of the time, we do a lot of things are imitating the implementation of the system, in guessing how the effect of the system is achieved, and then step by step to verify whether our idea is correct.
When you open up my demo and run it, what you see is the one on the left, and now I’m going to tell you, it actually looks like the one on the right. That is to say, at the moment when the user slides left, we need to add the elements needed to prepare the right image for animation, including the screenshot B of the View of the current controller and the screenshot C of the View of the controller to be pushed, and then add them to the container provided by the system for animation according to this layer relationship. The animation provider tells the system that we need the frames of the two elements B and C at the beginning of the animation, and the frames of the two elements at the end of the animation. This gesture-driven process, because we’ve given this listening process to the gesture-monitor and returned it to the system, so the process system takes care of it for us.
But the question is, why do we animate a screenshot instead of a View with two controllers? The reason for this is that when the tabBar is displayed in the current window, the tabBar layer is on top of the animation container layer, so we can’t elegantly do the perci gesture drive. So take this approach. However, the pop gesture of the system is not in the form of screenshots, but directly using the View of the two controllers to do animation, just like the following, but due to the permission problem, we can not do it like the system, but it does not rule out that some students think of clever ways to achieve it.
Take a look at the source of the animation provider:
- (void)animateTransitionEvent {
// Mix shadow for toViewController'view. CGFloat scale = [UIScreen mainScreen].scale/2.0; [self.containerView insertSubview:self.toViewController.view aboveSubview:self.fromViewController.view]; UIImage *snapImage = [JPSnapTool mixShadowWithView:self.toViewController.view]; // Alloc toView's ImageView
UIImageView *ivForToView = [[UIImageView alloc]initWithImage:snapImage];
[self.toViewController.view removeFromSuperview];
ivForToView.frame = CGRectMake(JPScreenWidth, 0, snapImage.size.width, JPScreenHeight);
[self.containerView insertSubview:ivForToView aboveSubview:self.fromViewController.view];
// Alloc fromView's ImageView UIImageView *ivForSnap = [[UIImageView alloc]initWithImage:self.snapImage]; ivForSnap.frame = CGRectMake(0, 0, JPScreenWidth, JPScreenHeight); [self.containerView insertSubview:ivForSnap belowSubview:ivForToView]; // Hide tabBar if need. UIViewController *rootVc = [UIApplication sharedApplication].keyWindow.rootViewController; if ([rootVc isKindOfClass:[UITabBarController class]]) { UITabBarController *r = (UITabBarController *)rootVc; UITabBar *tabBar = r.tabBar; tabBar.hidden = YES; } self.fromViewController.view.hidden = YES; [UIView animateWithDuration:self.transitionDuration animations:^{ // Interative transition animation. ivForToView.frame = CGRectMake(-shadowWidth*scale, 0, snapImage.size.width, JPScreenHeight); ivForSnap.frame = CGRectMake(-moveFactor*JPScreenWidth, 0, JPScreenWidth, JPScreenHeight); }completion:^(BOOL finished) { self.toViewController.view.frame = CGRectMake(0, 0, JPScreenWidth, JPScreenHeight); [self.containerView insertSubview:self.toViewController.view belowSubview:ivForToView]; [ivForToView removeFromSuperview]; [ivForSnap removeFromSuperview]; self.fromViewController.view.hidden = NO; [self completeTransition]; }]; }Copy the code
5.5. Complete the Push gesture
At this point, you’re almost done with push. Just tell the system at the end of the gesture whether the push succeeded or failed.
if (progress > JPPushBorderlineDelta) {
[self.interactivePopTransition finishInteractiveTransition];
}
else {
[self.interactivePopTransition cancelInteractiveTransition];
}
Copy the code
06, pay attention to
Note: tabBar always defaults to YES, and cannot be modified as transparent using JPNavigationCotroller. This is because after Xcode 9, Apple made some changes inside the navigation controller. If you make tabBar opaque, the UI in the current architecture will be confused. Or setting backgroundColor to an opaque color value is the same error.