Skip to content

Instantly share code, notes, and snippets.

@heitara
Created February 2, 2020 14:09
Show Gist options
  • Save heitara/95c062155a73959d139342c3f2ddb9d4 to your computer and use it in GitHub Desktop.
Save heitara/95c062155a73959d139342c3f2ddb9d4 to your computer and use it in GitHub Desktop.
Clean Red Badge implementation in Swift
// Created by © 2020 SwiftEverywhere. Can be used free of charge.
import UIKit
//protocol
protocol BadgeContainer: class {
var timer: Timer? { get set }
var badgeView: UIView? { get set }
var label: UILabel? { get set }
func showBadge(blink: Bool, text: String?)
func hideBadge()
}
//default protocol implementation
extension BadgeContainer where Self: UIView {
func showBadge(blink: Bool, text: String?) {
timer?.invalidate()
if badgeView != nil {
if badgeView?.isHidden == false {
return
}
} else {
badgeView = UIView()
}
badgeView?.backgroundColor = .red
guard let badgeViewUnwrapped = badgeView else {
return
}
//adds the badge at the top
addSubview(badgeViewUnwrapped)
badgeViewUnwrapped.translatesAutoresizingMaskIntoConstraints = false
badgeViewUnwrapped.pinTo([.trailing, .top])
if let textUnwrapped = text {
if label == nil {
label = UILabel()
}
guard let labelUnwrapped = label else {
return
}
labelUnwrapped.text = textUnwrapped
labelUnwrapped.textColor = .white
labelUnwrapped.font = .systemFont(ofSize: 8)
badgeViewUnwrapped.addSubview(labelUnwrapped)
labelUnwrapped.pinTo(.center)
let calculatedSize = CGFloat(12 + 2 * textUnwrapped.count)
badgeViewUnwrapped.set(.width(calculatedSize))
badgeViewUnwrapped.set(.height(calculatedSize))
badgeViewUnwrapped.cornerRadius = calculatedSize / 2
} else {
badgeViewUnwrapped.set(.width(6))
badgeViewUnwrapped.set(.height(6))
badgeViewUnwrapped.cornerRadius = 3
}
if blink {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
badgeViewUnwrapped.isHidden.toggle()
})
}
}
func hideBadge() {
timer?.invalidate()
badgeView?.removeFromSuperview()
badgeView = nil
label = nil
}
}
//custom UIButton with badge
class BadgeButton: UIButton, BadgeContainer {
var timer: Timer?
var badgeView: UIView?
var label: UILabel?
}
//extension of UIView for proper positioning of visual children
extension UIView {
@IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
}
}
struct ConstraintEdge: OptionSet {
let rawValue: Int
static let safeAreaTop = ConstraintEdge(rawValue: 1 << 0)
static let top = ConstraintEdge(rawValue: 1 << 1)
static let safeAreaBottom = ConstraintEdge(rawValue: 1 << 2)
static let bottom = ConstraintEdge(rawValue: 1 << 3)
static let leading = ConstraintEdge(rawValue: 1 << 4)
static let trailing = ConstraintEdge(rawValue: 1 << 5)
static let centerX = ConstraintEdge(rawValue: 1 << 6)
static let centerY = ConstraintEdge(rawValue: 1 << 7)
static let fill: ConstraintEdge = [.top, .bottom, .leading, .trailing]
static let safeAreaFill: ConstraintEdge = [.safeAreaTop, .safeAreaBottom, .leading, .trailing]
static let center: ConstraintEdge = [.centerX, .centerY]
}
enum Constraint {
case height(CGFloat)
case width(CGFloat)
case equalHeight(UIView)
case equalWidth(UIView)
indirect case greaterThanOrEqualTo(Constraint)
indirect case lessThanOrEqualTo(Constraint)
}
enum Relation {
case equal
case greaterThanOrEqualTo
case lessThanOrEqualTo
}
func pinTo(_ edges: ConstraintEdge, other: UIView? = nil, relation: Relation = .equal) {
guard let otherView = (other ?? superview) else {
return
}
translatesAutoresizingMaskIntoConstraints = false
func superviewTopAnchor() -> NSLayoutYAxisAnchor {
return (otherView as? UIScrollView)?.contentLayoutGuide.topAnchor ?? otherView.topAnchor
}
func superviewBottomAnchor() -> NSLayoutYAxisAnchor {
return (otherView as? UIScrollView)?.contentLayoutGuide.bottomAnchor ?? otherView.bottomAnchor
}
func superviewLeadingAnchor() -> NSLayoutXAxisAnchor {
return (otherView as? UIScrollView)?.contentLayoutGuide.leadingAnchor ?? otherView.leadingAnchor
}
func superviewTrailingAnchor() -> NSLayoutXAxisAnchor {
return (otherView as? UIScrollView)?.contentLayoutGuide.trailingAnchor ?? otherView.trailingAnchor
}
if edges.contains(.centerY) {
switch relation {
case .equal:
centerYAnchor.constraint(equalTo: otherView.centerYAnchor).isActive = true
case .greaterThanOrEqualTo:
centerYAnchor.constraint(greaterThanOrEqualTo: otherView.centerYAnchor).isActive = true
case .lessThanOrEqualTo:
centerYAnchor.constraint(lessThanOrEqualTo: otherView.centerYAnchor).isActive = true
}
} else {
if edges.contains(.safeAreaTop) {
switch relation {
case .equal:
topAnchor.constraint(equalTo: otherView.safeAreaLayoutGuide.topAnchor).isActive = true
case .greaterThanOrEqualTo:
topAnchor.constraint(greaterThanOrEqualTo: otherView.safeAreaLayoutGuide.topAnchor).isActive = true
case .lessThanOrEqualTo:
topAnchor.constraint(lessThanOrEqualTo: otherView.safeAreaLayoutGuide.topAnchor).isActive = true
}
} else if edges.contains(.top) {
switch relation {
case .equal:
topAnchor.constraint(equalTo: superviewTopAnchor()).isActive = true
case .greaterThanOrEqualTo:
topAnchor.constraint(greaterThanOrEqualTo: superviewTopAnchor()).isActive = true
case .lessThanOrEqualTo:
topAnchor.constraint(lessThanOrEqualTo: superviewTopAnchor()).isActive = true
}
}
if edges.contains(.safeAreaBottom) {
switch relation {
case .equal:
bottomAnchor.constraint(equalTo: otherView.safeAreaLayoutGuide.bottomAnchor).isActive = true
case .greaterThanOrEqualTo:
bottomAnchor.constraint(greaterThanOrEqualTo: otherView.safeAreaLayoutGuide.bottomAnchor).isActive = true
case .lessThanOrEqualTo:
bottomAnchor.constraint(lessThanOrEqualTo: otherView.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
} else if edges.contains(.bottom) {
switch relation {
case .equal:
bottomAnchor.constraint(equalTo: superviewBottomAnchor()).isActive = true
case .greaterThanOrEqualTo:
bottomAnchor.constraint(greaterThanOrEqualTo: superviewBottomAnchor()).isActive = true
case .lessThanOrEqualTo:
bottomAnchor.constraint(lessThanOrEqualTo: superviewBottomAnchor()).isActive = true
}
}
}
if edges.contains(.centerX) {
switch relation {
case .equal:
centerXAnchor.constraint(equalTo: otherView.centerXAnchor).isActive = true
case .greaterThanOrEqualTo:
centerXAnchor.constraint(greaterThanOrEqualTo: otherView.centerXAnchor).isActive = true
case .lessThanOrEqualTo:
centerXAnchor.constraint(lessThanOrEqualTo: otherView.centerXAnchor).isActive = true
}
} else {
if edges.contains(.leading) {
switch relation {
case .equal:
leadingAnchor.constraint(equalTo: superviewLeadingAnchor()).isActive = true
case .greaterThanOrEqualTo:
leadingAnchor.constraint(greaterThanOrEqualTo: superviewLeadingAnchor()).isActive = true
case .lessThanOrEqualTo:
leadingAnchor.constraint(lessThanOrEqualTo: superviewLeadingAnchor()).isActive = true
}
}
if edges.contains(.trailing) {
switch relation {
case .equal:
trailingAnchor.constraint(equalTo: superviewTrailingAnchor()).isActive = true
case .greaterThanOrEqualTo:
trailingAnchor.constraint(greaterThanOrEqualTo: superviewTrailingAnchor()).isActive = true
case .lessThanOrEqualTo:
trailingAnchor.constraint(lessThanOrEqualTo: superviewTrailingAnchor()).isActive = true
}
}
}
}
func set(_ constraint: Constraint, relation: Relation = .equal) {
switch constraint {
case .height(let constant):
switch relation {
case .equal:
heightAnchor.constraint(equalToConstant: constant).isActive = true
case .greaterThanOrEqualTo:
heightAnchor.constraint(greaterThanOrEqualToConstant: constant).isActive = true
case .lessThanOrEqualTo:
heightAnchor.constraint(lessThanOrEqualToConstant: constant).isActive = true
}
case .width(let constant):
switch relation {
case .equal:
widthAnchor.constraint(equalToConstant: constant).isActive = true
case .greaterThanOrEqualTo:
widthAnchor.constraint(greaterThanOrEqualToConstant: constant).isActive = true
case .lessThanOrEqualTo:
widthAnchor.constraint(lessThanOrEqualToConstant: constant).isActive = true
}
case .equalHeight(let other):
switch relation {
case .equal:
heightAnchor.constraint(equalTo: other.heightAnchor).isActive = true
case .greaterThanOrEqualTo:
heightAnchor.constraint(greaterThanOrEqualTo: other.heightAnchor).isActive = true
case .lessThanOrEqualTo:
heightAnchor.constraint(lessThanOrEqualTo: other.heightAnchor).isActive = true
}
case .equalWidth(let other):
switch relation {
case .equal:
widthAnchor.constraint(equalTo: other.widthAnchor).isActive = true
case .greaterThanOrEqualTo:
widthAnchor.constraint(greaterThanOrEqualTo: other.widthAnchor).isActive = true
case .lessThanOrEqualTo:
widthAnchor.constraint(lessThanOrEqualTo: other.widthAnchor).isActive = true
}
case .greaterThanOrEqualTo(let constraint):
set(constraint, relation: .greaterThanOrEqualTo)
case .lessThanOrEqualTo(let constraint):
set(constraint, relation: .lessThanOrEqualTo)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment