2025.01.31 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。
借着建立新项目(成长体系)的时候,写了一个通用导航栏供业务使用,并对旧业务(上进青年)中的自定义导航栏进行重构替换。
一、设计思路
从兼容 上进青年APP 所有现有的业务考虑出发,需要考虑快捷兼容3种常用的样式:
1.不需要额外设置,默认为系统样式(固定显示)
2.从0到1滑动显示的样式(跟随滚动透明度变化)
3.自定义样式(右侧item \ 返回按钮 \ 白色和彩色底色 \ 分割线)
二、基础类具体实现
好的封装是可以一个简单的方法就完成调用的,常用的样式配置,只需要一个自定义的初始化方法就可以完成。
初始化:
/**
导航栏初始化
参数:
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
如图,有一些特殊的常见页面也需要右侧的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)
后,具体效果如下:
五、具体源码
在 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
}
}