iOS 小组件 - 自定义导航栏 + 原有业务自定义导航栏替换

682 阅读6分钟

2025.01.31 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

借着建立新项目(成长体系)的时候,写了一个通用导航栏供业务使用,并对旧业务(上进青年)中的自定义导航栏进行重构替换。

一、设计思路

从兼容 上进青年APP 所有现有的业务考虑出发,需要考虑快捷兼容3种常用的样式:

1.不需要额外设置,默认为系统样式(固定显示) tapd_44062861_1716891573_115.jpg

2.从0到1滑动显示的样式(跟随滚动透明度变化) tapd_44062861_1716891583_635.jpg

3.自定义样式(右侧item \ 返回按钮 \ 白色和彩色底色 \ 分割线) tapd_44062861_1716892527_658.jpg

二、基础类具体实现

好的封装是可以一个简单的方法就完成调用的,常用的样式配置,只需要一个自定义的初始化方法就可以完成。

初始化:

    /**
         导航栏初始化
         参数:
         style: UIUserInterfaceStyle 普通模式 / 暗黑模式(已禁用,无效)
         alwaysDisplay: 固定显示(默认不固定)
         navTitle: 标题
         navBGColor: 背景颜色(默认纯白)
         needBackButton: 需要左侧返回按钮
         needDivider: 需要分割线(默认不需要)
         */
        init(style: YGUIUserInterfaceStyle = .light, alwaysDisplay: Bool = false, navTitle: String = "", navBGColor: UIColor = .white, needBackButton: Bool = true, needDivider: Bool = false)

如果所有参数不传,只传标题文本,就会得到一个简单的默认导航栏,开箱即用。

三、更多的自定义实现

1. 跟随页面滑动,透明度变化的导航栏

日常的导航栏样式中,还需要兼容一个 根据滑动,0-1透明度变化的样式,只需要监听滑动的时候调用一下 scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat) 方法即可。

/**
 滚动页面,改变导航栏状态
 
 viewController: 持有YGNavView的控制器
 currentContentOffsetY: 当前y轴滑动距离
 maxOffsetY: 直到完全显示的最大滚动距离
 */
func scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat) {
    // 导航栏渐变透明度
    var alpha: CGFloat = 0
    // 超出最大距离,完全显示导航栏
    if currentContentOffsetY >= (maxOffsetY - navBar_Height) {
        alpha = 1
        self.navTitleLabel.isHidden = false
    } else {
        alpha = currentContentOffsetY / (maxOffsetY - navBar_Height)
        self.navTitleLabel.isHidden = true
    }
    self.backgroundColor = self.navBGColor.withAlphaComponent(alpha)
}

2. 自定义右侧item

tapd_44062861_1716892527_658.jpg

如图,有一些特殊的常见页面也需要右侧的item(比如分享按钮)

这时候只需要调用一下 reloadRightItem(items: [YGNavItem], itemSpacing: CGFloat = 8.0) 即可。

    /**
     加载右侧自定义item
     
     items: 从右到左排列的自定义item
     itemSpacing: item间距
     */
    func reloadRightItem(items: [YGNavItem], itemSpacing: CGFloat = 8.0, itemsAction: @escaping ((Int, YGNavItem) -> ())) {
        self.navItemModelArray = items
        // 移除原有的items
        for (_, item) in rightItems.enumerated() {
            item.removeFromSuperview()
        }
        rightItems.removeAll()
        
        // 添加items
        for (index, item) in items.enumerated() {
            // 容错处理
            if item.localImage == nil && item.imageUrl == nil && item.itemTitle == nil { return }
            // 根据item属性生成button
            let button = createUIButtonWith(item, itemSpacing: itemSpacing, index: index)
            rightItems.append(button)
        }
        /// 赋值点击事件
        self.itemsAction = itemsAction
    }

四、实际应用

项目里重构替换,比如设计思路中,图1默认样式的调用:

    /// 导航栏
    lazy var navView: YGNavView = {
        let navView = YGNavView(alwaysDisplay: true, navTitle: "上进分明细", needDivider: true)
        return navView
    }()

viewDidLoad() 中,调用 view.addSubview(navView) 后,具体效果如下:

tapd_44062861_1716891573_115.jpg

五、具体源码

在 iOS 17.0 系统版本之后的苹果手机里,iPhone状态栏已经可以归系统管理,能够自动适应APP背景来对状态栏进行管理,故在源码中去掉了手动管理状态栏的代码

因此在源码中的 style: YGUIUserInterfaceStyle 已经没有管理状态栏,只对标题和返回按钮进行管理,如果有需要的话请自己适配。

//  Created by Yim on 2024/3/7.
//  通用导航栏

import UIKit

public enum YGUIUserInterfaceStyle: Int {
    
//    case unspecified = 0

    case light = 1

    case dark = 2
}


/// 通用导航栏
class YGNavView: UIView {
    
    /// 导航栏样式
    var navBarStyle: YGUIUserInterfaceStyle = .light
    /// 导航栏背景颜色
    var navBGColor: UIColor = .white
    /// 右侧item回调 ( 从右到左的index, 当前model )
    var itemsAction: ((Int, YGNavItem) -> ())?
    /// item模型
    var navItemModelArray: [YGNavItem]?
    
    /**
     导航栏初始化
     参数:
     style: UIUserInterfaceStyle 普通模式 / 暗黑模式(已禁用,无效)
     alwaysDisplay: 固定显示(默认不固定)
     navTitle: 标题
     navBGColor: 背景颜色(默认纯白)
     needBackButton: 需要左侧返回按钮
     needDivider: 需要分割线(默认不需要)
     */
    init(style: YGUIUserInterfaceStyle = .light, alwaysDisplay: Bool = false, navTitle: String = "", navBGColor: UIColor = .white, needBackButton: Bool = true, needDivider: Bool = false) {
        super.init(frame: CGRect.init(x: 0, y: 0, width: screenWidth, height: navBar_Height))
        self.navBarStyle = style
        self.navTitleLabel.text = navTitle
        self.navTitleLabel.isHidden = !alwaysDisplay
        self.navBGColor = navBGColor
        self.backgroundColor = alwaysDisplay ? navBGColor : navBGColor.withAlphaComponent(0.0)
        self.grayLine.isHidden = !needDivider
        
        // 普通模式,白底黑字
        if style == .light {
            backBtn.setImage(UIImage.yg_image("navi_leftBack_black"), for: .normal)
            navTitleLabel.textColor = contentColor
        }
        // 暗黑模式,彩底白字
        else if style == .dark {
            backBtn.setImage(UIImage.yg_image("navi_leftBack_white"), for: .normal)
            navTitleLabel.textColor = .white
        }
        
        // ———————— setupUI ————————

        addSubview(navTitleLabel)
        navTitleLabel.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.bottom.equalToSuperview().offset(-10.5)
            make.height.equalTo(21)
        }
        
        addSubview(grayLine)
        grayLine.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.bottom.equalToSuperview()
            make.height.equalTo(0.5)
        }
        
        // 是否需要返回按钮
        if needBackButton {
            addSubview(backBtn)
            backBtn.snp.makeConstraints { make in
                make.left.equalToSuperview().offset(14)
                make.size.equalTo(CGSize(width: 32, height: 32))
                make.centerY.equalTo(navTitleLabel)
            }
            // rxSwift的按钮点击事件绑定,请自己更换按钮事件方式
            backBtn.rx.tap.subscribe(onNext: { _ in
                // 退出页面
                YAYNavigator.shared.pop()
            })
            .disposed(by: rx.disposeBag)
        }
        
        // 记录进来页面的状态栏(已弃用,iOS 17.0之后的系统会自动管理状态栏)
        if originalStyle == nil {
            if #available(iOS 13.0, *) {
                switch traitCollection.userInterfaceStyle {
                case .light:
                    self.originalStyle = .light
                    
                case .dark:
                    self.originalStyle = .dark
                    
                default:
                    break
                }
            } else {
                // iOS 13.0以下不做适配
            }
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    /**
     加载右侧自定义item
     
     items: 从右到左排列的自定义item
     itemSpacing: item间距
     */
    func reloadRightItem(items: [YGNavItem], itemSpacing: CGFloat = 8.0, itemsAction: @escaping ((Int, YGNavItem) -> ())) {
        self.navItemModelArray = items
        // 移除原有的items
        for (_, item) in rightItems.enumerated() {
            item.removeFromSuperview()
        }
        rightItems.removeAll()
        
        // 添加items
        for (index, item) in items.enumerated() {
            // 容错处理
            if item.localImage == nil && item.imageUrl == nil && item.itemTitle == nil { return }
            // 根据item属性生成button
            let button = createUIButtonWith(item, itemSpacing: itemSpacing, index: index)
            rightItems.append(button)
        }
        /// 赋值点击事件
        self.itemsAction = itemsAction
    }
    
    /**
     滚动页面,改变导航栏状态
     
     viewController: 持有YGNavView的控制器
     currentContentOffsetY: 当前y轴滑动距离
     maxOffsetY: 直到完全显示的最大滚动距离
     */
    func scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat) {
        // 导航栏渐变透明度
        var alpha: CGFloat = 0
        // 超出最大距离,完全显示导航栏
        if currentContentOffsetY >= (maxOffsetY - navBar_Height) {
            alpha = 1
            self.navTitleLabel.isHidden = false
        } else {
            alpha = currentContentOffsetY / (maxOffsetY - navBar_Height)
            self.navTitleLabel.isHidden = true
        }
        self.backgroundColor = self.navBGColor.withAlphaComponent(alpha)
    }
    
    
    // MARK: - Private
    
    /// 之前导航栏样式(用来恢复)(已弃用,iOS 17.0之后的系统会自动管理状态栏)
    private var originalStyle: YGUIUserInterfaceStyle?
    
    /// 右侧自定义item集合
    private var rightItems: [UIButton] = []
    
    /// 点击右侧items
    @objc private func clickItem(sender: UIButton) {
        itemsAction?(sender.tag, navItemModelArray?[sender.tag] ?? YGNavItem())
    }
    
    private func createUIButtonWith(_ item: YGNavItem, itemSpacing: CGFloat, index: Int) -> UIButton {
        let button = UIButton(type: .custom)
        addSubview(button)
        button.tag = index
        button.snp.makeConstraints { make in
            make.centerY.equalTo(navTitleLabel)
            make.height.equalTo(32)
            // 图片类型,固定宽度
            if item.localImage != nil || item.imageUrl != nil {
                make.width.equalTo(32)
            }
            // 从右往左排约束布局
            if index == 0 {
                make.right.equalToSuperview().offset(-14)
            }
            else {
                make.right.equalTo(rightItems[index - 1].snp.left).offset(-itemSpacing)
            }
        }

        // 本地图片类型
        if item.localImage != nil {
            button.setImage(item.localImage, for: .normal)
        }
        // 网络图片类型
        else if item.imageUrl != nil {
            button.kf.setImage(with: URL(string: item.imageUrl ?? ""), for: .normal)
        }
        // 文本类型
        else if item.itemTitle != nil {
            button.setTitle(item.itemTitle, for: .normal)
            button.setTitleColor(item.itemTitleColor, for: .normal)
            button.titleLabel?.font = item.itemTitleFont
        }
        button.addTarget(self, action: #selector(clickItem), for: .touchUpInside)
        return button
    }
    
    
    // MARK: - Lazy
    
    lazy var backBtn: UIButton = {
        let button = UIButton(type: .custom)
        return button
    }()
    
    
    lazy var navTitleLabel = UILabel().then {
        $0.font = UIFont.getRegularFontSize(fontSize: 15, fontWeight: .medium)
        $0.textAlignment = .center
    }
    
    lazy var grayLine: UIView = {
        let view = UIView()
        view.backgroundColor = .lightGray.withAlphaComponent(0.3)
        return view
    }()
    
}


// MARK: —————— 导航栏自定义item ——————


/// 导航栏自定义item
class YGNavItem: NSObject {
    
    /// 本地图片
    var localImage: UIImage?
    /// 网络图片
    var imageUrl: String?
    /// 标题文本
    var itemTitle: String?
    /// 标题字体
    var itemTitleFont: UIFont = .systemFont(ofSize: 12)
    /// 标题颜色
    var itemTitleColor: UIColor = subContentColor

    /**
     navItem初始化,越靠前的参数,优先级越高
     
     localImage: 本地图片
     imageUrl: 网络图片
     itemTitle: 标题文本
     itemTitleFont: 标题字体
     itemTitleColor: 标题颜色
     */
    init(localImage: UIImage? = nil, imageUrl: String? = nil, itemTitle: String? = nil, itemTitleFont: UIFont = .systemFont(ofSize: 12), itemTitleColor: UIColor = subContentColor) {
        self.localImage = localImage
        self.imageUrl = imageUrl
        self.itemTitle = itemTitle
        self.itemTitleFont = itemTitleFont
        self.itemTitleColor = itemTitleColor

    }
    
}

最最最后,完结撒花

告辞.jpeg