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

[iOS] Support gradient colors for selected item in TabBarView #2107

Merged
merged 3 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -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,35 @@ class TabBarViewDemoController: DemoController {
private var badgeNumbers: [UInt] = Constants.initialBadgeNumbers
private var higherBadgeNumbers: [UInt] = Constants.initialHigherBadgeNumbers

private lazy var gradient: CAGradientLayer = {
// let gradientColors = [
// UIColor(red: 0.45, green: 0.29, blue: 0.79, alpha: 1).cgColor,
// UIColor(red: 0.18, green: 0.45, blue: 0.96, alpha: 1).cgColor,
// UIColor(red: 0.36, green: 0.80, blue: 0.98, alpha: 1).cgColor,
// UIColor(red: 0.45, green: 0.72, blue: 0.22, alpha: 1).cgColor,
// UIColor(red: 0.97, green: 0.78, blue: 0.27, alpha: 1).cgColor,
// UIColor(red: 0.94, green: 0.52, blue: 0.20, alpha: 1).cgColor,
// UIColor(red: 0.92, green: 0.26, blue: 0.16, alpha: 1).cgColor,
// UIColor(red: 0.45, green: 0.29, blue: 0.79, alpha: 1).cgColor]
//
// let colorfulGradient = CAGradientLayer()
// colorfulGradient.colors = gradientColors
// colorfulGradient.startPoint = CGPoint(x: 0.5, y: 0.5)
// colorfulGradient.endPoint = CGPoint(x: 0.5, y: 0)
// colorfulGradient.type = .conic
// return colorfulGradient
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 +86,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 +128,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
Loading