Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
//
// UIBarButtonItem+Badge.swift
// PiGuardMobile
//
// Created by Stefano Vettor on 12/04/16.
// Copyright © 2016 Stefano Vettor. All rights reserved.
//
import UIKit
extension CAShapeLayer {
private func drawCircleAtLocation(location: CGPoint, withRadius radius: CGFloat, andColor color: UIColor, filled: Bool) {
fillColor = filled ? color.CGColor : UIColor.whiteColor().CGColor
strokeColor = color.CGColor
let origin = CGPoint(x: location.x - radius, y: location.y - radius)
path = UIBezierPath(ovalInRect: 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) {
return b as? CAShapeLayer
} else {
return nil
}
}
func addBadge(number number: Int, withOffset offset: CGPoint = CGPoint.zero, andColor color: UIColor = UIColor.redColor(), andFilled filled: Bool = true) {
guard let view = self.valueForKey("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, withRadius: radius, andColor: color, filled: filled)
view.layer.addSublayer(badge)
// Initialiaze Badge's label
let label = CATextLayer()
label.string = "\(number)"
label.alignmentMode = kCAAlignmentCenter
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.whiteColor().CGColor : color.CGColor
label.backgroundColor = UIColor.clearColor().CGColor
label.contentsScale = UIScreen.mainScreen().scale
badge.addSublayer(label)
// Save Badge as UIBarButtonItem property
objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
func updateBadge(number number: Int) {
if let text = badgeLayer?.sublayers?.filter({ $0 is CATextLayer }).first as? CATextLayer {
text.string = "\(number)"
}
}
func removeBadge() {
badgeLayer?.removeFromSuperlayer()
}
}
@Adil-Yousuf
Copy link

Adil-Yousuf commented May 4, 2017

thanks its work fine.

@ugenlik
Copy link

ugenlik commented Jun 20, 2017

I needed same thing for a regular UIButton,

extension UIButton
{
    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 , addedView:UIView?) {
        guard let view = addedView else { return }
        
        badgeLayer?.removeFromSuperlayer()
        
        var badgeWidth = 8
        var numberOffset = 4
        
        if number > 9 {
            badgeWidth = 12
            numberOffset = 6
        }
        
        // 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 = kCAAlignmentCenter
        label.fontSize = 11
        label.frame = CGRect(origin: CGPoint(x: location.x - CGFloat(numberOffset), y: offset.y), size: CGSize(width: badgeWidth, 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 UIButtonItem 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()
    }

}

then wherever

    override func viewDidLoad() {
        super.viewDidLoad()
        button.tag = tagNumber   
    }

then

                  if let foundView = self.button.superView.viewWithTag(tagNumber) {
                    self.button.addBadge(number: 2, addedView: foundView)
                }

@sebastienboulogne
Copy link

sebastienboulogne commented Aug 22, 2017

Hi all,

after doing this in viewDidLoad (and adding the extension code), i can see the bar button item in the navigation bar but not the badge :(

let inboxButton:UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "inbox"), style: .plain, target: self, action: #selector(presentInbox))
let point = CGPoint(x: 0, y: 0)
inboxButton.addBadge(number: 2, withOffset: point, andColor: UIColor.black, andFilled: false)
self.navigationItem.leftBarButtonItem = inboxButton

Has someone a simple working example? Thank you very much Guys :)

@mohn93
Copy link

mohn93 commented Sep 3, 2017

https://gist.github.com/mohn93/bd950eeed82749b2ecad40ba3d4d0f1d

This fork has more features, hope it helps

@febinfathah
Copy link

febinfathah commented Oct 22, 2017

Swift 4 version of the same

import UIKit

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
    }
}

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(text: String?, withOffsetFromTopRight offset: CGPoint = CGPoint.zero, andColor color:UIColor = UIColor.red, andFilled filled: Bool = true, andFontSize fontSize: CGFloat = 11)
    {
        badgeLayer?.removeFromSuperlayer()
        
        if (text == nil || text == "") {
            return
        }
        
        addBadge(text: text!, withOffset: offset, andColor: color, andFilled: filled)
    }
    
    private func addBadge(text: String, withOffset offset: CGPoint = CGPoint.zero, andColor color: UIColor = UIColor.red, andFilled filled: Bool = true, andFontSize fontSize: CGFloat = 11)
    {
        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: UIFont.Weight.regular)
        }
        
        let badgeSize = text.size(withAttributes: [NSAttributedStringKey.font: font])
        
        // Initialize Badge
        let badge = CAShapeLayer()
        
        let height = badgeSize.height;
        var width = badgeSize.width + 2 /* padding */
        
        //make sure we have at least a circle
        if (width < height) {
            width = height
        }
        
        //x position is offset from right-hand side
        let x = view.frame.width - width + offset.x
        
        let badgeFrame = CGRect(origin: CGPoint(x: x, y: offset.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.string = text
        label.alignmentMode = kCAAlignmentCenter
        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)
    }
    
    private func removeBadge() {
        badgeLayer?.removeFromSuperlayer()
    }
}

@scurioni
Copy link

scurioni commented Nov 3, 2017

Has anyone been able to use this with xcode 9?

This code does not work anymore:
guard let view = self.value(forKey: "view") as? UIView else { return }

@smifsud
Copy link

smifsud commented Nov 5, 2017

works for me 9.0.1

@zeeip6
Copy link

zeeip6 commented Nov 13, 2017

** #guard let view = self.value(forKey: "view") as? UIView else { return }** here view is getting value nil because of which it does not show badge how do i fix this?

@nithingwl
Copy link

nithingwl commented Nov 22, 2017

@zeeip6, I faced the same issue. It guess the name of the key for view might be changed to something else and because of this its returning nil. The workaround I did for that was to use customView as below

guard let view = self.customView else { return }

and initialize a BarButtonItem by giving an UIButton as its custom view.

let bellButton = UIButton(type: .custom)
bellButton.setBackgroundImage(UIImage(named:"notification"), for: .normal)
bellButton.addTarget(self, action: #selector(didClickNotificationButton(_:)), for: .touchUpInside)

let notificationBarButton = UIBarButtonItem(customView: bellButton)

Note: Offset needs to be adjusted as required when adding a badge.

@piotrros
Copy link

piotrros commented Nov 28, 2017

@febinfathah code is working, but in my case badge was a truncated a bit from the top and I had to add an offset, not a big deal. But then I realized that badge is drawing behind icon, which is unacceptable. I fixed it by adding the following line on the bottom off "addBadge":

badge.zPosition = 1000

So, the new code is:

import UIKit

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
    }
    
}

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(text: String?, withOffsetFromTopRight offset: CGPoint = CGPoint.zero, andColor color:UIColor = UIColor.red, andFilled filled: Bool = true, andFontSize fontSize: CGFloat = 11) {
        badgeLayer?.removeFromSuperlayer()
        
        if (text == nil || text == "") {
            return
        }
        
        addBadge(text: text!, withOffset: offset, andColor: color, andFilled: filled)
    }
    
    private func addBadge(text: String, withOffset offset: CGPoint = CGPoint.zero, andColor color: UIColor = UIColor.red, andFilled filled: Bool = true, andFontSize fontSize: CGFloat = 11) {
        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: UIFont.Weight.regular)
        }
        
        let badgeSize = text.size(withAttributes: [NSAttributedStringKey.font: font])
        
        //initialize Badge
        let badge = CAShapeLayer()
        
        let height = badgeSize.height;
        var width = badgeSize.width + 2 /* padding */
        
        //make sure we have at least a circle
        if (width < height) {
            width = height
        }
        
        //x position is offset from right-hand side
        let x = view.frame.width - width + offset.x
        
        let badgeFrame = CGRect(origin: CGPoint(x: x, y: offset.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.string = text
        label.alignmentMode = kCAAlignmentCenter
        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 = 1000
    }
    
    private func removeBadge() {
        badgeLayer?.removeFromSuperlayer()
    }
    
}

Tested on both iPhone and iPad with iOS 11.1. Maintains its appearance after changing orientation as well.

@johnxy84
Copy link

johnxy84 commented Nov 30, 2017

Hello guys, I was trying to implement this using Xamarin but I always stop here because the view returned is always null. Am I missing out something or do I not understand how swift works? Here's where the error is and my c# implementation of that same code.
Swift
guard let view = self.value(forKey: "view") as? UIView else { return }
C#
var view = this.ValueForKey((Foundation.NSString)"view") as UIView;
if (view==null) return;

@khusro017
Copy link

khusro017 commented Dec 5, 2017

Hi,
Any of you have solved this.
guard let view = self.value(forKey: "view") as? UIView else { return }
This always returns a null value (Swift 3).
thank you.

@kdieu001
Copy link

kdieu001 commented Dec 13, 2017

guard let view = self.value(forKey: "view") as? UIView else { return }
This code will always return nil, if you set the badge in the wrong spot.

So if you want to display the badge, you would have to set the badge outside of viewDidLoad and you should see the badge. It seems setting the badge in viewDidAppear works for me there.

If you have been setting the badge in viewDidLoad, in iOS11 it handles it differently. Any related UI work will break or wont work. Try setting the badge outside of viewDidLoad and see if that works.

I hope this helps

@mahmoudkhudairi
Copy link

mahmoudkhudairi commented Jan 12, 2018

As others have mentioned the view will be nil if you set the badge on viewDidLoad on iOS 11 and Xcode 9. To solve this just set the badge to the bar button Item in the " viewDidAppear" function.

@Hexfire
Copy link

Hexfire commented Jan 15, 2018

Thank you all for the solution, neat and concise.

@Sega-Zero
Copy link

Sega-Zero commented Jan 25, 2018

Instead of global handle var, it is better to use static struct, IMHO

private struct AssociatedKey {
        static var badgeLayer = "badgeLayer"
    }

    fileprivate var badgeLayer: CAShapeLayer? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKey.badgeLayer) as? CAShapeLayer
        }
        set {
            if let newValue = newValue {
                objc_setAssociatedObject(
                    self,
                    &AssociatedKey.badgeLayer,
                    newValue as CAShapeLayer?,
                    .OBJC_ASSOCIATION_RETAIN_NONATOMIC
                )
            }
        }
    }

@Sega-Zero
Copy link

Sega-Zero commented Jan 25, 2018

Does anyone know why this doesn't work with back button?

@dipeshparashar
Copy link

dipeshparashar commented Jan 26, 2018

Its not working in Swift4.

@basememara
Copy link

basememara commented Jan 30, 2018

Different approach and works with Swift 4 and various view controller states: https://gist.github.com/yonat/75a0f432d791165b1fd6

@mukulm24
Copy link

mukulm24 commented Jul 5, 2018

I have used this for tabItem but I want to show the layer on top of tabImage but layer is added behind it. Any way through which it can be attained?

@Dellybro
Copy link

Dellybro commented Jul 18, 2018

This doesn't work unless it is a custom uibarbutton unforunately. :(

@z563721
Copy link

z563721 commented Jan 31, 2019

I have used this for tabItem but I want to show the layer on top of tabImage but layer is added behind it. Any way through which it can be attained?

u can get it by the property "badgeLayer"

@z563721
Copy link

z563721 commented Jan 31, 2019

This doesn't work unless it is a custom uibarbutton unforunately. :(

u can extension a button to do it , c the #ugenlik comment

@ahmedAlmasri
Copy link

ahmedAlmasri commented Feb 20, 2019

swift 4.2

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()
        
        var badgeWidth = 8
        var numberOffset = 4
        
        if number > 9 {
            badgeWidth = 12
            numberOffset = 6
        }
        
        // 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 - CGFloat(numberOffset), y: offset.y), size: CGSize(width: badgeWidth, 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()
    }
}

@pandeyshivang
Copy link

pandeyshivang commented Jul 17, 2019

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()
    
    var badgeWidth = 8
    var numberOffset = 4
    
    if number > 9 {
        badgeWidth = 12
        numberOffset = 6
    }
    
    // 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 - CGFloat(numberOffset), y: offset.y), size: CGSize(width: badgeWidth, 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 is not working.

Copy link

ghost commented Sep 12, 2019

ok this code is not work for me:
guard let view = self.valueForKey("view") as? UIView else { return }
It always returns nil.
This is my code (Xcode 10, swift 5):
`private lazy var btnNoti: UIBarButtonItem = {
return EBarButtonItem.notification.create(target: self, action: #selector(btnBell_Touched))
}()

self.navigationItem.setLeftBarButton(btnNoti, animated: true)
btnNoti.addBadge(number:10)
`

@adam-leitgeb
Copy link

adam-leitgeb commented Feb 17, 2020

Update to Swift 5 + fix SwiftLint warnings:

import UIKit

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(text: String?, offset: CGPoint = .zero, color: UIColor = .red, filled: Bool = true, fontSize: CGFloat = 11.0) {
        badgeLayer?.removeFromSuperlayer()

        guard let text = text, !text.isEmpty else {
            return
        }
        addBadge(text: text, offset: offset, color: color, filled: filled)
    }

    private func addBadge(text: String, offset: CGPoint = .zero, color: UIColor = .red, filled: Bool = true, fontSize: CGFloat = 11) {
        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)
        }

        let badgeSize = text.size(withAttributes: [.font: font])

        // initialize Badge
        let badge = CAShapeLayer()

        let height = badgeSize.height
        var width = badgeSize.width + 2 // padding

        // make sure we have at least a circle
        if width < height {
            width = height
        }

        // x position is offset from right-hand side
        let x = view.frame.width - width + offset.x

        let badgeFrame = CGRect(origin: CGPoint(x: x, y: offset.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.string = text
        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
    }
}

@AP-94
Copy link

AP-94 commented Oct 5, 2020

Working in swift 5, same code of @adam-leitgeb , but i only wanted the red dot, without the number, so if anyone is looking for the same, i leave the code here 👍 .

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 - width + offset.x
    
    // I suggest you try the x and y sets, for my case, i will use this coordinates for better result,
    // but depends on the syze of your image
    // let x = view.frame.width + offset.x - 17
    // let y = view.frame.height + offset.y - 34

    let badgeFrame = CGRect(origin: CGPoint(x: x, y: offset.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
}
}

@Arneoldenhave
Copy link

Arneoldenhave commented Sep 30, 2021

I made the extension more generic so to work on any old NSObject aswel as UIViews.

A new class can confirm by implementing the var badgeParent variable.

Also added some standardisation to the API;

NOTE: remove default parameters from initial functions!

Be wary of autolayout update cycles with the view since it uses relative positioning with points

usage:

let badgeView = UIView(frame: .zero).addBrandedBadge(text: "1", size: .small, color: .regular)

private var handle: UInt8 = 0

public protocol Badge {
    var badgeParentView : UIView? { get }
}

extension UIBarButtonItem : Badge {
    public var badgeParentView: UIView? {
        self.value(forKey: "view") as? UIView
    }
}

extension UIView : Badge {
    
    public var badgeParentView: UIView? { self }
}


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

    public func setBadge(text: String?, offset: CGPoint , color: UIColor, borderColor: UIColor, borderWidth: CGFloat,  textColor: UIColor, filled: Bool = true, fontSize: CGFloat) {
        badgeLayer?.removeFromSuperlayer()

        guard let text = text, !text.isEmpty else {
            return
        }
        addBadge(text: text, offset: offset, color: color,borderColor: borderColor, borderWidth: borderWidth, textColor: textColor,  filled: filled, fontSize: fontSize)
    }

    private func addBadge(text: String, offset: CGPoint , color: UIColor, borderColor: UIColor, borderWidth: CGFloat,  textColor: UIColor, filled: Bool = true, fontSize: CGFloat) {
        guard let view = badgeParentView else {  return  }
        
        var font = UIFont.systemFont(ofSize: fontSize)

        if #available(iOS 9.0, *) {
            font = UIFont.monospacedDigitSystemFont(ofSize: fontSize, weight: .regular)
        }

        let badgeSize = text.size(withAttributes: [.font: font])

        // initialize Badge
        let badge = CAShapeLayer()

        let height = badgeSize.height
        var width = badgeSize.width + 2 // padding

        // make sure we have at least a circle
        if width < height {
            width = height
        }

        // x position is offset from right-hand side
        let x = view.frame.width - width + offset.x

        let badgeFrame = CGRect(origin: CGPoint(x: x, y: offset.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.string = text
        label.alignmentMode = .center
        label.font = font
        label.fontSize = font.pointSize
        label.foregroundColor = textColor.cgColor

        label.borderWidth = borderWidth
        label.borderColor = borderColor.cgColor

        
        label.frame = badgeFrame
        label.cornerRadius = label.frame.height / 2
        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
    }

    public func removeBadge() {
        badgeLayer?.removeFromSuperlayer()
    }
}
public extension Badge {
    
    @discardableResult
    func addBrandedBadge(text: String, size: BadgeSizeVariant, color: BadgeColorVariant)  -> Self {
        self.setBadge(text: text, offset: size.offSet, color: color.backroundColor, borderColor: color.borderColor, borderWidth: size.borderWidth, textColor: color.textColor, filled: true, fontSize: size.fontSize)
        return self
    }
}


public struct BadgeSizeVariant {
    
    public let borderWidth: CGFloat
    public let fontSize : CGFloat
    public let offSet : CGPoint
    
    public init(borderWidth: CGFloat, fontSize: CGFloat, offSet: CGPoint) {
        self.borderWidth = borderWidth
        self.fontSize = fontSize
        self.offSet = offSet
    }
}

public extension BadgeSizeVariant {
    
    static var small = Self(borderWidth: 1, fontSize: 12, offSet: CGPoint(x: -8, y: 4))
    static var medium = Self(borderWidth: 1.5, fontSize: 16, offSet: CGPoint(x: -4, y: 2))
    static var large = Self(borderWidth: 3, fontSize: 24, offSet: CGPoint(x: 8, y: -8))

}


public struct BadgeColorVariant {

    public let borderColor : UIColor
    public let backroundColor : UIColor
    public let textColor : UIColor

    
    public init(border: UIColor, background: UIColor, text: UIColor) {
        self.borderColor = border
        self.backroundColor = background
        self.textColor = text
    }
}



public extension BadgeColorVariant {
    
    static var regular = Self(border: .white, background: .red, text: .white)
    static var success = Self(border: .white, background: .green, text: .white)
    
}


// MARK: - Utilities
public extension CAShapeLayer {
    
    public 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
    }
}

@fukemy
Copy link

fukemy commented Nov 29, 2021

@AP-94 love u dude, but im not gay 😎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment