29

I am trying to display badge on my notification button, in app as displayed on AppIcon.

So far whatever i have researched is related to Obj. C, but nothing that specifically discussed way to implement that solution into Swift,

Please help to find a solution to add a custom class / code to achieve Badge on UiBarbutton and UiButton.

Researched so far:

https://github.com/Marxon13/M13BadgeView

along with MKBadge class etc.

Krish Munot
  • 1,093
  • 2
  • 18
  • 29
coolagrwal
  • 293
  • 1
  • 4
  • 7
  • For `UIBarButtonItem` you can try this project: [ENMBadgeBarButtonItem](https://github.com/enmiller/ENMBadgedBarButtonItem-Swift) – Sourabh Sharma Oct 15 '15 at 09:36

13 Answers13

29

There is a more elegant solution with an extension for UIButtonItem

extension CAShapeLayer {
    func drawCircleAtLocation(location: CGPoint, withRadius radius: CGFloat, andColor color: UIColor, filled: Bool) {
        fillColor = filled ? color.cgColor : UIColor.white.cgColor
        strokeColor = color.cgColor
        let origin = CGPoint(x: location.x - radius, y: location.y - radius)
        path = UIBezierPath(ovalIn: CGRect(origin: origin, size: CGSize(width: radius * 2, height: radius * 2))).cgPath
    }
}

private var handle: UInt8 = 0

extension UIBarButtonItem {
    private var badgeLayer: CAShapeLayer? {
        if let b: AnyObject = objc_getAssociatedObject(self, &handle) as AnyObject? {
            return b as? CAShapeLayer
        } else {
            return nil
        }
    }

    func addBadge(number: Int, withOffset offset: CGPoint = CGPoint.zero, andColor color: UIColor = UIColor.red, andFilled filled: Bool = true) {
        guard let view = self.value(forKey: "view") as? UIView else { return }

        badgeLayer?.removeFromSuperlayer()

        // Initialize Badge
        let badge = CAShapeLayer()
        let radius = CGFloat(7)
        let location = CGPoint(x: view.frame.width - (radius + offset.x), y: (radius + offset.y))
        badge.drawCircleAtLocation(location: location, withRadius: radius, andColor: color, filled: filled)
        view.layer.addSublayer(badge)

        // Initialiaze Badge's label
        let label = CATextLayer()
        label.string = "\(number)"
        label.alignmentMode = CATextLayerAlignmentMode.center
        label.fontSize = 11
        label.frame = CGRect(origin: CGPoint(x: location.x - 4, y: offset.y), size: CGSize(width: 8, height: 16))
        label.foregroundColor = filled ? UIColor.white.cgColor : color.cgColor
        label.backgroundColor = UIColor.clear.cgColor
        label.contentsScale = UIScreen.main.scale
        badge.addSublayer(label)

        // Save Badge as UIBarButtonItem property
        objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }

    func updateBadge(number: Int) {
        if let text = badgeLayer?.sublayers?.filter({ $0 is CATextLayer }).first as? CATextLayer {
            text.string = "\(number)"
        }
    }

    func removeBadge() {
        badgeLayer?.removeFromSuperlayer()
    }
}

This great code was created by Stefano Vettor and you can find all the details at: https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4

Mushrankhan
  • 749
  • 9
  • 12
Julio Bailon
  • 3,735
  • 2
  • 33
  • 34
  • I don't think it will show the long text of the badge correctly. For example 1000. – couldDog Aug 06 '20 at 08:46
  • @couldDog it would be very simple to alter the size of the frame to accommodate. But that is a different topic. Sometimes you need to write some code by yourself. – Julio Bailon Sep 24 '20 at 16:03
28

Working Solution :

Step 1: Firstly create new swift file which is a subclass to UIButton as follows:

import UIKit

class BadgeButton: UIButton {

    var badgeLabel = UILabel()

    var badge: String? {
        didSet {
            addbadgetobutton(badge: badge)
        }
    }

    public var badgeBackgroundColor = UIColor.red {
        didSet {
            badgeLabel.backgroundColor = badgeBackgroundColor
        }
    }

    public var badgeTextColor = UIColor.white {
        didSet {
            badgeLabel.textColor = badgeTextColor
        }
    }

    public var badgeFont = UIFont.systemFont(ofSize: 12.0) {
        didSet {
            badgeLabel.font = badgeFont
        }
    }

    public var badgeEdgeInsets: UIEdgeInsets? {
        didSet {
            addbadgetobutton(badge: badge)
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        addbadgetobutton(badge: nil)
    }

    func addbadgetobutton(badge: String?) {
        badgeLabel.text = badge
        badgeLabel.textColor = badgeTextColor
        badgeLabel.backgroundColor = badgeBackgroundColor
        badgeLabel.font = badgeFont
        badgeLabel.sizeToFit()
        badgeLabel.textAlignment = .center
        let badgeSize = badgeLabel.frame.size

        let height = max(18, Double(badgeSize.height) + 5.0)
        let width = max(height, Double(badgeSize.width) + 10.0)

        var vertical: Double?, horizontal: Double?
        if let badgeInset = self.badgeEdgeInsets {
            vertical = Double(badgeInset.top) - Double(badgeInset.bottom)
            horizontal = Double(badgeInset.left) - Double(badgeInset.right)

            let x = (Double(bounds.size.width) - 10 + horizontal!)
            let y = -(Double(badgeSize.height) / 2) - 10 + vertical!
            badgeLabel.frame = CGRect(x: x, y: y, width: width, height: height)
        } else {
            let x = self.frame.width - CGFloat((width / 2.0))
            let y = CGFloat(-(height / 2.0))
            badgeLabel.frame = CGRect(x: x, y: y, width: CGFloat(width), height: CGFloat(height))
        }

        badgeLabel.layer.cornerRadius = badgeLabel.frame.height/2
        badgeLabel.layer.masksToBounds = true
        addSubview(badgeLabel)
        badgeLabel.isHidden = badge != nil ? false : true
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.addbadgetobutton(badge: nil)
        fatalError("init(coder:) is not implemented")
    }
}

Step 2: Create a function in your base file which u can use in each View Controller :

  func addBadge(itemvalue: String) {

        let bagButton = BadgeButton()
        bagButton.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
        bagButton.tintColor = UIColor.darkGray
        bagButton.setImage(UIImage(named: "ShoppingBag")?.withRenderingMode(.alwaysTemplate), for: .normal)
        bagButton.badgeEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 15)
        bagButton.badge = itemvalue
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: bagButton)
    }

Step 3 : Use above function from any View Controller in this way :

self.addBadge(itemvalue: localStorage.string(forKey: "total_products_in_cart") ?? "0")
Shivam Tripathi
  • 1,405
  • 3
  • 19
  • 37
  • Works but I have 2 problems. 1. it stays round only if there are single digits, if it goes to double or triple digits it adapts a horizontal rectangular shape with round corners. 2. I use it within the vc and not on the navItem so I use anchors and switch badgeButton.translatesAutoresizingMaskIntoConstraints = false. I've tried increasing it's size hoping it would keep the rounds shape round as the text grows but his doesn't work badgeButton.widthAnchor...(equalToConstant: 100) && badgeButton.heightAnchor...(equalToConstant: 100) both are true. I only want it to stay round, not get bigger – Lance Samaria Jul 13 '19 at 11:08
12

First create label, then right bar button. On right bar button add subview which will be badge count. Finally add navigation right bar button.

SWIFT 5

let badgeCount = UILabel(frame: CGRect(x: 22, y: -05, width: 20, height: 20))
badgeCount.layer.borderColor = UIColor.clear.cgColor
badgeCount.layer.borderWidth = 2
badgeCount.layer.cornerRadius = badgeCount.bounds.size.height / 2
badgeCount.textAlignment = .center
badgeCount.layer.masksToBounds = true
badgeCount.textColor = .white
badgeCount.font = badgeCount.font.withSize(12)
badgeCount.backgroundColor = .red
badgeCount.text = "4"


let rightBarButton = UIButton(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
rightBarButton.setBackgroundImage(UIImage(named: "NotificationBell"), for: .normal)
rightBarButton.addTarget(self, action: #selector(self.onBtnNotification), for: .touchUpInside)
rightBarButton.addSubview(badgeCount)


let rightBarButtomItem = UIBarButtonItem(customView: rightBarButton)
navigationItem.rightBarButtonItem = rightBarButtomItem
Abdul Karim Khan
  • 4,256
  • 1
  • 26
  • 30
4

I had the same task. I didn't want to use third-party libraries. Firstly, I tried Stefano's solution and it's great however I decided to implement my own way to solve it.

In my humble opinion, there are simple steps described below briefly:

  1. Create UIView instance within .xib file and put necessary items like UILabel or UIImageView instance depending on your design requirements. enter image description here

The final action I did in this step is putting invisible button in the top of view's hierarchy.

enter image description here

  1. Create YourCustomView.swift and link all @IBOutlets from xib to current file inside your custom view class implementation.

enter image description here

  1. Next, implement class function in YourCustomView class which will load custom view from xib and return it as YourCustomView instance.

enter image description here

  1. Finally, add your custom badge to your custom view controller instance!

enter image description here

My result is..

enter image description here

P.S. If you need to implement @IBActions I recommend to link your custom view and custom view controller through the delegate pattern.

devshok
  • 737
  • 1
  • 8
  • 19
2

using M13BadgeView.. use this code

(im using fontawesome.swift for buttons :: https://github.com/thii/FontAwesome.swift)

    let rightButton = UIButton(frame: CGRect(x:0,y:0,width:30,height:30))
    rightButton.titleLabel?.font = UIFont.fontAwesome(ofSize: 22)
    rightButton.setTitle(String.fontAwesomeIcon(name: .shoppingBasket), for: .normal)

    let rightButtonItem : UIBarButtonItem = UIBarButtonItem(customView: rightButton)

    let badgeView = M13BadgeView()
        badgeView.text = "1"
        badgeView.textColor = UIColor.white
        badgeView.badgeBackgroundColor = UIColor.red
        badgeView.borderWidth = 1.0
        badgeView.borderColor = UIColor.white
        badgeView.horizontalAlignment = M13BadgeViewHorizontalAlignmentLeft
        badgeView.verticalAlignment = M13BadgeViewVerticalAlignmentTop
        badgeView.hidesWhenZero = true

    rightButton.addSubview(badgeView)

    self.navigationItem.rightBarButtonItem = rightButtonItem
keithics
  • 8,576
  • 2
  • 48
  • 35
1

Good answer @Julio Bailon (https://stackoverflow.com/a/45948819/1898973)!

Here is the author's site with full explanation: http://www.stefanovettor.com/2016/04/30/adding-badge-uibarbuttonitem/.

It seems not to be working on iOS 11, maybe because the script try to access the "view" property of the UIBarButtonItem. I made it work:

  1. By creating a UIButton and then creating the UIBarButtonItem using the UIButton as a customView:

    navigationItem.rightBarButtonItem = UIBarButtonItem.init(
            customView: shoppingCartButton)
    
  2. By replacing the line in the UIBarButtonItem extension:

    guard let view = self.value(forKey: "view") as? UIView else { return }
    

    with the following:

    guard let view = self.customView else { return }
    

Seems elegant to me and, best of all, it worked!

Duncan Jones
  • 67,400
  • 29
  • 193
  • 254
Leandro Fournier
  • 927
  • 1
  • 9
  • 21
  • If use UIButton, you can use 1 UILabel instead of 2 CALayers, save lots code, plus, you can use auto layout to position the label. – mmk Mar 07 '19 at 00:46
1

You can set below constraints to UILabel with respect to UIButton

align UILabel's top and trailing to UIButton

And when you need to show badge set text to UILabel and when you don't want to show badge then set empty string to UILabel

Sayali Shinde
  • 311
  • 2
  • 6
1

Download This

For BarButtonItem : Drag and Drop UIBarButtonItem+Badge.h and UIBarButtonItem+Badge.m class in project.

Write this code for set Badges:

self.navigationItem.rightBarButtonItem.badgeValue = "2"
self.navigationItem.rightBarButtonItem.badgeBGColor = UIColor.black

For UIButtton : Drag and Drop UIButton+Badge.h and UIButton+Badge.m class in project.

self.notificationBtn.badgeValue = "2"
self.notificationBtn.badgeBGColor = UIColor.black
Deep
  • 416
  • 6
  • 15
1

Answer with extension from Julio will not work.

Starting from iOS 11 this code will not work cause line of code below will not cast UIView. Also it's counting as private API and seems to be will not pass AppStore review.

guard let view = self.value(forKey: "view") as? UIView else { return } 

Thread on Apple Developer Forum

Second thing that this snippet always draws circle, so it can't fit numbers bigger than 9.

Serj Rubens
  • 621
  • 8
  • 12
1

enter image description hereHere the simplified version by using custom view

enter image description here

Heman hijack
  • 209
  • 3
  • 9
1

Easy and clear solution if you are looking for only adding the red dot without the number;

private var handle: UInt8 = 0;

extension UIBarButtonItem {
    
    
    private var badgeLayer: CAShapeLayer? {
        if let b: AnyObject = objc_getAssociatedObject(self, &handle) as AnyObject? {
            return b as? CAShapeLayer
        } else {
            return nil
        }
    }
    
    func setBadge(offset: CGPoint = .zero, color: UIColor = .red, filled: Bool = true, fontSize: CGFloat = 11) {
        badgeLayer?.removeFromSuperlayer()
        guard let view = self.value(forKey: "view") as? UIView else {
            return
        }
        
        var font = UIFont.systemFont(ofSize: fontSize)
        
        if #available(iOS 9.0, *) {
            font = UIFont.monospacedDigitSystemFont(ofSize: fontSize, weight: .regular)
        }
        
        //Size of the dot
        let badgeSize = UILabel(frame: CGRect(x: 22, y: -05, width: 10, height: 10))
        
        // initialize Badge
        let badge = CAShapeLayer()
        
        let height = badgeSize.height
        let width = badgeSize.width
        
        // x position is offset from right-hand side
        let x = view.frame.width + offset.x - 17
        let y = view.frame.height + offset.y - 34
        
        let badgeFrame = CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
        
        badge.drawRoundedRect(rect: badgeFrame, andColor: color, filled: filled)
        view.layer.addSublayer(badge)
        
        // initialiaze Badge's label
        let label = CATextLayer()
        label.alignmentMode = .center
        label.font = font
        label.fontSize = font.pointSize
        
        label.frame = badgeFrame
        label.foregroundColor = filled ? UIColor.white.cgColor : color.cgColor
        label.backgroundColor = UIColor.clear.cgColor
        label.contentsScale = UIScreen.main.scale
        badge.addSublayer(label)
        
        // save Badge as UIBarButtonItem property
        objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        
        // bring layer to front
        badge.zPosition = 1_000
    }
    
    private func removeBadge() {
        badgeLayer?.removeFromSuperlayer()
    }
    
}

// MARK: - Utilities

extension CAShapeLayer {
    func drawRoundedRect(rect: CGRect, andColor color: UIColor, filled: Bool) {
        fillColor = filled ? color.cgColor : UIColor.white.cgColor
        strokeColor = color.cgColor
        path = UIBezierPath(roundedRect: rect, cornerRadius: 7).cgPath
    }
}

The source of the code: https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4 I only made a few changes to eliminate the number.

Alessandro Pace
  • 206
  • 4
  • 8
-1

The MIBadgeButton-Swift is working also on UIBarButtonItems. Here is my code after the navigation bar is created:

let rightBarButtons = self.navigationItem.rightBarButtonItems

let alarmsBarButton = rightBarButtons?.last

let alarmsButton = alarmsBarButton.customView as! MIBadgeButton?

alarmsButton.badgeString = "10"
Petter Friberg
  • 21,252
  • 9
  • 60
  • 109
-5

You can do it programmatically with

self.tabBarItem.badgeColor = .red

or use the storyboard. See:
storyboard

Cœur
  • 37,241
  • 25
  • 195
  • 267