导航栏研究

1,889 阅读7分钟

在研究导航栏之前, 我们先看一下项目中导航栏是怎么被添加进来的. 先来个演示层级结构的完整添加方式:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ViewController *vc = [[ViewController alloc] init];
    
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
    
    UITabBarController *tabBar = [[UITabBarController alloc] init];
    tabBar.viewControllers = @[nav];
    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.rootViewController = tabBar;
    [self.window makeKeyAndVisible];
    return YES;
}

这个层级结构就是:

  • 1: UIWindow 上添加 UITabBarController
  • 2: UITabBarController 上添加 UINavigationController
  • 3: UINavigationController 上添加 ViewController

那么我们先来思考一下.

  • 1: UINavigationController在哪一层?
  • 2: 它到底扮演着一个什么样的角色?
  • 3: 我们为什么可以push和pop?
  • 4: 项目中为什么有那么多莫名其妙的问题?

带着上面的问题, 我们先从最初的UIWindow说起.
我们先看看UIWindow在Appdelegate里面的角色.

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

在最初创建的时候, 是没有Window的.那它什么时候创建的呢? 根据 UIKit中的几个核心对象的介绍:UIApplication,UIWindow,UIViewController,UIView(layer)简单介绍的介绍.创建时机如下.
App的启动过程:
打开程序之后-》

  • 1: Main函数

  • 2: UIapplicationMain函数

  • 3: 初始化UIApplication(创建)

  • 4: 设置UIApplication代理和相应的代理属性

  • 4: 开启事件循环,监听系统事件

  • 6: 监测info.plist文件,看看是否有Main.StoryBoard文件存在


如果有:

  • 1:加载Main.StoryBoard

  • 2:在StoryBoard上面创建一个UIWindow,

  • 3:设置Window的根控制器

  • 4:遍历控制器上面的所有子控件,没有则创建对应的控件


如果没有:

  • 1:通过一个强引用创建UIWindow self.window = [[UIWindow alloc] init];
  • 2:设置Window的frame为屏幕的bounds self.window.frame = [UIScreen mainScreen].bounds;
  • 3:设置window的根控制器 self.window.rootViewController = [[UIViewController alloc] init];
  • 4:将window作为主窗口并且显示到界面上
  • [self.window makeKeyAndVisible];

UIWindow是一种特殊的UIView,通常在一个app中至少会有一个UIWindow
iOS程序启动完毕后,创建的第一个视图控件就是UIWindow,接着创建控制器的view,最后将控制器的view添加到UIWindow上,于是控制器的view就显示在屏幕上了. 为了更生动的描述UiWindow, 我们引入下图:

我们看到的手机屏幕会给人一种2维结构的错觉. 其实我们的手机屏幕是3维结构的. 我们看手机屏幕等同于从windowLevel轴向下俯视. 看到的是width和heigh轴组成的二维平面.
其中UIStatusBar是windowLevel轴值为1000的view, UIAlert是windowLevel轴值为2000的view. 我们平时用到的, 默认的windowLevel是值为0的View.

我们先来看一个最简单的例子.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ViewController *vc = [[ViewController alloc] init];
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.rootViewController = vc;
    // windowLevel 是一个CGFloat类型的值. 我这样写是可以正常显示的. 
    self.window.windowLevel = -100.f;
    [self.window makeKeyAndVisible];
    return YES;
}

通过这种方式创建的UIViewController, 它的结构很简单

所以我们不能利用self.navigationController或者self.tabBarController进行任何调度操作.

我们目前的主题是研究导航栏的, 所以现在开始进入正题.创建一个只有导航栏的VC.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ViewController *vc = [[ViewController alloc] init];
    
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.rootViewController = nav;
    [self.window makeKeyAndVisible];
    
    return YES;
}

它的层级结构变的似乎很复杂. 我们挑重点讲

  • 1: UIWindow: 承载该windowLevel下的View容器.
  • 2: UINavigationController: UIWindow的根视图控制器
  • 3: UILayoutContainerView:最底层包含所有视图的容器。
  • 4: UINavigationTransitionView:导航控制器在这里发生转场行为的容器视图。
  • 5: UIViewControllerWrapperView: 包含 viewController 的 view属性的封装视图。
  • 6: UIView: viewController 的最上层视图 (与viewController的view 属性一致)
  • 7: UINavigationBar: 管理UINavigationController头部的视觉控件.
  • 8: _UIBarBackground: iOS10 及以前是 _UINavigationBarBackground 这是一个UIImageView类型的背景视图。设置bar的背景图片,设置bar透明等都需要操作这个视图。
  • 9: UIVisualEffectView: 控制毛玻璃效果的View
  • 10: UINavigationBarContentView: iOS10 及以前是UINavigationItemView, 标题, 返回键等的存放处.

那么我们现在开始研究一下UINavigationBar吧.

@property(nonatomic,assign) UIBarStyle barStyle

设置导航条风格,包括状态栏字体、title 字体颜色都跟着改变
默认时白底黑字,如果 translucent 为 YES 时,底不是纯白色会有透明效果 Black 时黑底白字. 如果 translucent 为 NO 时,底不是纯黑色会有透明效果
默认颜色的大家都见过.来个黑色的


@property(nullable,nonatomic,weak) id<UINavigationBarDelegate> delegate;
- (void)pushNavigationItem:(UINavigationItem *)item animated:(BOOL)animated;
- (nullable UINavigationItem *)popNavigationItemAnimated:(BOOL)animated;
- (void)setItems:(nullable NSArray<UINavigationItem *> *)items animated:(BOOL)animated

根据苹果爸爸文档里的意思就是, 如果你要自己写一个UINavigationBar的话, 你就可以正常使用. 如果是UINavigationController创建的, 那么它是受UINavigationController管理的, 你就不要动它了. 反正动了就crash. 我们目前不涉及自定义tabBar, 所以暂且不表.


@property(nonatomic,assign,getter=isTranslucent) BOOL translucent
@property(null_resettable, nonatomic,strong) UIColor *tintColor;
@property(nullable, nonatomic,strong) UIColor *barTintColo   NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR;
- (void)setBackgroundImage:(nullable UIImage *)backgroundImage forBarPosition:(UIBarPosition)barPosition barMetrics:(UIBarMetrics)barMetrics
- (nullable UIImage *)backgroundImageForBarPosition:(UIBarPosition)barPosition barMetrics:(UIBarMetrics)barMetrics
- (void)setBackgroundImage:(nullable UIImage *)backgroundImage forBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
- (nullable UIImage *)backgroundImageForBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;

translucent半透明的属性。这个属性关乎着UINavigationBar是不是有半透明的毛玻璃效果。默认是 ture.我们常常会看到UINavigationBar是半透明的。改为 false,就不再透明。
tintColor 负责子视图的tintColor
barTintColor 是负责背景色的tintColor
想了解tintColor请参见详解 UIView 的 Tint Color 属性. 设置 navigationBar 的背景图片
barMetrics 参数表示屏幕的方向,Default 表示横竖屏用同一张背景图,Compact 表示横屏时用的背景图
可以调用该方法两次,分别设置竖屏时的背景图和横屏时的背景图 返回指定 barPositionbarMetrics 下的 UIImage

我把这几个属性写在一起是因为它们之间的联系比较紧密, 也比较有意思.

先来看看官方文档的一段话

New behavior on iOS 7.
 Default is YES.
 You may force an opaque background by setting the property to NO.
 If the navigation bar has a custom background image, the default is inferred 
 from the alpha values of the image—YES if it has any pixel with alpha < 1.0
 If you send setTranslucent:YES to a bar with an opaque custom background image
 it will apply a system opacity less than 1.0 to the image.
 If you send setTranslucent:NO to a bar with a translucent custom background image
 it will provide an opaque background for the image using the bar's barTintColor if defined, or black
 for UIBarStyleBlack or white for UIBarStyleDefault if barTintColor is nil.

大意就是, 你可以设置translucentYES或者NO强制让UINavigationBar的背景半透明或者不透明. 如果不设置的话translucent值也会隐式改变, 改变规则是, 通过- (nullable UIImage *)backgroundImageForBarPosition:(UIBarPosition)barPosition barMetrics:(UIBarMetrics)barMetrics或者- (void)setBackgroundImage:(nullable UIImage *)backgroundImage forBarPosition:(UIBarPosition)barPosition barMetrics:(UIBarMetrics)barMetrics两种方法设置的背景图如果图片不含alpha通道或者图片的每一个像素点的alpha通道都大于1, 那么translucent会隐式的变为NO.
这里还有个有意思的事就是. ViewControllerView是从什么位置开始的. 如果translucentYES那么开始位置是(0, 0), 如果translucentNO, 那么开始位置是(0, 64). 这里又联系到了ViewController的三个属性edgesForExtendedLayout,extendedLayoutIncludesOpaqueBars, automaticallyAdjustsScrollViewInsets,详见全屏布局(fullScreenLayout)那些事, 说的就是View从哪开始的.


@property(nullable, nonatomic,readonly,strong) UINavigationItem *topItem;
@property(nullable, nonatomic,readonly,strong) UINavigationItem *backItem;
@property(nullable,nonatomic,copy) NSArray<UINavigationItem *> *items;

UINavigationBar最顶层的topItem和backItem和栈里面的UINavigationItem 通常在viewDidLoad中这样写效果不理想:

self.navigationController.navigationBar.topItem.title = @"VC: topItem";
self.navigationController.navigationBar.backItem.title = @"VC: backItem";

是因为, VC什么时候是UINavigationBar最顶层的栈Item没搞清楚. 要在

- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item;

以后, 再写才算生效. 但是在viewDidLoad里这样写:

    self.navigationItem.title = @"VC: topItem";
    self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"VC: backItem" style:UIBarButtonItemStylePlain target:nil action:nil];
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"VC: leftItem" style:UIBarButtonItemStylePlain target:self action:@selector(leftItemClick)];

是一定能保证页面push过来以后. 设置生效的. 因为它一定是栈顶的navigationItem. 下面借用一张图来说明一下

图片取自: 自定义iOS的Back按钮(backBarButtonItem)和pop交互手势(interactivepopgesturerecognizer).


@property(nullable,nonatomic,copy) NSDictionary<NSAttributedStringKey, id> *titleTextAttributes
@property(nullable, nonatomic, copy) NSDictionary<NSAttributedStringKey, id> *largeTitleTextAttributes UI_APPEARANCE_SELECTOR API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);

富文本title和iOS11+的大标题富文本


- (void)setTitleVerticalPositionAdjustment:(CGFloat)adjustment forBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
- (CGFloat)titleVerticalPositionAdjustmentForBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;

设置和返回title 的垂直偏移, 对 largeTitle 无效.


@property(nullable,nonatomic,strong) UIImage *backIndicatorImage NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR __TVOS_PROHIBITED;
@property(nullable,nonatomic,strong) UIImage *backIndicatorTransitionMaskImage NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR __TVOS_PROHIBITED;

iOS 7 后出现的,更改 backBarButtonItem 的图片,要两个属性同时修改,只更改一个不起作用


当我快把属性写完的时候, 偶然发现, 有大佬的一篇文章iOS_UI开发中容易被忽略的细节之--UINavigationBar.h写的更详细. 然后我也借鉴了大佬的部分内容加以补充. 本文旨在通过了解属性去关心他们之间的联系. 大佬的主旨是对属性的解析. 互相促进.感谢前人. 感谢