カスタム NavigationBar -- UIView による描画

iOS に付属している UINavigationBar は強力ですが、使用中にさまざまな小さな問題があり、対処するのが非常に面倒です。もちろん、サードパーティ製の NavigationBar も非常に高機能で使いやすい優れたものもありますが、それらは UINavigationBar から継承したサブクラスに基づいているため、モーダルの表示などの問題もあります。弾丸箱。UINavigationBar は基本的に UIViewController のプロパティにフックできる UIView ベースの特別なクラスですが、後者のフックは難しいため、前者は UIView ベースで簡単に実装できます。

この記事ではUIViewをベースに「左エリア・中エリア・右エリア」を基本設計コンセプトとし、「シンプル・便利・美しい」を目指します。物語性が無くてごめんなさい。

一連の考え

NavigationBar で最もよく使用されるシナリオは、タイトル バーと左右のボタンだけであり、左側のボタンのほとんどは存在し、右側のボタンは特定のビジネス ニーズに応じて実装されます。これにより、使用されている実装アプローチ (「左ゾーン、中間ゾーン、右ゾーン」) が抽象化されます。つまり、子コントロールをラップする 3 つのコンテナ クラス UIView を実装します。通常、コントロールを書くときに面倒なのはレイアウトですが、スナップキットでもフレームレイアウトでも、特定の状況に合わせてパラメータを計算してレイアウトする必要があります。スペースのサイズを決定するだけで、NavigationBar にそのフレームを自動的に計算させる場合、機能するでしょうか? 答えは間違いなく「はい」です。通常の NavigationBar コントロールのレイアウトは、複雑または無秩序ではなく規則的であるため、つまり、指定されたコントロールが指定された位置のコンテナーに配置されている限り、親コンテナーのフレーム パラメーターが存在するように中央に配置されます。サイズパラメータ、フレーム自体も決定されます。

非常に単純なコンストラクターを使用してクラス、特にコントロール クラスを初期化したい場合があります。たとえば、左側に戻るボタン、中央にタイトル テキストがある NavigationBar を実装する場合、戻るボタンの画像と中央にタイトル テキストのみを渡して、イベントを実装します。余分なコードをあまり必要とせずに、「戻る」ボタンのプロパティを確認できます。

UINavigationBarにはガウスぼかし効果があり、適切に設定すると非常に美しく見えるので、この機能も期待しています。もちろん、ガウスぼかしには背景画像と色が不可欠です。

UINavigationBar の厄介な点は下部にある灰色の線で、これを削除するには数行の余分な記述が必要になる場合があり、非常に肥大化します。ただし、面倒なため直接実装することはできず、コントラストが弱い場合にはグレーの線が役立つため、この機能も考慮する必要があります。

では、「左、中央、右」のようなレイアウトが必要ない場合はどうなるでしょうか。この場合、UINavigationBar は役に立ちませんが、実装するカスタム View を渡すことができる便利なコンストラクターを検討する必要があります。このようなビジネスは稀であり、必要とする人は他の方法を使用して実現する必要があるため、この点の実現は完了していません。

成分

受信サブクラスを管理するために 3 つのコンテンツ コンテナを定義します。渡されるサブクラスは 1 つだけですが、サブクラスはサブクラスのサブクラスのコレクションである可能性があることに注意してください。

    private lazy var leftContent:      UIView = self.createLeftContent()
    private lazy var centerContent:    UIView = self.createCenterContent()
    private lazy var rightContent:     UIView = self.createRightContent()

この実装は初期化に遅延読み込みに依存しています。

初期化:

ここでは、ユーザーによって渡されたサブクラス コントロールを初期化するための便利なコンストラクターが定義されています。setupView()このメソッドは、コンテナに追加し、そのフレームを計算するために使用されます。

    convenience init(leftView: UIView? = nil, centerView: UIView? = nil, rightView: UIView? = nil) {
    
    
        self.init()
        
        setupView(leftView: leftView, centerView: centerView, rightView: rightView)
    }

便利なコンストラクター:

追加の「左ボタン、中央のタイトル バー」メソッドを提供します。

    convenience init(title: String, leftText: String) {
    
    
        self.init()
        self.init(leftView: self.createLeftButton(text: leftText),
                  centerView: self.createCenterLabel(text: title),
                  rightView: nil)
    }

次に、「左ボタン、中央のタイトル バー、右ボタン」メソッドを提供します。

    convenience init(title: String, leftText: String, rightText: String) {
    
    
        self.init()
        self.init(leftView: self.createLeftButton(text: leftText),
                  centerView: self.createCenterLabel(text: title),
                  rightView: self.createRightButton(text: rightText))
    }

このようにして、ユーザーは使用時にあまり多くの初期化コードを記述する必要がありません。

背景画像/ぼかした画像:

まず UIVisualEffectView オブジェクトを実装し、次にisBlurガウスぼかしの背景を表示するかどうかを制御するプロパティを定義します。

    private var _isBlur: Bool?
    /// NavigationBar whether should be presenting a blur view.
    public var isBlur: Bool? {
    
    
        set{
    
    
            _isBlur = newValue ?? false
            if _isBlur == true {
    
    
                self.effectView.isHidden = false
            } else {
    
    
                self.effectView.isHidden = true
            }
        }
        get{
    
    
            return _isBlur
        }
    }

画像はシンプルです。ここでは、barBackgroundImageUIImage 背景画像を受け入れるプロパティも定義しています。

    private var _barBackgroundImage: UIImage?
    /// NavigationBar background image.
    public var barBackgroundImage: UIImage?{
    
    
        set{
    
    
            _barBackgroundImage = newValue ?? UIImage(named: "")
            self.backgroundColor = UIColor(patternImage: _barBackgroundImage!)
        }
        get{
    
    
            return _barBackgroundImage
        }
    }

他の:

ここでは分割線を表示するかどうか、NavigationBar を全画面表示するかどうかを決定するために実装isEnbaleDividerLineおよび使用されます。isFullScreen

    private var _isFullScreen: Bool?
    /// NavigationBar whether should be override status bar.
    public var isFullScreen: Bool?{
    
    
        set{
    
    
            _isFullScreen = newValue ?? false
            if _isFullScreen == true {
    
    
                self.resetFrameInFullScreen()
            } else {
    
    
                self.resetFrameInUnfullScreen()
            }
        }
        get{
    
    
            return _isFullScreen
        }
    }
    
    private var _isEnableDividerLine: Bool?
    /// NavigationBar whether should be show the divider line in the bottom.
    public var isEnableDividerLine: Bool?{
    
    
        set{
    
    
            _isEnableDividerLine = newValue ?? false
            if _isEnableDividerLine == true {
    
    
                self.addSubview(bottomLine)
            } else {
    
    
                if bottomLine.superview != nil {
    
    
                    bottomLine.removeFromSuperview()
                }
            }
        }
        get{
    
    
            return _isEnableDividerLine
        }
    }

returnイベントのクリックコールバックメソッドについては、Closure(ブロック)を使ってコールバックして処理を投げることもできますが、クロージャを書くほうが面倒なので、ここではトレーリングクロージャ機能を使って書くことで、処理を軽減することができます。コードの量も多く、見た目も非常にシンプルです。

Swiftで書かれているのでOCとの通信も考慮する必要があります。タイプの前に追加するだけです@objcMembers(ここでは追加しません)。トレーリング クロージャの機能は OC でも呼び出すことができるため、イベントの問題を心配する必要はありません。

コード:

フレームレイアウト方法は拡張機能に追加で記述されており、詳細はgithubに移動できます。

import UIKit
import AVFAudio

/**
 SGNavigationBar to replace UINavigationBar, which is designed by inherited from UIView to show various view.
 */
class SGNavigationBar: UIView {
    
    
    
    typealias ClickAction = () -> Void
    
    // MARK: - Private constant.
    
    /// Left and right container width.
    private let CONTENT_WIDTH: CGFloat = 50
    /// Right view padding for right content.
    private let RIGHT_PADDING: CGFloat = 13
    /// Right button image name.
    private let RIGHT_IMAGE_NAME: String = "back"
    /// Left button image name.
    private let LEFT_IMAGE_NAME: String = "back"
    
    // MARK: - Set & get varibales.
    
    private var _barTintColor: UIColor = UIColor.white
    /// NavigationBar tint color.
    public var barTintColor: UIColor? {
    
    
        set{
    
    
            _barTintColor = newValue ?? UIColor.white
            self.backgroundColor = _barTintColor
        }
        get{
    
    
            return _barTintColor
        }
    }
    
    private var _barBackgroundImage: UIImage?
    /// NavigationBar background image.
    public var barBackgroundImage: UIImage?{
    
    
        set{
    
    
            _barBackgroundImage = newValue ?? UIImage(named: "")
            self.backgroundColor = UIColor(patternImage: _barBackgroundImage!)
        }
        get{
    
    
            return _barBackgroundImage
        }
    }
    
    private var _isBlur: Bool?
    /// NavigationBar whether should be presenting a blur view.
    public var isBlur: Bool? {
    
    
        set{
    
    
            _isBlur = newValue ?? false
            if _isBlur == true {
    
    
                self.effectView.isHidden = false
            } else {
    
    
                self.effectView.isHidden = true
            }
        }
        get{
    
    
            return _isBlur
        }
    }
    
    private var _isFullScreen: Bool?
    /// NavigationBar whether should be override status bar.
    public var isFullScreen: Bool?{
    
    
        set{
    
    
            _isFullScreen = newValue ?? false
            if _isFullScreen == true {
    
    
                self.resetFrameInFullScreen()
            } else {
    
    
                self.resetFrameInUnfullScreen()
            }
        }
        get{
    
    
            return _isFullScreen
        }
    }
    
    private var _isEnableDividerLine: Bool?
    /// NavigationBar whether should be show the divider line in the bottom.
    public var isEnableDividerLine: Bool?{
    
    
        set{
    
    
            _isEnableDividerLine = newValue ?? false
            if _isEnableDividerLine == true {
    
    
                self.addSubview(bottomLine)
            } else {
    
    
                if bottomLine.superview != nil {
    
    
                    bottomLine.removeFromSuperview()
                }
            }
        }
        get{
    
    
            return _isEnableDividerLine
        }
    }
    
    // MARK: - Private variables.
    
    private var leftActionClosure:   ClickAction?
    private var centerActionClosure: ClickAction?
    private var rightActionClosure:  ClickAction?

    private lazy var leftContent:      UIView = self.createLeftContent()
    private lazy var centerContent:    UIView = self.createCenterContent()
    private lazy var rightContent:     UIView = self.createRightContent()
    private lazy var bottomLine:       UIView = self.createBottomLine()
    private lazy var blurEffect: UIBlurEffect = self.createBlurEffect()
    private lazy var effectView: UIVisualEffectView = self.createEffectView()
    
    override init(frame: CGRect) {
    
    
        super.init(frame: frame)

        self.frame = CGRect(x: 0,
                            y: kSafeTopOffset(),
                            width: kScreenWidth(),
                            height: kNavigationBarHight())
        
        _ = self.effectView
        
        self.addSubview(self.leftContent)
        self.addSubview(self.centerContent)
        self.addSubview(self.rightContent)
    }

    required init?(coder: NSCoder) {
    
    
        fatalError("init(coder:) has not been implemented")
    }

}

// MARK: - Boot Convenience Init.
extension SGNavigationBar {
    
    
    
    convenience init(leftView: UIView? = nil, centerView: UIView? = nil, rightView: UIView? = nil) {
    
    
        self.init()
        
        setupView(leftView: leftView, centerView: centerView, rightView: rightView)
    }
    
    private func setupView(leftView: UIView? = nil, centerView: UIView? = nil, rightView: UIView? = nil){
    
    
        if leftView != nil {
    
    
            assert(leftView!.frame.size != .zero, "Optional leftView must be defined and can not be CGSizeZero")
            leftContent.addSubview(leftView!)
            leftView!.center = CGPoint(x: halfWidth(leftContent), y: halfHeight(leftContent))
        }
        if centerView != nil {
    
    
            assert(centerView!.frame.size != .zero, "Optional centerView must be defined and can not be CGSizeZero")
            centerContent.addSubview(centerView!)
            centerView!.center = CGPoint(x: halfWidth(self) - self.leftContent.frame.width, y: halfHeight(centerContent))
        }
        if rightView != nil {
    
    
            assert(rightView!.frame.size != .zero, "Optional rightView must be defined and can not be CGSizeZero")
            rightContent.addSubview(rightView!)
            rightView!.center = CGPoint(x: CONTENT_WIDTH - RIGHT_PADDING - halfWidth(rightView!), y: halfHeight(rightContent))
        }

    }
    
    private func createLeftContent() -> UIView{
    
    
        let view = UIView()
        view.frame = CGRect(x: 0, y: 0, width: CONTENT_WIDTH, height: kNavigationBarHight())
        return view
    }
    
    private func createCenterContent() -> UIView{
    
    
        let view = UIView()
        view.frame = CGRect(x: CONTENT_WIDTH, y: 0, width: self.bounds.width - (CONTENT_WIDTH * 2), height: kNavigationBarHight())
        return view
    }
    
    private func createRightContent() -> UIView{
    
    
        let view = UIView()
        view.frame = CGRect(x: self.centerContent.frame.maxX, y: 0, width: CONTENT_WIDTH, height: kNavigationBarHight())
        return view
    }
    
    private func createBottomLine() -> UIView{
    
    
        let view = UIView()
        view.backgroundColor = .gray.withAlphaComponent(0.3)
        view.frame = CGRect(x: 0, y: kNavigationBarHight() - 0.5, width: kScreenWidth(), height: 0.5)
        return view
    }
    
    private func createBlurEffect() -> UIBlurEffect{
    
    
        let blurEffect = UIBlurEffect(style: .light)
        return blurEffect
    }
    
    private func createEffectView() -> UIVisualEffectView{
    
    
        let view = UIVisualEffectView(effect: blurEffect)
        view.frame = CGRect(x: self.frame.origin.x,
                                y: 0,
                                width: self.frame.width,
                                height: self.frame.height)
        self.addSubview(view)
        return view
    }
    
}

// MARK: - Outside method.
extension SGNavigationBar{
    
    
    
    public func scroll(_ x: CGFloat){
    
    
        let rate = x / UIScreen.main.bounds.width
        self.alpha = rate
    }
}

// MARK: - Inside method.
extension SGNavigationBar{
    
    
    
    private func resetFrameInFullScreen(){
    
    
        self.frame = CGRect(x: self.frame.origin.x,
                            y: 0,
                            width: self.frame.width,
                            height: self.frame.height + kSafeTopOffset())
        self.effectView.frame = CGRect(x: self.effectView.frame.origin.x,
                                       y: 0,
                                       width: self.effectView.frame.width,
                                       height: self.effectView.frame.height + kSafeTopOffset())
        self.leftContent.frame = CGRect(x: 0,
                                        y: kSafeTopOffset(),
                                        width: CONTENT_WIDTH,
                                        height: kNavigationBarHight())
        self.centerContent.frame = CGRect(x: self.leftContent.frame.maxX,
                                          y: kSafeTopOffset(),
                                          width: kScreenWidth() - (CONTENT_WIDTH * 2),
                                          height: self.centerContent.frame.height)
        self.rightContent.frame = CGRect(x: self.centerContent.frame.maxX,
                                         y: kSafeTopOffset(),
                                         width: CONTENT_WIDTH,
                                         height: self.rightContent.frame.height)
        self.bottomLine.frame = CGRect(x: 0,
                                       y: kNavigationBarHight() + kStatusBarHeight() - 0.5,
                                       width: kScreenWidth(),
                                       height: 0.5)
    }
    
    private func resetFrameInUnfullScreen(){
    
    
        self.frame = CGRect(x: 0,
                            y: kSafeTopOffset(),
                            width: kScreenWidth(),
                            height: kNavigationBarHight())
        self.effectView.frame = CGRect(x: self.frame.origin.x,
                                       y: 0,
                                       width: self.frame.width,
                                       height: self.frame.height)
        self.leftContent.frame = CGRect(x: 0,
                                        y: kSafeTopOffset(),
                                        width: CONTENT_WIDTH,
                                        height: self.leftContent.frame.height)
        self.centerContent.frame = CGRect(x: CONTENT_WIDTH,
                                          y: 0,
                                          width: self.bounds.width - (CONTENT_WIDTH * 2),
                                          height: kNavigationBarHight())
        self.rightContent.frame = CGRect(x: self.centerContent.frame.maxX,
                                         y: 0,
                                         width: CONTENT_WIDTH,
                                         height: kNavigationBarHight())
        self.bottomLine.frame = CGRect(x: 0,
                                       y: kNavigationBarHight() - 0.5,
                                       width: kScreenWidth(),
                                       height: 0.5)
    }
    
}

// MARK: - Left button and center title.
extension SGNavigationBar {
    
    
    
    convenience init(title: String, leftText: String) {
    
    
        self.init()
        self.init(leftView: self.createLeftButton(text: leftText),
                  centerView: self.createCenterLabel(text: title),
                  rightView: nil)
    }
    
    private func createLeftButton(text: String) -> UIButton{
    
    
        let button = UIButton()
        button.frame.size = CGSize(width: 24, height: 24)
        if text != "" {
    
    
            button.setTitle(text, for: .normal)
        } else {
    
    
            button.setImage(UIImage(named: LEFT_IMAGE_NAME), for: .normal)
        }
        button.addTarget(self, action: #selector(leftButtonAction), for: .touchUpInside)
        return button
    }
    
    private func createCenterLabel(text: String) -> UILabel {
    
    
        let label = UILabel()
        label.textAlignment = .center
        label.textColor = .black
        label.text = text
        label.frame.size = CGSize(width: self.centerContent.bounds.width, height: kNavigationBarHight())
        return label
    }
    
    @objc private final func leftButtonAction(){
    
    
        if self.leftActionClosure != nil {
    
    
            leftActionClosure!()
        }
    }
    
    /**
     When click left area to callback this method.
     */
    public func setOnLeftClickListener(listener: ClickAction?){
    
    
        leftActionClosure = {
    
    
            if listener != nil {
    
    
                listener!()
            }
        }
    }
    
}

// MARK: - Left button and center title and right button.
// Some method use above extension content.
extension SGNavigationBar{
    
    
    
    /**
     Convenience generate a NavigationBar with three parameters.
     - Parameter title: Center text.
     - Parameter leftText: Left button title, input `""` to use image for button otherwise show the text parameter.
     - Parameter rightText: Right buttom title, input `""` to use image for button otherwise show the text parameter.
     */
    convenience init(title: String, leftText: String, rightText: String) {
    
    
        self.init()
        self.init(leftView: self.createLeftButton(text: leftText),
                  centerView: self.createCenterLabel(text: title),
                  rightView: self.createRightButton(text: rightText))
    }
    
    private func createRightButton(text: String) -> UIButton{
    
    
        let button = UIButton()
        button.frame.size = CGSize(width: 24, height: 24)
        if text != "" {
    
    
            button.setTitle(text, for: .normal)
        } else {
    
    
            button.setImage(UIImage(named: RIGHT_IMAGE_NAME), for: .normal)
        }
        button.addTarget(self, action: #selector(rightButtonAction), for: .touchUpInside)
        return button
    }

    @objc private final func rightButtonAction(){
    
    
        if self.rightActionClosure != nil {
    
    
            rightActionClosure!()
        }
    }
    
    /**
     When click right area to callback this method.
     */
    public func setOnRightClickListener(listener: ClickAction?){
    
    
        rightActionClosure = {
    
    
            if listener != nil {
    
    
                listener!()
            }
        }
    }
    
}

使用

まず UINavigationBar を無効にすると、いくつかの短いコードで問題ありません (NavigationBar のフレームを設定する必要はありません。全画面と非全画面のレイアウトがすでに内部にあります)。

let navigationBar = SGNavigationBar(title:"OS X", leftText: "")
navigationBar.isBlur = true
navigationBar.barBackgroundImage = UIImage(named:"wall")
navigationBar.isFullScreen = true
navigationBar.setOnClickLeftListener {
    
    
// Click left .
}
self.view.addSubview(navigationBar)


レンダリング

ここに画像の説明を挿入
ガウスぼかしは確かに美しい

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/kicinio/article/details/126072782