I have a label which is supposed to be a badge for unread notifications count, but since refactoring to use UIButton.Configuration the label always gets rendered behind the button label and image, no matter what.
This is the desired layout:
But I get this for some reason
The code is following:
Configuration for UIButton
private func getNotificationButtonConfiguration() -> UIButton.Configuration {
var container = AttributeContainer()
container.font = .sansSemiBold(size: 14)
container.foregroundColor = .warmGray
var configuration = UIButton.Configuration.plain()
configuration.contentInsets = .zero
configuration.attributedTitle = AttributedString("Notifications".localize, attributes: container)
configuration.imagePadding = 3
return configuration
}
Then after successfully getting the number of unread notifications I call this block to show the badge
private func showBadge(withCount count: Int) {
let badge = UILabel.badgeLabel(withCount: count)
badge.tag = 9830384
notificationButton.addSubview(badge)
let size: CGSize = (badge.text! as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.sansBold(size: 12)])
badge.snp.makeConstraints { make in
make.trailing.equalTo(notificationButton.snp.leading).offset(25)
make.top.equalTo(notificationButton.snp.top)
make.width.equalTo(size.width + 10)
make.height.equalTo(16)
}
}
Function to generate the badge label is
static func badgeLabel(withCount count: Int) -> UILabel {
let badgeCount = UILabel(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
badgeCount.translatesAutoresizingMaskIntoConstraints = false
badgeCount.layer.cornerRadius = badgeCount.bounds.size.height / 2
badgeCount.textAlignment = .center
badgeCount.layer.masksToBounds = true
badgeCount.textColor = .white
badgeCount.font = .sansBold(size: 12)
badgeCount.backgroundColor = .heartRed
badgeCount.text = String(count)
return badgeCount
}
I've tried calling notificationButton.bringSubviewToFront(badge)
inside showBadge
function, but it did not have any effect on layout.
Any ideas are greatly appreciated!
Adding a subview on UIButton sends that subview to back all the time instead of it being rendered over button label and image.
Button initialization:
let notificationButton = UIButton(type: .custom)
notificationButton.setImage(UIImage(named: "icon_bell_filled"), for: .normal)
notificationButton.configuration = getNotificationButtonConfiguration()
notificationButton.titleLabel?.tag = 200
notificationButton.addTarget(self, action: #selector(openNotificationsButtonAction), for: .touchUpInside)
As you mentioned in your comment, you could wrap the button and badge in a UIView
Alternatively, you could subclass UIButton
and add attach/detach methods.
Quick example:
class BadgedButton: UIButton {
public weak var theBadgeView: UIView!
public func attachBadge(_ v: UIView) -> Bool {
// we must have a superview!
guard let sv = self.superview else { return false }
if let curBadge = theBadgeView {
curBadge.removeFromSuperview()
}
v.translatesAutoresizingMaskIntoConstraints = false
sv.addSubview(v)
let sz: CGSize = v.intrinsicContentSize
NSLayoutConstraint.activate([
v.trailingAnchor.constraint(equalTo: self.leadingAnchor, constant: 25.0),
v.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
v.widthAnchor.constraint(equalToConstant: sz.width + 10.0),
v.heightAnchor.constraint(equalToConstant: 16.0),
])
self.theBadgeView = v
return true;
}
public func detachBadge() {
if let curBadge = self.theBadgeView {
curBadge.removeFromSuperview()
}
}
override func didMoveToSuperview() {
// remove the badge view when self is removed from superview
if nil == self.superview,
let curBadge = self.theBadgeView
{
curBadge.removeFromSuperview()
}
}
}
class BadgeTestVC: UIViewController {
var btn: BadgedButton!
var badgeCount: Int = 0
let incLabel = UILabel()
let decLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
var cfg = getNotificationButtonConfiguration()
btn = BadgedButton(configuration: cfg)
btn.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
])
[incLabel, decLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.numberOfLines = 0
v.textAlignment = .center
}
incLabel.text = "Tap here to\nIncrement\nBadge Count"
decLabel.text = "Tap here to\nDecrement\nBadge Count"
incLabel.backgroundColor = .cyan
decLabel.backgroundColor = .yellow
NSLayoutConstraint.activate([
decLabel.topAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
decLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
decLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
incLabel.topAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
incLabel.leadingAnchor.constraint(equalTo: decLabel.trailingAnchor, constant: 20.0),
incLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
incLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
incLabel.widthAnchor.constraint(equalTo: decLabel.widthAnchor),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let p = t.location(in: self.view)
if decLabel.frame.contains(p) {
badgeCount -= 1
}
if incLabel.frame.contains(p) {
badgeCount += 1
}
badgeCount = max(badgeCount, 0)
showBadge(withCount: badgeCount)
}
private func getNotificationButtonConfiguration() -> UIButton.Configuration {
var container = AttributeContainer()
container.font = .systemFont(ofSize: 14.0, weight: .bold) //.sansSemiBold(size: 14)
container.foregroundColor = .gray // .warmGray
var configuration = UIButton.Configuration.plain()
configuration.contentInsets = .zero
configuration.attributedTitle = AttributedString("Notifications", attributes: container)
configuration.imagePadding = 3
if let img = UIImage(systemName: "bell.fill") {
configuration.image = img
}
return configuration
}
private func showBadge(withCount count: Int) {
if count == 0 {
btn.detachBadge()
return
}
let badge = UILabel.badgeLabel(withCount: count)
badge.tag = 9830384
if !btn.attachBadge(badge) {
print("Attach failed!")
}
}
}
extension UILabel {
static func badgeLabel(withCount count: Int) -> UILabel {
let badgeCount = UILabel(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
badgeCount.translatesAutoresizingMaskIntoConstraints = false
badgeCount.layer.cornerRadius = badgeCount.bounds.size.height / 2
badgeCount.textAlignment = .center
badgeCount.layer.masksToBounds = true
badgeCount.textColor = .white
badgeCount.font = .systemFont(ofSize: 12.0, weight: .bold) //.sansBold(size: 12)
badgeCount.backgroundColor = .red // .heartRed
badgeCount.text = String(count)
return badgeCount
}
}
Looks like this when running:
tapping once on Increment:
and tapping 12 times:
The attachBadge(...)
method removes an "already attached" view (label) before adding the new one.
It could easily be optimized to "update" the label text, if desired.