Iganinのブログ

日頃の開発で学んだ知見を中心に記事を書いています。

【iOS】NavigationBarのUIBarButtonItemにバッジを付与する方法

はじめに

アプリを作成する際に通知の数をバッジで表現することがよくあります。たとえば通知タブを作成し、タブにバッジを表示する、UIApplication.shared.badgeNumberに値を設定してアプリアイコンにバッジをつけるといった対応です。

開発を行う中で画面のナビゲーションバー部分にバッジを付与したいと行ったケースが発生することがあります。ただ、UIBarButtonItemには通常バッジはデフォルトでは表示できないためひと工夫が必要です。

本記事ではサードパーティーのライブラリに頼らずUIBarButtonItemにバッジを表示する方法を記載します。

環境設定

以下の環境を使用しています。

内容

調査して見つかった方法

外部ライブラリ

UIBarButtonItem-Badge

スターの数も比較的多く、UIButtonにもバッジを付与することができます。 ただiOS11以降では下記のIssueがあがっているようにCustomViewを設定しないとバッジを付与できないため注意が必要です。また、更新日付をみるとかなり長い間メンテナンスされていないことがわかります。

IOS 11.2: BarButtonItem Badge is not working · Issue #37 · mikeMTOL/UIBarButtonItem-Badge · GitHub

PPBadgeView

ドキュメントが中国語なので少々読むのが大変ですが、所々ある英単語から雰囲気は掴めるかと思います。最近もメンテナンスされていることが伺えますので導入するならば良いのではないでしょうか。

コード実装

ios - How to put a badge on customized UIBarButtonItem - Stack Overflow

上記のStackOverflowの投稿が見つかりました。 ViewControllerのライフサイクルの中でUIBarButtonItemにLabelをaddSubViewする、viewをObserveValueして呼び出しのタイミングでLabelの初期化を行うなどの方法が記載されています。

UIBarButtonItem with badge, Swift 4, iOS 9/10/11 · GitHub

上記の実装も参考になりました。 badgeNumberが更新されるたびにLabelのupdateを行なっており、badgeNumber > 0 で labelをaddし更新、 badgeNumber <= 0 で LabelをremoveFromSuperViewしています。

今回の実装

以下が今回採用した実装です。注意点はコードでの生成を前提としているためInterface Builder上では機能しないことです。必要に応じて適宜 required init?(coder: NSCoder), IBDesignable, IBInspectableの実装を行なってください。

本実装の問題意識としては、Labelの初期化を本クラスの初期化時にのみ行うようにしたいという点があります。なお、1桁の数字の表示を前提とした実装のため、2桁以上の表示を行う場合は、Label部分の制約を調整してください。

public class BadgeBarButtonItem: UIBarButtonItem {
    
    struct Constants {
        // 表示するバッジの大きさです。
        // 円形バッジを前提として固定値としていますが、2桁以上でも崩れないように
        // するためにはUILabel内にpaddingを設定する等し、Widthの固定値を外して下さい
        static let labelHeight: CGFloat = 18.0
        static let labelWidth: CGFloat = 18.0
        // バッジ位置です。BarButtonItemの中央からの差分をとっています
        static let defaultOffset: CGPoint = .init(x: 12.0, y: -12.0)
    }
    
    // MARK: - Property
    
    public var badgeNumber: Int = 0 {
        didSet { self.updateBadge() }
    }
    
    private lazy var label: UILabel = {
        let label = UILabel()
        // バッジの背景色です。必要に応じて変数化してください。
        label.backgroundColor = .red
        // 1桁の数字を前提としているため円形としています。
        // こちらも必要に応じて変更してください。
        label.layer.cornerRadius = Constants.labelHeight / 2.0
        label.clipsToBounds = true
        label.isUserInteractionEnabled = false
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .center
        // ラベルの文字色です。必要に応じて変数化してください。
        label.textColor = .white
        label.font = .systemFont(ofSize: 12.0)
        // 前面にでるようにするためにzPosition=1としています。
        label.layer.zPosition = 1
        return label
    }()
    
    // MARK: - Life Cycle
    public init(image: UIImage, target: Any?, action: Selector) {
        super.init()
        let button = UIButton(type: .system)
        button.setImage(image, for: .normal)
        button.addTarget(target, action: action, for: .touchUpInside)
        // labelをaddする対象としてCustomViewを使用しています。
        // そのため、UIButtonをCustomViewとしてSetしています。
        self.customView = button
        setupBadgeLabel()
        updateBadge()
    }
    
    required init?(coder: NSCoder) {
        // Interface Builderでの使用は想定していません。
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: - Private Function
private extension BadgeBarButtonItem {
    func updateBadge() {
        // badgeNumberが更新される都度 Labelの数字を更新します。
        label.text = "\(badgeNumber)"
        label.isHidden = badgeNumber == 0
    }
    
    func setupBadgeLabel() {
        guard let customView = customView else { return }
        customView.addSubview(label)
        label.widthAnchor.constraint(equalToConstant: Constants.labelWidth).isActive = true
        label.heightAnchor.constraint(equalToConstant: Constants.labelHeight).isActive = true
        label.centerXAnchor.constraint(equalTo: customView.centerXAnchor, constant: Constants.defaultOffset.x).isActive = true
        label.centerYAnchor.constraint(equalTo: customView.centerYAnchor, constant: Constants.defaultOffset.y).isActive = true
    }
}

まとめ

ナビゲーションバーに設定ボタンやお知らせボタンを配置するというケースは割と想定される仕様かと思います。そのような要望が出た際に参考にしていただけると嬉しいです。

参考