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()
}
}
@notoriousturtle

This comment has been minimized.

Copy link

@notoriousturtle notoriousturtle commented Oct 18, 2016

Updated for Swift 3. It would be awesome if someone could make this support more than 1 number in the badge.

import UIKit

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 = 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.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()
    }
}
@austimkelly

This comment has been minimized.

Copy link

@austimkelly austimkelly commented Nov 30, 2016

This one supports double digits, but not triple. Just a small change on the badge width and label offset, but I'll post the whole thing:

import UIKit

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 = 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 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()
    }
}
@ghost

This comment has been minimized.

Copy link

@ghost ghost commented Dec 11, 2016

Badge don't seem to work on UIBarButtonItem that are on UIToolbar, any ideas?

@dubayb

This comment has been minimized.

Copy link

@dubayb dubayb commented Jan 5, 2017

I've been searching for nearly a full day and implemented several others' attempts..This one is the best and easiest way!!! Thank you so much!!

@mctoys

This comment has been minimized.

Copy link

@mctoys mctoys commented Jan 12, 2017

I hate to ask, but how do I use this? I am new to IOS development. I have added the swift file and created a navigation button like this:

navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(lockedButton))

Not sure where I can use the addBadge method? I tried referencing it like this:

navigationItem.rightBarButtonItem = UIBarButtonItem.addBadge()

But there is a red line through the addBadge option as I type, and it doesn't like it if I add it in manually...

@mctoys

This comment has been minimized.

Copy link

@mctoys mctoys commented Jan 12, 2017

Ok, sorry, I found the answer. For anyone else looking, you need to reference the button in a variable and add the method on that...

navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(lockedButton))

	let rightBarButtons = self.navigationItem.rightBarButtonItems
	
	let lastBarButton = rightBarButtons?.last
	
	lastBarButton?.addBadge(number: 4)
@mglasgow

This comment has been minimized.

Copy link

@mglasgow mglasgow commented Jan 29, 2017

This was a very useful starting point. I've taken the Swift 3 version and enhanced the function so it supports text (not just numbers). It also draws a rounded rectangle instead of a circle, to better support wider strings. The code dynamically calculates the badge size based on the badge text.

Finally, I've changed the signature to be setBadge. If the badge is null or empty, then the badge will be removed.

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: UIFontWeightRegular)
        }
        
        let badgeSize = text.size(attributes: [NSFontAttributeName: 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()
    }
}
@tosbaha

This comment has been minimized.

Copy link

@tosbaha tosbaha commented Feb 14, 2017

I have tested with Xcode 8 and Swift 3. When device orientation changes, badge is removed. I checked the view hierarchy and observed that badge is gone. I hope someone writes a fix for that.

@RTimal

This comment has been minimized.

Copy link

@RTimal RTimal commented Mar 17, 2017

Thanks guys, very useful.

@iahmedhendi

This comment has been minimized.

Copy link

@iahmedhendi iahmedhendi commented Apr 23, 2017

@tosbaha
if you put the code of addBadge inside viewDidLayoutSubviews() it will work fine with orientation

@Adil-Yousuf

This comment has been minimized.

Copy link

@Adil-Yousuf Adil-Yousuf commented May 4, 2017

thanks its work fine.

@ugenlik

This comment has been minimized.

Copy link

@ugenlik 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

This comment has been minimized.

Copy link

@sebastienboulogne 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

This comment has been minimized.

Copy link

@mohn93 mohn93 commented Sep 3, 2017

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

This fork has more features, hope it helps

@febinfathah

This comment has been minimized.

Copy link

@febinfathah 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

This comment has been minimized.

Copy link

@scurioni 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

This comment has been minimized.

Copy link

@smifsud smifsud commented Nov 5, 2017

works for me 9.0.1

@zeeip6

This comment has been minimized.

Copy link

@zeeip6 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

This comment has been minimized.

Copy link

@nithingwl 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

This comment has been minimized.

Copy link

@piotrros 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

This comment has been minimized.

Copy link

@johnxy84 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

This comment has been minimized.

Copy link

@khusro017 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

This comment has been minimized.

Copy link

@kdieu001 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

This comment has been minimized.

Copy link

@mahmoudkhudairi 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

This comment has been minimized.

Copy link

@Hexfire Hexfire commented Jan 15, 2018

Thank you all for the solution, neat and concise.

@Sega-Zero

This comment has been minimized.

Copy link

@Sega-Zero 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

This comment has been minimized.

Copy link

@Sega-Zero Sega-Zero commented Jan 25, 2018

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

@dipeshparashar

This comment has been minimized.

Copy link

@dipeshparashar dipeshparashar commented Jan 26, 2018

Its not working in Swift4.

@basememara

This comment has been minimized.

Copy link

@basememara basememara commented Jan 30, 2018

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

@mukulm24

This comment has been minimized.

Copy link

@mukulm24 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

This comment has been minimized.

Copy link

@Dellybro Dellybro commented Jul 18, 2018

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

@z563721

This comment has been minimized.

Copy link

@z563721 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

This comment has been minimized.

Copy link

@z563721 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

This comment has been minimized.

Copy link

@ahmedAlmasri 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

This comment has been minimized.

Copy link

@pandeyshivang 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.

@nkmhang

This comment has been minimized.

Copy link

@nkmhang nkmhang 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

This comment has been minimized.

Copy link

@adam-leitgeb 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

This comment has been minimized.

Copy link

@AP-94 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
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.