Rename MainFeed and MainTimeline folders to Sidebar and Timeline.

This commit is contained in:
Brent Simmons
2025-02-02 11:34:11 -08:00
parent b294fbcc58
commit 2c6c8a7240
26 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
//
// MainFeedRowIdentifier.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 10/20/21.
// Copyright © 2021 Ranchero Software. All rights reserved.
//
import Foundation
final class MainFeedRowIdentifier: NSObject, NSCopying {
var indexPath: IndexPath
init(indexPath: IndexPath) {
self.indexPath = indexPath
}
func copy(with zone: NSZone? = nil) -> Any {
return self
}
}

View File

@@ -0,0 +1,243 @@
//
// MainFeedTableViewCell.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
import Account
import RSTree
protocol MainFeedTableViewCellDelegate: AnyObject {
func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool)
}
final class MainFeedTableViewCell: VibrantTableViewCell {
weak var delegate: MainFeedTableViewCellDelegate?
override var accessibilityLabel: String? {
get {
if unreadCount > 0 {
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
return "\(name) \(unreadCount) \(unreadLabel)"
} else {
return name
}
}
set {
}
}
var iconImage: IconImage? {
didSet {
iconView.iconImage = iconImage
}
}
var isDisclosureAvailable = false {
didSet {
if isDisclosureAvailable != oldValue {
setNeedsLayout()
}
}
}
var isSeparatorShown = true {
didSet {
if isSeparatorShown != oldValue {
if isSeparatorShown {
showView(bottomSeparatorView)
} else {
hideView(bottomSeparatorView)
}
}
}
}
var unreadCount: Int {
get {
return unreadCountView.unreadCount
}
set {
if unreadCountView.unreadCount != newValue {
unreadCountView.unreadCount = newValue
unreadCountView.isHidden = (newValue < 1)
setNeedsLayout()
}
}
}
var name: String {
get {
return titleView.text ?? ""
}
set {
if titleView.text != newValue {
titleView.text = newValue
setNeedsLayout()
}
}
}
private let titleView: UILabel = {
let label = NonIntrinsicLabel()
label.numberOfLines = 0
label.allowsDefaultTighteningForTruncation = false
label.adjustsFontForContentSizeCategory = true
label.lineBreakMode = .byTruncatingTail
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private let iconView = IconView()
private let bottomSeparatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.separator
view.alpha = 0.5
return view
}()
private var isDisclosureExpanded = false
private var disclosureButton: UIButton?
private var unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
private var isShowingEditControl = false
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func setDisclosure(isExpanded: Bool, animated: Bool) {
isDisclosureExpanded = isExpanded
let duration = animated ? 0.3 : 0.0
UIView.animate(withDuration: duration) {
if self.isDisclosureExpanded {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796)
} else {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0)
}
}
}
override func willTransition(to state: UITableViewCell.StateMask) {
super.willTransition(to: state)
isShowingEditControl = state.contains(.showingEditControl)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let layout = MainFeedTableViewCellLayout(
cellWidth: bounds.size.width,
insets: safeAreaInsets,
label: titleView,
unreadCountView: unreadCountView,
showingEditingControl: isShowingEditControl,
indent: indentationLevel == 1,
shouldShowDisclosure: isDisclosureAvailable
)
return CGSize(width: bounds.width, height: layout.height)
}
override func layoutSubviews() {
super.layoutSubviews()
let layout = MainFeedTableViewCellLayout(
cellWidth: bounds.size.width,
insets: safeAreaInsets,
label: titleView,
unreadCountView: unreadCountView,
showingEditingControl: isShowingEditControl,
indent: indentationLevel == 1,
shouldShowDisclosure: isDisclosureAvailable
)
layoutWith(layout)
}
@objc func buttonPressed(_ sender: UIButton) {
if isDisclosureAvailable {
setDisclosure(isExpanded: !isDisclosureExpanded, animated: true)
delegate?.mainFeedTableViewCellDisclosureDidToggle(self, expanding: isDisclosureExpanded)
}
}
override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated)
let iconTintColor: UIColor
if isHighlighted || isSelected {
iconTintColor = AppColor.vibrantText
} else {
if let preferredColor = iconImage?.preferredColor {
iconTintColor = UIColor(cgColor: preferredColor)
} else {
iconTintColor = AppColor.secondaryAccent
}
}
if animated {
UIView.animate(withDuration: Self.duration) {
self.iconView.tintColor = iconTintColor
}
} else {
self.iconView.tintColor = iconTintColor
}
updateLabelVibrancy(titleView, color: labelColor, animated: animated)
}
}
private extension MainFeedTableViewCell {
func commonInit() {
addSubviewAtInit(unreadCountView)
addSubviewAtInit(iconView)
addSubviewAtInit(titleView)
addDisclosureView()
addSubviewAtInit(bottomSeparatorView)
}
func addDisclosureView() {
disclosureButton = NonIntrinsicButton(type: .roundedRect)
disclosureButton!.addTarget(self, action: #selector(buttonPressed(_:)), for: UIControl.Event.touchUpInside)
disclosureButton?.setImage(AppImage.disclosure, for: .normal)
disclosureButton?.tintColor = AppColor.controlBackground
disclosureButton?.imageView?.contentMode = .center
disclosureButton?.imageView?.clipsToBounds = false
disclosureButton?.addInteraction(UIPointerInteraction())
addSubviewAtInit(disclosureButton!)
}
func addSubviewAtInit(_ view: UIView) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
}
func layoutWith(_ layout: MainFeedTableViewCellLayout) {
iconView.setFrameIfNotEqual(layout.faviconRect)
titleView.setFrameIfNotEqual(layout.titleRect)
unreadCountView.setFrameIfNotEqual(layout.unreadCountRect)
disclosureButton?.setFrameIfNotEqual(layout.disclosureButtonRect)
disclosureButton?.isHidden = !isDisclosureAvailable
bottomSeparatorView.setFrameIfNotEqual(layout.separatorRect)
}
func hideView(_ view: UIView) {
if !view.isHidden {
view.isHidden = true
}
}
func showView(_ view: UIView) {
if view.isHidden {
view.isHidden = false
}
}
}

View File

@@ -0,0 +1,148 @@
//
// MainFeedTableViewCellLayout.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/24/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
struct MainFeedTableViewCellLayout {
private static let indentWidth = CGFloat(integerLiteral: 42)
private static let editingControlIndent = CGFloat(integerLiteral: 40)
private static let imageSize = CGSize(width: 24, height: 24)
private static let imageMarginRight = CGFloat(integerLiteral: 11)
private static let labelMarginRight = CGFloat(integerLiteral: 8)
private static let unreadCountMarginRight = CGFloat(integerLiteral: 16)
private static let disclosureButtonSize = CGSize(width: 44, height: 44)
private static let verticalPadding = CGFloat(integerLiteral: 11)
private static let minRowHeight = CGFloat(integerLiteral: 44)
static let faviconCornerRadius = CGFloat(integerLiteral: 2)
let faviconRect: CGRect
let titleRect: CGRect
let unreadCountRect: CGRect
let disclosureButtonRect: CGRect
let separatorRect: CGRect
let height: CGFloat
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView, showingEditingControl: Bool, indent: Bool, shouldShowDisclosure: Bool) {
var initialIndent = insets.left
if indent {
initialIndent += MainFeedTableViewCellLayout.indentWidth
}
let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0)
// Disclosure Button
var rDisclosure = CGRect.zero
if shouldShowDisclosure {
rDisclosure.size = MainFeedTableViewCellLayout.disclosureButtonSize
rDisclosure.origin.x = bounds.origin.x
}
// Favicon
var rFavicon = CGRect.zero
if !shouldShowDisclosure {
let x = bounds.origin.x
let y = UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding) +
label.font.lineHeight / 2.0 -
MainFeedTableViewCellLayout.imageSize.height / 2.0
rFavicon = CGRect(x: x, y: y, width: MainFeedTableViewCellLayout.imageSize.width, height: MainFeedTableViewCellLayout.imageSize.height)
}
// Unread Count
let unreadCountSize = unreadCountView.contentSize
let unreadCountIsHidden = unreadCountView.unreadCount < 1
var rUnread = CGRect.zero
if !unreadCountIsHidden {
rUnread.size = unreadCountSize
rUnread.origin.x = bounds.maxX - (MainFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width)
}
// Title
var rLabelx = insets.left + MainFeedTableViewCellLayout.disclosureButtonSize.width
if !shouldShowDisclosure {
rLabelx = rLabelx + MainFeedTableViewCellLayout.imageSize.width + MainFeedTableViewCellLayout.imageMarginRight
}
let rLabely = UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding)
var labelWidth = CGFloat.zero
if !unreadCountIsHidden {
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight + (cellWidth - rUnread.minX))
} else {
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight)
}
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
// Now that we've got everything (especially the label) computed without the editing controls, update for them.
// We do this because we don't want the row height to change when the editing controls are brought out. We will
// handle the missing space, but removing it from the label and truncating.
if showingEditingControl {
rDisclosure.origin.x += MainFeedTableViewCellLayout.editingControlIndent
rFavicon.origin.x += MainFeedTableViewCellLayout.editingControlIndent
rLabelx += MainFeedTableViewCellLayout.editingControlIndent
if !unreadCountIsHidden {
rUnread.origin.x -= MainFeedTableViewCellLayout.editingControlIndent
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight + (cellWidth - rUnread.minX))
} else {
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight + MainFeedTableViewCellLayout.editingControlIndent)
}
}
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding)
let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY()
var cellHeight = max(paddedLabelHeight, maxGraphicsHeight)
if cellHeight < MainFeedTableViewCellLayout.minRowHeight {
cellHeight = MainFeedTableViewCellLayout.minRowHeight
}
// Center in Cell
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
if !unreadCountIsHidden {
rUnread = MainFeedTableViewCellLayout.centerVertically(rUnread, newBounds)
}
if shouldShowDisclosure {
rDisclosure = MainFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds)
}
// Small fonts and the Favicon need centered if we hit the minimum row height
if cellHeight == MainFeedTableViewCellLayout.minRowHeight {
rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
rFavicon = MainFeedTableViewCellLayout.centerVertically(rFavicon, newBounds)
}
// Separator Insets
let separatorInset = MainFeedTableViewCellLayout.disclosureButtonSize.width
separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5)
// Assign the properties
self.height = cellHeight
self.faviconRect = rFavicon
self.unreadCountRect = rUnread
self.disclosureButtonRect = rDisclosure
self.titleRect = rLabel
}
// Ideally this will be implemented in RSCore (see RSGeometry)
static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect {
var result = originalRect
result.origin.y = containerRect.midY - (result.height / 2.0)
result = result.integral
result.size = originalRect.size
return result
}
}

View File

@@ -0,0 +1,209 @@
//
// MainFeedTableViewSectionHeader.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/18/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
protocol MainFeedTableViewSectionHeaderDelegate: AnyObject {
func mainFeedTableViewSectionHeaderDisclosureDidToggle(_ sender: MainFeedTableViewSectionHeader)
}
final class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
weak var delegate: MainFeedTableViewSectionHeaderDelegate?
override var accessibilityLabel: String? {
get {
if unreadCount > 0 {
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
return "\(name) \(unreadCount) \(unreadLabel) \(expandedStateMessage) "
} else {
return "\(name) \(expandedStateMessage) "
}
}
set {
}
}
private var expandedStateMessage: String {
if disclosureExpanded {
return NSLocalizedString("Expanded", comment: "Disclosure button expanded state for accessibility")
}
return NSLocalizedString("Collapsed", comment: "Disclosure button collapsed state for accessibility")
}
var unreadCount: Int {
get {
return unreadCountView.unreadCount
}
set {
if unreadCountView.unreadCount != newValue {
unreadCountView.unreadCount = newValue
updateUnreadCountView()
setNeedsLayout()
}
}
}
var name: String {
get {
return titleView.text ?? ""
}
set {
if titleView.text != newValue {
titleView.text = newValue
setNeedsLayout()
}
}
}
var disclosureExpanded = false {
didSet {
updateExpandedState(animate: true)
updateUnreadCountView()
}
}
var isLastSection = false
private let titleView: UILabel = {
let label = NonIntrinsicLabel()
label.numberOfLines = 0
label.allowsDefaultTighteningForTruncation = false
label.adjustsFontForContentSizeCategory = true
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private let unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
private lazy var disclosureButton: UIButton = {
let button = NonIntrinsicButton()
button.tintColor = UIColor.tertiaryLabel
button.setImage(AppImage.disclosure, for: .normal)
button.contentMode = .center
button.addInteraction(UIPointerInteraction())
button.addTarget(self, action: #selector(toggleDisclosure), for: .touchUpInside)
return button
}()
private let topSeparatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.separator
return view
}()
private let bottomSeparatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.separator
return view
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView)
return CGSize(width: bounds.width, height: layout.height)
}
override func layoutSubviews() {
super.layoutSubviews()
let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: contentView.bounds.size.width,
insets: contentView.safeAreaInsets,
label: titleView,
unreadCountView: unreadCountView)
layoutWith(layout)
}
}
private extension MainFeedTableViewSectionHeader {
@objc func toggleDisclosure() {
delegate?.mainFeedTableViewSectionHeaderDisclosureDidToggle(self)
}
func commonInit() {
addSubviewAtInit(unreadCountView)
addSubviewAtInit(titleView)
addSubviewAtInit(disclosureButton)
updateExpandedState(animate: false)
addBackgroundView()
addSubviewAtInit(topSeparatorView)
addSubviewAtInit(bottomSeparatorView)
}
func updateExpandedState(animate: Bool) {
if !isLastSection && self.disclosureExpanded {
self.bottomSeparatorView.isHidden = false
}
let duration = animate ? 0.3 : 0.0
UIView.animate(
withDuration: duration,
animations: {
if self.disclosureExpanded {
self.disclosureButton.transform = CGAffineTransform(rotationAngle: 1.570796)
} else {
self.disclosureButton.transform = CGAffineTransform(rotationAngle: 0)
}
}, completion: { _ in
if !self.isLastSection && !self.disclosureExpanded {
self.bottomSeparatorView.isHidden = true
}
})
}
func updateUnreadCountView() {
if !disclosureExpanded && unreadCount > 0 {
UIView.animate(withDuration: 0.3) {
self.unreadCountView.alpha = 1
}
} else {
UIView.animate(withDuration: 0.3) {
self.unreadCountView.alpha = 0
}
}
}
func addSubviewAtInit(_ view: UIView) {
contentView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
}
func layoutWith(_ layout: MainFeedTableViewSectionHeaderLayout) {
titleView.setFrameIfNotEqual(layout.titleRect)
unreadCountView.setFrameIfNotEqual(layout.unreadCountRect)
disclosureButton.setFrameIfNotEqual(layout.disclosureButtonRect)
let x = -safeAreaInsets.left
let width = safeAreaInsets.left + safeAreaInsets.right + frame.width
let height = 0.33
let top = CGRect(x: x, y: 0, width: width, height: height)
topSeparatorView.setFrameIfNotEqual(top)
let bottom = CGRect(x: x, y: frame.height - height, width: width, height: height)
bottomSeparatorView.setFrameIfNotEqual(bottom)
}
func addBackgroundView() {
self.backgroundView = UIView(frame: self.bounds)
self.backgroundView?.backgroundColor = AppColor.sectionHeader
}
}

View File

@@ -0,0 +1,90 @@
//
// MainFeedTableViewSectionHeaderLayout.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 11/5/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
struct MainFeedTableViewSectionHeaderLayout {
private static let labelMarginRight = CGFloat(integerLiteral: 8)
private static let unreadCountMarginRight = CGFloat(integerLiteral: 16)
private static let disclosureButtonSize = CGSize(width: 44, height: 44)
private static let verticalPadding = CGFloat(integerLiteral: 11)
private static let minRowHeight = CGFloat(integerLiteral: 44)
let titleRect: CGRect
let unreadCountRect: CGRect
let disclosureButtonRect: CGRect
let height: CGFloat
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView) {
let bounds = CGRect(x: insets.left, y: 0.0, width: floor(cellWidth - insets.right), height: 0.0)
// Disclosure Button
var rDisclosure = CGRect.zero
rDisclosure.size = MainFeedTableViewSectionHeaderLayout.disclosureButtonSize
rDisclosure.origin.x = bounds.origin.x
// Unread Count
let unreadCountSize = unreadCountView.contentSize
let unreadCountIsHidden = unreadCountView.unreadCount < 1
var rUnread = CGRect.zero
if !unreadCountIsHidden {
rUnread.size = unreadCountSize
rUnread.origin.x = bounds.maxX - (MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight + unreadCountSize.width)
}
// Max Unread Count
// We can't reload Section Headers so we don't let the title extend into the (probably) worse case Unread Count area.
let maxUnreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
maxUnreadCountView.unreadCount = 888
let maxUnreadCountSize = maxUnreadCountView.contentSize
// Title
let rLabelx = insets.left + MainFeedTableViewSectionHeaderLayout.disclosureButtonSize.width
let rLabely = UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding)
var labelWidth = CGFloat.zero
labelWidth = cellWidth - (rLabelx + MainFeedTableViewSectionHeaderLayout.labelMarginRight + maxUnreadCountSize.width + MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight)
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding)
let maxGraphicsHeight = [rUnread, rDisclosure].maxY()
var cellHeight = max(paddedLabelHeight, maxGraphicsHeight)
if cellHeight < MainFeedTableViewSectionHeaderLayout.minRowHeight {
cellHeight = MainFeedTableViewSectionHeaderLayout.minRowHeight
}
// Center in Cell
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
if !unreadCountIsHidden {
rUnread = MainFeedTableViewCellLayout.centerVertically(rUnread, newBounds)
}
rDisclosure = MainFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds)
// Small fonts need centered if we hit the minimum row height
if cellHeight == MainFeedTableViewSectionHeaderLayout.minRowHeight {
rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
}
// Assign the properties
self.height = cellHeight
self.unreadCountRect = rUnread
self.disclosureButtonRect = rDisclosure
self.titleRect = rLabel
}
}

View File

@@ -0,0 +1,122 @@
//
// MainFeedUnreadCountView.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/22/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
class MainFeedUnreadCountView: UIView {
var padding: UIEdgeInsets {
return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0)
}
let cornerRadius = 8.0
let bgColor = AppColor.controlBackground
var textColor: UIColor {
return UIColor.white
}
var textAttributes: [NSAttributedString.Key: AnyObject] {
let textFont = UIFont.preferredFont(forTextStyle: .caption1).bold()
return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()]
}
var textSizeCache = [Int: CGSize]()
var unreadCount = 0 {
didSet {
contentSizeIsValid = false
invalidateIntrinsicContentSize()
setNeedsDisplay()
}
}
var unreadCountString: String {
return unreadCount < 1 ? "" : "\(unreadCount)"
}
private var contentSizeIsValid = false
private var _contentSize = CGSize.zero
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
textSizeCache = [Int: CGSize]()
contentSizeIsValid = false
setNeedsDisplay()
}
var contentSize: CGSize {
if !contentSizeIsValid {
var size = CGSize.zero
if unreadCount > 0 {
size = textSize()
size.width += (padding.left + padding.right)
size.height += (padding.top + padding.bottom)
}
_contentSize = size
contentSizeIsValid = true
}
return _contentSize
}
// Prevent autolayout from messing around with our frame settings
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
func textSize() -> CGSize {
if unreadCount < 1 {
return CGSize.zero
}
if let cachedSize = textSizeCache[unreadCount] {
return cachedSize
}
var size = unreadCountString.size(withAttributes: textAttributes)
size.height = ceil(size.height)
size.width = ceil(size.width)
textSizeCache[unreadCount] = size
return size
}
func textRect() -> CGRect {
let size = textSize()
var r = CGRect.zero
r.size = size
r.origin.x = (bounds.maxX - padding.right) - r.size.width
r.origin.y = padding.top
return r
}
override func draw(_ dirtyRect: CGRect) {
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: cornerRadii)
bgColor.setFill()
path.fill()
if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
}
}
}