Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement red dot support for UIBarButtonItem #1974

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
var showSearchProgressSpinner: Bool = true
var showRainbowRingForAvatar: Bool = false
var showBadgeOnBarButtonItem: Bool = false
var showRedDotOnBarButtonItem: Bool = false

var allowsCellSelection: Bool = false {
didSet {
Expand Down Expand Up @@ -624,6 +625,20 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
}

if indexPath.row == 3 {
guard let cell = tableView.dequeueReusableCell(withIdentifier: BooleanCell.identifier, for: indexPath) as? BooleanCell else {
return UITableViewCell()
}
cell.setup(title: "Show Red Dot on right bar button items",
isOn: showRedDotOnBarButtonItem,
isSwitchEnabled: navigationItem.titleStyle == .largeLeading)
cell.titleNumberOfLines = 0
cell.onValueChanged = { [weak self, weak cell] in
self?.shouldShowRedDot(isOn: cell?.isOn ?? false)
}
return cell
}

if indexPath.row == 4 {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ActionsCell.identifier, for: indexPath) as? ActionsCell else {
return UITableViewCell()
}
Expand Down Expand Up @@ -748,6 +763,16 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
showBadgeOnBarButtonItem = isOn
}

@objc private func shouldShowRedDot(isOn: Bool) {
guard let items = navigationItem.rightBarButtonItems, !items.isEmpty else {
return
}
for item in items {
item.showRedDot(isOn)
}
showRedDotOnBarButtonItem = isOn
}

@objc private func showTooltipButtonPressed() {
let navigationBar = msfNavigationController?.msfNavigationBar
guard let view = navigationBar?.barButtonItemView(with: BarButtonItemTag.threeDay.rawValue) else {
Expand Down
4 changes: 4 additions & 0 deletions ios/FluentUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
A542A9D7226FC01100204A52 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A559BB81212B6FA40055E107 /* Localizable.strings */; };
A542A9D8226FC01700204A52 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = A5DF1EAD2213B26900CC741A /* Localizable.stringsdict */; };
A5CEC16020D980B30016922A /* FluentUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEC15F20D980B30016922A /* FluentUITests.swift */; };
B70204342B7DD41200E5B549 /* UIBarButtonItem+RedDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70204332B7DD41200E5B549 /* UIBarButtonItem+RedDot.swift */; };
C708B05F260A8778007190FA /* SegmentPillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C708B055260A86FA007190FA /* SegmentPillButton.swift */; };
C708B064260A87F7007190FA /* SegmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C708B04B260A8696007190FA /* SegmentItem.swift */; };
C77A04B825F03DD1001B3EB6 /* String+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77A04B625F03DD1001B3EB6 /* String+Date.swift */; };
Expand Down Expand Up @@ -417,6 +418,7 @@
B4E782C62179509A00A7DFCE /* CenteredLabelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteredLabelCell.swift; sourceTree = "<group>"; };
B4EF53C2215AF1AB00573E8F /* Persona.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persona.swift; sourceTree = "<group>"; };
B4EF66502294A664007FEAB0 /* TableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewHeaderFooterView.swift; sourceTree = "<group>"; };
B70204332B7DD41200E5B549 /* UIBarButtonItem+RedDot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+RedDot.swift"; sourceTree = "<group>"; };
C0938E43235E8ED500256251 /* AnimationSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationSynchronizer.swift; sourceTree = "<group>"; };
C0A0D76B233AEF6C00F432FD /* ShimmerLinesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerLinesView.swift; sourceTree = "<group>"; };
C0EAAEAC2347E1DF00C7244E /* ShimmerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1215,6 +1217,7 @@
667E54012A12B6F800728F93 /* TwoLineTitleView+Navigation.swift */,
FD41C8BD22DD47120086F899 /* UINavigationItem+Navigation.swift */,
FD9DA7B4232C33A80013E41B /* UIViewController+Navigation.swift */,
B70204332B7DD41200E5B549 /* UIBarButtonItem+RedDot.swift */,
6ED4C11A2695A6E800C30BD6 /* UIBarButtonItem+BadgeValue.swift */,
6FBFD62429CBB5B9002F3C81 /* SearchBar */,
FD41C87222DD13230086F899 /* Helpers */,
Expand Down Expand Up @@ -1684,6 +1687,7 @@
ECA9218627A3301C00B66117 /* MSFAvatarGroup.swift in Sources */,
5314E0EC25F012C40099271A /* NavigationAnimator.swift in Sources */,
5314E17225F0191C0099271A /* Separator.swift in Sources */,
B70204342B7DD41200E5B549 /* UIBarButtonItem+RedDot.swift in Sources */,
5314E14225F016860099271A /* CardPresenterNavigationController.swift in Sources */,
5314E11725F015EA0099271A /* PersonaCell.swift in Sources */,
5314E23025F022C80099271A /* UIScrollView+Extensions.swift in Sources */,
Expand Down
138 changes: 98 additions & 40 deletions ios/FluentUI/Navigation/BadgeLabelButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class BadgeLabelButton: UIButton {
var item: UIBarButtonItem? {
didSet {
setupButton()
prepareButtonForBadgeLabel()
prepareButtonForBadge()
}
}

Expand All @@ -25,6 +25,10 @@ class BadgeLabelButton: UIButton {

configuration = UIButton.Configuration.plain()

NotificationCenter.default.addObserver(self,
selector: #selector(redDotVisibilitDidChanged),
name: UIBarButtonItem.redDotValueDidChangeNotification,
object: item)
NotificationCenter.default.addObserver(self,
selector: #selector(badgeValueDidChange),
name: UIBarButtonItem.badgeValueDidChangeNotification,
Expand All @@ -42,7 +46,7 @@ class BadgeLabelButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()

prepareButtonForBadgeLabel()
prepareButtonForBadge()
}

private struct Constants {
Expand All @@ -54,13 +58,23 @@ class BadgeLabelButton: UIButton {
static let badgeHorizontalPadding: CGFloat = 10
static let badgeCornerRadii: CGFloat = 10

static let redDotWidth: CGFloat = 10
static let redDotHeight: CGFloat = 10
static let redDotCornerRadius: CGFloat = 5

// These are consistent with UIKit's default navigation bar buttons
static let maximumContentSizeCategory: UIContentSizeCategory = .extraExtraLarge
static let minimumContentSizeCategory: UIContentSizeCategory = .large
}

private let badgeLabel = BadgeLabel()

private lazy var redDotView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()

private var badgeWidth: CGFloat {
return min(max(badgeLabel.intrinsicContentSize.width + Constants.badgeHorizontalPadding,
Constants.badgeMinWidth),
Expand All @@ -71,31 +85,44 @@ class BadgeLabelButton: UIButton {
return (frame.size.height - intrinsicContentSize.height) / 2 - Constants.badgeHeight / 2 - Constants.badgeVerticalOffset
}

private var badgeFrameOriginX: CGFloat {
private var redDotVerticalPosition: CGFloat {
return (frame.size.height - intrinsicContentSize.height) / 2 - Constants.redDotHeight / 2 - Constants.badgeVerticalOffset
}

private func badgeFrameOriginX(_ viewWidth: CGFloat) -> CGFloat {
let xOrigin: CGFloat = {
return isLeftToRightUserInterfaceLayoutDirection ?
frame.size.width - (configuration?.contentInsets.leading ?? 0) :
configuration?.contentInsets.trailing ?? 0
}()

return (xOrigin - badgeWidth / 2)
return (xOrigin - viewWidth / 2)
}

private var badgeLabelFrame: CGRect {
let targetView = isItemTitlePresent ? titleLabel : imageView

return CGRect(x: badgeFrameOriginX - (targetView?.frame.origin.x ?? 0),
return CGRect(x: badgeFrameOriginX(badgeWidth) - (targetView?.frame.origin.x ?? 0),
y: badgeVerticalPosition - (targetView?.frame.origin.y ?? 0),
width: badgeWidth,
height: Constants.badgeHeight)
}

private var badgeBoundsOriginX: CGFloat {
private var redDotFrame: CGRect {
let targetView = isItemTitlePresent ? titleLabel : imageView

return CGRect(x: badgeFrameOriginX(Constants.redDotWidth) - (targetView?.frame.origin.x ?? 0),
y: redDotVerticalPosition - (targetView?.frame.origin.y ?? 0),
width: Constants.redDotWidth,
height: Constants.redDotHeight)
}

private func badgeBoundsOriginX(_ viewWidth: CGFloat) -> CGFloat {
let xOrigin: CGFloat = 0
if isLeftToRightUserInterfaceLayoutDirection {
return xOrigin
} else {
return xOrigin - badgeWidth / 2
return xOrigin - viewWidth / 2
}
}

Expand Down Expand Up @@ -149,59 +176,86 @@ class BadgeLabelButton: UIButton {
isPointerInteractionEnabled = true
}

private func prepareButtonForBadgeLabel() {
private func prepareButtonForBadge() {
if isItemTitlePresent, let titleLabel = titleLabel {
titleLabel.addSubview(badgeLabel)
titleLabel.addSubview(redDotView)
titleLabel.isHidden = false
} else if let imageView = imageView {
imageView.addSubview(badgeLabel)
imageView.addSubview(redDotView)
imageView.isHidden = false
imageView.clipsToBounds = false
}

updateBadgeLabel()
updateBadge()
}

private func updateBadgeLabel() {
private func updateBadge() {
badgeLabel.text = item?.badgeValue
let isNilBadgeValue = item?.badgeValue == nil
badgeLabel.isHidden = isNilBadgeValue
redDotView.isHidden = (item?.shouldShowRedDot != true || !isNilBadgeValue)

if isNilBadgeValue {
layer.mask = nil
if item?.shouldShowRedDot == true {
showRedDot()
} else {
layer.mask = nil
}
} else {
badgeLabel.frame = badgeLabelFrame
addBadgeOrRedDot(false)
}
}

let badgeLabelLayer = CAShapeLayer()
badgeLabelLayer.path = UIBezierPath(roundedRect: badgeLabel.bounds,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: Constants.badgeCornerRadii, height: Constants.badgeCornerRadii)).cgPath
badgeLabel.layer.mask = badgeLabelLayer
private func showRedDot() {
redDotView.frame = redDotFrame
addBadgeOrRedDot(true)
}

private func addBadgeOrRedDot(_ isRedDot: Bool) {
let viewBounds = isRedDot ? redDotView.bounds : badgeLabel.bounds
let viewCornerRadius = isRedDot ? Constants.redDotCornerRadius : Constants.badgeCornerRadii

let badgeLabelLayer = CAShapeLayer()
badgeLabelLayer.path = UIBezierPath(roundedRect: viewBounds,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(
width: viewCornerRadius,
height: viewCornerRadius)).cgPath

let computedBadgeWidth = badgeWidth
let badgeBounds = CGRect(x: badgeFrameOriginX,
y: badgeVerticalPosition,
width: computedBadgeWidth,
height: Constants.badgeHeight)
let badgeCutoutPath = UIBezierPath(rect: CGRect(x: badgeBoundsOriginX,
y: badgeBounds.origin.y,
width: frame.size.width + computedBadgeWidth / 2,
height: frame.size.height))
// Adding the path for the cutout on the button's titleLabel or imageView where the badge label will be placed on top of.
badgeCutoutPath.append(UIBezierPath(roundedRect: badgeCutoutRect(for: badgeBounds),
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: Constants.badgeCornerRadii,
height: Constants.badgeCornerRadii)))
// Adding the path that will display the badge label with rounded corners on top of the cutout.
badgeCutoutPath.append(UIBezierPath(roundedRect: badgeBounds,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: Constants.badgeCornerRadii,
height: Constants.badgeCornerRadii)))
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.path = badgeCutoutPath.cgPath
layer.mask = maskLayer
if isRedDot {
redDotView.layer.mask = badgeLabelLayer
} else {
badgeLabel.layer.mask = badgeLabelLayer
}

let computedBadgeWidth = isRedDot ? Constants.redDotWidth : badgeWidth
let badgeBounds = CGRect(x: badgeFrameOriginX(computedBadgeWidth),
y: isRedDot ? redDotVerticalPosition : badgeVerticalPosition,
width: computedBadgeWidth,
height: isRedDot ? Constants.redDotHeight : Constants.badgeHeight)


let badgeCutoutPath = UIBezierPath(rect: CGRect(x: badgeBoundsOriginX(computedBadgeWidth),
y: badgeBounds.origin.y,
width: frame.size.width + computedBadgeWidth / 2,
height: frame.size.height))
// Adding the path for the cutout on the button's titleLabel or imageView where the badge label OR Red Dot will be placed on top of.
badgeCutoutPath.append(UIBezierPath(roundedRect: badgeCutoutRect(for: badgeBounds),
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: viewCornerRadius,
height: viewCornerRadius)))
// Adding the path that will display the badge label with rounded corners on top of the cutout.
badgeCutoutPath.append(UIBezierPath(roundedRect: badgeBounds,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: viewCornerRadius,
height: viewCornerRadius)))
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.path = badgeCutoutPath.cgPath
layer.mask = maskLayer
}

private func badgeCutoutRect(for frame: CGRect) -> CGRect {
Expand All @@ -212,10 +266,14 @@ class BadgeLabelButton: UIButton {
}

@objc private func badgeValueDidChange() {
updateBadgeLabel()
updateBadge()
updateAccessibilityLabel()
}

@objc private func redDotVisibilitDidChanged() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling

updateBadge()
}

@objc private func contentSizeCategoryDidChange(notification: Notification) {
guard let titleLabel = titleLabel else {
return
Expand Down
33 changes: 33 additions & 0 deletions ios/FluentUI/Navigation/UIBarButtonItem+RedDot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//

import UIKit

@objc public extension UIBarButtonItem {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally do not want to create public extensions to system classes in Fluent, as it forces all downstream consumers to deal with our extension in their namespace. Instead, either use a custom UIBarButtonItem subclass, or provide some form of helper class that can perform the mutation for you.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we are following same styling for Badge value as well. I am just adding one extra functionality on top of badge button by using same mechanism.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existence of technical debt does not automatically justify the addition of more. We should move the badge styling away from this pattern as well; adding more that needs to be changed will only increase future work, especially when it comes to public APIs that downstream partners expect to remain supported.

private struct AssociatedKeys {
static var redDotValue: UInt8 = 0
}

static let redDotValueDidChangeNotification = NSNotification.Name(rawValue: "UIBarButtonItemRedDotValueDidChangeNotification")

/// This Bool indicate if we need to display red dot on top right cornet of button.
/// Red dot will be override by badge value, in case user set for badge value and red dot, it will give preference to badge value.
@objc var shouldShowRedDot: Bool {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.redDotValue) as? Bool ?? false
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.redDotValue, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
NotificationCenter.default.post(name: UIBarButtonItem.redDotValueDidChangeNotification, object: self)
}
}

/// Use this method on bar button item's instance to set the red to visibility value.
/// - Parameters:
/// - shouldShowRedDot: Bool value indicating if we need to show red dot OR not.
@objc func showRedDot(_ shouldShowRedDot: Bool) {
self.shouldShowRedDot = shouldShowRedDot
}
}
Loading