Skip to content

Commit

Permalink
[iOS] Support gradient colors for selected item in TabBarView (#2107)
Browse files Browse the repository at this point in the history
* Adding an API for gradient highlights on `TabBar`

* Adding additional comments

* Delete old commented test code
  • Loading branch information
mischreiber authored Nov 14, 2024
1 parent 0e6e9a1 commit 8b6ed6d
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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()

Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
53 changes: 45 additions & 8 deletions Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -266,20 +277,46 @@ 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() {
// Normally we'd set imageView.image and imageView.highlightedImage separately. However, there's a known issue with
// 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)
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit 8b6ed6d

Please sign in to comment.