From 8b6ed6d8d571887d29e737a212b5ea3ca592194f Mon Sep 17 00:00:00 2001 From: Mike Schreiber Date: Thu, 14 Nov 2024 14:45:36 -0800 Subject: [PATCH] [iOS] Support gradient colors for selected item in `TabBarView` (#2107) * Adding an API for gradient highlights on `TabBar` * Adding additional comments * Delete old commented test code --- .../Demos/TabBarViewDemoController.swift | 22 ++++++++ .../Components/Tab Bar/TabBarItemView.swift | 53 ++++++++++++++++--- .../Components/Tab Bar/TabBarView.swift | 11 ++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/TabBarViewDemoController.swift b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/TabBarViewDemoController.swift index 025d5afad4..8c0db196c9 100644 --- a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/TabBarViewDemoController.swift +++ b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/TabBarViewDemoController.swift @@ -19,10 +19,12 @@ class TabBarViewDemoController: DemoController { private var showsItemTitles: Bool { return itemTitleVisibilitySwitch.isOn } private var showBadgeNumbers: Bool { return showBadgeNumbersSwitch.isOn } private var useHigherBadgeNumbers: Bool { return useHigherBadgeNumbersSwitch.isOn } + private var useGradientSelection: Bool { return useGradientSelectionSwitch.isOn } private let itemTitleVisibilitySwitch = BrandedSwitch() private let showBadgeNumbersSwitch = BrandedSwitch() private let useHigherBadgeNumbersSwitch = BrandedSwitch() + private let useGradientSelectionSwitch = BrandedSwitch() private lazy var incrementBadgeButton: Button = { return createButton(title: "+", action: #selector(incrementBadgeNumbers)) @@ -37,6 +39,19 @@ class TabBarViewDemoController: DemoController { private var badgeNumbers: [UInt] = Constants.initialBadgeNumbers private var higherBadgeNumbers: [UInt] = Constants.initialHigherBadgeNumbers + private lazy var gradient: CAGradientLayer = { + let gradientColors = [ + UIColor.red.cgColor, + UIColor.green.cgColor + ] + let colorfulGradient = CAGradientLayer() + colorfulGradient.colors = gradientColors + colorfulGradient.startPoint = CGPoint(x: 0.0, y: 0.0) + colorfulGradient.endPoint = CGPoint(x: 1.0, y: 1.0) + colorfulGradient.type = .axial + return colorfulGradient + }() + override func viewDidLoad() { super.viewDidLoad() @@ -55,6 +70,9 @@ class TabBarViewDemoController: DemoController { addRow(text: "Use higher badge numbers", items: [useHigherBadgeNumbersSwitch], textWidth: Constants.switchSettingTextWidth) useHigherBadgeNumbersSwitch.addTarget(self, action: #selector(handleOnSwitchValueChanged), for: .valueChanged) + addRow(text: "Use gradient selection", items: [useGradientSelectionSwitch], textWidth: Constants.switchSettingTextWidth) + useGradientSelectionSwitch.addTarget(self, action: #selector(handleOnSwitchValueChanged), for: .valueChanged) + addRow(text: "Modify badge numbers", items: [incrementBadgeButton, decrementBadgeButton], textWidth: Constants.buttonSettingTextWidth) setupTabBarView() @@ -94,6 +112,10 @@ class TabBarViewDemoController: DemoController { // If the open file item has been clicked, maintain that state through to the new item updatedTabBarView.items[2].isUnreadDotVisible = isOpenFileUnread + if useGradientSelection { + updatedTabBarView.selectedItemGradient = gradient + } + updatedTabBarView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(updatedTabBarView) diff --git a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift index 7a28fbffcd..ae9ce6b954 100644 --- a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift +++ b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift @@ -85,6 +85,15 @@ class TabBarItemView: UIControl, TokenizedControl { } } + /// The main gradient layer to be applied to the TabBarItemView with the gradient style. + var gradient: CAGradientLayer? { + didSet { + if oldValue != gradient { + updateColors() + } + } + } + init(item: TabBarItem, showsTitle: Bool, canResizeImage: Bool = true) { self.canResizeImage = canResizeImage self.item = item @@ -180,8 +189,10 @@ class TabBarItemView: UIControl, TokenizedControl { override func didMoveToWindow() { super.didMoveToWindow() - tokenSet.update(fluentTheme) - updateAppearance() + if window != nil { + tokenSet.update(fluentTheme) + updateAppearance() + } } private var badgeValue: String? { @@ -266,12 +277,38 @@ class TabBarItemView: UIControl, TokenizedControl { return alwaysShowTitleBelowImage || (traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular) } - private func updateColors() { - let selectedColor = tokenSet[.selectedColor].uiColor - let disabledColor = tokenSet[.disabledColor].uiColor + private var selectedImage: UIImage? { + let selectedImage = item.selectedImage(isInPortraitMode: isInPortraitMode, labelIsHidden: titleLabel.isHidden) + guard let gradient else { + return selectedImage + } + + // This is necessary because imageView.tintColor does not work with UIColor(patternImage:). + let mask = CALayer() + mask.contents = selectedImage?.cgImage + mask.frame = imageView.bounds + gradient.frame = imageView.bounds + gradient.mask = mask + let renderer = UIGraphicsImageRenderer(bounds: imageView.bounds) + let gradientImage = renderer.image { rendererContext in + gradient.render(in: rendererContext.cgContext) + } + return gradientImage + } - titleLabel.textColor = isEnabled ? (isSelected ? selectedColor : tokenSet[.unselectedTextColor].uiColor) : disabledColor - imageView.tintColor = isEnabled ? (isSelected ? selectedColor : tokenSet[.unselectedImageColor].uiColor) : disabledColor + private func updateColors() { + if isEnabled { + // We cannot use UIColor(patternImage:) for the tintColor of a UIView. Instead, we have to + // fully replace the image, so we should not re-tint it here when we have a gradient. + let shouldTint = isSelected && gradient == nil + let tintColor = tokenSet[.selectedColor].uiColor + titleLabel.textColor = shouldTint ? tintColor : tokenSet[.unselectedTextColor].uiColor + imageView.tintColor = shouldTint ? tintColor : tokenSet[.unselectedImageColor].uiColor + } else { + let disabledColor = tokenSet[.disabledColor].uiColor + titleLabel.textColor = disabledColor + imageView.tintColor = disabledColor + } } private func updateImage() { @@ -279,7 +316,7 @@ class TabBarItemView: UIControl, TokenizedControl { // UIImageView in iOS 16 where highlighted images lose their tint color in certain scenarios. While we wait for a fix, // this is a straightforward workaround that gets us the same effect without triggering the bug. imageView.image = isSelected ? - item.selectedImage(isInPortraitMode: isInPortraitMode, labelIsHidden: titleLabel.isHidden) : + selectedImage : item.unselectedImage(isInPortraitMode: isInPortraitMode, labelIsHidden: titleLabel.isHidden) } diff --git a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift index 6688e2eb06..7dd1093d82 100644 --- a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift +++ b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift @@ -64,6 +64,13 @@ open class TabBarView: UIView, TokenizedControl { } } } + + /// An optional gradient to display for the selected item. + @objc public var selectedItemGradient: CAGradientLayer? { + didSet { + updateAppearance() + } + } @objc public weak var delegate: TabBarViewDelegate? @@ -210,6 +217,10 @@ open class TabBarView: UIView, TokenizedControl { forToken: .titleLabelFontPortrait) tabBarItemTokenSet.setOverrideValue(tokenSet.overrideValue(forToken: .tabBarItemTitleLabelFontLandscape), forToken: .titleLabelFontLandscape) + + if let selectedItemGradient { + tabBarItemView.gradient = selectedItemGradient + } } } topBorderLine.tokenSet[.color] = tokenSet[.separatorColor]