Author: Wang Handong

background

Several projects developed recently use VCStack of the system, that is, UITabbarController + UINavigationController. This is a classic combination that can be used in a real world development scenario. However, the design rules of recent UI and UE drafts are a bit beyond the capabilities of this existing framework

  • Mask the half-floating layer of the full screen
  • Stack and stack complex animations
  • Across the VC stackpopandpushoperation

Under the existing structure of UITabbarController + UINavigationController, these functions have been realized, but the process is relatively complex, and a lot of logic now seems to have room for optimization. Based on this background, I plan to write a customized VCStack to solve the limitation of system space

The VCStack system has a dilemma

Before conceiving the custom VCStack, I reviewed the bottlenecks that exist in the daily development of system controls. These bottlenecks often trouble us in the daily business development of that, and drag down the efficiency of developers. In summary, there are the following points:

  • Page occlusion caused by high level of UI***Bar
  • Insufficiently friendly issue with push/push animation supportYes, we can through the way of NavigationControllerDelegate, the realization of the complete custom animation in the proxy. However, the access of this agent is often strongly dependent on a page, the level of abstraction is not enough, and the reusability is not high. Out of order
  • Problems with getTopVC at any point in timeThe operation of the stack is often accompanied by an animation, which contains time, and if we get getTopVC at the wrong time node it may cause subsequent UI operations to fail completely. For example, get topVC when the view is disappearing and add UI processing to vc.view
  • Problems with layout standards.Due to the existence of TopLayout and BottomLayout, our layout origin may change in some operations, which requires certain development experience to capture. Errors in layout can result from inadvertent omissions
  • Cross influence.Here's an example: Modifying the backItem of Navigation will cause the default optimization gesture to be invalidated. This function needs to be overwritten to take effect
  • Specifies a jump to the stack.The system currently does not provide a unified scheduling entrance to solve the problem of cross-VC jump, the current implementation is based on traversal to find the VC jump
  • Modal view continues to jump problemThis is a common scenario where there is a modal view, and on top of that modal view there are stack operations. Most current implementations pack a layer of NavigationController on top of the modal, giving it stack manipulation capabilities

The above cases enable us to customize the core problems solved by VCStack. This paper will also explain how to solve these problems one by one according to these pain points

What is a custom VCStack

What right now this VCStack first, the effect of the system NavigationController we are not strange, how to don’t NavigationController inheritance system implemented on the basis of a set of their own VCStack management mechanism (keep consistent effect principle)? From daily use, we know that the NavigationController of the system is actually a stack manager, among which the most important is the management of VC. It may be the top-level encapsulation that makes us know little about the whole management system. But a few things can be guessed at

2. All views are displayed on the root Window. 3

VC’s are independent and can be created and destroyed on any node. Our VCStack only needs to manage their display logic and existing life cycle. So VCStack just find the right point in time to stack and manage these VC’s. First, there is a unified entrance

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
Copy the code

In this node, the Window needs a rootViewController, which is the cut through which the VCStack accesses. A VC is created and held by the VCStack as a rootViewController. VCStackInstance. RootViewController as parameters to the Window. This step has laid the foundation for the VCStack, since all the vC. view superpositions now have rootViews. And then things get easier

1. Add vc.view to currentVC; 2. Remove vC. view from vC. view

There are a lot of things that need to be taken care of, like

1, VC life cycle consistent 2, gesture operation 3, animation access

Now that you have some idea of what you want to do, here are some implementation details

Then built

View level + layout origin

The custom VCStack will no longer have preset dependencies on TopLayout and BottomLayout. All views will be laid out from (0,0) on the window. NavigationBar and TabBar will also be replaced by CustomView to eliminate mask issues caused by large z-axis gaps between tiers.

Once the entire area of the Window is managed, the hierarchy and origin of the layout are no longer a problem, but this introduces other problems:

  1. Customizing navigationBar increases the cost per page development
  2. Customizing tabbars increases the cost per page of development

A good way to do this is to create a quick template class that encapsulates the common NavigationBar and common TabBar as template output to increase development efficiency

@interface UIViewController (NavigationBar) - (HDDefaultNaviBar *)defaultBar; @end - (HDDefaultNaviBar *)defaultBar { HDDefaultNaviBar *customerBar = [[HDDefaultNaviBar alloc] initWithFrame:CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.navigationBarHeight + HDScreenInfo.statusBarHeight)]; customerBar.backgroundColor = [UIColor whiteColor]; Customerbar. title = @" test title"; customerBar.backIcon = [UIImage imageNamed:@"NaviBack"]; customerBar.backAction = ^{ [self.vcStack popWithAnimation:[HDVCStackAnimation defaultAnimation]]; }; return customerBar; }Copy the code

Animation extensibility

The Navigation stack jumps don’t provide much API

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // Uses a horizontal slide transition. Has no effect if the view controller is already in the stack
- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated; // Returns the popped controller.
Copy the code

The animation supported in jumps is Bool, which limits the extensibility of animation in jumps. Design system of the people, of course, to be able to jump in the animation supported by higher granularity, implements the NavigationControllerDelegate this agreement, the VC in the integration of the agreement, the animation can be develop better, agreement is as follows:

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);
Copy the code

But there are still drawbacks. Think about it. At what level would such an agreement be implemented?

1. Direct coupling to VC that requires animation support? Unified proxy abstracted to UIViewController level?

In the actual use of 1, is one of the more, but there are problems of expansion and logical abstraction, the same problem in another scene, most of the reuse way is: copy + paste. You can make sense with fewer scenarios, but once you have more of them, the problem becomes obvious. Gradually, under the tone of using the system VCStack, some people will abstract the information at this level and make a unified management, forming the way of 2. However, there are problems in this way, so let’s take a look at the information at the abstract level:

  • currentVC
  • willShowVC
  • operation

The key point is operation, which is the enumeration type of the system, and does not fit well with the business scene, limiting the type of animation. This is equivalent to finding the pain point of this animation support. Now let’s talk about my thinking:

Handing out the animation entirely in the custom VCStack, in the form of an instance, seems a little hard to understand. How to unify the instance API? That’s where the protocol comes in. All animation instances inherit from the AnimationProtocol, which constrains the API so that all instances are scheduled consistently. The structure is as follows:

Below is the example generation API, which is written in practice for each of the unique animation protocols, and their implementation is included in the integrated protocol

@interface HDVCStackAnimation : NSObject <HDVCStackAnimationProtocol>
+ (instancetype)defaultAnimation;
@end
Copy the code

The protocol itself is consistent with the logic of the stack

@protocol HDVCStackAnimationProtocol <NSObject> - (void)pushWithWillShowVC:(UIViewController *)willShowVC currentVC:(UIViewController *)currentVC completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);  - (void)popWithWillShowVC:(UIViewController *)willShowVC currentVC:(UIViewController *)currentVC completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); @endCopy the code

The implementation of the protocol is also section-oriented, only need to pay attention to the current parameters and logic, for example, the following is a simulation system with stack animation protocol implementation

@implementation HDVCStackAnimation + (instancetype)defaultAnimation { return [HDVCStackAnimation new]; } - (void)pushWithWillShowVC:(UIViewController *)willShowVC currentVC:(UIViewController *)currentVC completion:(void Willshowvc.view. frame = CGRectMake(hdScreenInfo.width, 0, hdScreenInfo.width, HDScreenInfo.height); [UIView animateWithDuration:0.34 animations:^{willShowvc.view.frame = CGRectMake(hdScreenInfo.width / 3.0, 0, UIView animateWithDuration:0.34 animations:^{willShowvc.view.frame = CGRectMake(hdScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height); Currentvc.view. frame = CGRectMake(-hdScreenInfo.width / 3.0, 0, hdScreenInfo.width, hdScreenInfo.height); } completion:^(BOOL finished) {if (finished) {/* Restore the frame of the corresponding View to the logic without animation and ensure correct UI debugging */ willShowvc.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);  currentVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height); } completion(finished); }]; } - (void)popWithWillShowVC:(UIViewController *)willShowVC currentVC:(UIViewController *)currentVC completion:(void Willshowvc.view. frame = CGRectMake(-hdScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height); Currentvc.view. frame = CGRectMake(hdScreenInfo.width / 3.0, 0, hdScreenInfo.width, hdScreenInfo.height); [UIView animateWithDuration:0.34 animations:^{willShowvc.view.frame = CGRectMake(0, 0, hdScreenInfo.width, HDScreenInfo.height); currentVC.view.frame = CGRectMake(HDScreenInfo.width, 0, HDScreenInfo.width, HDScreenInfo.height);  } completion:^(BOOL finished) { completion(finished); }]; } @endCopy the code

Call API simplification:

[self.vcStack pushto:vc animation:[HDVCStackAnimation defaultAnimation]];
Copy the code

As you can see, the optimized animation API parameters are also three

  • currentVC
  • willShowVC
  • AnimationInstance

However, the space of animationInstance implementation is greatly increased. It only needs to inherit from AnimationProtocol, and the implementation of animation is completely left up to the business layer. If several sets of animation are adapted to the current scene in the design of the business layer, such abstraction will also be simplified to a few animation instances. It meets our requirements, extensibility and logical abstraction

GetTopVC + cross influence

After completely take over VCStack, for operating each details are at the mercy of the developer, when task touch up to may add AnimationCompletionHandle processing, to make this logic more robust. The existence of the same crossover influence is determined by the developer, and only if there is such crossover influence in the design can there be such logic in use. Design nodes are controlled by developers, and the need for logical interaction is no longer a black box

Specifies the VC jump

This function is often encountered in actual business, and is implemented on the basis of system Navigation as follows

1, traverse the navigationController. ViewControllers 2, find matching VC example 3, perform popToVC operations

The first two steps are almost inevitable, resulting in the existence of piles of code in the actual layout, which is not a good solution for code simplicity. Considering such requirements, VCStack integrates a set of quick jump apis to cover common service scenarios

/** push; Push an object to r in the current stack @param VC viewController that is about to be pushed @Param Animation pushto (UIViewController *) VC animation:(NSObject<HDVCStackAnimationProtocol> *)animation; / * * stack operation @ param animation stack animations * / - (void) popWithAnimation: (NSObject < HDVCStackAnimationProtocol > *) animation; /** the stack to root operation @param animation the stack animation type */ - (void)popToRootViewControllerWithAnimation:(NSObject<HDVCStackAnimationProtocol> *)animation; /** push to the specified vc operation, The matching condition is the current vc name @param vcName vc name to be displayed @param popAnimation stack animation */ - (void)popToVCWithName:(NSString *)vcName animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation; /** Stack to the specified VC, matching the instance object id pointer is equal. @param VC to display the VC instance @Param popAnimation Is mainly used for pop then push this operation * / - (void) popTo: (UIViewController *) vc animation: (NSObject < HDVCStackAnimationProtocol > *)popAnimation popCompleteHandle:(void (^)(BOOL))popCompletion; /** stack to the specified vc name, @param popVCName The name of the VC that will appear at the top of the stack @param popAnimation @param pushVC The INSTANCE of the VC that will be pushed @param pushAnimation */ - (void)popToVCWithName:(NSString *)popVCName animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation thenPushTo:(UIViewController *)pushVC animation:(NSObject<HDVCStackAnimationProtocol> *)pushAnimation; /** stack to the specified vc instance, @param popVC, the name of the VC that is going to appear at the top of the stack @param popAnimation, @param pushVC, the instance of the VC that is going to be pushed @param pushAnimation, */ - (void)popTo:(UIViewController *)popVC animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation thenPushTo:(UIViewController *)pushVC animation:(NSObject<HDVCStackAnimationProtocol> *)pushAnimation; @endCopy the code

The logical processing is already done inside the VCStack, and the business needs can be fulfilled with simple API calls

The modal view follows the stack jump

If there is still a stack jump in the modal view, the system VCStack base processing is basically a layer of VCStack on the modalVC, so that it has such a capability, but there is a problem, the two navigationStack indirect disconnection, If you do popToVC here it takes a lot of logical judgment. Using the custom VCStack, you can plan the presence of a modal view into a push, but the animation instance has changed here

@implementation HDModelAnimation + (instancetype)defaultAnimation { return [HDModelAnimation new]; } - (void)pushWithWillShowVC:(UIViewController *)willShowVC currentVC:(UIViewController *)currentVC completion:(void Willshowvc.view. frame = CGRectMake(0, hdScreenInfo.height, hdScreenInfo.width, HDScreenInfo.height); [UIView animateWithDuration:0.34 animations:^{willShowvc.view.frame = CGRectMake(0, 0, hdScreenInfo.width, HDScreenInfo.height); } completion:^(BOOL finished) { completion(finished); }]; } - (void)popWithWillShowVC:(UIViewController *)willShowVC currentVC:(UIViewController *)currentVC completion:(void [UIView animateWithDuration:0.34 animations:^{currentvc.viet.frame = CGRectMake(0, 0)  HDScreenInfo.height, HDScreenInfo.width, HDScreenInfo.height); } completion:^(BOOL finished) { completion(finished); }]; } @endCopy the code

This operation is equivalent to the appearance and disappearance of the modal view, while maintaining the VCStack chain

[self.vcStack pushto:vc animation:[HDModelAnimation defaultAnimation]];
[self.vcStack popWithAnimation:[HDModelAnimation defaultAnimation]];
Copy the code

details

In the custom VCStack design to a lot of detail operation, the improvement of these operations will make the whole VCStack more robust

Life cycle maintenance

In VCStack, in addition to view dependency management, synchronization operation also needs to manage the corresponding VC life cycle, which is the most frequently used in daily business scenarios

  • viewWillAppear
  • viewDidAppear
  • viewWillDisappear
  • viewDidDisappear
  • dealloc

In order to maintain consistency with the system life cycle, VC life cycle is manually handled in push and POP operations

- (void) pushto: (UIViewController *) vc animation: (NSObject < HDVCStackAnimationProtocol > *) animation {/ / add gestures to deal with [the self panGestureWithView:vc]; / / the ban on any gestures [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; [self.viewControllers addObject:vc]; [vc viewWillAppear:false]; [self.visibleViewController viewWillDisappear:false]; [self.visibleViewController.view addSubview:vc.view]; vc.vcStack = self; / / on the bottom of the tabBar do level operation if (vc) hdHideBottomBarWhenPushed) {/ / do nothing here [self. TabBarManager. View bringSubviewToFront: vc. View];  } the if (animation) {/ / animation start [animation pushWithWillShowVC: vc currentVC: self. VisibleViewController completion: ^ (BOOL finished) { if (finished) { [self.visibleViewController viewDidDisappear:true]; [vc viewDidAppear:true]; Self. VisibleViewController = vc; / / gestures disabled close [[UIApplication sharedApplication] endIgnoringInteractionEvents].}}]; } else {/ / gestures disabled close [self. VisibleViewController viewDidDisappear: false]; [vc viewDidAppear:false]; self.visibleViewController = vc; [[UIApplication sharedApplication] endIgnoringInteractionEvents]; } } - (void)popToVC:(UIViewController *)popToVC animation:(NSObject<HDVCStackAnimationProtocol> *)animation willDismissVC:(UIViewController *)willDismissVC popCompleteHandle:(void (^)(BOOL))popCompletion { if (popToVC) { // Base reference chain willdismissvc. vcStack = nil; / / the ban on any gestures [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; if (animation) { [popToVC viewWillAppear:true]; [willDismissVC viewWillDisappear:true]; [animation popWithWillShowVC:popToVC currentVC:willDismissVC completion:^(BOOL finished) { if (finished) { [willDismissVC.view removeFromSuperview]; [willDismissVC viewDidDisappear:true]; [popToVC viewDidAppear:true]; Self. VisibleViewController = popToVC; / / gestures disabled close [[UIApplication sharedApplication] endIgnoringInteractionEvents];  // completion handle if (popCompletion) { popCompletion(finished); } } }]; } else { [popToVC viewWillAppear:false]; [willDismissVC viewWillDisappear:false]; [willDismissVC.view removeFromSuperview]; [willDismissVC viewDidDisappear:false]; [popToVC viewDidAppear:false]; self.visibleViewController = popToVC; / / gestures disabled close [[UIApplication sharedApplication] endIgnoringInteractionEvents]; if (popCompletion) { popCompletion(YES); } } } else { if (popCompletion) { popCompletion(NO); }}}Copy the code

Dealloc can be detected by the system when the holding chain disappears and can be released normally. The current holding relationship is as follows:

  • VCStack holds an array
  • The array holds VC
  • Vc weakly holds VCStack

VC weak hold VCStack is in order to compatible with the existence of tabBarController, if the project is a single VCStack can be used to improve the singleton instance. Active release of all dependencies during pop

VC.vcStack = nil 
VCStack.array remove VC
Copy the code

Gesture system maintenance

Every time you push, you add a gesture system to the View hierarchy, and of course there is protocol support, if the VC implements the protocol

@protocol HDVCEnableDragBackProtocol <NSObject>
- (BOOL)enableDrag;
@end
Copy the code

And marked NO, this page does not support gestures. The concrete implementation is as follows:

- (void) pushto: (UIViewController *) vc animation: (NSObject < HDVCStackAnimationProtocol > *) animation {/ / add gestures to deal with [the self panGestureWithView:vc]; . } - (void)pangestureWithView:(UIView *)view completeHandle:(void (^)(void))completeHandle { UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; self.successBlock = completeHandle; [view addGestureRecognizer:panGesture]; } - (void)pan:(UIPanGestureRecognizer *)pan {// UIView *view = pan.view; / / is going to show the View of the if (self. ViewControllers. Count > 1) {UIViewController * bottomViewController = self.viewControllers[self.viewControllers.count - 2]; UIView *bottomView = bottomViewController.view; Static CGPoint startViewCenter; static CGPoint startBottomViewCenter; static BOOL continueFlag = YES; If (view && bottomView) {/ / drag start testing the if (pan) state = = UIGestureRecognizerStateBegan) {/ / drag the view at the beginning of the frame needs to change, Bottomview. frame = CGRectMake(-hdScreenInfo.width / 3.0, 0, hdScreenInfo.width, hdScreenInfo.height); View. frame = CGRectMake(hdScreenInfo.width / 3.0, 0, hdScreenInfo.width, hdScreenInfo.height); CGPoint startPoint = [pan locationInView:view]; // Check whether the current drag position is at the appropriate point. If (startPoint.x > (view.frame.sie.width / 3.0)) {continueFlag = NO; } else { continueFlag = YES; BottomView addSubview:self.maskView]; } startViewCenter = view.center; startBottomViewCenter = bottomView.center; } else if (pan) state = = UIGestureRecognizerStateChanged) {if (continueFlag) {/ / get the offset of a CGPoint transition = [pan translationInView:view]; View. center = CGPointMake(startViewCenter.x + transition.x / 3.0 * 2.0, startViewCenter.y); Bottomview. center = CGPointMake(startbottomViewCenter. x + transition.x / 3.0, startBottomViewCenter.y); }} else if (pan) state = = UIGestureRecognizerStateEnded) {if (continueFlag) {/ / remove mask view the if (self) maskView) superview ! = nil) { [self.maskView removeFromSuperview]; } if (view.center.x > (view.frame.size.width / 6.0 * 7.0)) {if (self.successBlock) {self.successBlock(); }} else {/ / banned user action [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; [UIView animateWithDuration:0.34 animations:^{view.frame = CGRectMake(hdScreenInfo.width / 3.0, 0, UIView animateWithDuration:0.34 animations:^{view.frame = CGRectMake(hdScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height); Bottomview. frame = CGRectMake(-hdScreenInfo.width / 3.0, 0, hdScreenInfo.width, hdScreeninfo.height); } completion:^(BOOL finished) {if (finished) {// Unlock user gestures [[UIApplication sharedApplication] EndIgnoringInteractionEvents]; / / to restore the position of the object the frame = CGRectMake (0, 0, HDScreenInfo width, HDScreenInfo, height);  bottomView.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height); } }]; } } } } } }Copy the code

Gesture isolation during animation

The custom VCStack provides many convenient operation apis, many of which are accompanied by animation operations. In order to avoid some unknown errors caused by user response gestures during animation, fault tolerance is made in the code section

- (void) pushto: (UIViewController *) vc animation: (NSObject < HDVCStackAnimationProtocol > *) animation {/ / add gestures to deal with [the self panGestureWithView:vc]; / / the ban on any gestures [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; . If (animation) {/ / animation start [animation pushWithWillShowVC: vc currentVC: self. VisibleViewController completion: ^ (BOOL {{if finished) (finished)... / / gestures to disable the close [[UIApplication sharedApplication] endIgnoringInteractionEvents];}}]; } else {// Gesture disable close..... [[UIApplication sharedApplication] endIgnoringInteractionEvents]; }} / / / / pop the same logic in the right gestures increased bottomVC at the bottom of the mask, avoid left sliding body response to problems in other events if (pan) state = = UIGestureRecognizerStateBegan) {... else { continueFlag = YES; BottomView addSubview:self.maskView]; }... }... Else if (pan) state = = UIGestureRecognizerStateEnded) {if (continueFlag) {/ / remove mask view the if (self) maskView) superview! = nil) { [self.maskView removeFromSuperview]; }}Copy the code

conclusion

In the implementation process, the initial implementation is carried out around a NavigationStack, which has met most requirements in the actual development, because most apps are managed in a Navigation way, even if there are multiple business Windows at the bottom. But the next level of the page closes the entry at the bottom. In order to support the mixed management of tabBar and VCStack, tabBarManager+VCStack is integrated on the original basis. Yes, the overall logic is closer to the management mode of TabBar+ Navigation.

Finally, the project is still being improved. If you are interested, you can improve it together. The project address is as follows: VCStack VCStack+TabBarManager