From 9995acfe2202b00ec9b3867a8d04fd6dfe165574 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Thu, 15 Feb 2024 10:47:20 +0530 Subject: [PATCH 1/3] implement red dot support for NavigationBarButtonItem --- .../NavigationControllerDemoController.swift | 27 +- ios/FluentUI.xcodeproj/project.pbxproj | 4 + .../Navigation/BadgeLabelButton.swift | 558 ++++++++++-------- .../Navigation/UIBarButtonItem+RedDot.swift | 33 ++ 4 files changed, 371 insertions(+), 251 deletions(-) create mode 100644 ios/FluentUI/Navigation/UIBarButtonItem+RedDot.swift diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift index 8a79d2fae9..4f5d7605ce 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift @@ -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 { @@ -623,7 +624,21 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe return cell } - if indexPath.row == 3 { + 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() } @@ -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 { diff --git a/ios/FluentUI.xcodeproj/project.pbxproj b/ios/FluentUI.xcodeproj/project.pbxproj index ccd3b922fa..2f6f490d83 100644 --- a/ios/FluentUI.xcodeproj/project.pbxproj +++ b/ios/FluentUI.xcodeproj/project.pbxproj @@ -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 */; }; @@ -417,6 +418,7 @@ B4E782C62179509A00A7DFCE /* CenteredLabelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteredLabelCell.swift; sourceTree = ""; }; B4EF53C2215AF1AB00573E8F /* Persona.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persona.swift; sourceTree = ""; }; B4EF66502294A664007FEAB0 /* TableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewHeaderFooterView.swift; sourceTree = ""; }; + B70204332B7DD41200E5B549 /* UIBarButtonItem+RedDot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+RedDot.swift"; sourceTree = ""; }; C0938E43235E8ED500256251 /* AnimationSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationSynchronizer.swift; sourceTree = ""; }; C0A0D76B233AEF6C00F432FD /* ShimmerLinesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerLinesView.swift; sourceTree = ""; }; C0EAAEAC2347E1DF00C7244E /* ShimmerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerView.swift; sourceTree = ""; }; @@ -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 */, @@ -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 */, diff --git a/ios/FluentUI/Navigation/BadgeLabelButton.swift b/ios/FluentUI/Navigation/BadgeLabelButton.swift index 2fccd8599b..f49e8e7792 100644 --- a/ios/FluentUI/Navigation/BadgeLabelButton.swift +++ b/ios/FluentUI/Navigation/BadgeLabelButton.swift @@ -7,254 +7,312 @@ import UIKit class BadgeLabelButton: UIButton { - var item: UIBarButtonItem? { - didSet { - setupButton() - prepareButtonForBadgeLabel() - } - } - - var badgeLabelStyle: BadgeLabelStyle = .system { - didSet { - badgeLabel.style = badgeLabelStyle - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - configuration = UIButton.Configuration.plain() - - NotificationCenter.default.addObserver(self, - selector: #selector(badgeValueDidChange), - name: UIBarButtonItem.badgeValueDidChangeNotification, - object: item) - NotificationCenter.default.addObserver(self, - selector: #selector(contentSizeCategoryDidChange(notification:)), - name: UIContentSizeCategory.didChangeNotification, - object: nil) - } - - required init?(coder aDecoder: NSCoder) { - preconditionFailure("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - prepareButtonForBadgeLabel() - } - - private struct Constants { - static let badgeVerticalOffset: CGFloat = -5 - static let badgeHeight: CGFloat = 16 - static let badgeMinWidth: CGFloat = 16 - static let badgeMaxWidth: CGFloat = 27 - static let badgeBorderWidth: CGFloat = 2 - static let badgeHorizontalPadding: CGFloat = 10 - static let badgeCornerRadii: CGFloat = 10 - - // 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 var badgeWidth: CGFloat { - return min(max(badgeLabel.intrinsicContentSize.width + Constants.badgeHorizontalPadding, - Constants.badgeMinWidth), - Constants.badgeMaxWidth) - } - - private var badgeVerticalPosition: CGFloat { - return (frame.size.height - intrinsicContentSize.height) / 2 - Constants.badgeHeight / 2 - Constants.badgeVerticalOffset - } - - private var badgeFrameOriginX: CGFloat { - let xOrigin: CGFloat = { - return isLeftToRightUserInterfaceLayoutDirection ? - frame.size.width - (configuration?.contentInsets.leading ?? 0) : - configuration?.contentInsets.trailing ?? 0 - }() - - return (xOrigin - badgeWidth / 2) - } - - private var badgeLabelFrame: CGRect { - let targetView = isItemTitlePresent ? titleLabel : imageView - - return CGRect(x: badgeFrameOriginX - (targetView?.frame.origin.x ?? 0), - y: badgeVerticalPosition - (targetView?.frame.origin.y ?? 0), - width: badgeWidth, - height: Constants.badgeHeight) - } - - private var badgeBoundsOriginX: CGFloat { - let xOrigin: CGFloat = 0 - if isLeftToRightUserInterfaceLayoutDirection { - return xOrigin - } else { - return xOrigin - badgeWidth / 2 - } - } - - private var isLeftToRightUserInterfaceLayoutDirection: Bool { - return effectiveUserInterfaceLayoutDirection == .leftToRight - } - - private var isItemTitlePresent: Bool { - return item?.title != nil - } - - private func setupButton() { - guard let item = item else { - return - } - - isEnabled = item.isEnabled - tag = item.tag - tintColor = item.tintColor - titleLabel?.font = item.titleTextAttributes(for: .normal)?[.font] as? UIFont - - var portraitImage = item.image - if portraitImage?.renderingMode == .automatic { - portraitImage = portraitImage?.withRenderingMode(.alwaysTemplate) - } - var landscapeImage = item.landscapeImagePhone ?? portraitImage - if landscapeImage?.renderingMode == .automatic { - landscapeImage = landscapeImage?.withRenderingMode(.alwaysTemplate) - } - - configuration?.image = traitCollection.verticalSizeClass == .regular ? portraitImage : landscapeImage - configuration?.title = item.title - - if let action = item.action { - addTarget(item.target, action: action, for: .touchUpInside) - } - - accessibilityIdentifier = item.accessibilityIdentifier - accessibilityLabel = item.accessibilityLabel - accessibilityHint = item.accessibilityHint - showsLargeContentViewer = true - - if let customLargeContentSizeImage = item.largeContentSizeImage { - largeContentImage = customLargeContentSizeImage - } - - if item.title == nil { - largeContentTitle = item.accessibilityLabel - } - - isPointerInteractionEnabled = true - } - - private func prepareButtonForBadgeLabel() { - if isItemTitlePresent, let titleLabel = titleLabel { - titleLabel.addSubview(badgeLabel) - titleLabel.isHidden = false - } else if let imageView = imageView { - imageView.addSubview(badgeLabel) - imageView.isHidden = false - imageView.clipsToBounds = false - } - - updateBadgeLabel() - } - - private func updateBadgeLabel() { - badgeLabel.text = item?.badgeValue - let isNilBadgeValue = item?.badgeValue == nil - badgeLabel.isHidden = isNilBadgeValue - - if isNilBadgeValue { - layer.mask = nil - } else { - badgeLabel.frame = badgeLabelFrame - - let badgeLabelLayer = CAShapeLayer() - badgeLabelLayer.path = UIBezierPath(roundedRect: badgeLabel.bounds, - byRoundingCorners: .allCorners, - cornerRadii: CGSize(width: Constants.badgeCornerRadii, height: Constants.badgeCornerRadii)).cgPath - badgeLabel.layer.mask = badgeLabelLayer - - 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 - } - } - - private func badgeCutoutRect(for frame: CGRect) -> CGRect { - return CGRect(x: frame.origin.x - Constants.badgeBorderWidth, - y: frame.origin.y - Constants.badgeBorderWidth, - width: frame.size.width + 2 * Constants.badgeBorderWidth, - height: frame.size.height + 2 * Constants.badgeBorderWidth) - } - - @objc private func badgeValueDidChange() { - updateBadgeLabel() - updateAccessibilityLabel() - } - - @objc private func contentSizeCategoryDidChange(notification: Notification) { - guard let titleLabel = titleLabel else { - return - } - - let requestedContentSizeCategory = (notification.userInfo?[UIContentSizeCategory.newValueUserInfoKey] as? UIContentSizeCategory) ?? .unspecified - - let cappedContentSizeCategory: UIContentSizeCategory - if requestedContentSizeCategory > Constants.maximumContentSizeCategory { - cappedContentSizeCategory = Constants.maximumContentSizeCategory - } else if requestedContentSizeCategory < Constants.minimumContentSizeCategory { - cappedContentSizeCategory = Constants.minimumContentSizeCategory - } else { - cappedContentSizeCategory = requestedContentSizeCategory - } - - // For some reason, titleLabel doesn't resize to fit the new font size, so we do it ourselves. - titleLabel.font = fluentTheme.typography(.body1, contentSizeCategory: cappedContentSizeCategory) - titleLabel.sizeToFit() - sizeToFit() - if superview != nil { - centerInSuperview(horizontally: false, vertically: true) - } - } - - private func updateAccessibilityLabel() { - guard let item = item else { - return - } - if let badgeAccessibilityLabel = item.badgeAccessibilityLabel { - if let itemAccessibilityLabel = item.accessibilityLabel { - accessibilityLabel = String.localizedStringWithFormat("Accessibility.BadgeLabelButton.LabelFormat".localized, - itemAccessibilityLabel, - badgeAccessibilityLabel) - } else { - accessibilityLabel = badgeAccessibilityLabel - } - } else { - accessibilityLabel = item.accessibilityLabel - } - } + var item: UIBarButtonItem? { + didSet { + setupButton() + prepareButtonForBadgeLabelAndRedDot() + } + } + + var badgeLabelStyle: BadgeLabelStyle = .system { + didSet { + badgeLabel.style = badgeLabelStyle + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + 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, + object: item) + NotificationCenter.default.addObserver(self, + selector: #selector(contentSizeCategoryDidChange(notification:)), + name: UIContentSizeCategory.didChangeNotification, + object: nil) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + prepareButtonForBadgeLabelAndRedDot() + } + + private struct Constants { + static let badgeVerticalOffset: CGFloat = -5 + static let badgeHeight: CGFloat = 16 + static let badgeMinWidth: CGFloat = 16 + static let badgeMaxWidth: CGFloat = 27 + static let badgeBorderWidth: CGFloat = 2 + 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), + Constants.badgeMaxWidth) + } + + private var badgeVerticalPosition: CGFloat { + return (frame.size.height - intrinsicContentSize.height) / 2 - Constants.badgeHeight / 2 - Constants.badgeVerticalOffset + } + + 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 - viewWidth / 2) + } + + private var badgeLabelFrame: CGRect { + let targetView = isItemTitlePresent ? titleLabel : imageView + + return CGRect(x: badgeFrameOriginX(badgeWidth) - (targetView?.frame.origin.x ?? 0), + y: badgeVerticalPosition - (targetView?.frame.origin.y ?? 0), + width: badgeWidth, + height: Constants.badgeHeight) + } + + 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 - viewWidth / 2 + } + } + + private var isLeftToRightUserInterfaceLayoutDirection: Bool { + return effectiveUserInterfaceLayoutDirection == .leftToRight + } + + private var isItemTitlePresent: Bool { + return item?.title != nil + } + + private func setupButton() { + guard let item = item else { + return + } + + isEnabled = item.isEnabled + tag = item.tag + tintColor = item.tintColor + titleLabel?.font = item.titleTextAttributes(for: .normal)?[.font] as? UIFont + + var portraitImage = item.image + if portraitImage?.renderingMode == .automatic { + portraitImage = portraitImage?.withRenderingMode(.alwaysTemplate) + } + var landscapeImage = item.landscapeImagePhone ?? portraitImage + if landscapeImage?.renderingMode == .automatic { + landscapeImage = landscapeImage?.withRenderingMode(.alwaysTemplate) + } + + configuration?.image = traitCollection.verticalSizeClass == .regular ? portraitImage : landscapeImage + configuration?.title = item.title + + if let action = item.action { + addTarget(item.target, action: action, for: .touchUpInside) + } + + accessibilityIdentifier = item.accessibilityIdentifier + accessibilityLabel = item.accessibilityLabel + accessibilityHint = item.accessibilityHint + showsLargeContentViewer = true + + if let customLargeContentSizeImage = item.largeContentSizeImage { + largeContentImage = customLargeContentSizeImage + } + + if item.title == nil { + largeContentTitle = item.accessibilityLabel + } + + isPointerInteractionEnabled = true + } + + private func prepareButtonForBadgeLabelAndRedDot() { + 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 + } + + updateButtonForBadgeLabelOrRedDotView() + } + + private func updateButtonForBadgeLabelOrRedDotView() { + badgeLabel.text = item?.badgeValue + let isNilBadgeValue = item?.badgeValue == nil + badgeLabel.isHidden = isNilBadgeValue + redDotView.isHidden = (item?.shouldShowRedDot != true || !isNilBadgeValue) + + if isNilBadgeValue { + if item?.shouldShowRedDot == true { + showRedDot() + } else { + layer.mask = nil + } + } else { + badgeLabel.frame = badgeLabelFrame + addBadgeOrRedDot(false) + } + } + + 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 + + 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 { + return CGRect(x: frame.origin.x - Constants.badgeBorderWidth, + y: frame.origin.y - Constants.badgeBorderWidth, + width: frame.size.width + 2 * Constants.badgeBorderWidth, + height: frame.size.height + 2 * Constants.badgeBorderWidth) + } + + @objc private func badgeValueDidChange() { + updateButtonForBadgeLabelOrRedDotView() + updateAccessibilityLabel() + } + + @objc private func redDotVisibilitDidChanged() { + updateButtonForBadgeLabelOrRedDotView() + } + + @objc private func contentSizeCategoryDidChange(notification: Notification) { + guard let titleLabel = titleLabel else { + return + } + + let requestedContentSizeCategory = (notification.userInfo?[UIContentSizeCategory.newValueUserInfoKey] as? UIContentSizeCategory) ?? .unspecified + + let cappedContentSizeCategory: UIContentSizeCategory + if requestedContentSizeCategory > Constants.maximumContentSizeCategory { + cappedContentSizeCategory = Constants.maximumContentSizeCategory + } else if requestedContentSizeCategory < Constants.minimumContentSizeCategory { + cappedContentSizeCategory = Constants.minimumContentSizeCategory + } else { + cappedContentSizeCategory = requestedContentSizeCategory + } + + // For some reason, titleLabel doesn't resize to fit the new font size, so we do it ourselves. + titleLabel.font = fluentTheme.typography(.body1, contentSizeCategory: cappedContentSizeCategory) + titleLabel.sizeToFit() + sizeToFit() + if superview != nil { + centerInSuperview(horizontally: false, vertically: true) + } + } + + private func updateAccessibilityLabel() { + guard let item = item else { + return + } + if let badgeAccessibilityLabel = item.badgeAccessibilityLabel { + if let itemAccessibilityLabel = item.accessibilityLabel { + accessibilityLabel = String.localizedStringWithFormat("Accessibility.BadgeLabelButton.LabelFormat".localized, + itemAccessibilityLabel, + badgeAccessibilityLabel) + } else { + accessibilityLabel = badgeAccessibilityLabel + } + } else { + accessibilityLabel = item.accessibilityLabel + } + } } diff --git a/ios/FluentUI/Navigation/UIBarButtonItem+RedDot.swift b/ios/FluentUI/Navigation/UIBarButtonItem+RedDot.swift new file mode 100644 index 0000000000..7f1dc22c24 --- /dev/null +++ b/ios/FluentUI/Navigation/UIBarButtonItem+RedDot.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import UIKit + +@objc public extension UIBarButtonItem { + 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 + } +} From b5729735d37175a7eff810a7066526ae1cf24663 Mon Sep 17 00:00:00 2001 From: kumarmohit5189 Date: Thu, 15 Feb 2024 14:42:37 +0530 Subject: [PATCH 2/3] refactor code for tabs --- .../Navigation/BadgeLabelButton.swift | 616 +++++++++--------- 1 file changed, 308 insertions(+), 308 deletions(-) diff --git a/ios/FluentUI/Navigation/BadgeLabelButton.swift b/ios/FluentUI/Navigation/BadgeLabelButton.swift index f49e8e7792..3b40a532f4 100644 --- a/ios/FluentUI/Navigation/BadgeLabelButton.swift +++ b/ios/FluentUI/Navigation/BadgeLabelButton.swift @@ -7,312 +7,312 @@ import UIKit class BadgeLabelButton: UIButton { - var item: UIBarButtonItem? { - didSet { - setupButton() - prepareButtonForBadgeLabelAndRedDot() - } - } - - var badgeLabelStyle: BadgeLabelStyle = .system { - didSet { - badgeLabel.style = badgeLabelStyle - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - 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, - object: item) - NotificationCenter.default.addObserver(self, - selector: #selector(contentSizeCategoryDidChange(notification:)), - name: UIContentSizeCategory.didChangeNotification, - object: nil) - } - - required init?(coder aDecoder: NSCoder) { - preconditionFailure("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - prepareButtonForBadgeLabelAndRedDot() - } - - private struct Constants { - static let badgeVerticalOffset: CGFloat = -5 - static let badgeHeight: CGFloat = 16 - static let badgeMinWidth: CGFloat = 16 - static let badgeMaxWidth: CGFloat = 27 - static let badgeBorderWidth: CGFloat = 2 - 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), - Constants.badgeMaxWidth) - } - - private var badgeVerticalPosition: CGFloat { - return (frame.size.height - intrinsicContentSize.height) / 2 - Constants.badgeHeight / 2 - Constants.badgeVerticalOffset - } - - 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 - viewWidth / 2) - } - - private var badgeLabelFrame: CGRect { - let targetView = isItemTitlePresent ? titleLabel : imageView - - return CGRect(x: badgeFrameOriginX(badgeWidth) - (targetView?.frame.origin.x ?? 0), - y: badgeVerticalPosition - (targetView?.frame.origin.y ?? 0), - width: badgeWidth, - height: Constants.badgeHeight) - } - - 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 - viewWidth / 2 - } - } - - private var isLeftToRightUserInterfaceLayoutDirection: Bool { - return effectiveUserInterfaceLayoutDirection == .leftToRight - } - - private var isItemTitlePresent: Bool { - return item?.title != nil - } - - private func setupButton() { - guard let item = item else { - return - } - - isEnabled = item.isEnabled - tag = item.tag - tintColor = item.tintColor - titleLabel?.font = item.titleTextAttributes(for: .normal)?[.font] as? UIFont - - var portraitImage = item.image - if portraitImage?.renderingMode == .automatic { - portraitImage = portraitImage?.withRenderingMode(.alwaysTemplate) - } - var landscapeImage = item.landscapeImagePhone ?? portraitImage - if landscapeImage?.renderingMode == .automatic { - landscapeImage = landscapeImage?.withRenderingMode(.alwaysTemplate) - } - - configuration?.image = traitCollection.verticalSizeClass == .regular ? portraitImage : landscapeImage - configuration?.title = item.title - - if let action = item.action { - addTarget(item.target, action: action, for: .touchUpInside) - } - - accessibilityIdentifier = item.accessibilityIdentifier - accessibilityLabel = item.accessibilityLabel - accessibilityHint = item.accessibilityHint - showsLargeContentViewer = true - - if let customLargeContentSizeImage = item.largeContentSizeImage { - largeContentImage = customLargeContentSizeImage - } - - if item.title == nil { - largeContentTitle = item.accessibilityLabel - } - - isPointerInteractionEnabled = true - } - - private func prepareButtonForBadgeLabelAndRedDot() { - 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 - } - - updateButtonForBadgeLabelOrRedDotView() - } - - private func updateButtonForBadgeLabelOrRedDotView() { - badgeLabel.text = item?.badgeValue - let isNilBadgeValue = item?.badgeValue == nil - badgeLabel.isHidden = isNilBadgeValue - redDotView.isHidden = (item?.shouldShowRedDot != true || !isNilBadgeValue) - - if isNilBadgeValue { - if item?.shouldShowRedDot == true { - showRedDot() - } else { - layer.mask = nil - } - } else { - badgeLabel.frame = badgeLabelFrame - addBadgeOrRedDot(false) - } - } - - 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 - - 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 { - return CGRect(x: frame.origin.x - Constants.badgeBorderWidth, - y: frame.origin.y - Constants.badgeBorderWidth, - width: frame.size.width + 2 * Constants.badgeBorderWidth, - height: frame.size.height + 2 * Constants.badgeBorderWidth) - } - - @objc private func badgeValueDidChange() { - updateButtonForBadgeLabelOrRedDotView() - updateAccessibilityLabel() - } - - @objc private func redDotVisibilitDidChanged() { - updateButtonForBadgeLabelOrRedDotView() - } - - @objc private func contentSizeCategoryDidChange(notification: Notification) { - guard let titleLabel = titleLabel else { - return - } - - let requestedContentSizeCategory = (notification.userInfo?[UIContentSizeCategory.newValueUserInfoKey] as? UIContentSizeCategory) ?? .unspecified - - let cappedContentSizeCategory: UIContentSizeCategory - if requestedContentSizeCategory > Constants.maximumContentSizeCategory { - cappedContentSizeCategory = Constants.maximumContentSizeCategory - } else if requestedContentSizeCategory < Constants.minimumContentSizeCategory { - cappedContentSizeCategory = Constants.minimumContentSizeCategory - } else { - cappedContentSizeCategory = requestedContentSizeCategory - } - - // For some reason, titleLabel doesn't resize to fit the new font size, so we do it ourselves. - titleLabel.font = fluentTheme.typography(.body1, contentSizeCategory: cappedContentSizeCategory) - titleLabel.sizeToFit() - sizeToFit() - if superview != nil { - centerInSuperview(horizontally: false, vertically: true) - } - } - - private func updateAccessibilityLabel() { - guard let item = item else { - return - } - if let badgeAccessibilityLabel = item.badgeAccessibilityLabel { - if let itemAccessibilityLabel = item.accessibilityLabel { - accessibilityLabel = String.localizedStringWithFormat("Accessibility.BadgeLabelButton.LabelFormat".localized, - itemAccessibilityLabel, - badgeAccessibilityLabel) - } else { - accessibilityLabel = badgeAccessibilityLabel - } - } else { - accessibilityLabel = item.accessibilityLabel - } - } + var item: UIBarButtonItem? { + didSet { + setupButton() + prepareButtonForBadge() + } + } + + var badgeLabelStyle: BadgeLabelStyle = .system { + didSet { + badgeLabel.style = badgeLabelStyle + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + 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, + object: item) + NotificationCenter.default.addObserver(self, + selector: #selector(contentSizeCategoryDidChange(notification:)), + name: UIContentSizeCategory.didChangeNotification, + object: nil) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + prepareButtonForBadge() + } + + private struct Constants { + static let badgeVerticalOffset: CGFloat = -5 + static let badgeHeight: CGFloat = 16 + static let badgeMinWidth: CGFloat = 16 + static let badgeMaxWidth: CGFloat = 27 + static let badgeBorderWidth: CGFloat = 2 + 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), + Constants.badgeMaxWidth) + } + + private var badgeVerticalPosition: CGFloat { + return (frame.size.height - intrinsicContentSize.height) / 2 - Constants.badgeHeight / 2 - Constants.badgeVerticalOffset + } + + 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 - viewWidth / 2) + } + + private var badgeLabelFrame: CGRect { + let targetView = isItemTitlePresent ? titleLabel : imageView + + return CGRect(x: badgeFrameOriginX(badgeWidth) - (targetView?.frame.origin.x ?? 0), + y: badgeVerticalPosition - (targetView?.frame.origin.y ?? 0), + width: badgeWidth, + height: Constants.badgeHeight) + } + + 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 - viewWidth / 2 + } + } + + private var isLeftToRightUserInterfaceLayoutDirection: Bool { + return effectiveUserInterfaceLayoutDirection == .leftToRight + } + + private var isItemTitlePresent: Bool { + return item?.title != nil + } + + private func setupButton() { + guard let item = item else { + return + } + + isEnabled = item.isEnabled + tag = item.tag + tintColor = item.tintColor + titleLabel?.font = item.titleTextAttributes(for: .normal)?[.font] as? UIFont + + var portraitImage = item.image + if portraitImage?.renderingMode == .automatic { + portraitImage = portraitImage?.withRenderingMode(.alwaysTemplate) + } + var landscapeImage = item.landscapeImagePhone ?? portraitImage + if landscapeImage?.renderingMode == .automatic { + landscapeImage = landscapeImage?.withRenderingMode(.alwaysTemplate) + } + + configuration?.image = traitCollection.verticalSizeClass == .regular ? portraitImage : landscapeImage + configuration?.title = item.title + + if let action = item.action { + addTarget(item.target, action: action, for: .touchUpInside) + } + + accessibilityIdentifier = item.accessibilityIdentifier + accessibilityLabel = item.accessibilityLabel + accessibilityHint = item.accessibilityHint + showsLargeContentViewer = true + + if let customLargeContentSizeImage = item.largeContentSizeImage { + largeContentImage = customLargeContentSizeImage + } + + if item.title == nil { + largeContentTitle = item.accessibilityLabel + } + + isPointerInteractionEnabled = true + } + + 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 + } + + updateBadge() + } + + private func updateBadge() { + badgeLabel.text = item?.badgeValue + let isNilBadgeValue = item?.badgeValue == nil + badgeLabel.isHidden = isNilBadgeValue + redDotView.isHidden = (item?.shouldShowRedDot != true || !isNilBadgeValue) + + if isNilBadgeValue { + if item?.shouldShowRedDot == true { + showRedDot() + } else { + layer.mask = nil + } + } else { + badgeLabel.frame = badgeLabelFrame + addBadgeOrRedDot(false) + } + } + + 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 + + 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 { + return CGRect(x: frame.origin.x - Constants.badgeBorderWidth, + y: frame.origin.y - Constants.badgeBorderWidth, + width: frame.size.width + 2 * Constants.badgeBorderWidth, + height: frame.size.height + 2 * Constants.badgeBorderWidth) + } + + @objc private func badgeValueDidChange() { + updateBadge() + updateAccessibilityLabel() + } + + @objc private func redDotVisibilitDidChanged() { + updateBadge() + } + + @objc private func contentSizeCategoryDidChange(notification: Notification) { + guard let titleLabel = titleLabel else { + return + } + + let requestedContentSizeCategory = (notification.userInfo?[UIContentSizeCategory.newValueUserInfoKey] as? UIContentSizeCategory) ?? .unspecified + + let cappedContentSizeCategory: UIContentSizeCategory + if requestedContentSizeCategory > Constants.maximumContentSizeCategory { + cappedContentSizeCategory = Constants.maximumContentSizeCategory + } else if requestedContentSizeCategory < Constants.minimumContentSizeCategory { + cappedContentSizeCategory = Constants.minimumContentSizeCategory + } else { + cappedContentSizeCategory = requestedContentSizeCategory + } + + // For some reason, titleLabel doesn't resize to fit the new font size, so we do it ourselves. + titleLabel.font = fluentTheme.typography(.body1, contentSizeCategory: cappedContentSizeCategory) + titleLabel.sizeToFit() + sizeToFit() + if superview != nil { + centerInSuperview(horizontally: false, vertically: true) + } + } + + private func updateAccessibilityLabel() { + guard let item = item else { + return + } + if let badgeAccessibilityLabel = item.badgeAccessibilityLabel { + if let itemAccessibilityLabel = item.accessibilityLabel { + accessibilityLabel = String.localizedStringWithFormat("Accessibility.BadgeLabelButton.LabelFormat".localized, + itemAccessibilityLabel, + badgeAccessibilityLabel) + } else { + accessibilityLabel = badgeAccessibilityLabel + } + } else { + accessibilityLabel = item.accessibilityLabel + } + } } From 5051a120b7d799c4ce2724e2c3f116efd8a69b1d Mon Sep 17 00:00:00 2001 From: kumarmohit5189 Date: Fri, 16 Feb 2024 14:04:52 +0530 Subject: [PATCH 3/3] fix tab issue in file --- .../NavigationControllerDemoController.swift | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift index 4f5d7605ce..5623e9b886 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift @@ -473,7 +473,7 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe var showSearchProgressSpinner: Bool = true var showRainbowRingForAvatar: Bool = false var showBadgeOnBarButtonItem: Bool = false - var showRedDotOnBarButtonItem: Bool = false + var showRedDotOnBarButtonItem: Bool = false var allowsCellSelection: Bool = false { didSet { @@ -624,19 +624,19 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe return cell } - 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 == 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 { @@ -763,15 +763,15 @@ 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 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