Skip to content

Instantly share code, notes, and snippets.

@freedom27
Last active March 21, 2024 08:51
Show Gist options
  • Star 90 You must be signed in to star a gist
  • Fork 15 You must be signed in to fork a gist
  • Save freedom27/c709923b163e26405f62b799437243f4 to your computer and use it in GitHub Desktop.
Save freedom27/c709923b163e26405f62b799437243f4 to your computer and use it in GitHub Desktop.
//
// 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()
}
}
@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

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

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

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