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

2018-02-08  本文已影响0人  明明是个呆子

上一篇: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界面开发—定制导航栏标题

上一篇下一篇

猜你喜欢

热点阅读