iOS界面开发—定制导航栏标题

上一篇:iOS界面开发—导航控制器和标签控制器
当前篇:iOS界面开发—定制导航栏标题

定制规则

在上一篇里,我们提到了定制导航栏标题,但是没有具体去实现,只是讨论了定制的导航栏标题视图的布局规则,这里篇我们实现一个具体的定制导航栏标题。

我们先确定定制视图的规则:

  1. 主标题,主标题在宽度不够的情况下末尾可以做省略显示
  2. 副标题,副标题不做省略显示,紧跟主标题后面
  3. 左侧视图,可以有多个
  4. 右侧视图,可以有多个

这样制定规则后,就可以完全实现类似微信的导航栏标题效果,我们可以在左侧或者右侧添加任意视图,方便扩展,接下来我们一步一步地实现。

在我们准备封装一个组件的时候,先尝试着制定这样的规则,会对我们的开发很帮助,我们应该尽量制定比较通用,扩展性良好的规则,然后按照规则去实现,不要一开始就把代码写死。

主标题和副标题

我们先来实现主标题和副标题,打开NavigationTitleView源文件,编写代码如下:


class NavigationTitleContainerView: UIView {
    
    override func layoutSubviews() {
        super.layoutSubviews()
        for subview in subviews {
            if subview is NavigationTitleView {
                subview.sizeToFit()
            }
        }
    }
    
    func trytoShowSubviewInCenter(_ subview: UIView) {
        //垂直居中
        subview.centerY = height / 2
        //根据屏幕宽度计算出子视图在容器中的中央位置
        guard let theWindow = window else { return }
        let windowCenter = CGPoint.init(x: theWindow.width / 2, y: 0)
        let theCenter = theWindow.convert(windowCenter, to: self)
        subview.centerX = theCenter.x
        //由于导航栏左右菜单栏的占位可能导致子视图超出容器范围,将子视图限定在容器中,达到尽量居中的效果
        if subview.left < 0 {
            subview.left = 0
        } else if subview.right > width {
            subview.right = width
        }
    }
    
}

class NavigationTitleView: UIView {
    
    /** 主标题*/
    let titleLabel: UILabel = {
        let label = UILabel()
        label.textColor = UIColor.white
        label.font = UIFont.boldSystemFont(ofSize: 17)
        label.numberOfLines = 1
        label.textAlignment = .center
        return label
    }()
    
    /** 副标题*/
    let subTitleLabel: UILabel = {
        let label = UILabel()
        label.textColor = UIColor.white
        label.font = UIFont.boldSystemFont(ofSize: 17)
        label.numberOfLines = 1
        label.textAlignment = .center
        return label
    }()

    /** 请将该视图为navigationItem.titleView*/
    let containerView = NavigationTitleContainerView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(titleLabel)
        addSubview(subTitleLabel)
        let screenSize = UIScreen.main.bounds.size
        let containerWidth = max(screenSize.height, screenSize.width)
        containerView.size = CGSize.init(width: containerWidth, height: 44)
        containerView.addSubview(self)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        if let superview = self.superview {
            if !(superview is NavigationTitleContainerView) {
                assertionFailure("do not add it to any superview, please use containerView")
            }
        }
    }
    
    /** 子视图宽度和*/
    var totalWidth: CGFloat {
        return titleLabel.width + subTitleLabel.width
    }
    
    /** 子视图最大高度*/
    var maxHeight: CGFloat {
        var height: CGFloat = 0
        for subview in subviews {
            if subview.height > height {
                height = subview.height
            }
        }
        return height
    }
    
    /** 每当修改内容时,调用sizeToFit来自适应宽度,同时重新设置居中*/
    override func sizeToFit() {
        super.sizeToFit()
        titleLabel.sizeToFit()
        subTitleLabel.sizeToFit()
        width = totalWidth
        if let superview = superview {
            width = min(superview.width, totalWidth)
        } else {
            width = totalWidth
        }
        height = maxHeight
        containerView.trytoShowSubviewInCenter(self)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let titleMaxWidth = width - subTitleLabel.width
        if titleLabel.width > titleMaxWidth {
            titleLabel.width = titleMaxWidth
        }
        titleLabel.left = 0
        titleLabel.centerY = height / 2
        subTitleLabel.left = titleLabel.right
        subTitleLabel.centerY = height / 2
    }
    
}

这样我们已经实现了主标题和副标题的效果,副标题在主标题后面,副标题不进行省略,如果总长度超标,削减主标题的长度,主标题缩略显示,在ViewController源文件中的viewDidLoad方法中设置我们定制的标题视图:

navigationItem.titleView = titleView.containerView
titleView.titleLabel.text = "这是一个群"
titleView.subTitleLabel.text = "(100)"
titleView.sizeToFit()
屏幕快照 2018-02-08 下午3.55.30.png

标题居中显示,主标题能够全部显示,我们把主标题改成下面这样:

navigationItem.titleView = titleView.containerView
titleView.titleLabel.text = "这是一个名字很长很长很长很长很长很长很长很长很长很长很长很长的群"
titleView.subTitleLabel.text = "(100)"
titleView.sizeToFit()
屏幕快照 2018-02-08 下午4.00.31.png

左右侧视图

现在我们让组件支持左右侧视图设置,方式很简单,跟之前一样,只不过需要多处理一些子视图而已

class NavigationTitleView: UIView {
    
    /** 主标题*/
    let titleLabel: UILabel = {
        let label = UILabel()
        label.textColor = UIColor.white
        label.font = UIFont.boldSystemFont(ofSize: 17)
        label.numberOfLines = 1
        label.textAlignment = .center
        return label
    }()
    
    /** 副标题*/
    let subTitleLabel: UILabel = {
        let label = UILabel()
        label.textColor = UIColor.white
        label.font = UIFont.boldSystemFont(ofSize: 17)
        label.numberOfLines = 1
        label.textAlignment = .center
        return label
    }()
    
    /** 左侧视图集合*/
    open var leftViews: [UIView]? {
        didSet {
            if let oldViews = oldValue {
                for oldView in oldViews {
                    oldView.removeFromSuperview()
                }
            }
            if let newViews = leftViews {
                for newView in newViews {
                    addSubview(newView)
                }
            }
            setNeedsSizeToFit()
        }
    }
    
    /** 右侧视图集合*/
    open var rightViews: [UIView]? {
        didSet {
            if let oldViews = oldValue {
                for oldView in oldViews {
                    oldView.removeFromSuperview()
                }
            }
            if let newViews = rightViews {
                for newView in newViews {
                    addSubview(newView)
                }
            }
            setNeedsSizeToFit()
        }
    }
    
    /** 请将该视图为navigationItem.titleView*/
    let containerView = NavigationTitleContainerView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(titleLabel)
        addSubview(subTitleLabel)
        let screenSize = UIScreen.main.bounds.size
        let containerWidth = max(screenSize.height, screenSize.width)
        containerView.size = CGSize.init(width: containerWidth, height: 44)
        containerView.addSubview(self)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        if let superview = self.superview {
            if !(superview is NavigationTitleContainerView) {
                assertionFailure("do not add it to any superview, please use containerView")
            }
        }
    }
    
    /** 子视图宽度和*/
    var totalWidth: CGFloat {
        return totalWidthExceptTitleLabel + titleLabel.width
    }
    
    /** 除了主标题外的子视图宽度和*/
    var totalWidthExceptTitleLabel: CGFloat {
        var width: CGFloat = 0
        if let leftViews = self.leftViews {
            for leftView in leftViews {
                width += leftView.width
            }
        }
        if let rightViews = self.rightViews {
            for rightView in rightViews {
                width += rightView.width
            }
        }
        width += subTitleLabel.width
        return width
    }
    
    /** 子视图最大高度*/
    var maxHeight: CGFloat {
        var height: CGFloat = 0
        for subview in subviews {
            if subview.height > height {
                height = subview.height
            }
        }
        return height
    }
    
    private var needSizeToFit = true
    
    func setNeedsSizeToFit() {
        needSizeToFit = true
        setNeedsLayout()
    }
    
    /** 每当修改内容时,调用sizeToFit来自适应宽度,同时重新设置居中*/
    override func sizeToFit() {
        needSizeToFit = false
        super.sizeToFit()
        titleLabel.sizeToFit()
        subTitleLabel.sizeToFit()
        if let superview = superview {
            width = min(superview.width, totalWidth)
        } else {
            width = totalWidth
        }
        height = maxHeight
        containerView.trytoShowSubviewInCenter(self)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if needSizeToFit {
            sizeToFit()
        }
        let titleMaxWidth = width - totalWidthExceptTitleLabel
        if titleLabel.width > titleMaxWidth && titleMaxWidth > 0 {
            titleLabel.width = titleMaxWidth
        }
        let centerY = height / 2
        if let leftViews = self.leftViews {
            for leftView in leftViews.enumerated() {
                if leftView.offset > 0 {
                    leftView.element.left = leftViews[leftView.offset - 1].right
                } else {
                    leftView.element.left = 0
                }
                leftView.element.centerY = centerY
            }
        }
        titleLabel.left = leftViews?.first?.right ?? 0
        titleLabel.centerY = centerY
        subTitleLabel.left = titleLabel.right
        subTitleLabel.centerY = centerY
        if let rightViews = self.rightViews {
            for rightView in rightViews.enumerated() {
                if rightView.offset > 0 {
                    rightView.element.left = rightViews[rightView.offset - 1].right
                } else {
                    rightView.element.left = subTitleLabel.right
                }
                rightView.element.centerY = centerY
            }
        }
    }
    
}

由于没有图片,我们就用普通的UIView来模拟微信聊天导航栏标题,我们在标题左侧放一个转圈的UIActivityIndicatorView,在右侧放两个不同颜色的UIView,打开ViewController源文件,修改代码如下:

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.titleView = titleView.containerView
        titleView.titleLabel.text = "这是一个名字很长很长很长很长很长很长很长很长很长很长很长很长的群"
        titleView.subTitleLabel.text = "(100)"
        let leftView = UIActivityIndicatorView.init(activityIndicatorStyle: .white)
        titleView.leftViews = [leftView]
        leftView.startAnimating()
        leftView.size = CGSize.init(width: 20, height: 20)
        let rightView1 = UIView()
        rightView1.backgroundColor = UIColor.yellow
        rightView1.size = CGSize.init(width: 20, height: 20)
        let rightView2 = UIView()
        rightView2.backgroundColor = UIColor.green
        rightView2.size = CGSize.init(width: 20, height: 20)
        self.titleView.rightViews = [rightView1, rightView2]
        navigationItem.setDefaultBackBarTitle()
        view.backgroundColor = UIColor.white
        view.addSubview(label)
        label.text = "Hello World"
        label.sizeToFit()
        
        let moreActionsItem = UIBarButtonItem.init(title: "更多", style: .plain, target: nil, action: nil)
        navigationItem.rightBarButtonItems = [moreActionsItem]
        
        let tap = UITapGestureRecognizer.init(target: self, action: #selector(onClickBackground))
        view.addGestureRecognizer(tap)
    }
屏幕快照 2018-02-08 下午5.23.50.png

为了方便别人使用,我们可以扩展一些属性,把会影响布局的设置提取出来,更改这些设置的时候自动重新布局,不需要再手动调用sizeToFit:

extension NavigationTitleView {
    
    /** 主标题*/
    open var title: String? {
        get {
            return titleLabel.text
        }
        set {
            if titleLabel.text != newValue {
                titleLabel.text = newValue
                setNeedsSizeToFit()
            }
        }
    }
    
    /** 富文本主标题*/
    open var attributedTitle: NSAttributedString? {
        get {
            return titleLabel.attributedText
        }
        set {
            if titleLabel.attributedText != newValue {
                titleLabel.attributedText = newValue
                setNeedsSizeToFit()
            }
        }
    }
    
    /** 主标题字体*/
    open var titleFont: UIFont! {
        get {
            return titleLabel.font
        }
        set {
            if titleLabel.font != newValue {
                titleLabel.font = newValue
                setNeedsSizeToFit()
            }
        }
    }
    
    /** 副标题*/
    open var subTitle: String? {
        get {
            return subTitleLabel.text
        }
        set {
            if subTitleLabel.text != newValue {
                subTitleLabel.text = newValue
                setNeedsSizeToFit()
            }
        }
    }
    
    /** 富文本副标题*/
    open var attributedSubTitle: NSAttributedString? {
        get {
            return subTitleLabel.attributedText
        }
        set {
            if subTitleLabel.attributedText != newValue {
                subTitleLabel.attributedText = newValue
                setNeedsSizeToFit()
            }
        }
    }
    
    /** 副标题字体*/
    open var subTitleFont: UIFont! {
        get {
            return subTitleLabel.font
        }
        set {
            if subTitleLabel.font != newValue {
                subTitleLabel.font = newValue
                setNeedsSizeToFit()
            }
        }
    }
    
}

到现在,我们最开始的需求就全部实现了,通过这个组件,我们可以任意定制标题的样式。

纯代码布局的优势就体现出来了,如果我们想写一个比较通用的组件,用纯代码会灵活很多,我们没有用可视化和自动布局同样实现了自动布局效果,包括横竖屏布局切换,代码量并不比用自动布局多,合理利用layoutSubviews方法就能实现绝大部分自动布局的需求了。

iOS 11中的返回按钮

以往的iOS版本中,导航栏的返回按钮的标题是跟上一个界面的navigationItem.title同步更新的,在iOS 11中不知道是不是有bug还是有其他更新,这个效果失效了,有些情况下我们又需要这个效果,例如微信聊天界面的返回按钮,我找到一个解决方法,首先创建一个菜单:

private let backBarItem = UIBarButtonItem.init(title: nil, style: .plain, target: nil, action: nil)

注意这个菜单是在上一个界面,也就是说如果要同步聊天界面的返回菜单,这个菜单就创建在会话列表界面中,然后在更新的时候需要重新设置返回菜单:

navigationItem.title = "聊天(10)"
if #available(iOS 11.0, *) {
    backBarItem.title = "聊天(10)"
    navigationItem.backBarButtonItem = nil
    navigationItem.backBarButtonItem = backBarItem
}

上一篇:iOS界面开发—导航控制器和标签控制器
当前篇:iOS界面开发—定制导航栏标题

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

推荐阅读更多精彩内容