我们在iOS项目开发中,有些时候需要修改标准控件的样式,我们今天就围绕一个具体项目需求,进行UINavigationBar
的继承与改造。
UIApperance协议属性定制
我们在UINavigationBar.h
头文件中,看到如下修改NavigationBar背景颜色的属性
@property(nullable, nonatomic,strong) UIColor *barTintColor NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR; // default is nil
注意到UI_APPEARANCE_SELECTOR
这个宏了么,用这个宏标记的属性,都是可以通过UIApperance
协议进行全局设置的属性。说的更直白一点,就是可以一次性,修改项目中所有的这个类的默认属性。
例如在iOS6之前,UILabel
的默认背景颜色不是透明色,而是白色。我们就可以使用如下方法,修改UILabel
的默认背景色
[[UILabel appearance] setBackgroundColor:[UIColor clearColor]];
UIApperance
协议就是这么神奇,所有的UIKit控件都遵守了这个协议,所有标记了UI_APPEARANCE_SELECTOR
宏的属性,都可以使用appearance
实例修改默认值,是不是很炫酷。
项目需求
上面一段与本文正题无关,下面我们看一下本文的项目需求
分析
这个页面就是一个标准的NavigationController
+ TableViewContoller
组合实现的设置页面,导航条和Table的样式需要订制。
前面说到的UIApperance
协议是可以实现的,我们换一种更为普遍的方式实现,继承。
我们继承UINavigationBar
,创建子类FWBar
。我们使用storyboard实例化大体框架模型,并将NavigationViewController
的NavigationBar
设置为我们的FWBar
类,并将UITableView
设置为Static
静态模式,直接编辑了Cell
的内容。
在FWBar.m
中加入如下代码
- (void)awakeFromNib
{
[self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsCompact];
self.shadowImage = [UIImage new];
//把之前的View统统隐藏
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj setHidden:YES];
}];
[self addSubview:self.fakeBackgroundView];
self.fakeBackgroundView.userInteractionEnabled = NO;
[self sendSubviewToBack:self.fakeBackgroundView];
self.titleTextAttributes = @{
NSFontAttributeName: [UIFont fontWithName:@"NotoSansHans-DemiLight" size:16],
NSForegroundColorAttributeName:[UIColor colorWithRed:57.0/255 green:207.0/255 blue:218.0/255 alpha:1]
};
//rgba(165, 195, 205, 1)
self.tintColor = [UIColor colorWithRed:165.0/255 green:195.0/255 blue:205.0/255 alpha:1];
}
解释 因为原生的NaviBar背景View下方有一条灰色的边,这条边不是用layer生成的,我没搞明白是怎么实现的,所以直接将这个View隐藏掉了。顺便吧shadowImage
也换成空图。
这里的self.fakeBackgroundView
是我们添加的背景,颜色是白色。这里我们将它移到最下层,并且触摸属性关掉,userInteractionEnabled
设为NO
。
titleTextAttributes
这个属性,是用来修改title的样式的。
tintColor
这个属性,是用来修改导航条左右按钮颜色的。
这些操作做完,还不够。
我们无法通过暴露出来的接口修改左右按钮的字体和位置。这也是我们选择继承而不是UIApperance的原因
继承大杀器,高度自定义
- (void)didAddSubview:(UIView *)subview
{
NSLog(@"%@",subview);
if ([subview isKindOfClass:NSClassFromString(@"UINavigationButton")]) {
if ([subview isKindOfClass:[UIButton class]]) {
[(UIButton*)subview setAttributedTitle:[[NSAttributedString alloc] initWithString:[(UIButton*)subview titleForState:UIControlStateNormal] attributes:@{
NSFontAttributeName: [UIFont fontWithName:@"AvenirNext-Regular" size:17],
NSForegroundColorAttributeName:self.tintColor
}] forState:UIControlStateNormal];
}
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
if ([subview isKindOfClass:NSClassFromString(@"UINavigationButton")]) {
if ([subview isKindOfClass:[UIButton class]] && subview.frame.origin.x < self.frame.size.width/2) {
[subview setFrame:({
CGRect rect = subview.frame;
rect.origin.x = 8;
rect.size.width = 69;
rect;
})];
}
}
}];
}
解释 重写- (void)didAddSubview:(UIView *)subview
方法,检测了系统控件根据NavigationItem
向NavigationBar
添加按钮这个事件,然后对按钮进行甄别,定制。
我们找到Cancel
这个按钮,他虽然是UINavigationButton
类型,但是一定是继承了UIButton
,所以我们直接强转成她的父类,修改其文字字体和frame。
重写layoutSubviews
这个方法,是为了实时更新我们的按钮位置。这个其实也可以不更改的,但是我们的项目需求中,Cancel
这个字段太长,字体变大以后导致了显示不全,所以我们将这个做按钮的frame变大了。
注意几点
NSClassFromString(@"UINavigationButton")
这个方法是我们无法获取内部类的时候,获取Class类型的方法。UINavigationButton
这个类名是NSLog输出时看到的。这一段使用了特殊的语法糖,有兴趣了解的参考这篇sunnyxx大神的博文,全文搜索关键字
小括号内联复合表达式
[subview setFrame:({
CGRect rect = subview.frame;
rect.origin.x = 8;
rect.size.width = 69;
rect;
})];
最后的实现效果。
结语
截屏的效果不是太好,细心的朋友可能会发现,我们的FWBar
在TableView
向上滑动的过程中会渐出阴影。
我把这段代码分享给大家,但是这段代码偷懒没用KVO,而是用了ReactiveCocoa
这个庞大的庞大框架的小小功能,所以,就没放倒教程里。
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
UIViewController *presentingViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (presentingViewController.presentedViewController) presentingViewController = presentingViewController.presentedViewController;
__block BOOL has = NO;
[[presentingViewController childViewControllers] enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isKindOfClass:[UINavigationController class]]) {
[[obj childViewControllers] enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull obj2, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj2 isKindOfClass:[UITableViewController class]]) {
has = YES;
UITableViewController* tVC = obj2;
if (self.tableViewOffsetDisposable) {
[self.tableViewOffsetDisposable dispose];
}
self.tableViewOffsetDisposable = [RACObserve(tVC.tableView, contentOffset) subscribeNext:^(id x) {
CGPoint p = [x CGPointValue];
if (p.y <= 0 && p.y >= - 64) {
self.fakeBackgroundView.layer.shadowOpacity = fabs(64 + p.y) / 64 * 0.7;
}
else if (p.y > 0)
{
if (self.fakeBackgroundView.layer.shadowOpacity != 0.7) {
self.fakeBackgroundView.layer.shadowOpacity = 0.7;
}
}
else
{
if (self.fakeBackgroundView.layer.shadowOpacity != 0) {
self.fakeBackgroundView.layer.shadowOpacity = 0;
}
}
}];
}
}];
}
}];
}