iOS 小組件 - 自定義導(dǎo)航欄 + 原有業(yè)務(wù)自定義導(dǎo)航欄替換

2025.01.31 工作變動原因,故將一些工作期間Tapd內(nèi)部寫的Wiki文檔轉(zhuǎn)移到個人博客。

借著建立新項目(成長體系)的時候,寫了一個通用導(dǎo)航欄供業(yè)務(wù)使用,并對舊業(yè)務(wù)(上進(jìn)青年)中的自定義導(dǎo)航欄進(jìn)行重構(gòu)替換。

一、設(shè)計思路

從兼容 上進(jìn)青年APP 所有現(xiàn)有的業(yè)務(wù)考慮出發(fā),需要考慮快捷兼容3種常用的樣式:

1.不需要額外設(shè)置,默認(rèn)為系統(tǒng)樣式(固定顯示)


tapd_44062861_1716891573_115.jpg

2.從0到1滑動顯示的樣式(跟隨滾動透明度變化)


tapd_44062861_1716891583_635.jpg

3.自定義樣式(右側(cè)item \ 返回按鈕 \ 白色和彩色底色 \ 分割線)


tapd_44062861_1716892527_658.jpg

二、基礎(chǔ)類具體實現(xiàn)

好的封裝是可以一個簡單的方法就完成調(diào)用的,常用的樣式配置,只需要一個自定義的初始化方法就可以完成。

初始化:

    /**
         導(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)題文本,就會得到一個簡單的默認(rèn)導(dǎo)航欄,開箱即用。

三、更多的自定義實現(xiàn)

1. 跟隨頁面滑動,透明度變化的導(dǎo)航欄

日常的導(dǎo)航欄樣式中,還需要兼容一個 根據(jù)滑動,0-1透明度變化的樣式,只需要監(jiān)聽滑動的時候調(diào)用一下 scrollToChangeStatus(_ viewController: UIViewController, currentContentOffsetY: CGFloat, maxOffsetY: CGFloat) 方法即可。

/**
 滾動頁面,改變導(dǎo)航欄狀態(tài)
 
 viewController: 持有YGNavView的控制器
 currentContentOffsetY: 當(dāng)前y軸滑動距離
 maxOffsetY: 直到完全顯示的最大滾動距離
 */
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

tapd_44062861_1716892527_658.jpg

如圖,有一些特殊的常見頁面也需要右側(cè)的item(比如分享按鈕)

這時候只需要調(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() {
            // 容錯處理
            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)
        }
        /// 賦值點擊事件
        self.itemsAction = itemsAction
    }

四、實際應(yīng)用

項目里重構(gòu)替換,比如設(shè)計思路中,圖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) 后,具體效果如下:

tapd_44062861_1716891573_115.jpg

五、具體源碼

在 iOS 17.0 系統(tǒng)版本之后的蘋果手機(jī)里,iPhone狀態(tài)欄已經(jīng)可以歸系統(tǒng)管理,能夠自動適應(yīng)APP背景來對狀態(tài)欄進(jìn)行管理,故在源碼中去掉了手動管理狀態(tài)欄的代碼

因此在源碼中的 style: YGUIUserInterfaceStyle 已經(jīng)沒有管理狀態(tài)欄,只對標(biāo)題和返回按鈕進(jìn)行管理,如果有需要的話請自己適配。

//  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的按鈕點擊事件綁定,請自己更換按鈕事件方式
            backBtn.rx.tap.subscribe(onNext: { _ in
                // 退出頁面
                YAYNavigator.shared.pop()
            })
            .disposed(by: rx.disposeBag)
        }
        
        // 記錄進(jìn)來頁面的狀態(tài)欄(已棄用,iOS 17.0之后的系統(tǒ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() {
            // 容錯處理
            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)
        }
        /// 賦值點擊事件
        self.itemsAction = itemsAction
    }
    
    /**
     滾動頁面,改變導(dǎo)航欄狀態(tài)
     
     viewController: 持有YGNavView的控制器
     currentContentOffsetY: 當(dāng)前y軸滑動距離
     maxOffsetY: 直到完全顯示的最大滾動距離
     */
    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)會自動管理狀態(tài)欄)
    private var originalStyle: YGUIUserInterfaceStyle?
    
    /// 右側(cè)自定義item集合
    private var rightItems: [UIButton] = []
    
    /// 點擊右側(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)先級越高
     
     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

    }
    
}

最最最后,完結(jié)撒花

告辭.jpeg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容