swift 导航栏 返回中文

2017-08-24 09:16:12 zhuweideng 阅读数 1162

https://juejin.im/post/599c4a89518825244d203dba

作为 iOS 开发者,难免要和导航栏打交道,通常呢,像微信这样优秀且友好的应用,全局使用系统导航栏交互效果就非常好了。然而为了更进一步,总是需要更深入地定制化导航栏,包括却不止像(半)透明滑动渐变等交互效果,以及标题颜色偏移还有对应状态栏(StatusBar)的变化。

阅读参考了诸多开发者的经验分享,并发起了一个腾讯投票以了解大多数人是倾向于采用什么样的方式来处理导航栏的问题,于是决定采用系统导航栏+自定义导航栏共用的方式来处理。

发起的iOS导航栏自定义实现方式偏好投票发起的iOS导航栏自定义实现方式偏好投票

开始折腾之前,先简单说下我对这三种方式的理解。

修改系统导航栏

以添加Catagory(OC)Extension(Swift)重载系统方法等形式,拿到并修改系统导航栏的View,或添加所需要的View来实现自己定制化的需求。

优点:

  1. 实现好后,各控制器定制起来调用方便,往往一两行代码就可以了。
  2. 能够保留侧滑返回的导航栏过渡效果(这个依需求而定,也并完全算优点)

缺点:

  1. 实现方式复杂,涉及系统属性方法的修改,容易遇上各种未知的坑

这种方式可参考这几篇中文分享,写得非常详细:

完全使用自定义导航栏

隐藏系统导航栏,各页面采用自定义导航栏进行需求定制。

优点

  1. 避免系统导航栏存在的各种未知坑
  2. 实现效果可高度自定义,高兴的话可以设计成波浪形,还带动画交互的那种
  3. 一般有些应用采用底部导航栏的设计,基本都是完全使用自定义导航栏实现

缺点

  1. 一般没有系统导航栏的侧滑过渡效果,可参考手淘。(不算完全意义上的缺点)
  2. 依据不同的需求和实现方式,工作量可能较大
  3. 侧滑返回手势、滑动隐藏、触控隐藏等一些系统交互需自行实现
  4. 需要额外处理系统导航栏能够自动处理的in call等系统响应

系统导航栏与自定义导航栏共用

一般来说,一个优秀且友好的应用,多会遵循苹果官方的设计规范,故而绝大多数页面还是能够方便地采用系统导航栏进行处理,此时,部分页面出彩的交互设计,则可以暂时隐藏系统导航栏,采用自定义导航栏进行实现。

优点

  1. 避免修改系统导航栏可能遇到的坑
  2. 仅部分页面针对性采用自定义导航栏,工作量相对可控
  3. 采用系统导航栏的页面之间保留侧滑过渡效果

缺点

  1. 若是需要自定义导航栏的页面较多,工作增量较大
  2. 自定义导航栏页面的侧滑返回等效果需要额外处理

小结

总的来说,三种方式各有优缺,主要还是按照不同的需求采用不同的方案,若是导航栏真的需要水波烂漫的交互效果,侧滑返回的时候还要有个小船划回去,这若非要挑战通过修改系统导航栏的方式实现,费劲踩坑估计在所难免。

开始折腾 - [系统导航栏+自定义导航栏方案]

添加自定义导航栏

fileprivate lazy var customNavigationItem: UINavigationItem = UINavigationItem(title: "Profile")
fileprivate lazy var customNavigationBar: UINavigationBar = {

        let bar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 64))

        bar.tintColor = UIColor.white
        bar.tintAdjustmentMode = .normal
        bar.alpha = 0
        bar.setItems([self.customNavigationItem], animated: false)

        bar.backgroundColor = UIColor.clear
        bar.barStyle = UIBarStyle.blackTranslucent
        bar.isTranslucent = true
        bar.shadowImage = UIImage()
        bar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)

        let textAttributes = [
            NSForegroundColorAttributeName: UIColor.white,
            NSFontAttributeName: UIFont.systemFont(ofSize: 16)
        ]

        bar.titleTextAttributes = textAttributes

        return bar
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(customNavigationBar)

        prepareData()

    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        navigationController?.setNavigationBarHidden(true, animated: true)

        // 便于自定义BarButtomItem
        setBackButton()
        customNavigationBar.alpha = 1.0
    }

    func setBackButton() {
        let backBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "back_white"), style: .plain, target: self, action: #selector(DataProjectDetailViewController.back(_:)))

        self.customNavigationItem.leftBarButtonItem = backBarButtonItem
    }

    @objc fileprivate func back(_ sender: AnyObject) {
        if let presentingViewController = presentingViewController {
            presentingViewController.dismiss(animated: true, completion: nil)
        } else {
            _ = navigationController?.popViewController(animated: true)
        }
    }

若是需要对导航栏进行滑动动画或渐变等处理,则在ScrollView代理方法中对自定义导航栏的属性进行修改。

需要额外强调的是,最好在BaseViewController中对系统导航栏的一些属性做统一初始化处理,以期所有的控制器达到期望的统一效果,以避免自定义页面对系统导航栏的隐藏等修改影响到其它页面的系统导航栏。

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        guard let navigationController = navigationController else {
            return
        }

        // 仅处理导航栏隐藏后重新显示,可在此做更多导航栏的统一效果处理
        if navigationController.isNavigationBarHidden {
            navigationController.setNavigationBarHidden(false, animated: animated)
        }
    }

处理StatusBar状态栏样式

    override var preferredStatusBarStyle : UIStatusBarStyle {
        return UIStatusBarStyle.lightContent
    }

处理边缘侧滑返回

重点!敲黑板、敲黑板了。处理边缘侧滑返回,需要接管实现导航控制器的边缘侧滑返回交互手势代理。好在所有的导航控制器来继承了BaseNavigationController,因而可以在基类进行统一处理。

class BaseNavigationController: UINavigationController {
    override func setNavigationBarHidden(_ hidden: Bool, animated: Bool) {
        super.setNavigationBarHidden(hidden, animated: animated)

        // 接管导航控制器的边缘侧滑返回交互手势代理
        interactivePopGestureRecognizer?.delegate = self
    }
}

extension BaseNavigationController: UIGestureRecognizerDelegate {
    // 让边缘侧滑手势在合适的情况下生效
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if (self.viewControllers.count > 1) {
            return true;
        }
        return false;
    }

    // 允许同时响应多个手势
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

    // 避免响应边缘侧滑返回手势时,当前控制器中的ScrollView跟着滑动
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return gestureRecognizer.isKind(of: UIScreenEdgePanGestureRecognizer.self)
    }

}

这样就通过自定义添加方式实现了导航栏的定制化,其他页面则继续愉快使用系统导航栏即可。以上就是所有自定义导航栏需要的核心代码了,故没有另外的Demo项目。若是希望继续了解修改系统导航栏的实现方式,可参考文中所提及的几篇分享,强烈推荐。



2015-12-11 15:22:29 u012701023 阅读数 3942
项目中常常会使用 UINavigationController 对各个页面进行导航,导航栏左侧的返回按钮默认标题文字是上级页面的title

但如果上级页面的标题很长,那么这个返回按钮字很多就会很丑:
   原文:Swift - 修改导航栏“返回”按钮文字,图标   原文:Swift - 修改导航栏“返回”按钮文字,图标

当文字极其长时返回文字就会变成“back”: 
   原文:Swift - 修改导航栏“返回”按钮文字,图标   原文:Swift - 修改导航栏“返回”按钮文字,图标

一,要修改“返回按钮”的文字,有如下两种方式:

1,在父页面中设置
navigationItem.backBarButtonItem设为自定义的UIBarButtonItem
这种方法所有的子界面返回时都变成了我们定义的文字,同时文字前面任然保留返回箭头。
原文:Swift - 修改导航栏“返回”按钮文字,图标
1
2
let item = UIBarButtonItem(title: "返回", style: .Plain, target: self, action: nil)
self.navigationItem.backBarButtonItem = item;
或者也可以直接把文字设为空字符串,这样就只有一个箭头了。
原文:Swift - 修改导航栏“返回”按钮文字,图标
1
2
let item = UIBarButtonItem(title: "", style: .Plain, target: self, action: nil)
self.navigationItem.backBarButtonItem = item;

2,在子页面中设置
navigationItem.leftBarButtonItem为自定义的UIBarButtonItem
这种方式可以给各个子页面返回按钮单独设置不同的文字,但文字前面是没有小箭头的。
原文:Swift - 修改导航栏“返回”按钮文字,图标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import UIKit
 
class DetailViewControllerUIViewController {
 
    override func viewDidLoad() {
        super.viewDidLoad()
         
        let leftBarBtn = UIBarButtonItem(title: "返回", style: .Plain, target: self,
            action: "backToPrevious")
        self.navigationItem.leftBarButtonItem = leftBarBtn
    }
     
    //返回按钮点击响应
    func backToPrevious(){
        self.navigationController?.popViewControllerAnimated(true)
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

二,修改“返回按钮”图标 
从上面最后一个例子可以看到,在子页面修改返回按钮的话只有文字没有图片。如果想要使用自定义图片,或者图片文字都需要的话可以进行如下操作:
1,如果只需要图片,不需要文字
比如我们想要用左侧这个图片(back@2x.png)作为返回图标
     原文:Swift - 修改导航栏“返回”按钮文字,图标          原文:Swift - 修改导航栏“返回”按钮文字,图标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import UIKit
 
class DetailViewControllerUIViewController {
 
    override func viewDidLoad() {
        super.viewDidLoad()
         
        let leftBarBtn = UIBarButtonItem(title: "", style: .Plain, target: self,
            action: "backToPrevious")
        leftBarBtn.image = UIImage(named: "back")
         
        //用于消除左边空隙,要不然按钮顶不到最前面
        let spacer = UIBarButtonItem(barButtonSystemItem: .FixedSpace, target: nil, action: nil)
        spacer.width = -10;
         
        self.navigationItem.leftBarButtonItems = [spacer, leftBarBtn]
    }
     
    //返回按钮点击响应
    func backToPrevious(){
        self.navigationController?.popViewControllerAnimated(true)
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

2,既需要图片也需要文字
原文:Swift - 修改导航栏“返回”按钮文字,图标

 
这个时候就要通过创建UIButton来实现了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit
 
class DetailViewControllerUIViewController {
     
    override func viewDidLoad() {
         
        let button =   UIButton(type: .System)
        button.frame = CGRectMake(0, 0, 65, 30)
        button.setImage(UIImage(named:"back"), forState: .Normal)
        button.setTitle("返回", forState: .Normal)
        button.addTarget(self, action: "backToPrevious", forControlEvents: .TouchUpInside)
         
        let leftBarBtn = UIBarButtonItem(customView: button)
         
        //用于消除左边空隙,要不然按钮顶不到最前面
        let spacer = UIBarButtonItem(barButtonSystemItem: .FixedSpace, target: nil, action: nil)
        spacer.width = -10;
         
        self.navigationItem.leftBarButtonItems = [spacer,leftBarBtn]
    }
     
    //返回按钮点击响应
    func backToPrevious(){
        self.navigationController?.popViewControllerAnimated(true)
    }
     
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

原文出自:www.hangge.com  转载请保留原文链接:http://www.hangge.com/blog/cache/detail_957.html
2019-03-13 22:44:46 XieYupeng520 阅读数 438

如果我们使用iOS系统的导航栏,自己设置titleView,leftItem和rightItem,当titleView长度达到一定时,push会出现titleView左右跳变的情况,本文将分析跳变原因及解决办法。

导航栏的内部布局

在一个全新的APP,自定义导航栏的左中右后,查看布局,会发现,导航栏内部布局如下

在这里插入图片描述

设置了自定义leftItem,titleView和rightItem,在导航栏中,我们自定义的view都会被_UITAMICAdaptorView包裹,其中leftItem和rightItem在_UITAMICAdaptorView外还会包裹一层_UIButtonBarStackView,最后布局在_UINavigationBarContentView中。

在导航栏内部布局的左边块、中间块和右边块,以下简称ABC,整个屏幕宽为Width。

以下以iPhone XS Max为例,gap1为20,gap2为6。

安全区域

A不论宽度如何(包括为0),一定会距离左边gap1。

C不论宽度如何(包括为0),一定会距离右边gap1。

B就算再宽,也一定会距离A和C各gap2。

在这里插入图片描述(A设置宽40,B设置宽414,C设置宽40)

当A和C宽度设为0时,B距离屏幕左右各(gap1+gap2)。

在这里插入图片描述

当A和C设置为nil时,B距离屏幕左右各12(gap3)。

在这里插入图片描述

对齐方式

当增加A的宽度时,A是以左边不动,右边增加来加宽的,B的宽度会因A宽度增加而压缩,A最宽不超过C.left-gap2*2。

在这里插入图片描述

当增加C的宽度时,C是以右边不动,左边增加来加宽的,B的宽度会因C宽度增加而压缩,C最宽不超过A.right-gap2*2。

在这里插入图片描述

当调节B的宽度时,B默认是以导航栏中心为锚点,左右同时增加,且最大不会超过 162(Width-A.width-B.width-gap12-gap22)

在这里插入图片描述

当把ABC全部调成屏幕宽时,B会被完全挤没,AC平分除了安全区域的所有空间(Width-gap12-gap22)

在这里插入图片描述

导航栏标题栏动画

从左到右的跳变的产生

首先理解了前面的布局,可知道B的x坐标的相对于A的计算公式

B.left = Max( (Width - B.width)/2 , A.right+gap2)

B的x坐标理想情况下是(Width - B.width)/2,也就是动画结束位置,实际x坐标位置可能是(Width - B.width)/2或者(A.right+gap2)(两者取最大值),也就是最后布局位置。

当实际位置为A.right+gap2时,说明动画初始位置在实际位置左边,就会出现push时,导航栏title左侧有个从左到右的跳变。

在这里插入图片描述

从右到左的跳变的产生

同理,B的right坐标的相对于C的计算公式

B.right = Min( (Width + B.width)/2 , C.left-gap2)

B的right坐标理想情况下是 (Width + B.width)/2,也就是动画结束位置,实际位置可能是(Width + B.width)/2或者(C.left-gap2)(两者取最小值),也就是最后布局位置。

当实际位置为(C.left-gap2)时,说明动画初始位置在实际位置右边,就会出现push时,导航栏title右侧有个从右到左的跳变。

在这里插入图片描述

防止跳变的结论

为了防止上述两种跳变,只要令B的left实际位置为 (Width - B.width)/2,B的right实际位置为 (Width + B.width)/2,也就是

求 (Width - B.width)/2 > (A.right+gap2) 且 (Width + B.width)/2 < C.left-gap2 的 B.width的取值范围?
因已知 A.right = gap1 + A.width + gap2,且 C.left = Width - gap2 - C.width - gap1
可求得B的宽度限制为
B.width < Width - gap12 - gap22 - A.width2 且 B.width < Width - gap12 - gap22 - C.width2
也就是 B.width < Width - gap12 - gap22 - Max(A.width, C.width)*2

翻译成中文就是B的宽度不能超过屏幕宽减去固定的安全区域再减去A和C之中最宽的2倍。

解决了?

不,还没完,到目前这步,是手Q8.0.0之前的做法,设定了A和C可能存在的最大宽度(因为AC的宽度是可能会变的,比如左边没有未读消息和有99条未读宽度是不一样的,再比如右边可能有一个图标或两个图标),然后得到的B的宽度就很窄了。

如图,B和A之间还有一大段距离没有利用上,如果想利用上这段空间,又不希望出现跳变,该怎么办呢?

推翻从右到左的跳变

首先要再回到导航栏标题栏动画 - 从右到左的跳变的产生,其实因为系统动画本身就是从右到左,所以看不出来有跳变,会令人以为是正常的动画,以下两张图,就动画而言,不会令人有跳变的感觉。

在这里插入图片描述 在这里插入图片描述

会有跳变的感觉是因为加上内容后,B的内容从C中滑过

在这里插入图片描述 在这里插入图片描述

但一般情况下,C放置的都是图标,空白区域很大,B的内容从C有动画滑过其实可以接受。

如果可以接受,那么B的宽度就变为了只依赖A的宽度

B.width < Width - gap12 - gap22 - A.width*2

不接受“推翻从右到左的跳变”

不行,追求完美的人说,我就是这么一点点跳变都不能接受,而且,上面的方法只解决了C大于A的情况,A大于C的情况还是有问题呀!

好,下面重点介绍下planB——

内容越界方案

首先,ABC里的内容,是可以超过ABC的宽度限制显示的!(后面ABC的内容各称为abc)

什么意思呢,回到上一张图,当我把A的内容“< left”的x坐标设为-20,a就顶着屏幕左边出现了。

如果我把ABC宽度都调为0,再看内容的显示:
在这里插入图片描述

可以看到除了a的x坐标被我设了-20,b和c都是以B和C的x坐标为原点显示的,并且是全部显示,不会因为宽度为0就不显示,也就是结论:ABC内容的显示不会被其宽度影响,但是会位置会受ABC的x坐标的影响。(当然前提你自己不能给自定义的view设置clipsToBounds为真)

也就是说,在"防止跳变的结论"基础上,我们可以把b的位置根据AC宽度进行调整,如下图

在这里插入图片描述

C比A宽,B和A之间空余了X的宽度(X.width = C.width - A.width),那么b的x起始点位置就可以计算为 -X.width(也就是A.width - C.width),b的最大宽度为Width - A.width - C.width - gap12 - gap22;

在这里插入图片描述

同理假如A比C宽,B和C之间就空余了X的宽度(X.width = A.width - C.width),那么b的x坐标为0,b的宽度为Width - A.width - C.width - gap12 - gap22。

在这里插入图片描述

综上,计算b的公式为

b.left = Min(0, A.width - C.width)
b.width = Width - A.width - C.width - gap12 - gap22

当B的背景颜色置为透明时,看效果就只看到B的内容了(以下两图区别在于右图B背景设为透明)

在这里插入图片描述 在这里插入图片描述

(PS.由实践看出,当a的x坐标处于安全区域gap1内时,push动画会有一个该区域从无到有的变化,同理当c的right位置处于最右边的安全区域也有,所以建议A和C的内容不要越过安全区域,但是这个也是有解决办法的,以后再说。)

基于以上方案,也可以一开始就把B的宽度设为0,然后每次只需要计算b的坐标和宽度就行了,还可以通过计算令B把左右gap2的区域也占掉。

在手Q上的实践效果:左图长标题,右图短标题(左边的未读消息数从无到有)

在这里插入图片描述 在这里插入图片描述

附:不同机型下gap1和gap2的值

新增gap3(当A和C设为nil,B距离屏幕左右距离)
在这里插入图片描述

综上,可以判断

if (SCREEN_WIDTH > 375) {
    gap1 = 20;
    gap3 = 12
} else {
    gap1 = 16;
    gap3 = 8;
}
    gap2 = 6;

Demo源码:https://github.com/Xieyupeng520/AZNavigationBar/tree/master
如果有帮助到你,请给我Github上一个Star鼓励一下O(∩_∩)O谢谢!

2016-01-29 13:49:37 u011096206 阅读数 29675

引言

在iOS开发过程中,NavigetionController(导航栏)算是比较常用的一种控件,而系统自带的样式有时候往往不是我们所需要的,这时候就要求我们必须自定义或修改一下。


修改返回键的标题

1、错误使用:以下三种方式都不能修改返回键的title(方式三可以修改NavigetionItem的leftBarButtonItem,但不能修改backBarButtonItem的样式)

    //最近iOS项目中要求导航栏的返回按钮只保留那个箭头,去掉后边的文字,在网上查了一些资料,最简单且没有副作用的方法就是
    [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];
    // 方式一
    self.navigationItem.leftBarButtonItem.title = @"返回";
    // 方式二
    self.navigationItem.backBarButtonItem.title = @"返回";
    // 方式三
    UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(0, 0, 80, 49);
    [button setTitle:@"返回" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    UIBarButtonItem * backButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
    self.navigationItem.backBarButtonItem = backButtonItem;


2、正确使用:

    // 细节: 本界面上设置, 下个界面上显示
    // 方式一
    self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:nil action:nil];
    // 方式二
    UIBarButtonItem * backButtonItem = [[UIBarButtonItem alloc] init];
    backButtonItem.title = @"返回";
    self.navigationItem.backBarButtonItem = backButtonItem;


修改返回键的颜色


由图可以看出,想要修改返回键的颜色可以:
self.navigationController.navigationBar.tintColor = [UIColor redColor];


最后说一下使用pushViewController切换到下一个视图时,navigation controller按照以下3条顺序更改导航栏的左侧按钮(本段摘自网络):

1、如果B视图有一个自定义的左侧按钮(leftBarButtonItem),则会显示这个自定义按钮;
2、如果B没有自定义按钮,但是A视图的backBarButtonItem属性有自定义项,则显示这个自定义项;
3、如果前2条都没有,则默认显示一个后退按钮,后退按钮的标题是A视图的标题;


扩展1:
    // 设置导航条的色调 理解为"混合色"
    self.navigationController.navigationBar.barTintColor = [UIColor blackColor];
    // 导航栏默认是半透明状态
    self.navigationController.navigationBar.backgroundColor = [UIColor blueColor];
    // 左边返回按键颜色
    // 导航栏标题颜色
    [self.navigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor whiteColor]}];
    self.navigationController.navigationBar.translucent = NO;

扩展2:
UITextAttributeFont - 字体
UITextAttributeTextColor - 文字颜色
UITextAttributeTextShadowColor - 文字阴影颜色
UITextAttributeTextShadowOffset - 偏移用于文本阴影

参考: