Related
As you can see above i trying to code an simple(!) subclass of UILabel to make an marquee or scrolling text effect if the text of the label is too long. I know that there are already good classes out there (e.g https://cocoapods.org/pods/MarqueeLabel), but i want to make my own :)
Down below you can see my current class.
I can't also fix an issue where the new label(s) are scrolling right, but there is also a third label which shouldn't be there. I think it's the label itself. But when i try the replace the first additional label with that label i won't work. I hope it's not too confusing :/
It's important to me that i only have to assign the class in the storyboard to the label. So that there is no need go and add code e.g in an view controller (beside the outlets). I hope it's clear what i want :D
So again:
Simple subclass of UILabel
scrolling label
should work without any additional code in other classes (except of outlets to change the labels text for example,...)
(it's my first own subclass, so feel free to teach me how to do it right :) )
Thank you very much !
It's by far not perfect, but this is my current class:
import UIKit
class LoopLabel: UILabel {
var labelText : String?
var rect0: CGRect!
var rect1: CGRect!
var labelArray = [UILabel]()
var isStop = false
var timeInterval: TimeInterval!
let leadingBuffer = CGFloat(25.0)
let loopStartDelay = 2.0
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.lineBreakMode = .byClipping
}
override var text: String? {
didSet {
labelText = text
setup()
}
}
func setup() {
let label = UILabel()
label.frame = CGRect.zero
label.text = labelText
timeInterval = TimeInterval((labelText?.characters.count)! / 5)
let sizeOfText = label.sizeThatFits(CGSize.zero)
let textIsTooLong = sizeOfText.width > frame.size.width ? true : false
rect0 = CGRect(x: leadingBuffer, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
rect1 = CGRect(x: rect0.origin.x + rect0.size.width, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
label.frame = rect0
super.clipsToBounds = true
labelArray.append(label)
self.addSubview(label)
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: 0, height: 0))
if textIsTooLong {
let additionalLabel = UILabel(frame: rect1)
additionalLabel.text = labelText
self.addSubview(additionalLabel)
labelArray.append(additionalLabel)
animateLabelText()
}
}
func animateLabelText() {
if(!isStop) {
let labelAtIndex0 = labelArray[0]
let labelAtIndex1 = labelArray[1]
UIView.animate(withDuration: timeInterval, delay: loopStartDelay, options: [.curveLinear], animations: {
labelAtIndex0.frame = CGRect(x: -self.rect0.size.width,y: 0,width: self.rect0.size.width,height: self.rect0.size.height)
labelAtIndex1.frame = CGRect(x: labelAtIndex0.frame.origin.x + labelAtIndex0.frame.size.width,y: 0,width: labelAtIndex1.frame.size.width,height: labelAtIndex1.frame.size.height)
}, completion: { finishied in
labelAtIndex0.frame = self.rect1
labelAtIndex1.frame = self.rect0
self.labelArray[0] = labelAtIndex1
self.labelArray[1] = labelAtIndex0
self.animateLabelText()
})
} else {
self.layer.removeAllAnimations()
}
}
}
First of, I would keep the variables private if you don't need them to be accessed externally, especially labelText (since you're using the computed property text to be set).
Second, since you're adding labels as subviews, I'd rather use a UIView as container instead of UILabel. The only difference in the storyboard would be to add a View instead of a Label.
Third, if you use this approach, you should not set the frame (of the view) to zero.
Something like that would do:
import UIKit
class LoopLabelView: UIView {
private var labelText : String?
private var rect0: CGRect!
private var rect1: CGRect!
private var labelArray = [UILabel]()
private var isStop = false
private var timeInterval: TimeInterval!
private let leadingBuffer = CGFloat(25.0)
private let loopStartDelay = 2.0
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var text: String? {
didSet {
labelText = text
setup()
}
}
func setup() {
self.backgroundColor = UIColor.yellow
let label = UILabel()
label.text = labelText
label.frame = CGRect.zero
timeInterval = TimeInterval((labelText?.characters.count)! / 5)
let sizeOfText = label.sizeThatFits(CGSize.zero)
let textIsTooLong = sizeOfText.width > frame.size.width ? true : false
rect0 = CGRect(x: leadingBuffer, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
rect1 = CGRect(x: rect0.origin.x + rect0.size.width, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
label.frame = rect0
super.clipsToBounds = true
labelArray.append(label)
self.addSubview(label)
//self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: 0, height: 0))
if textIsTooLong {
let additionalLabel = UILabel(frame: rect1)
additionalLabel.text = labelText
self.addSubview(additionalLabel)
labelArray.append(additionalLabel)
animateLabelText()
}
}
func animateLabelText() {
if(!isStop) {
let labelAtIndex0 = labelArray[0]
let labelAtIndex1 = labelArray[1]
UIView.animate(withDuration: timeInterval, delay: loopStartDelay, options: [.curveLinear], animations: {
labelAtIndex0.frame = CGRect(x: -self.rect0.size.width,y: 0,width: self.rect0.size.width,height: self.rect0.size.height)
labelAtIndex1.frame = CGRect(x: labelAtIndex0.frame.origin.x + labelAtIndex0.frame.size.width,y: 0,width: labelAtIndex1.frame.size.width,height: labelAtIndex1.frame.size.height)
}, completion: { finishied in
labelAtIndex0.frame = self.rect1
labelAtIndex1.frame = self.rect0
self.labelArray[0] = labelAtIndex1
self.labelArray[1] = labelAtIndex0
self.animateLabelText()
})
} else {
self.layer.removeAllAnimations()
}
}
}
I have tested the following code on all simulators and it works fine except on the iPad Pro. On the the iPad Pro is does not display. The gradient layer will work fine, it is only when I try and apply a mask that it will not appear on the iPad Pro Simulator:
func createOverlay()
{
if !(gradientLayer != nil)
{
self.gradientLayer = CAGradientLayer()
self.layer.addSublayer(gradientLayer)
}
gradientLayer.frame = self.bounds
print(gradientLayer.frame)
gradientLayer.colors = [appColour.CGColor, appColourDark.CGColor]
//--------FROM HERE ON DOES NOT WORK ON IPAD PRO, NO CRASH BUT LAYER DOES NOT APPEAR
self.alpha = maskAlpha
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
let rect: CGRect = CGRect(x: xOffset - offset, y: yOffset - offset, width: circleWidth + (offset * 2), height: circleHeight + (offset * 2))
let bPath = UIBezierPath(ovalInRect: rect)
CGPathAddRect(path, nil, CGRectMake(0, 0, self.frame.width, self.frame.height))
CGPathAddPath(path, nil, bPath.CGPath)
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
I am hoping this is just a simulator issue but if you see something in my code that might be causing a problem please let me know.
I have tried replacing the gradient layer with a normal layer but it still does not display.
Here is full code, it is a custom sub-class of UIView and is the top layer of a view controller setup in IB:
import UIKit
protocol TipSpeechDelegate
{
func stopSpeaking()
}
#IBDesignable
class HoleMaskView: UIView
{
var xOffset : CGFloat = 0.0
var yOffset : CGFloat = 0.0
var circleWidth: CGFloat = 0.0
var circleHeight: CGFloat = 0.0
var maskAlpha: CGFloat = 0.9
var offset: CGFloat = 10.0
var inset: CGFloat = 8.0
var tipText: String = ""
var myLabel: UILabel?
var gradientLayer: CAGradientLayer!
var relativeCorner: RelativeCornerType = RelativeCornerType.upperLeftCorner
var delegate: TipSpeechDelegate!
override func layoutSubviews()
{
super.layoutSubviews()
}
override func drawRect(rect: CGRect)
{
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(HoleMaskView.handleTap(_:)))
self.addGestureRecognizer(gestureRecognizer)
}
func drawTipText()
{
createOverlay()
let height: CGFloat = self.frame.size.height
if (self.myLabel != nil)
{
myLabel?.removeFromSuperview()
}
if (yOffset < height/2)
{
self.myLabel = UILabel(frame: CGRect(x: inset, y: (yOffset + circleHeight), width: self.frame.width-(inset*2), height: self.frame.height-(yOffset + circleHeight)))
}
else
{
self.myLabel = UILabel(frame: CGRect(x: inset, y: 0, width: self.frame.width-(inset*2), height: yOffset))
}
self.myLabel?.text = tipText
self.myLabel?.textColor = UIColor.whiteColor()
self.myLabel?.font = UIFont(name: "Avenir-Medium", size: 20.0)
self.myLabel?.textAlignment = .Center
self.myLabel?.lineBreakMode = .ByWordWrapping
self.myLabel?.numberOfLines = 0
self.myLabel?.setNeedsLayout()
self.addSubview(myLabel!)
}
func updateTipText(text: String, circle: CGRect)
{
self.tipText = text
yOffset = circle.origin.y
xOffset = circle.origin.x
circleWidth = circle.size.width
circleHeight = circle.size.height
self.drawTipText()
}
func tipText(text: String, rFrame: CGRect, inView: UIView) -> Bool
{
showTipMask()
let convertedPoint = inView.convertPoint(rFrame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = rFrame.size.width
circleHeight = rFrame.size.height
self.drawTipText()
return true
}
func tipText(text: String, button: UIButton) -> Bool
{
if button.hidden
{
return false
}
showTipMask()
let convertedPoint = button.superview!.convertPoint(button.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = button.frame.size.width
circleHeight = button.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, label: UILabel) -> Bool
{
if label.hidden
{
return false
}
showTipMask()
let convertedPoint = label.superview!.convertPoint(label.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = label.frame.size.width
circleHeight = label.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, textView: UITextView) -> Bool
{
if textView.hidden
{
return false
}
showTipMask()
let convertedPoint = textView.superview!.convertPoint(textView.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = textView.frame.size.width
circleHeight = textView.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String) -> Bool
{
showTipMask()
self.tipText = text
yOffset = 0.0
xOffset = self.frame.size.width/2
circleWidth = 0.0
circleHeight = 0.0
self.drawTipText()
return true
}
func tipText(text: String, view: UIView) -> Bool
{
if view.hidden
{
return false
}
showTipMask()
let convertedPoint = view.superview!.convertPoint(view.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = view.frame.size.width
circleHeight = view.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, stepper: UIStepper) -> Bool
{
if stepper.hidden
{
return false
}
showTipMask()
let convertedPoint = stepper.superview!.convertPoint(stepper.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = stepper.frame.size.width
circleHeight = stepper.frame.size.height
self.drawTipText()
return true
}
func showTipMask()
{
self.alpha = alphaHide
self.hidden = false
UIView.animateWithDuration(0.5, animations:
{
self.alpha = alphaShow
}
)
}
func handleTap(gestureRecognizer: UIGestureRecognizer)
{
if delegate != nil
{
delegate.stopSpeaking()
}
print("tapped internal")
UIView.animateWithDuration(0.25, delay: 0.0, options: UIViewAnimationOptions.TransitionNone, animations:
{
() -> Void in
self.alpha = alphaHide
},
completion:
{
(finished: Bool) -> Void in
self.hidden = true
}
)
}
func createOverlay()
{
if !(gradientLayer != nil)
{
self.gradientLayer = CAGradientLayer()
self.layer.addSublayer(gradientLayer)
}
gradientLayer.frame = self.bounds
print(gradientLayer.frame)
gradientLayer.colors = [appColour.CGColor, appColourDark.CGColor]
self.alpha = maskAlpha
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
let rect: CGRect = CGRect(x: xOffset - offset, y: yOffset - offset, width: circleWidth + (offset * 2), height: circleHeight + (offset * 2))
let bPath = UIBezierPath(ovalInRect: rect)
CGPathAddRect(path, nil, CGRectMake(0, 0, self.frame.width, self.frame.height))
CGPathAddPath(path, nil, bPath.CGPath)
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
}
Thanks
Greg
I am facing the same issue.
Currently I
turn on the Debug -> Optimize Rendering for Window Scale
scale it down to 50%
then the graph appears correctly.
But one of my colleges told me that he has to turn off the ORWS option. Wired.
I am trying to display badge on my notification button, in app as displayed on AppIcon.
So far whatever i have researched is related to Obj. C, but nothing that specifically discussed way to implement that solution into Swift,
Please help to find a solution to add a custom class / code to achieve Badge on UiBarbutton and UiButton.
Researched so far:
https://github.com/Marxon13/M13BadgeView
along with MKBadge class etc.
There is a more elegant solution with an extension for UIButtonItem
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 = CATextLayerAlignmentMode.center
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()
}
}
This great code was created by Stefano Vettor and you can find all the details at:
https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4
Working Solution :
Step 1:
Firstly create new swift file which is a subclass to UIButton as follows:
import UIKit
class BadgeButton: UIButton {
var badgeLabel = UILabel()
var badge: String? {
didSet {
addbadgetobutton(badge: badge)
}
}
public var badgeBackgroundColor = UIColor.red {
didSet {
badgeLabel.backgroundColor = badgeBackgroundColor
}
}
public var badgeTextColor = UIColor.white {
didSet {
badgeLabel.textColor = badgeTextColor
}
}
public var badgeFont = UIFont.systemFont(ofSize: 12.0) {
didSet {
badgeLabel.font = badgeFont
}
}
public var badgeEdgeInsets: UIEdgeInsets? {
didSet {
addbadgetobutton(badge: badge)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addbadgetobutton(badge: nil)
}
func addbadgetobutton(badge: String?) {
badgeLabel.text = badge
badgeLabel.textColor = badgeTextColor
badgeLabel.backgroundColor = badgeBackgroundColor
badgeLabel.font = badgeFont
badgeLabel.sizeToFit()
badgeLabel.textAlignment = .center
let badgeSize = badgeLabel.frame.size
let height = max(18, Double(badgeSize.height) + 5.0)
let width = max(height, Double(badgeSize.width) + 10.0)
var vertical: Double?, horizontal: Double?
if let badgeInset = self.badgeEdgeInsets {
vertical = Double(badgeInset.top) - Double(badgeInset.bottom)
horizontal = Double(badgeInset.left) - Double(badgeInset.right)
let x = (Double(bounds.size.width) - 10 + horizontal!)
let y = -(Double(badgeSize.height) / 2) - 10 + vertical!
badgeLabel.frame = CGRect(x: x, y: y, width: width, height: height)
} else {
let x = self.frame.width - CGFloat((width / 2.0))
let y = CGFloat(-(height / 2.0))
badgeLabel.frame = CGRect(x: x, y: y, width: CGFloat(width), height: CGFloat(height))
}
badgeLabel.layer.cornerRadius = badgeLabel.frame.height/2
badgeLabel.layer.masksToBounds = true
addSubview(badgeLabel)
badgeLabel.isHidden = badge != nil ? false : true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addbadgetobutton(badge: nil)
fatalError("init(coder:) is not implemented")
}
}
Step 2:
Create a function in your base file which u can use in each View Controller :
func addBadge(itemvalue: String) {
let bagButton = BadgeButton()
bagButton.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
bagButton.tintColor = UIColor.darkGray
bagButton.setImage(UIImage(named: "ShoppingBag")?.withRenderingMode(.alwaysTemplate), for: .normal)
bagButton.badgeEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 15)
bagButton.badge = itemvalue
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: bagButton)
}
Step 3 :
Use above function from any View Controller in this way :
self.addBadge(itemvalue: localStorage.string(forKey: "total_products_in_cart") ?? "0")
First create label, then right bar button. On right bar button add subview which will be badge count. Finally add navigation right bar button.
SWIFT 5
let badgeCount = UILabel(frame: CGRect(x: 22, y: -05, width: 20, height: 20))
badgeCount.layer.borderColor = UIColor.clear.cgColor
badgeCount.layer.borderWidth = 2
badgeCount.layer.cornerRadius = badgeCount.bounds.size.height / 2
badgeCount.textAlignment = .center
badgeCount.layer.masksToBounds = true
badgeCount.textColor = .white
badgeCount.font = badgeCount.font.withSize(12)
badgeCount.backgroundColor = .red
badgeCount.text = "4"
let rightBarButton = UIButton(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
rightBarButton.setBackgroundImage(UIImage(named: "NotificationBell"), for: .normal)
rightBarButton.addTarget(self, action: #selector(self.onBtnNotification), for: .touchUpInside)
rightBarButton.addSubview(badgeCount)
let rightBarButtomItem = UIBarButtonItem(customView: rightBarButton)
navigationItem.rightBarButtonItem = rightBarButtomItem
I had the same task. I didn't want to use third-party libraries. Firstly, I tried Stefano's solution and it's great however I decided to implement my own way to solve it.
In my humble opinion, there are simple steps described below briefly:
Create UIView instance within .xib file and put necessary items like UILabel or UIImageView instance depending on your design requirements.
The final action I did in this step is putting invisible button in the top of view's hierarchy.
Create YourCustomView.swift and link all #IBOutlets from xib to current file inside your custom view class implementation.
Next, implement class function in YourCustomView class which will load custom view from xib and return it as YourCustomView instance.
Finally, add your custom badge to your custom view controller instance!
My result is..
P.S. If you need to implement #IBActions I recommend to link your custom view and custom view controller through the delegate pattern.
using M13BadgeView.. use this code
(im using fontawesome.swift for buttons :: https://github.com/thii/FontAwesome.swift)
let rightButton = UIButton(frame: CGRect(x:0,y:0,width:30,height:30))
rightButton.titleLabel?.font = UIFont.fontAwesome(ofSize: 22)
rightButton.setTitle(String.fontAwesomeIcon(name: .shoppingBasket), for: .normal)
let rightButtonItem : UIBarButtonItem = UIBarButtonItem(customView: rightButton)
let badgeView = M13BadgeView()
badgeView.text = "1"
badgeView.textColor = UIColor.white
badgeView.badgeBackgroundColor = UIColor.red
badgeView.borderWidth = 1.0
badgeView.borderColor = UIColor.white
badgeView.horizontalAlignment = M13BadgeViewHorizontalAlignmentLeft
badgeView.verticalAlignment = M13BadgeViewVerticalAlignmentTop
badgeView.hidesWhenZero = true
rightButton.addSubview(badgeView)
self.navigationItem.rightBarButtonItem = rightButtonItem
Good answer #Julio Bailon (https://stackoverflow.com/a/45948819/1898973)!
Here is the author's site with full explanation: http://www.stefanovettor.com/2016/04/30/adding-badge-uibarbuttonitem/.
It seems not to be working on iOS 11, maybe because the script try to access the "view" property of the UIBarButtonItem. I made it work:
By creating a UIButton and then creating the UIBarButtonItem using the UIButton as a customView:
navigationItem.rightBarButtonItem = UIBarButtonItem.init(
customView: shoppingCartButton)
By replacing the line in the UIBarButtonItem extension:
guard let view = self.value(forKey: "view") as? UIView else { return }
with the following:
guard let view = self.customView else { return }
Seems elegant to me and, best of all, it worked!
You can set below constraints to UILabel with respect to UIButton
align UILabel's top and trailing to UIButton
And when you need to show badge set text to UILabel and when you don't want to show badge then set empty string to UILabel
Download This
For BarButtonItem : Drag and Drop UIBarButtonItem+Badge.h and UIBarButtonItem+Badge.m class in project.
Write this code for set Badges:
self.navigationItem.rightBarButtonItem.badgeValue = "2"
self.navigationItem.rightBarButtonItem.badgeBGColor = UIColor.black
For UIButtton : Drag and Drop UIButton+Badge.h and UIButton+Badge.m class in project.
self.notificationBtn.badgeValue = "2"
self.notificationBtn.badgeBGColor = UIColor.black
Answer with extension from Julio will not work.
Starting from iOS 11 this code will not work cause line of code below will not cast UIView. Also it's counting as private API and seems to be will not pass AppStore review.
guard let view = self.value(forKey: "view") as? UIView else { return }
Thread on Apple Developer Forum
Second thing that this snippet always draws circle, so it can't fit numbers bigger than 9.
Here the simplified version by using custom view
Easy and clear solution if you are looking for only adding the red dot without the number;
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 + offset.x - 17
let y = view.frame.height + offset.y - 34
let badgeFrame = CGRect(origin: CGPoint(x: x, y: 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
}
}
The source of the code:
https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4
I only made a few changes to eliminate the number.
The MIBadgeButton-Swift is working also on UIBarButtonItems.
Here is my code after the navigation bar is created:
let rightBarButtons = self.navigationItem.rightBarButtonItems
let alarmsBarButton = rightBarButtons?.last
let alarmsButton = alarmsBarButton.customView as! MIBadgeButton?
alarmsButton.badgeString = "10"
You can do it programmatically with
self.tabBarItem.badgeColor = .red
or use the storyboard. See:
The question below is similar as mine.
How to use a custom UIImage as an UITabBarItem Badge?
Is it possible to use a background image instead of drawing it myself?
I think it's fine since my app will only use a 1-digit badgeValue.
If it is not really possible, I want to know if we can change the badge color instead.
The answers in the question below are not really helping.
Is it possible to change UITabBarItem badge color
This is your best bet. Add this extension at the file scope and you can customise the badges however you like. Just call self.tabBarController!.setBadges([1,0,2]) in any of your root view controllers.
To be clear that is for a tab bar with three items, with the badge values going from left to right.
If you want to add images instead just change the addBadge method
extension UITabBarController {
func setBadges(badgeValues: [Int]) {
var labelExistsForIndex = [Bool]()
for _ in badgeValues {
labelExistsForIndex.append(false)
}
for view in self.tabBar.subviews where view is PGTabBadge {
let badgeView = view as! PGTabBadge
let index = badgeView.tag
if badgeValues[index] == 0 {
badgeView.removeFromSuperview()
}
labelExistsForIndex[index] = true
badgeView.text = String(badgeValues[index])
}
for i in 0...(labelExistsForIndex.count - 1) where !labelExistsForIndex[i] && (badgeValues[i] > 0) {
addBadge(index: i, value: badgeValues[i], color: .red, font: UIFont(name: "Helvetica-Light", size: 11)!)
}
}
func addBadge(index: Int, value: Int, color: UIColor, font: UIFont) {
let itemPosition = CGFloat(index + 1)
let itemWidth: CGFloat = tabBar.frame.width / CGFloat(tabBar.items!.count)
let bgColor = color
let xOffset: CGFloat = 5
let yOffset: CGFloat = -12
let badgeView = PGTabBadge()
badgeView.frame.size = CGSize(width: 12, height: 12)
badgeView.center = CGPoint(x: (itemWidth * itemPosition) - (itemWidth / 2) + xOffset, y: 20 + yOffset)
badgeView.layer.cornerRadius = badgeView.bounds.width/2
badgeView.clipsToBounds = true
badgeView.textColor = UIColor.white
badgeView.textAlignment = .center
badgeView.font = font
badgeView.text = String(value)
badgeView.backgroundColor = bgColor
badgeView.tag = index
tabBar.addSubview(badgeView)
}
}
class PGTabBadge: UILabel { }
Here is another solution based on TimWhiting's answer:
extension UITabBar {
func setBadge(value: String?, at index: Int, withConfiguration configuration: TabBarBadgeConfiguration = TabBarBadgeConfiguration()) {
let existingBadge = subviews.first { ($0 as? TabBarBadge)?.hasIdentifier(for: index) == true }
existingBadge?.removeFromSuperview()
guard let tabBarItems = items,
let value = value else { return }
let itemPosition = CGFloat(index + 1)
let itemWidth = frame.width / CGFloat(tabBarItems.count)
let itemHeight = frame.height
let badge = TabBarBadge(for: index)
badge.frame.size = configuration.size
badge.center = CGPoint(x: (itemWidth * itemPosition) - (0.5 * itemWidth) + configuration.centerOffset.x,
y: (0.5 * itemHeight) + configuration.centerOffset.y)
badge.layer.cornerRadius = 0.5 * configuration.size.height
badge.clipsToBounds = true
badge.textAlignment = .center
badge.backgroundColor = configuration.backgroundColor
badge.font = configuration.font
badge.textColor = configuration.textColor
badge.text = value
addSubview(badge)
}
}
class TabBarBadge: UILabel {
var identifier: String = String(describing: TabBarBadge.self)
private func identifier(for index: Int) -> String {
return "\(String(describing: TabBarBadge.self))-\(index)"
}
convenience init(for index: Int) {
self.init()
identifier = identifier(for: index)
}
func hasIdentifier(for index: Int) -> Bool {
let has = identifier == identifier(for: index)
return has
}
}
class TabBarBadgeConfiguration {
var backgroundColor: UIColor = .red
var centerOffset: CGPoint = .init(x: 12, y: -9)
var size: CGSize = .init(width: 17, height: 17)
var textColor: UIColor = .white
var font: UIFont! = .systemFont(ofSize: 11) {
didSet { font = font ?? .systemFont(ofSize: 11) }
}
static func construct(_ block: (TabBarBadgeConfiguration) -> Void) -> TabBarBadgeConfiguration {
let new = TabBarBadgeConfiguration()
block(new)
return new
}
}
You can use a more robust solution #UITabbarItem-CustomBadge.
Demo
Simple two line of code can get you going
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//supplying the animation parameter
[UITabBarItem setDefaultAnimationProvider:[[DefaultTabbarBadgeAnimation alloc] init]];
[UITabBarItem setDefaultConfigurationProvider:[[DefaultSystemLikeBadgeConfiguration alloc] init]];
//rest of your code goes following...
return YES;
}
well... changing the background of the built-in badge does not seem possible to me. But what IS possible instead, is the following:
Subclass the TabBarController, build a custom view laid out in a NIB, add it to the view hierarchy at the location in the tab bar, where you want it to be.
In the custom view you can set up an image as background and a label on top of that background, that will display the number value you can then change by code.
Then you need to figure out the horizontal and vertical location of your custom view where you want to place your view inside of the tab bar.
Hope this helps a little bit.
Sebastian
TimWhiting's answer updated to Swift 4 with the removal of some force unwrapping.
extension UITabBarController {
func setBadges(badgeValues: [Int]) {
var labelExistsForIndex = [Bool]()
for _ in badgeValues {
labelExistsForIndex.append(false)
}
for view in tabBar.subviews {
if let badgeView = view as? PGTabBadge {
let index = badgeView.tag
if badgeValues[index] == 0 {
badgeView.removeFromSuperview()
}
labelExistsForIndex[index] = true
badgeView.text = String(badgeValues[index])
}
}
for i in 0 ... labelExistsForIndex.count - 1 where !labelExistsForIndex[i] && badgeValues[i] > 0 {
addBadge(
index: i,
value: badgeValues[i],
color: UIColor(red: 4 / 255, green: 110 / 255, blue: 188 / 255, alpha: 1),
font: UIFont(name: "Helvetica-Light", size: 11)!
)
}
}
func addBadge(index: Int, value: Int, color: UIColor, font: UIFont) {
guard let tabBarItems = tabBar.items else { return }
let itemPosition = CGFloat(index + 1)
let itemWidth: CGFloat = tabBar.frame.width / CGFloat(tabBarItems.count)
let bgColor = color
let xOffset: CGFloat = 12
let yOffset: CGFloat = -9
let badgeView = PGTabBadge()
badgeView.frame.size = CGSize(width: 17, height: 17)
badgeView.center = CGPoint(x: (itemWidth * itemPosition) - (itemWidth / 2) + xOffset, y: 20 + yOffset)
badgeView.layer.cornerRadius = badgeView.bounds.width / 2
badgeView.clipsToBounds = true
badgeView.textColor = UIColor.white
badgeView.textAlignment = .center
badgeView.font = font
badgeView.text = String(value)
badgeView.backgroundColor = bgColor
badgeView.tag = index
tabBar.addSubview(badgeView)
}
}
class PGTabBadge: UILabel {}
Sample Image
I bring the issue forward which I face. I am creating a UITextField programmatically as below.
UItextField *mobileNumberField = [[UITextField alloc] initWithFrame:CGRectMake(10, 195, 300, 41)];
mobileNumberField.delegate = self;
mobileNumberField.borderStyle = UITextBorderStyleRoundedRect;
[mobileNumberField.layer setCornerRadius:14.0f];
mobileNumberField.placeholder = #"Mobile Number";
[self.paymentsHomeView addSubview:mobileNumberField];
The output is the attached image.
I dont know why is it breaking at the corners. Help me to fix my text field like the image attached below.
Just remove this line...
mobileNumberField.borderStyle = UITextBorderStyleRoundedRect;
and add this code also..
[mobileNumberField setBackgroundColor:[UIColor whiteColor]];
[mobileNumberField.layer setBorderColor:[UIColor grayColor].CGColor];
[mobileNumberField.layer setBorderWidth:1.0];
Update your like below.
UITextField *mobileNumberField = [[UITextField alloc] initWithFrame:CGRectMake(10, 195, 300, 41)];
mobileNumberField.delegate = self;
mobileNumberField.layer.borderWidth = 1.0f;
mobileNumberField.layer.borderColor = [UIColor lightGrayColor].CGColor;
mobileNumberField.
// mobileNumberField.borderStyle = UITextBorderStyleRoundedRect;
[mobileNumberField.layer setCornerRadius:14.0f];
mobileNumberField.placeholder = #"Mobile Number";
[self.paymentsHomeView addSubview:mobileNumberField];
textField.layer.cornerRadius=textfield.frame.size.height/2;
textField.clipsToBounds=YES;
The reason the corners are cut is because there is an enclosing view to the text field. When you set the corner radius, applies to THAT view and thus the corners of the inside text field seem to be cut - in reality they have not even changed.
The solution is to put the UITextField inside UIView, set textfield borderstyle to none. Then apply the border and corner radius specification to the uiview. Note the borderColor, which is very close, if not the same, to the UITextField borderColor.
As of writing, tested and works in Xcode 7.3.1, Swift 2.2, iOS 8 and 9.
Swift:
textField.borderStyle = UITextBorderStyle.None
textBorderView.layer.cornerRadius = 5
textBorderView.layer.borderWidth = 1
textBorderView.layer.borderColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.2).CGColor
The following code has given me the following result in Swift 5, XCode 11.4.
Definitely more bells and whistles can be added. I think this good MVP
naviTextField = UITextField.init(frame: CGRect(x: 0, y: 0, width: (self.navigationController?.navigationBar.frame.size.width)!, height: 21))
self.navigationItem.titleView = naviTextField
naviTextField.becomeFirstResponder()
naviTextField.placeholder = "Type target name here"
naviTextField.borderStyle = .roundedRect
naviTextField.layer.cornerRadius = 5.0
naviTextField.textAlignment = .center
Here is the solution of your problem
UITextField * txtField = [[UITextField alloc]initWithFrame:CGRectMake(0, 0, 200, 50)];
[txtField setBorderStyle:UITextBorderStyleNone];
[txtField.layer setMasksToBounds:YES];
[txtField.layer setCornerRadius:10.0f];
[txtField.layer setBorderColor:[[UIColor lightGrayColor]CGColor]];
[txtField.layer setBorderWidth:1];
[txtField setTextAlignment:UITextAlignmentCenter];
[txtField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
[self.view addSubview:txtField];
Swift 3 solution:
I have written separate function to set border and corner radius to any layer in swift, you have to just pass the layer of any view, border width, corner radius and border color to the following function
` func setBorderAndCornerRadius(layer: CALayer, width: CGFloat, radius: CGFloat,color : UIColor ) {
layer.borderColor = color.cgColor
layer.borderWidth = width
layer.cornerRadius = radius
layer.masksToBounds = true
}
`
swift solution:
textField.layer.borderWidth = anyWidth
textField.layer.borderColor = anyColor
textField.layer.cornerRadius = textField.frame.size.height/2
textField.clipsToBounds = true
All above answer is good , but here I am adding the code through #IBDesignable.
#IBDesignable class DesignableUITextField: UITextField {
// Provides left padding for images
override func leftViewRect(forBounds bounds: CGRect) -> CGRect {
var textRect = super.leftViewRect(forBounds: bounds)
textRect.origin.x += leftPadding
return textRect
}
// Provides right padding for images
override func rightViewRect(forBounds bounds: CGRect) -> CGRect {
var textRect = super.rightViewRect(forBounds: bounds)
textRect.origin.x -= rightPadding
return textRect
}
#IBInspectable var leftImage: UIImage? {
didSet {
updateView()
}
}
#IBInspectable var rightImage: UIImage? {
didSet {
updateRightView()
}
}
#IBInspectable var leftPadding: CGFloat = 0
#IBInspectable var rightPadding: CGFloat = 0
#IBInspectable var gapPadding: CGFloat = 0
#IBInspectable var color: UIColor = UIColor.lightGray {
didSet {
updateView()
}
}
#IBInspectable var cornerRadius: CGFloat = 0
#IBInspectable var borderColor: UIColor? = .lightGray
override func draw(_ rect: CGRect) {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
layer.borderWidth = 1
layer.borderColor = borderColor?.cgColor
}
//#IBInspectable var roundCornersRadius: CGFloat = 0 {
// didSet{
// roundCornersRadiusTextField(radius: roundCornersRadius)
// }
// }
func roundCornersRadiusTextField(radius:CGFloat) {
roundCorners(corners: [UIRectCorner.topLeft, UIRectCorner.topRight, UIRectCorner.bottomLeft, UIRectCorner.bottomRight], radius:radius)
}
func roundBottomCornersRadius(radius:CGFloat) {
roundCorners(corners: [UIRectCorner.topLeft, UIRectCorner.topRight], radius:radius)
}
func updateView() {
if let image = leftImage {
leftViewMode = UITextField.ViewMode.always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.contentMode = .scaleAspectFit
imageView.image = image
// Note: In order for your image to use the tint color, you have to select the image in the Assets.xcassets and change the "Render As" property to "Template Image".
imageView.tintColor = color
leftView = imageView
} else {
leftViewMode = UITextField.ViewMode.never
leftView = nil
}
// Placeholder text color
attributedPlaceholder = NSAttributedString(string: placeholder != nil ? placeholder! : "", attributes:[NSAttributedString.Key.foregroundColor: color])
}
func updateRightView() {
if let image = rightImage {
rightViewMode = UITextField.ViewMode.always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.contentMode = .scaleAspectFit
imageView.image = image
// Note: In order for your image to use the tint color, you have to select the image in the Assets.xcassets and change the "Render As" property to "Template Image".
imageView.tintColor = color
rightView = imageView
} else {
rightViewMode = UITextField.ViewMode.never
rightView = nil
}
// Placeholder text color
attributedPlaceholder = NSAttributedString(string: placeholder != nil ? placeholder! : "", attributes:[NSAttributedString.Key.foregroundColor: color])
}
func roundCorners(corners:UIRectCorner, radius:CGFloat) {
let bounds = self.bounds
let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer
let frameLayer = CAShapeLayer()
frameLayer.frame = bounds
frameLayer.path = maskPath.cgPath
frameLayer.strokeColor = UIColor.darkGray.cgColor
frameLayer.fillColor = UIColor.init(red: 247, green: 247, blue: 247, alpha: 0).cgColor
self.layer.addSublayer(frameLayer)
}
private var textPadding: UIEdgeInsets {
let p: CGFloat = leftPadding + gapPadding + (leftView?.frame.width ?? 0)
return UIEdgeInsets(top: 0, left: p, bottom: 0, right: 5)
}
override open func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: textPadding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: textPadding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: textPadding)
}}