diff --git a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/CommandBarDemoController.swift b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/CommandBarDemoController.swift index 35da364fb..546f80c12 100644 --- a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/CommandBarDemoController.swift +++ b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/CommandBarDemoController.swift @@ -180,7 +180,7 @@ class CommandBarDemoController: DemoController { container.addArrangedSubview(createLabelWithText("Default")) - let commandBar = CommandBar(itemGroups: createItemGroups(), leadingItemGroups: [[newItem(for: .keyboard)]]) + let commandBar = CommandBar(itemGroups: createItemGroups(), leadingItemGroups: [[newItem(for: .keyboard)]], style: .glass) commandBar.delegate = self commandBar.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(commandBar) @@ -275,7 +275,7 @@ class CommandBarDemoController: DemoController { container.addArrangedSubview(createLabelWithText("With Fixed Button")) - let fixedButtonCommandBar = CommandBar(itemGroups: createItemGroups(), leadingItemGroups: [[newItem(for: .copy)]], trailingItemGroups: [[newItem(for: .keyboard)]]) + let fixedButtonCommandBar = CommandBar(itemGroups: createItemGroups(), leadingItemGroups: [[newItem(for: .copy)]], trailingItemGroups: [[newItem(for: .keyboard)]], style: .glass) fixedButtonCommandBar.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(fixedButtonCommandBar) @@ -293,7 +293,7 @@ class CommandBarDemoController: DemoController { container.addArrangedSubview(textFieldContainer) - let accessoryCommandBar = CommandBar(itemGroups: createItemGroups(), trailingItemGroups: [[newItem(for: .keyboard)]]) + let accessoryCommandBar = CommandBar(itemGroups: createItemGroups(), trailingItemGroups: [[newItem(for: .keyboard)]], style: .glass) accessoryCommandBar.translatesAutoresizingMaskIntoConstraints = false #if os(iOS) textField.inputAccessoryView = accessoryCommandBar diff --git a/Sources/FluentUI_iOS/Components/CommandBar/CommandBar.swift b/Sources/FluentUI_iOS/Components/CommandBar/CommandBar.swift index df41c96f2..3c2e48805 100644 --- a/Sources/FluentUI_iOS/Components/CommandBar/CommandBar.swift +++ b/Sources/FluentUI_iOS/Components/CommandBar/CommandBar.swift @@ -78,10 +78,21 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { trailingItemGroups: trailingItems) } + @objc public convenience init(itemGroups: [CommandBarItemGroup], + leadingItemGroups: [CommandBarItemGroup]? = nil, + trailingItemGroups: [CommandBarItemGroup]? = nil) { + self.init(itemGroups: itemGroups, + leadingItemGroups: leadingItemGroups, + trailingItemGroups: trailingItemGroups, + style: .primary) + } + @objc public init(itemGroups: [CommandBarItemGroup], leadingItemGroups: [CommandBarItemGroup]? = nil, - trailingItemGroups: [CommandBarItemGroup]? = nil) { - self.tokenSet = CommandBarTokenSet() + trailingItemGroups: [CommandBarItemGroup]? = nil, + style: CommandBarStyle) { + self.style = style + self.tokenSet = CommandBarTokenSet(style: { style }) leadingCommandGroupsView = CommandBarCommandGroupsView(itemGroups: leadingItemGroups, buttonsPersistSelection: false, @@ -152,13 +163,11 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { public override func layoutSubviews() { super.layoutSubviews() - let cornerRadius = bounds.height / 2 - layer.cornerRadius = cornerRadius - commandBarContainerStackView.layer.cornerRadius = cornerRadius commandBarContainerStackView.layoutIfNeeded() updateShadow() updateScrollViewShadow() + updateCornerRadius() } #if DEBUG @@ -191,6 +200,9 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { public typealias TokenSetKeyType = CommandBarTokenSet.Tokens public var tokenSet: CommandBarTokenSet + /// The visual style of the CommandBar. + public let style: CommandBarStyle + /// Items shown in the center of the CommandBar @objc public var itemGroups: [CommandBarItemGroup] { get { @@ -246,6 +258,8 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { // MARK: - Private properties + private var glassEffectView: UIVisualEffectView? + /// Container UIStackView that holds the leading, main and trailing views private var commandBarContainerStackView: UIStackView @@ -324,8 +338,6 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { leadingCommandGroupsView.isHidden = leadingCommandGroupsView.itemGroups.isEmpty trailingCommandGroupsView.isHidden = trailingCommandGroupsView.itemGroups.isEmpty - addSubview(commandBarContainerStackView) - commandBarContainerStackView.addArrangedSubview(leadingCommandGroupsView) commandBarContainerStackView.addArrangedSubview(containerView) commandBarContainerStackView.addArrangedSubview(trailingCommandGroupsView) @@ -334,11 +346,40 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { updateViewHierarchy() updateMainCommandGroupsViewConstraints() + let rootView: UIView + switch style { + case .primary: + rootView = commandBarContainerStackView + case .glass: + let effectView = UIVisualEffectView() + effectView.effect = UIBlurEffect(style: .systemMaterial) + effectView.layer.masksToBounds = true +#if !os(visionOS) + if #available(iOS 26, *) { + let glassEffect = UIGlassEffect(style: .regular) + glassEffect.tintColor = tokenSet[.backgroundColor].uiColor + effectView.effect = glassEffect + effectView.layer.masksToBounds = false + } +#endif + effectView.translatesAutoresizingMaskIntoConstraints = false + let contentView = effectView.contentView + contentView.addSubview(commandBarContainerStackView) + NSLayoutConstraint.activate([ + commandBarContainerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + commandBarContainerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + commandBarContainerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + commandBarContainerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + ]) + glassEffectView = effectView + rootView = effectView + } + addSubview(rootView) NSLayoutConstraint.activate([ - commandBarContainerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), - commandBarContainerStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), - commandBarContainerStackView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), - commandBarContainerStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor) + rootView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + rootView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + rootView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + rootView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor) ]) if UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .rightToLeft { @@ -424,12 +465,41 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { } private func updateShadow() { - let shadowInfo = tokenSet[.shadow].shadowInfo - shadowInfo.applyShadow(to: self) + switch style { + case .primary: + let shadowInfo = tokenSet[.shadow].shadowInfo + shadowInfo.applyShadow(to: self) + case .glass: +#if !os(visionOS) + if #unavailable(iOS 26) { + layer.shadowColor = CommandBarTokenSet.glassEffectShadowColor + layer.shadowOpacity = CommandBarTokenSet.glassEffectShadowOpacity + layer.shadowOffset = CommandBarTokenSet.glassEffectShadowOffset + layer.shadowRadius = CommandBarTokenSet.glassEffectShadowRadius + } +#else + layer.shadowColor = CommandBarTokenSet.glassEffectShadowColor + layer.shadowOpacity = CommandBarTokenSet.glassEffectShadowOpacity + layer.shadowOffset = CommandBarTokenSet.glassEffectShadowOffset + layer.shadowRadius = CommandBarTokenSet.glassEffectShadowRadius +#endif + } } private func updateBackgroundColor() { - backgroundColor = tokenSet[.backgroundColor].uiColor + switch style { + case .primary: + backgroundColor = tokenSet[.backgroundColor].uiColor + case .glass: + backgroundColor = .clear +#if !os(visionOS) + if #available(iOS 26, *), let glassEffectView { + let glassEffect = UIGlassEffect(style: .regular) + glassEffect.tintColor = tokenSet[.backgroundColor].uiColor + glassEffectView.effect = glassEffect + } +#endif + } } private func updateButtonTokens() { @@ -438,6 +508,20 @@ public class CommandBar: UIView, Shadowable, TokenizedControl { trailingCommandGroupsView.updateButtonsShown() } + private func updateCornerRadius() { + let cornerRadius = commandBarContainerStackView.bounds.height / 2 + layer.cornerRadius = cornerRadius + commandBarContainerStackView.layer.cornerRadius = cornerRadius + + if style == .glass, let glassEffectView { + if #available(iOS 26, visionOS 26, *) { + glassEffectView.cornerConfiguration = .corners(radius: UICornerRadius.fixed(cornerRadius)) + } else { + glassEffectView.layer.cornerRadius = cornerRadius + } + } + } + /// Updates the provided `CommandBarCommandGroupsView` with the `items` array and marks the view as needing a layout private func setupGroupsView(_ commandGroupsView: CommandBarCommandGroupsView, with items: [CommandBarItemGroup]?) { commandGroupsView.itemGroups = items ?? [] diff --git a/Sources/FluentUI_iOS/Components/CommandBar/CommandBarTokenSet.swift b/Sources/FluentUI_iOS/Components/CommandBar/CommandBarTokenSet.swift index 34c0ce99b..61deddc20 100644 --- a/Sources/FluentUI_iOS/Components/CommandBar/CommandBarTokenSet.swift +++ b/Sources/FluentUI_iOS/Components/CommandBar/CommandBarTokenSet.swift @@ -8,6 +8,15 @@ import FluentUI_common #endif import UIKit +@objc(MSFCommandBarStyle) +public enum CommandBarStyle: Int { + /// Default style — solid background color. + case primary + + /// Glass material background (UIGlassEffect on iOS 26+, UIBlurEffect on earlier). + case glass +} + public enum CommandBarToken: Int, TokenSetKey { /// The background color of the Command Bar. case backgroundColor @@ -57,11 +66,19 @@ public enum CommandBarToken: Int, TokenSetKey { /// Design token set for the `CommandBar` control. public class CommandBarTokenSet: ControlTokenSet { - init() { - super.init { token, theme in + init(style: @escaping () -> CommandBarStyle) { + self.style = style + super.init { [style] token, theme in switch token { case .backgroundColor: - return .uiColor { theme.color(.background2) } + return .uiColor { + switch style() { + case .primary: + return theme.color(.background2) + case .glass: + return .clear + } + } case .cornerRadius: return .float { GlobalTokens.corner(.radius120) } @@ -107,6 +124,16 @@ public class CommandBarTokenSet: ControlTokenSet { } } } + + var style: () -> CommandBarStyle +} + +// MARK: Constants +extension CommandBarTokenSet { + static let glassEffectShadowColor: CGColor = UIColor.black.cgColor + static let glassEffectShadowOpacity: Float = 0.25 + static let glassEffectShadowOffset: CGSize = CGSize(width: 0, height: 2) + static let glassEffectShadowRadius: CGFloat = 8 } // MARK: - Constants