2025.01.31 工作變動(dòng)原因,故將一些工作期間Tapd內(nèi)部寫的Wiki文檔轉(zhuǎn)移到個(gè)人博客。
借著建立新項(xiàng)目(成長體系)的時(shí)候,寫了一個(gè)通用導(dǎo)航欄供業(yè)務(wù)使用居扒,并對(duì)舊業(yè)務(wù)(上進(jìn)青年)中的自定義導(dǎo)航欄進(jìn)行重構(gòu)替換。
一丑慎、設(shè)計(jì)思路
從兼容 上進(jìn)青年APP 所有現(xiàn)有的業(yè)務(wù)考慮出發(fā)喜喂,需要考慮快捷兼容3種常用的樣式:
1.不需要額外設(shè)置瓤摧,默認(rèn)為系統(tǒng)樣式(固定顯示)
2.從0到1滑動(dòng)顯示的樣式(跟隨滾動(dòng)透明度變化)
3.自定義樣式(右側(cè)item \ 返回按鈕 \ 白色和彩色底色 \ 分割線)
二、基礎(chǔ)類具體實(shí)現(xiàn)
好的封裝是可以一個(gè)簡單的方法就完成調(diào)用的玉吁,常用的樣式配置照弥,只需要一個(gè)自定義的初始化方法就可以完成。
初始化:
/**
導(dǎo)航欄初始化
參數(shù):
style: UIUserInterfaceStyle 普通模式 / 暗黑模式(已禁用进副,無效)
alwaysDisplay: 固定顯示(默認(rèn)不固定)
navTitle: 標(biāo)題
navBGColor: 背景顏色(默認(rèn)純白)
needBackButton: 需要左側(cè)返回按鈕
needDivider: 需要分割線(默認(rèn)不需要)
*/
init(style: YGUIUserInterfaceStyle = .light, alwaysDisplay: Bool = false, navTitle: String = "", navBGColor: UIColor = .white, needBackButton: Bool = true, needDivider: Bool = false)
如果所有參數(shù)不傳这揣,只傳標(biāo)題文本,就會(huì)得到一個(gè)簡單的默認(rèn)導(dǎo)航欄影斑,開箱即用给赞。
三、更多的自定義實(shí)現(xiàn)
1. 跟隨頁面滑動(dòng)鸥昏,透明度變化的導(dǎo)航欄
日常的導(dǎo)航欄樣式中塞俱,還需要兼容一個(gè) 根據(jù)滑動(dòng)姐帚,0-1透明度變化的樣式吏垮,只需要監(jiān)聽滑動(dòng)的時(shí)候調(diào)用一下 scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat)
方法即可。
/**
滾動(dòng)頁面罐旗,改變導(dǎo)航欄狀態(tài)
viewController: 持有YGNavView的控制器
currentContentOffsetY: 當(dāng)前y軸滑動(dòng)距離
maxOffsetY: 直到完全顯示的最大滾動(dòng)距離
*/
func scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat) {
// 導(dǎo)航欄漸變透明度
var alpha: CGFloat = 0
// 超出最大距離膳汪,完全顯示導(dǎo)航欄
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. 自定義右側(cè)item
如圖,有一些特殊的常見頁面也需要右側(cè)的item(比如分享按鈕)九秀。
這時(shí)候只需要調(diào)用一下 reloadRightItem(items: [YGNavItem], itemSpacing: CGFloat = 8.0)
即可遗嗽。
/**
加載右側(cè)自定義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() {
// 容錯(cuò)處理
if item.localImage == nil && item.imageUrl == nil && item.itemTitle == nil { return }
// 根據(jù)item屬性生成button
let button = createUIButtonWith(item, itemSpacing: itemSpacing, index: index)
rightItems.append(button)
}
/// 賦值點(diǎn)擊事件
self.itemsAction = itemsAction
}
四、實(shí)際應(yīng)用
項(xiàng)目里重構(gòu)替換鼓蜒,比如設(shè)計(jì)思路中痹换,圖1默認(rèn)樣式的調(diào)用:
/// 導(dǎo)航欄
lazy var navView: YGNavView = {
let navView = YGNavView(alwaysDisplay: true, navTitle: "上進(jìn)分明細(xì)", needDivider: true)
return navView
}()
在 viewDidLoad()
中,調(diào)用 view.addSubview(navView)
后都弹,具體效果如下:
五娇豫、具體源碼
在 iOS 17.0 系統(tǒng)版本之后的蘋果手機(jī)里,iPhone狀態(tài)欄已經(jīng)可以歸系統(tǒng)管理畅厢,能夠自動(dòng)適應(yīng)APP背景來對(duì)狀態(tài)欄進(jìn)行管理冯痢,故在源碼中去掉了手動(dòng)管理狀態(tài)欄的代碼。
因此在源碼中的 style: YGUIUserInterfaceStyle
已經(jīng)沒有管理狀態(tài)欄框杜,只對(duì)標(biāo)題和返回按鈕進(jìn)行管理浦楣,如果有需要的話請(qǐng)自己適配。
// Created by Yim on 2024/3/7.
// 通用導(dǎo)航欄
import UIKit
public enum YGUIUserInterfaceStyle: Int {
// case unspecified = 0
case light = 1
case dark = 2
}
/// 通用導(dǎo)航欄
class YGNavView: UIView {
/// 導(dǎo)航欄樣式
var navBarStyle: YGUIUserInterfaceStyle = .light
/// 導(dǎo)航欄背景顏色
var navBGColor: UIColor = .white
/// 右側(cè)item回調(diào) ( 從右到左的index, 當(dāng)前model )
var itemsAction: ((Int, YGNavItem) -> ())?
/// item模型
var navItemModelArray: [YGNavItem]?
/**
導(dǎo)航欄初始化
參數(shù):
style: UIUserInterfaceStyle 普通模式 / 暗黑模式(已禁用咪辱,無效)
alwaysDisplay: 固定顯示(默認(rèn)不固定)
navTitle: 標(biāo)題
navBGColor: 背景顏色(默認(rèn)純白)
needBackButton: 需要左側(cè)返回按鈕
needDivider: 需要分割線(默認(rèn)不需要)
*/
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的按鈕點(diǎn)擊事件綁定油狂,請(qǐng)自己更換按鈕事件方式
backBtn.rx.tap.subscribe(onNext: { _ in
// 退出頁面
YAYNavigator.shared.pop()
})
.disposed(by: rx.disposeBag)
}
// 記錄進(jìn)來頁面的狀態(tài)欄(已棄用历恐,iOS 17.0之后的系統(tǒng)會(huì)自動(dòng)管理狀態(tài)欄)
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")
}
/**
加載右側(cè)自定義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() {
// 容錯(cuò)處理
if item.localImage == nil && item.imageUrl == nil && item.itemTitle == nil { return }
// 根據(jù)item屬性生成button
let button = createUIButtonWith(item, itemSpacing: itemSpacing, index: index)
rightItems.append(button)
}
/// 賦值點(diǎn)擊事件
self.itemsAction = itemsAction
}
/**
滾動(dòng)頁面庐杨,改變導(dǎo)航欄狀態(tài)
viewController: 持有YGNavView的控制器
currentContentOffsetY: 當(dāng)前y軸滑動(dòng)距離
maxOffsetY: 直到完全顯示的最大滾動(dòng)距離
*/
func scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat) {
// 導(dǎo)航欄漸變透明度
var alpha: CGFloat = 0
// 超出最大距離,完全顯示導(dǎo)航欄
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
/// 之前導(dǎo)航欄樣式(用來恢復(fù))(已棄用夹供,iOS 17.0之后的系統(tǒng)會(huì)自動(dòng)管理狀態(tài)欄)
private var originalStyle: YGUIUserInterfaceStyle?
/// 右側(cè)自定義item集合
private var rightItems: [UIButton] = []
/// 點(diǎn)擊右側(cè)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)
}
// 網(wǎng)絡(luò)圖片類型
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: —————— 導(dǎo)航欄自定義item ——————
/// 導(dǎo)航欄自定義item
class YGNavItem: NSObject {
/// 本地圖片
var localImage: UIImage?
/// 網(wǎng)絡(luò)圖片
var imageUrl: String?
/// 標(biāo)題文本
var itemTitle: String?
/// 標(biāo)題字體
var itemTitleFont: UIFont = .systemFont(ofSize: 12)
/// 標(biāo)題顏色
var itemTitleColor: UIColor = subContentColor
/**
navItem初始化,越靠前的參數(shù)哮洽,優(yōu)先級(jí)越高
localImage: 本地圖片
imageUrl: 網(wǎng)絡(luò)圖片
itemTitle: 標(biāo)題文本
itemTitleFont: 標(biāo)題字體
itemTitleColor: 標(biāo)題顏色
*/
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
}
}