Created
February 2, 2020 14:09
-
-
Save heitara/95c062155a73959d139342c3f2ddb9d4 to your computer and use it in GitHub Desktop.
Clean Red Badge implementation in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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