Skip to content

Instantly share code, notes, and snippets.

@yonat
Last active December 22, 2022 09:40
Show Gist options
  • Save yonat/75a0f432d791165b1fd6 to your computer and use it in GitHub Desktop.
Save yonat/75a0f432d791165b1fd6 to your computer and use it in GitHub Desktop.
Rounded UILabel and UIButton, Badged UIBarButtonItem
//
// Badge.swift
// Extensions for Rounded UILabel and UIButton, Badged UIBarButtonItem.
//
// Usage:
// let label = UILabel(badgeText: "Rounded Label");
// let button = UIButton(type: .System); button.rounded = true
// let barButton = UIBarButtonItem(badge: "42", title: "How Many Roads", target: self, action: "answer")
//
// Created by Yonat Sharon on 06.04.2015.
// Copyright (c) 2015 Yonat Sharon. All rights reserved.
//
import UIKit
public extension UILabel {
public convenience init(badgeText: String, color: UIColor = .red, fontSize: CGFloat = UIFont.smallSystemFontSize) {
self.init()
text = badgeText.count > 1 ? " \(badgeText) " : badgeText
textAlignment = .center
textColor = .white
backgroundColor = color
font = UIFont.systemFont(ofSize: fontSize)
layer.cornerRadius = fontSize * CGFloat(0.6)
clipsToBounds = true
translatesAutoresizingMaskIntoConstraints = false
addConstraint(NSLayoutConstraint(item: self, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: self, attribute: .height, multiplier: 1, constant: 0))
}
}
extension UIButton {
/// show background as rounded rect, like mail addressees
public var rounded: Bool {
get { return layer.cornerRadius > 0 }
set { roundWithTitleSize(newValue ? titleSize : 0) }
}
/// removes other title attributes
public var titleSize: CGFloat {
get {
let titleFont = attributedTitle(for: .normal)?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont
return titleFont?.pointSize ?? UIFont.buttonFontSize
}
set {
if UIFont.buttonFontSize == newValue || 0 == newValue {
setTitle(currentTitle, for: .normal)
} else {
let attrTitle = NSAttributedString(string: currentTitle ?? "", attributes:
[.font: UIFont.systemFont(ofSize: newValue), .foregroundColor: currentTitleColor])
setAttributedTitle(attrTitle, for: .normal)
}
if rounded {
roundWithTitleSize(newValue)
}
}
}
/// regular `setTitle()` fails after using `setAttributedTitle()` (which called if you changed `titleSize`)
public func changeTitleButKeepAttributes(_ newTitle: String) {
setAttributedTitle(nil, for: .normal)
setTitle(newTitle, for: .normal)
}
func roundWithTitleSize(_ size: CGFloat) {
let padding = size / 4
layer.cornerRadius = padding + size * 1.2 / 2
let sidePadding = padding * 1.5
contentEdgeInsets = UIEdgeInsets(top: padding, left: sidePadding, bottom: padding, right: sidePadding)
if size.isZero {
backgroundColor = .clear
setTitleColor(tintColor, for: .normal)
} else {
backgroundColor = tintColor
let currentTitleColor = titleColor(for: .normal)
if currentTitleColor == nil || currentTitleColor == tintColor {
setTitleColor(.white, for: .normal)
}
}
}
// swiftlint:disable override_in_extension
open override func tintColorDidChange() {
super.tintColorDidChange()
if rounded {
backgroundColor = tintColor
}
}
// swiftlint:enable override_in_extension
}
public extension UIBarButtonItem {
convenience init(badge: String?, title: String, target: AnyObject?, action: Selector) {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: UIFont.buttonFontSize)
button.addTarget(target, action: action, for: .touchUpInside)
button.sizeToFit()
if let badge = badge {
let badgeLabel = UILabel(badgeText: badge)
button.addSubview(badgeLabel)
button.addConstraint(NSLayoutConstraint(item: badgeLabel, attribute: .top, relatedBy: .equal, toItem: button, attribute: .top, multiplier: 1, constant: 0))
button.addConstraint(NSLayoutConstraint(item: badgeLabel, attribute: .centerX, relatedBy: .equal, toItem: button, attribute: .trailing, multiplier: 1, constant: 0))
}
self.init(customView: button)
}
var badgeLabel: UILabel? {
return customView?.viewWithTag(UIBarButtonItem.badgeTag) as? UILabel
}
var badgedButton: UIButton? {
return customView as? UIButton
}
var badgeString: String? {
get { return badgeLabel?.text?.trimmingCharacters(in: .whitespaces) }
set {
if let badgeLabel = badgeLabel {
badgeLabel.text = nil == newValue ? nil : " \(newValue!) "
badgeLabel.sizeToFit()
badgeLabel.isHidden = nil == newValue
}
}
}
var badgedTitle: String? {
get { return badgedButton?.title(for: .normal) }
set { badgedButton?.setTitle(newValue, for: .normal); badgedButton?.sizeToFit() }
}
private static let badgeTag = 7373
}
@basememara
Copy link

Nice code, thx for sharing! I added an extra convenience init to UIButtonBarItem so you can make the button an image instead of text:

extension UIBarButtonItem {
    convenience init(badge: String?, button: UIButton, target: AnyObject?, action: Selector) {
        button.addTarget(target, action: action, forControlEvents: .TouchUpInside)
        button.sizeToFit()

        let badgeLabel = UILabel(badgeText: badge ?? "")
        button.addSubview(badgeLabel)
        button.addConstraint(NSLayoutConstraint(item: badgeLabel, attribute: .Top, relatedBy: .Equal, toItem: button, attribute: .Top, multiplier: 1, constant: 0))
        button.addConstraint(NSLayoutConstraint(item: badgeLabel, attribute: .CenterX, relatedBy: .Equal, toItem: button, attribute: .Trailing, multiplier: 1, constant: 0))
        if nil == badge {
            badgeLabel.hidden = true
        }
        badgeLabel.tag = UIBarButtonItem.badgeTag

        self.init(customView: button)
    }

    convenience init(badge: String?, image: UIImage, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .Custom)
        button.frame = CGRectMake(0, 0, image.size.width, image.size.height)
        button.setBackgroundImage(image, forState: .Normal)

        self.init(badge: badge, button: button, target: target, action: action)
    }

    convenience init(badge: String?, title: String, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .System)
        button.setTitle(title, forState: .Normal)
        button.titleLabel?.font = UIFont.systemFontOfSize(UIFont.buttonFontSize())

        self.init(badge: badge, button: button, target: target, action: action)
    }

    var badgeLabel: UILabel? {
        return customView?.viewWithTag(UIBarButtonItem.badgeTag) as? UILabel
    }

    var badgedButton: UIButton? {
        return customView as? UIButton
    }

    var badgeString: String? {
        get { return badgeLabel?.text?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()) }
        set {
            if let badgeLabel = badgeLabel {
                badgeLabel.text = nil == newValue ? nil : " \(newValue!) "
                badgeLabel.sizeToFit()
                badgeLabel.hidden = nil == newValue
            }
        }
    }

    var badgedTitle: String? {
        get { return badgedButton?.titleForState(.Normal) }
        set { badgedButton?.setTitle(newValue, forState: .Normal); badgedButton?.sizeToFit() }
    }

    private static let badgeTag = 7373
}

@tvillafane
Copy link

This is great. Looked all over for a nifty solution; this is it. Thanks a million!

@MagnusNordin
Copy link

I have made a fork with an update to Swift 3.0: https://gist.github.com/MagnusNordin/de81ead08cfbc33796ffd71649292ca6

@basememara
Copy link

Swift 4 and sub-class, etc. updates:

import UIKit

public class BadgeBarButtonItem: UIBarButtonItem {
    
    private(set) lazy var badgeLabel: UILabel = {
        let label = UILabel()
        
        label.text = badgeText?.isEmpty == false ? " \(badgeText!) " : nil
        label.isHidden = badgeText?.isEmpty != false
        label.textColor = badgeFontColor
        label.backgroundColor = badgeBackgroundColor
        
        label.font = .systemFont(ofSize: badgeFontSize)
        label.layer.cornerRadius = badgeFontSize * CGFloat(0.6)
        label.clipsToBounds = true
        
        label.translatesAutoresizingMaskIntoConstraints = false
        label.addConstraint(
            NSLayoutConstraint(
                item: label,
                attribute: .width,
                relatedBy: .greaterThanOrEqual,
                toItem: label,
                attribute: .height,
                multiplier: 1,
                constant: 0
            )
        )
        
        return label
    }()
    
    var badgeButton: UIButton? {
        return customView as? UIButton
    }
    
    var badgeText: String? {
        didSet {
            badgeLabel.text = badgeText?.isEmpty == false ? " \(badgeText!) " : nil
            badgeLabel.isHidden = badgeText?.isEmpty != false
            badgeLabel.sizeToFit()
        }
    }
    
    var badgeBackgroundColor: UIColor = .red {
        didSet { badgeLabel.backgroundColor = badgeBackgroundColor }
    }
    
    var badgeFontColor: UIColor = .white {
        didSet { badgeLabel.textColor = badgeFontColor }
    }
    
    var badgeFontSize: CGFloat = UIFont.smallSystemFontSize {
        didSet {
            badgeLabel.font = .systemFont(ofSize: badgeFontSize)
            badgeLabel.layer.cornerRadius = badgeFontSize * CGFloat(0.6)
            badgeLabel.sizeToFit()
        }
    }
}

public extension BadgeBarButtonItem {
    
    convenience init(button: UIButton, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        self.init(customView: button)
        
        self.badgeText = badgeText
        
        button.addTarget(target, action: action, for: .touchUpInside)
        button.sizeToFit()
        button.addSubview(badgeLabel)
        
        button.addConstraint(
            NSLayoutConstraint(
                item: badgeLabel,
                attribute: button.currentTitle?.isEmpty == false ? .top : .centerY,
                relatedBy: .equal,
                toItem: button,
                attribute: .top,
                multiplier: 1,
                constant: 0
            )
        )
        
        button.addConstraint(
            NSLayoutConstraint(
                item: badgeLabel,
                attribute: .centerX,
                relatedBy: .equal,
                toItem: button,
                attribute: .trailing,
                multiplier: 1,
                constant: 0
            )
        )
    }
}

public extension BadgeBarButtonItem {
    
    convenience init(image: UIImage, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .custom)
        button.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
        button.setBackgroundImage(image, for: .normal)
        
        self.init(button: button, badgeText: badgeText, target: target, action: action)
    }
    
    convenience init(title: String, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: UIFont.buttonFontSize)
        
        self.init(button: button, badgeText: badgeText, target: target, action: action)
    }
    
    convenience init?(image: String, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        guard let image = UIImage(named: image) else { return nil }
        self.init(image: image, badgeText: badgeText, target: target, action: action)
    }
}

Example usage:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button1 = BadgeBarButtonItem(image: "chat", target: self, action: #selector(chatButtonTapped))!
        let button2 = BadgeBarButtonItem(button: UIButton(type: .system), target: self, action: #selector(chatButtonTapped))
        let button3 = BadgeBarButtonItem(title: "Messages", badgeText: "3", target: self, action: #selector(chatButtonTapped))
        
        navigationItem.rightBarButtonItems = [button1, button2, button3]

        button1.badgeText = "2"
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            button1.badgeText = "test"
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
            button1.badgeText = "8"
            button2.badgeText = "99"
            button2.badgeFontColor = .black
            button2.badgeBackgroundColor = .yellow
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
            button1.badgeText = nil
            button3.badgeText = "some test"
            button3.badgeFontSize = 14
        }
    }

@bstillitano
Copy link

@basememara this doesn't work. I just get an 'ambiguous' error

@basememara
Copy link

public class BadgeBarButtonItem: UIBarButtonItem {
    
    private(set) public lazy var badgeLabel: UILabel = {
        let label = UILabel()
        
        label.text = badgeText?.isEmpty == false ? " \(badgeText!) " : nil
        label.isHidden = badgeText?.isEmpty != false
        label.textColor = badgeFontColor
        label.backgroundColor = badgeBackgroundColor
        
        label.font = .systemFont(ofSize: badgeFontSize)
        label.layer.cornerRadius = badgeFontSize * CGFloat(0.6)
        label.clipsToBounds = true
        label.isUserInteractionEnabled = true
        
        label.translatesAutoresizingMaskIntoConstraints = false
        label.addConstraint(
            NSLayoutConstraint(
                item: label,
                attribute: .width,
                relatedBy: .greaterThanOrEqual,
                toItem: label,
                attribute: .height,
                multiplier: 1,
                constant: 0
            )
        )
        
        return label
    }()
    
    public var badgeButton: UIButton? {
        return customView as? UIButton
    }
    
    public var badgeText: String? {
        didSet {
            badgeLabel.text = badgeText?.isEmpty == false ? " \(badgeText!) " : nil
            badgeLabel.isHidden = badgeText?.isEmpty != false
            badgeLabel.sizeToFit()
        }
    }
    
    public var badgeBackgroundColor: UIColor = .red {
        didSet { badgeLabel.backgroundColor = badgeBackgroundColor }
    }
    
    public var badgeFontColor: UIColor = .white {
        didSet { badgeLabel.textColor = badgeFontColor }
    }
    
    public var badgeFontSize: CGFloat = UIFont.smallSystemFontSize {
        didSet {
            badgeLabel.font = .systemFont(ofSize: badgeFontSize)
            badgeLabel.layer.cornerRadius = badgeFontSize * CGFloat(0.6)
            badgeLabel.sizeToFit()
        }
    }
}

public extension BadgeBarButtonItem {
    
    convenience init(button: UIButton, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        self.init(customView: button)
        
        self.badgeText = badgeText
        
        button.addTarget(target, action: action, for: .touchUpInside)
        button.sizeToFit()
        button.addSubview(badgeLabel)
        
        badgeLabel.addGestureRecognizer(
            UITapGestureRecognizer(target: target, action: action)
        )
        
        button.addConstraint(
            NSLayoutConstraint(
                item: badgeLabel,
                attribute: button.currentTitle?.isEmpty == false ? .top : .centerY,
                relatedBy: .equal,
                toItem: button,
                attribute: .top,
                multiplier: 1,
                constant: 0
            )
        )
        
        button.addConstraint(
            NSLayoutConstraint(
                item: badgeLabel,
                attribute: .centerX,
                relatedBy: .equal,
                toItem: button,
                attribute: .trailing,
                multiplier: 1,
                constant: 0
            )
        )
    }
}

public extension BadgeBarButtonItem {
    
    convenience init(image: UIImage, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .custom)
        button.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
        button.setBackgroundImage(image, for: .normal)
        
        self.init(button: button, badgeText: badgeText, target: target, action: action)
    }
    
    convenience init(title: String, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: UIFont.buttonFontSize)
        
        self.init(button: button, badgeText: badgeText, target: target, action: action)
    }
    
    convenience init?(image: String, badgeText: String? = nil, target: AnyObject?, action: Selector) {
        guard let image = UIImage(named: image) else { return nil }
        self.init(image: image, badgeText: badgeText, target: target, action: action)
    }
}

@DiwakarThapa
Copy link

swift 4.2

import UIKit

public extension UILabel {
    public convenience init(badgeText: String, color: UIColor = .red, fontSize: CGFloat = UIFont.smallSystemFontSize) {
        self.init()
        text = badgeText.count > 1 ? " \(badgeText) " : badgeText
        textAlignment = .center
        textColor = .white
        backgroundColor = color
        
        font = UIFont.systemFont(ofSize: fontSize)
        layer.cornerRadius = fontSize * CGFloat(0.6)
        clipsToBounds = true
        
        translatesAutoresizingMaskIntoConstraints = false
        addConstraint(NSLayoutConstraint(item: self, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: self, attribute: .height, multiplier: 1, constant: 0))
    }
}

extension UIBarButtonItem {
    convenience init(badge: String?, button: UIButton, target: AnyObject?, action: Selector) {
        button.addTarget(target, action: action, for: .touchUpInside)
        button.sizeToFit()
        
        let badgeLabel = UILabel(badgeText: badge ?? "")
        button.addSubview(badgeLabel)
        button.addConstraint(NSLayoutConstraint(item: badgeLabel, attribute: .top, relatedBy: .equal, toItem: button, attribute: .top, multiplier: 1, constant: 0))
        button.addConstraint(NSLayoutConstraint(item: badgeLabel, attribute: .centerX, relatedBy: .equal, toItem: button, attribute: .trailing, multiplier: 1, constant: 0))
        if nil == badge {
            badgeLabel.isHidden = true
        }
        badgeLabel.tag = UIBarButtonItem.badgeTag
        
        self.init(customView: button)
    }
    
    convenience init(badge: String?, image: UIImage, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .custom)
        button.frame = CGRect(x: 0, y: 0,width: image.size.width, height: image.size.height)
        button.setBackgroundImage(image, for: .normal)
        
        self.init(badge: badge, button: button, target: target, action: action)
    }
    
    convenience init(badge: String?, title: String, target: AnyObject?, action: Selector) {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: UIFont.buttonFontSize)
        self.init(badge: badge, button: button, target: target, action: action)
    }
    
    var badgeLabel: UILabel? {
        return customView?.viewWithTag(UIBarButtonItem.badgeTag) as? UILabel
    }
    
    var badgedButton: UIButton? {
        return customView as? UIButton
    }
    
    var badgeString: String? {
        get { return badgeLabel?.text?.trimmingCharacters(in: NSCharacterSet.whitespaces)}
        
        set {
            if let badgeLabel = badgeLabel {
                badgeLabel.text = nil == newValue ? nil : " \(newValue!) "
                badgeLabel.sizeToFit()
                badgeLabel.isHidden = nil == newValue
            }
        }
    }
    
    var badgedTitle: String? {
        get { return badgedButton?.title(for: .normal) }
        set { badgedButton?.setTitle(newValue, for: .normal); badgedButton?.sizeToFit() }
    }
    
    private static let badgeTag = 7373
}

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