自定义导航栏RTRootNavigationController的底层实现原理

RTRootNavigationController实现了什么?

1、可以像往常一样使用导航栏
2、可以在不同的控制器里实现不同风格的导航效果
3、内部已经实现了UIScreenEdgePanGestureRecognizer滑动返回手势,整屏滑动
4、对于导航栏的隐藏,屏幕旋转等等操作并不影响

具体使用

1、当UITabBarController作为window的rootControoler,则Tabbar的每一个子控制器的根控制器是RTRootNavigationController

//rootViewController
    TabBarController *tabbar = [[TabBarController alloc]init];
     
     RTRootNavigationController *firstContainVC = [[RTRootNavigationController alloc]initWithRootViewController:[[FirstViewController alloc]init]];
     firstContainVC.tabBarItem = [[UITabBarItem alloc]initWithTitle:@"第一页" image:nil tag:0];
     RTRootNavigationController *secondContainVC = [[RTRootNavigationController alloc]initWithRootViewController:[[SecondViewController alloc]init]];
     secondContainVC.tabBarItem = [[UITabBarItem alloc]initWithTitle:@"第二页" image:nil tag:1];
     
     tabbar.viewControllers = @[firstContainVC,secondContainVC];
        
     self.window.rootViewController = tabbar;

2、由于实际项目只有个别页面导航不同,所以我们可以定义一个基类BaseViewController,实现总体导航风格,并设置返回按钮样式

@implementation BaseViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationController.navigationBar.barTintColor = [UIColor redColor];
    self.navigationController.navigationBar.tintColor = [UIColor redColor];

    [self.navigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor whiteColor]}];
//默认使用的是RTRoot框架内部的导航效果和返回按钮,如果要自定义,必须将此属性设置为NO,然后实现下方方法;self.rt_navigationController.useSystemBackBarButtonItem = NO;
}

- (UIBarButtonItem *)rt_customBackItemWithTarget:(id)target action:(SEL)action{

}

以上设置导航的方法,可以在每个控制器里使用,设置自己的导航

3、自定义导航栏实现,实现此方法

- (Class)rt_navigationBarClass
{
    return [RTCustomNavigationBar class];
}

4、push控制器的方法
要想实现每个控制器都有自己的导航栏,push操作的对象必须是self.rt_navigationController, 方法中有一个complete回调,在此回调方法中,我们还可以移除push前的控制器[self.rt_navigationController removeViewController:self];

 [self.rt_navigationController pushViewController:detailVC animated:YES complete:nil];

5、隐藏导航栏,只需要一句话,千万记住可不是rt_navigationController

self.navigationController.navigationBar.hidden = YES

RTRootNavigationController的内部实现

使用RTRootNavigationController后的层级关系

--UITabBarController
---RTRootNavigationController
------RTContainerController
--------RTContainerNavigationController
-----------UIViewController

---RTRootNavigationController
------RTContainerController
--------RTContainerNavigationController
-----------UIViewController

为什么RTRootNavigationController的childViewControllers是RTContainerController

1、目的是为了让每个controller有自己的导航
2、如何做到的?

我们都知道UINavigationController初始化UIVIewController之后,都会执行pushViewController: animated:(BOOL)animated 方法,我们来分析一下RTRootNavigationController里面的这个方法的实现

- (void)pushViewController:(UIViewController *)viewController
                  animated:(BOOL)animated
{
    if (self.viewControllers.count > 0) {
        UIViewController *currentLast = RTSafeUnwrapViewController(self.viewControllers.lastObject); //获取到当前controller
        [super pushViewController:RTSafeWrapViewController(viewController,
                                                           viewController.rt_navigationBarClass,
                                                           self.useSystemBackBarButtonItem,
                                                           currentLast.navigationItem.backBarButtonItem,
                                                           currentLast.title)  animated:animated];
    }
    else {
        [super pushViewController:RTSafeWrapViewController(viewController, viewController.rt_navigationBarClass)
                         animated:animated];
    }
}

1、核心方法 RTSafeWrapViewController,内部实现是这样的:
1.1 初始化一个RTContainerController对象
1.2 要push的controller作为其contentViewController, 用要push的controller的navigationBarClass初始化一个RTContainerNavigationController
1.3 要push的controller作为RTContainerNavigationController的childViewController, RTContainerNavigationController作为RTContainerController的childViewController

初始化好的RTContainerController就是这样的:
--RTContainerController
----RTContainerNavigationController
------pushViewController

返回的RTContainerController就是RTRootNavigationController的push对象,就形成了以上的层级关系,所以代码中push操作一定是使用self.rt_navigationController,而不是self.navigationController(RTContainerNavigationController),

 [RTContainerController containerControllerWithController:controller
                                                     navigationBarClass:navigationBarClass
                                              withPlaceholderController:withPlaceholder];
                                              
- (instancetype)initWithController:(UIViewController *)controller
                navigationBarClass:(Class)navigationBarClass
         withPlaceholderController:(BOOL)yesOrNo
                 backBarButtonItem:(UIBarButtonItem *)backItem
                         backTitle:(NSString *)backTitle
{
    self = [super init];
    if (self) {
        
        self.contentViewController = controller;
        self.containerNavigationController = [[RTContainerNavigationController alloc] initWithNavigationBarClass:navigationBarClass
                                                                                                    toolbarClass:nil];
        if (yesOrNo) {
            UIViewController *vc = [UIViewController new];
            vc.title = backTitle;
            vc.navigationItem.backBarButtonItem = backItem;
            self.containerNavigationController.viewControllers = @[vc, controller];
        }
        else
            self.containerNavigationController.viewControllers = @[controller];
        
        [self addChildViewController:self.containerNavigationController];
        [self.containerNavigationController didMoveToParentViewController:self];
    }
    return self;
}

接着分析:
1、 上面1.2步用要push的controller的rt_navigationBarClass初始化一个RTContainerNavigationController,这里就实现了用自定义的UINavigationBar创建导航,controller内部实现rt_navigationBarClass的get方法就可以了

/* Use this initializer to make the navigation controller use your custom bar class. 
   Passing nil for navigationBarClass will get you UINavigationBar, nil for toolbarClass gets UIToolbar.
   The arguments must otherwise be subclasses of the respective UIKit classes.
 */
[[RTContainerNavigationController alloc] initWithNavigationBarClass:navigationBarClass                                                                                      toolbarClass:nil];

以上,就实现了在不同的controller里有不同的导航

关于返回按钮,框架单独做了处理,特别方便

1、self.rt_navigationController.useSystemBackBarButtonItem = NO;
2、实现 rt_customBackItemWithTarget:(id)target action:(SEL)action方法,返回自定义leftBarbuttonItem,返回的响应action需要的话可以自定义,不需要的话,就用target,就会响应框架内部的返回方法

在willShowViewController方法中,会判断是否实现上述条件,实现了就会调用自定义的

- (void)navigationController:(UINavigationController *)navigationController
      willShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated
{
    BOOL isRootVC = viewController == navigationController.viewControllers.firstObject;
    if (!isRootVC) {
        viewController = RTSafeUnwrapViewController(viewController);
        
        BOOL hasSetLeftItem = viewController.navigationItem.leftBarButtonItem != nil;
        if (hasSetLeftItem && !viewController.rt_hasSetInteractivePop) {
            viewController.rt_disableInteractivePop = YES;
        }
        else if (!viewController.rt_hasSetInteractivePop) {
            viewController.rt_disableInteractivePop = NO;
        }
        if (!self.useSystemBackBarButtonItem && !hasSetLeftItem) {
            if ([viewController respondsToSelector:@selector(rt_customBackItemWithTarget:action:)]) {
                viewController.navigationItem.leftBarButtonItem = [viewController rt_customBackItemWithTarget:self
                                                                                                       action:@selector(onBack:)];
            }
            else if ([viewController respondsToSelector:@selector(customBackItemWithTarget:action:)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
                viewController.navigationItem.leftBarButtonItem = [viewController customBackItemWithTarget:self
                                                                                                    action:@selector(onBack:)];
#pragma clang diagnostic pop
            }
            else {
                viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Back", nil)
                                                                                                   style:UIBarButtonItemStylePlain
                                                                                                  target:self
                                                                                                  action:@selector(onBack:)];
            }
        }
    }
    
    if ([self.rt_delegate respondsToSelector:@selector(navigationController:willShowViewController:animated:)]) {
        [self.rt_delegate navigationController:navigationController
                        willShowViewController:viewController
                                      animated:animated];
    }
}
层级关系变了,还能像平时一样获取指定层级的控制器吗

当然不能了,self.navigationController(RTContainerNavigationController) 和 self是一一对应的内部关系,而外部关系self.rt_navigationController是RTRootNavigationController,而其childViewControllers是RTContainerController,所以都不能直接获取到目的控制器,而要通过遍历和判断,举两个例子

1、获取真正执行push操作的导航控制器

self.rt_navigationController

- (RTRootNavigationController *)rt_navigationController
{
   UIViewController *vc = self;
   while (vc && ![vc isKindOfClass:[RTRootNavigationController class]]) {
       vc = vc.navigationController;
   }
   return (RTRootNavigationController *)vc;
}

2、获取栈顶控制器

self.rt_navigationController.rt_topViewController

//分析
self.rt_navigationController.topViewController是栈顶的RTContainerController,上述我们讲过,
RTContainerController的contentViewController就是我们要的UIViewController,从这里也表明,将UIViewController和RTContainerNavigationController作为其属性存在的意义

彩蛋

podfile安装RTRootNavigationController之后,会发现有一个分类UINavigationController (RTInteractivePush),可以实现屏幕右侧的UIScreenEdgePanGestureRecognizer

//实现,控制器内部实现
- (UIViewController*)rt_nextSiblingController
返回右滑要push的controller

总结

框架内部用到很多runtime运行时,用于属性绑定(分类中就用此实现原本不能添加的属性); 框架中有自定义类似swift中的高阶函数;注意到很多数组的遍历都用到enumerateObjectsUsingBlock,有很多好的方式和方法以及规则都值得一看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容

  • 一个app往往有很多界面,而界面之间的跳转也就是对应控制器的跳转,控制器的跳转一般有两种情况 push 或者 mo...
    Dariel阅读 15,415评论 73 186
  • *7月8日上午 N:Block :跟一个函数块差不多,会对里面所有的内容的引用计数+1,想要解决就用__block...
    炙冰阅读 2,480评论 1 14
  • 1.自定义控件 a.继承某个控件 b.重写initWithFrame方法可以设置一些它的属性 c.在layouts...
    圍繞的城阅读 3,376评论 2 4
  • 前言的前言 唐巧前辈在微信公众号「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各项指标...
    VincentHK阅读 5,359评论 3 44
  • 首先,请允许大女孩来张棒棒哒自拍。 (请自动忽略掉那杂乱的头发,六点二十起床到现在的发型有点乱正常哈。) 步入正题...
    李笑笑啊阅读 485评论 1 0