-
-
Save freedom27/c709923b163e26405f62b799437243f4 to your computer and use it in GitHub Desktop.
Thank you all for the solution, neat and concise.
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
)
}
}
}
Does anyone know why this doesn't work with back button?
Its not working in Swift4.
Different approach and works with Swift 4 and various view controller states: https://gist.github.com/yonat/75a0f432d791165b1fd6
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?
This doesn't work unless it is a custom uibarbutton unforunately. :(
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"
This doesn't work unless it is a custom uibarbutton unforunately. :(
u can extension a button to do it , c the #ugenlik comment
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()
}
}
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.
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)
`
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
}
}
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
}
}
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
}
}
@AP-94 love u dude, but im not gay 😎
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.