From 9dd66699399284a2bc5bd2782585a3585bb929e6 Mon Sep 17 00:00:00 2001 From: James Edwards Date: Mon, 21 Nov 2022 14:09:16 -0800 Subject: [PATCH] [Fluent 2 Tokens] Merge main (#1384) * Drop support for iOS 14 and remove availability checks (#1357) * Update FluentUI project * Update FluentUI xcconfig * Update FluentUI.Demo project * Update MicrosoftFluentUI.podspec * Clean up Button * Clean up CommandBarButton * Clean up CommandBarButtonGroupView * Clean up LargeContentViewerModifier * Clean up Drawer * Clean up NavigationBar * Clean up BadgeLabelButton * Clean up PillButton * Clean up PillButtonBar * Clean up SegmentedPillButton * Clean up SideTabBar * Clean up TabBarView * Clean up PillButton v2 * Clean up PillButton v3 * Revert "Update FluentUI project" This reverts commit 1c97631bcd56b3b040492868a38aa0b570d13189. * Update README.md * Update Package.swift * Make ShimmerLinesView inherit ShimmerView init (#1362) * Make ShimmerLinesView inherit ShimmerView init * move shimmer to fluent2 and add new api to demo * Bring fluent2-tokens Segmented Control to main (#1363) * Bring in fluent2-token control * Add token set to project * Update dependent controls * Add colored background for demo * Update demos * Update demo order * Fix whitespace error * Add demo to show notification bar from top (#1364) * Update PR Template to include binary change (#1368) * Fix PersonaListViewDemoController navigation bar transparency bug (#1372) * Change from DemoController to UIViewController * Revert "Change from DemoController to UIViewController" This reverts commit 3b3aeb0dc1469067f3849e95c3d3b79d00c913b1. * Fix bug * Remove retain cycles (#1374) * update xcode version to 14.1 for ci (#1371) (#1378) * Implement the Unread Dot for TabBar and SideTabBar (#1349) * Implementing the unread dot on TabBarView * Better naming for variables and events * Updating SideTabBar demo to show the unread dot * Pr feedback - reorganizing, renaming, better documentation * Updating variable names and locations to follow guidance * Added accessibility string for unread dot * Switching to using a badge label for the unread dot * Cleaning up commented out code * Simplifying logic, using a single view for both the badge and the unreadDot * PR cleanup * Add isViewLoaded check (#1379) * Revert "Drop support for iOS 14 and remove availability checks (#1357)" (#1383) * Revert "Drop support for iOS 14 and remove availability checks (#1357)" This reverts commit 4fad398e5107420e4a67e8646373a23f35ffb260. * Fix whitespace error * Resolving merge conflicts * Reverting Package.resolved to main * Whitespace in Package.resolved Co-authored-by: huwilkes <67026548+huwilkes@users.noreply.github.com> Co-authored-by: Jeanie Huynh <31874971+jeaniehuynh@users.noreply.github.com> Co-authored-by: Lamine Male <106181067+laminesm@users.noreply.github.com> Co-authored-by: Mike Schreiber Co-authored-by: Harrie Shin --- .github/PULL_REQUEST_TEMPLATE.md | 11 ++ .../FluentUI.Demo.xcodeproj/project.pbxproj | 4 + .../NotificationViewDemoController.swift | 12 ++ .../Demos/PersonaListViewDemoController.swift | 1 + .../Demos/PillButtonBarDemoController.swift | 1 + .../ShimmerLinesViewDemoController.swift | 9 ++ .../Demos/SideTabBarDemoController.swift | 3 + .../Demos/TabBarViewDemoController.swift | 5 + ios/FluentUI.xcodeproj/project.pbxproj | 4 + .../Localization/en.lproj/Localizable.strings | 3 + .../SegmentedControl/SegmentedControl.swift | 4 +- ios/FluentUI/Shimmer/ShimmerLinesView.swift | 60 +++----- ios/FluentUI/Shimmer/ShimmerView.swift | 4 +- ios/FluentUI/Tab Bar/TabBarItem.swift | 14 ++ ios/FluentUI/Tab Bar/TabBarItemTokenSet.swift | 24 +++ ios/FluentUI/Tab Bar/TabBarItemView.swift | 142 +++++++++++++----- ios/FluentUI/Table View/TableViewCell.swift | 4 +- scripts/xcode_select_current_version.sh | 2 +- 18 files changed, 222 insertions(+), 85 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a9c99fff92..2e6b7743db 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,17 @@ ### Description of changes +Binary change: + +| Before | After | +|--------|-------| +| | | + (a summary of the changes made, often organized by file) ### Verification diff --git a/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj b/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj index 9412c17b5d..4ba6c233fb 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj +++ b/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ E6842996247C350700A29C40 /* DemoColorThemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6842995247C350700A29C40 /* DemoColorThemes.swift */; }; EC02A5F9274EED9200E81B3E /* DividerDemoController_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC02A5F8274EED9200E81B3E /* DividerDemoController_SwiftUI.swift */; }; EC24DBC628B97EB70026EF92 /* PopupMenuObjCDemoController.m in Sources */ = {isa = PBXBuildFile; fileRef = EC24DBC528B97EB70026EF92 /* PopupMenuObjCDemoController.m */; }; + EC1C31752923032000CF052C /* ColoredPillBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1C31742923032000CF052C /* ColoredPillBackgroundView.swift */; }; EC6A71EC273DE6DF0076A586 /* DividerDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6A71EB273DE6DF0076A586 /* DividerDemoController.swift */; }; ECCABFB227DFFA070037C70C /* ColoredPillBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECCABFB127DFFA070037C70C /* ColoredPillBackgroundView.swift */; }; FC414E3725888BC300069E73 /* CommandBarDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC414E3625888BC300069E73 /* CommandBarDemoController.swift */; }; @@ -152,6 +153,7 @@ E6842973247B672000A29C40 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; E6842995247C350700A29C40 /* DemoColorThemes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoColorThemes.swift; sourceTree = ""; }; EC02A5F8274EED9200E81B3E /* DividerDemoController_SwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DividerDemoController_SwiftUI.swift; sourceTree = ""; }; + EC1C31742923032000CF052C /* ColoredPillBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredPillBackgroundView.swift; sourceTree = ""; }; EC24DBC428B97E950026EF92 /* PopupMenuObjCDemoController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PopupMenuObjCDemoController.h; sourceTree = ""; }; EC24DBC528B97EB70026EF92 /* PopupMenuObjCDemoController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PopupMenuObjCDemoController.m; sourceTree = ""; }; EC6A71EB273DE6DF0076A586 /* DividerDemoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DividerDemoController.swift; sourceTree = ""; }; @@ -388,6 +390,7 @@ B4414793228F6FDF0040E88E /* OtherCellsSampleData.swift */, B4EF66552295F729007FEAB0 /* TableViewHeaderFooterSampleData.swift */, B4C39551227A4AC800539EC2 /* TableViewSampleData.swift */, + EC1C31742923032000CF052C /* ColoredPillBackgroundView.swift */, ); name = Utilities; sourceTree = ""; @@ -563,6 +566,7 @@ 92DD1E8D279F496300FDEE0F /* DemoAppearanceView.swift in Sources */, 53097D4027028AE500A6E4DC /* PersonaButtonCarouselDemoController.swift in Sources */, C0938E4A235F733100256251 /* ShimmerLinesViewDemoController.swift in Sources */, + EC1C31752923032000CF052C /* ColoredPillBackgroundView.swift in Sources */, 80AECC0C2630F1BB005AF2F3 /* BottomCommandingDemoController.swift in Sources */, C038992E2359307D00265026 /* TableViewCellShimmerDemoController.swift in Sources */, 53097D3D27028AD800A6E4DC /* NavigationControllerDemoController.swift in Sources */, diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController.swift index 9539a0f32a..e503c5649a 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController.swift @@ -20,6 +20,7 @@ class NotificationViewDemoController: DemoController { case persistentBarWithCancel case primaryToastWithStrikethroughAttribute case neutralBarWithFontAttribute + case neutralBarFromTop case neutralToastWithOverriddenTokens case neutralToastWithGradientBackground case warningToastWithFlexibleWidth @@ -50,6 +51,8 @@ class NotificationViewDemoController: DemoController { return "Primary Toast with Strikethrough Attribute" case .neutralBarWithFontAttribute: return "Neutral Bar with Font Attribute" + case .neutralBarFromTop: + return "Neutral Bar Presented from Top" case .neutralToastWithOverriddenTokens: return "Neutral Toast With Overridden Tokens" case .neutralToastWithGradientBackground: @@ -200,6 +203,15 @@ class NotificationViewDemoController: DemoController { notification.hide() } return notification + case .neutralBarFromTop: + let notification = MSFNotification(style: .neutralBar) + notification.state.message = "This is a bar presented from the top." + notification.state.showFromBottom = false + notification.state.actionButtonAction = { [weak self] in + self?.showMessage("`Dismiss` tapped") + notification.hide() + } + return notification case .neutralToastWithOverriddenTokens: let notification = MSFNotification(style: .neutralToast) notification.state.message = "The image color and spacing between the elements of this notification have been customized with override tokens." diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/PersonaListViewDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/PersonaListViewDemoController.swift index 69f5a7d9a1..fe3466362b 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/PersonaListViewDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/PersonaListViewDemoController.swift @@ -57,6 +57,7 @@ class PersonaListViewDemoController: DemoController { alert.addAction(action) self.present(alert, animated: true) } + scrollingContainer.removeFromSuperview() view.addSubview(personaListView) personaListView.frame = view.bounds personaListView.autoresizingMask = [.flexibleWidth, .flexibleHeight] diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/PillButtonBarDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/PillButtonBarDemoController.swift index 2d873562bc..98bba13774 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/PillButtonBarDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/PillButtonBarDemoController.swift @@ -103,6 +103,7 @@ class PillButtonBarDemoController: DemoController { return .brand } }() + let backgroundView = ColoredPillBackgroundView(style: backgroundStyle) backgroundView.addSubview(bar) let margins = UIEdgeInsets(top: 16.0, left: 0, bottom: 16.0, right: 0.0) diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/ShimmerLinesViewDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/ShimmerLinesViewDemoController.swift index 114f7a8ddb..0d2669517d 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/ShimmerLinesViewDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/ShimmerLinesViewDemoController.swift @@ -69,6 +69,15 @@ class ShimmerViewDemoController: DemoController { container.addArrangedSubview(ShimmerLinesView()) container.addArrangedSubview(dividers[1]) + container.addArrangedSubview(shimmerViewLabel("The middle lines of ShimmerLinesView are always at 100% width")) + container.addArrangedSubview(dividers[0]) + let shimmerLinesView = ShimmerLinesView(containerView: nil, + excludedViews: [], + animationSynchronizer: nil) + shimmerLinesView.lineCount = 6 + container.addArrangedSubview(shimmerLinesView) + container.addArrangedSubview(dividers[1]) + container.addArrangedSubview(shimmerViewLabel("ShimmerView shimmers all the top level subviews of its container view")) container.addArrangedSubview(dividers[2]) container.addArrangedSubview(shimmeringContentView(false)) diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/SideTabBarDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/SideTabBarDemoController.swift index d1662b80f0..90798d685f 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/SideTabBarDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/SideTabBarDemoController.swift @@ -91,6 +91,9 @@ class SideTabBarDemoController: DemoController { TabBarItem(title: "Open", image: UIImage(named: "Open_28")!, selectedImage: UIImage(named: "Open_Selected_28")!) ] + // Set the Open item to be unread + sideTabBar.topItems[2].isUnreadDotVisible = true + var premiumImage = UIImage(named: "ic_fluent_premium_24_regular")! if let window = view.window { let primaryColor = Colors.primary(for: window) diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/TabBarViewDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/TabBarViewDemoController.swift index 2a464e1819..5e4f3e4745 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/TabBarViewDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/TabBarViewDemoController.swift @@ -93,7 +93,9 @@ class TabBarViewDemoController: DemoController { private func setupTabBarView() { // remove the old tab bar View + var isOpenFileUnread = true if let oldTabBarView = tabBarView { + isOpenFileUnread = oldTabBarView.items[2].isUnreadDotVisible if let constraints = tabBarViewConstraints { NSLayoutConstraint.deactivate(constraints) } @@ -119,6 +121,9 @@ class TabBarViewDemoController: DemoController { ] } + // If the open file item has been clicked, maintain that state through to the new item + updatedTabBarView.items[2].isUnreadDotVisible = isOpenFileUnread + updatedTabBarView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(updatedTabBarView) diff --git a/ios/FluentUI.xcodeproj/project.pbxproj b/ios/FluentUI.xcodeproj/project.pbxproj index dbf6b3eba7..525d8daec3 100644 --- a/ios/FluentUI.xcodeproj/project.pbxproj +++ b/ios/FluentUI.xcodeproj/project.pbxproj @@ -220,6 +220,7 @@ EC24DBC828BD95980026EF92 /* PopupMenuTokenSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC24DBC728BD95980026EF92 /* PopupMenuTokenSet.swift */; }; EC24DBCA28BD99860026EF92 /* PopupMenuItemTokenSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC24DBC928BD99860026EF92 /* PopupMenuItemTokenSet.swift */; }; EC27B8662807A63C00A40B9A /* ResizingHandleTokenSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC27B8652807A63C00A40B9A /* ResizingHandleTokenSet.swift */; }; + EC1C31732923022E00CF052C /* SegmentedControlTokenSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1C31722923022E00CF052C /* SegmentedControlTokenSet.swift */; }; EC5982D827BF348700FD048D /* MSFAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5982D727BF348700FD048D /* MSFAvatar.swift */; }; EC5982DA27C703EE00FD048D /* CircleCutout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5982D927C703ED00FD048D /* CircleCutout.swift */; }; EC5982DC27CEC02100FD048D /* DrawerTokenSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5982DB27CEC02100FD048D /* DrawerTokenSet.swift */; }; @@ -430,6 +431,7 @@ EC24DBC728BD95980026EF92 /* PopupMenuTokenSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupMenuTokenSet.swift; sourceTree = ""; }; EC24DBC928BD99860026EF92 /* PopupMenuItemTokenSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupMenuItemTokenSet.swift; sourceTree = ""; }; EC27B8652807A63C00A40B9A /* ResizingHandleTokenSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizingHandleTokenSet.swift; sourceTree = ""; }; + EC1C31722923022E00CF052C /* SegmentedControlTokenSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlTokenSet.swift; sourceTree = ""; }; EC5982D727BF348700FD048D /* MSFAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSFAvatar.swift; sourceTree = ""; }; EC5982D927C703ED00FD048D /* CircleCutout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleCutout.swift; sourceTree = ""; }; EC5982DB27CEC02100FD048D /* DrawerTokenSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerTokenSet.swift; sourceTree = ""; }; @@ -1189,6 +1191,7 @@ EC5982DD27D2EA6100FD048D /* SegmentedControlTokenSet.swift */, C708B04B260A8696007190FA /* SegmentItem.swift */, C708B055260A86FA007190FA /* SegmentPillButton.swift */, + EC1C31722923022E00CF052C /* SegmentedControlTokenSet.swift */, ); path = SegmentedControl; sourceTree = ""; @@ -1735,6 +1738,7 @@ 5328D97326FBA3D700F3723B /* IndeterminateProgressBarModifiers.swift in Sources */, 923DB9D7274CB66D00D8E58A /* ControlHostingView.swift in Sources */, 5314E03B25F00E3D0099271A /* BadgeStringExtractor.swift in Sources */, + EC1C31732923022E00CF052C /* SegmentedControlTokenSet.swift in Sources */, 5314E19825F019650099271A /* SideTabBar.swift in Sources */, 5314E10A25F014600099271A /* Obscurable.swift in Sources */, 4B14B18F28F8DBE40099D52A /* TooltipTokenSet.swift in Sources */, diff --git a/ios/FluentUI/Resources/Localization/en.lproj/Localizable.strings b/ios/FluentUI/Resources/Localization/en.lproj/Localizable.strings index 9b1ac72b18..922db0151b 100644 --- a/ios/FluentUI/Resources/Localization/en.lproj/Localizable.strings +++ b/ios/FluentUI/Resources/Localization/en.lproj/Localizable.strings @@ -122,6 +122,9 @@ /* Format string for tab bar item accessbility labels. Format: ", <BadgeValue> items". Example: "Home, 5 items" */ "Accessibility.TabBarItemView.LabelFormat" = "%@, %@ items"; +/* Accessibility hint for TabBarItem in TabBarItemView. Indicates whether the item is unread or not. Format: "<Title>, unread". Example: "Files, unread" */ +"Accessibility.TabBarItemView.UnreadFormat" = "%@, unread"; + /* Format string for badge label button accessibility label. Format: "<Item Accessibility>, <Badge Label Accessibility>". Example: "Notifications, 5 new notifications" */ "Accessibility.BadgeLabelButton.LabelFormat" = "%@, %@"; diff --git a/ios/FluentUI/SegmentedControl/SegmentedControl.swift b/ios/FluentUI/SegmentedControl/SegmentedControl.swift index 678c7cfc9f..741b840ef5 100644 --- a/ios/FluentUI/SegmentedControl/SegmentedControl.swift +++ b/ios/FluentUI/SegmentedControl/SegmentedControl.swift @@ -415,7 +415,9 @@ open class SegmentedControl: UIView, TokenizedControlInternal { } public typealias TokenSetKeyType = SegmentedControlTokenSet.Tokens - lazy public var tokenSet: SegmentedControlTokenSet = .init(style: { self.style }) + lazy public var tokenSet: SegmentedControlTokenSet = .init(style: { [weak self] in + return self?.style ?? .primaryPill + }) var tokenSetSink: AnyCancellable? var selectionChangeAnimationDuration: TimeInterval { return 0.2 } diff --git a/ios/FluentUI/Shimmer/ShimmerLinesView.swift b/ios/FluentUI/Shimmer/ShimmerLinesView.swift index f1ae5ff54b..fd8740bfad 100644 --- a/ios/FluentUI/Shimmer/ShimmerLinesView.swift +++ b/ios/FluentUI/Shimmer/ShimmerLinesView.swift @@ -11,6 +11,26 @@ import UIKit @objc(MSFShimmerLinesView) open class ShimmerLinesView: ShimmerView { + /// Number of lines that will shimmer in this view. Use 0 if the number of lines should fill the available space. + @objc open var lineCount: Int = 3 { + didSet { + setNeedsLayout() + } + } + /// The percent the first line (if 2+ lines) should fill the available horizontal space + @objc open var firstLineFillPercent: CGFloat = 0.94 { + didSet { + setNeedsLayout() + } + } + + /// The percent the last line should fill the available horizontal space. + @objc open var lastLineFillPercent: CGFloat = 0.6 { + didSet { + setNeedsLayout() + } + } + open override func layoutSubviews() { super.layoutSubviews() @@ -18,16 +38,8 @@ open class ShimmerLinesView: ShimmerView { for (index, linelayer) in viewCoverLayers.enumerated() { let fillPercent: CGFloat = { if index == 0 && viewCoverLayers.count > 2 { - guard let firstLineFillPercent = firstLineFillPercent else { - return 1 - } - return firstLineFillPercent } else if index == viewCoverLayers.count - 1 { - guard let lastLineFillPercent = lastLineFillPercent else { - return 1 - } - return lastLineFillPercent } else { return 1 @@ -57,34 +69,6 @@ open class ShimmerLinesView: ShimmerView { return CGSize(width: UIView.noIntrinsicMetric, height: sizeThatFits(CGSize(width: frame.width, height: .infinity)).height) } - /// Creates the ShimmerLinesView. - /// - Parameters: - /// - lineCount: Number of lines that will shimmer in this view. Use 0 if the number of lines should fill the available space. - @objc public init(lineCount: Int) { - self.lineCount = lineCount - - super.init() - } - - /// Creates the ShimmerLinesView. - /// - Parameters: - /// - lineCount: Number of lines that will shimmer in this view. Use 0 if the number of lines should fill the available space. - /// - firstLineFillPercent: The percent the first line (if 2+ lines) should fill the available horizontal space. - /// - lastLineFillPercent: The percent the last line should fill the available horizontal space. - @objc public convenience init(lineCount: Int = 3, - firstLineFillPercent: CGFloat = 0.94, - lastLineFillPercent: CGFloat = 0.6) { - self.init(lineCount: lineCount) - - self.firstLineFillPercent = firstLineFillPercent - self.lastLineFillPercent = lastLineFillPercent - } - - @available(*, unavailable) - required public init?(coder: NSCoder) { - preconditionFailure("init(coder:) has not been implemented") - } - override func updateViewCoverLayers() { var newLineLayers = [CALayer]() let desiredLineCount = lineCount(for: frame.height) @@ -114,8 +98,4 @@ open class ShimmerLinesView: ShimmerView { return lineCount } } - - private var lineCount: Int - private var firstLineFillPercent: CGFloat? - private var lastLineFillPercent: CGFloat? } diff --git a/ios/FluentUI/Shimmer/ShimmerView.swift b/ios/FluentUI/Shimmer/ShimmerView.swift index f399f47706..7a5cac79a0 100644 --- a/ios/FluentUI/Shimmer/ShimmerView.swift +++ b/ios/FluentUI/Shimmer/ShimmerView.swift @@ -75,7 +75,9 @@ open class ShimmerView: UIView, TokenizedControlInternal { // MARK: - TokenizedControl public typealias TokenSetKeyType = ShimmerTokenSet.Tokens - public lazy var tokenSet: ShimmerTokenSet = .init(style: { self.style }) + public lazy var tokenSet: ShimmerTokenSet = .init(style: { [weak self] in + return self?.style ?? .concealing + }) var tokenSetSink: AnyCancellable? diff --git a/ios/FluentUI/Tab Bar/TabBarItem.swift b/ios/FluentUI/Tab Bar/TabBarItem.swift index 55f068469d..6c855b8aad 100644 --- a/ios/FluentUI/Tab Bar/TabBarItem.swift +++ b/ios/FluentUI/Tab Bar/TabBarItem.swift @@ -19,6 +19,17 @@ open class TabBarItem: NSObject { } } + /// This value will determine whether or not to show the mark that represents the "unread" state. + /// If the badgeValue is set, the unreadDot will not be visible. + /// The default value of this property is false. + @objc public var isUnreadDotVisible: Bool = false { + didSet { + if oldValue != isUnreadDotVisible { + NotificationCenter.default.post(name: TabBarItem.isUnreadValueDidChangeNotification, object: self) + } + } + } + /// Convenience method to set the badge value to a number. /// If the number is zero, the badge value will be hidden. @objc public func setBadgeNumber(_ number: UInt) { @@ -90,6 +101,9 @@ open class TabBarItem: NSObject { /// Notification sent when the tab bar item's badge value changes. static let badgeValueDidChangeNotification = NSNotification.Name(rawValue: "TabBarItemBadgeValueDidChangeNotification") + /// Notification sent when item's `isUnread` value changes. + static let isUnreadValueDidChangeNotification = NSNotification.Name(rawValue: "TabBarItemisUnreadValueDidChangeNotification") + let image: UIImage let selectedImage: UIImage? let landscapeImage: UIImage? diff --git a/ios/FluentUI/Tab Bar/TabBarItemTokenSet.swift b/ios/FluentUI/Tab Bar/TabBarItemTokenSet.swift index 3265401fba..e0d86e526d 100644 --- a/ios/FluentUI/Tab Bar/TabBarItemTokenSet.swift +++ b/ios/FluentUI/Tab Bar/TabBarItemTokenSet.swift @@ -59,6 +59,18 @@ class TabBarItemTokenSet: ControlTokenSet<TabBarItemTokenSet.Tokens> { /// The radii of the four corners of the `BadgeLabel` case badgeCornerRadii + /// The X offset of the unread dot in portrait mode + case unreadDotPortraitOffsetX + + /// The X offset of the unread dot + case unreadDotOffsetX + + /// The Y offset of the unread dot + case unreadDotOffsetY + + /// The size of the unread dot + case unreadDotSize + /// Font info for the title label when in portrait view case titleLabelFontPortrait @@ -127,6 +139,18 @@ class TabBarItemTokenSet: ControlTokenSet<TabBarItemTokenSet.Tokens> { case .badgeCornerRadii: return .float { 10.0 } + case .unreadDotPortraitOffsetX: + return .float { 6.0 } + + case .unreadDotOffsetX: + return .float { 4.0 } + + case .unreadDotOffsetY: + return .float { 20.0 } + + case .unreadDotSize: + return .float { 8.0 } + case .titleLabelFontPortrait: return .fontInfo { return .init(size: 10, weight: .medium) } diff --git a/ios/FluentUI/Tab Bar/TabBarItemView.swift b/ios/FluentUI/Tab Bar/TabBarItemView.swift index ef8f1b4913..0c3b980de1 100644 --- a/ios/FluentUI/Tab Bar/TabBarItemView.swift +++ b/ios/FluentUI/Tab Bar/TabBarItemView.swift @@ -42,6 +42,10 @@ class TabBarItemView: UIControl, TokenizedControlInternal { updateImage() updateColors() if isSelected { + if item.isUnreadDotVisible { + item.isUnreadDotVisible = false + updateBadgeView() + } accessibilityTraits.insert(.selected) } else { accessibilityTraits.remove(.selected) @@ -117,16 +121,21 @@ class TabBarItemView: UIControl, TokenizedControlInternal { scalesLargeContentImage = true NSLayoutConstraint.activate([ - container.centerXAnchor.constraint(equalTo: centerXAnchor), - container.centerYAnchor.constraint(equalTo: centerYAnchor), - container.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) - ]) + container.centerXAnchor.constraint(equalTo: centerXAnchor), + container.centerYAnchor.constraint(equalTo: centerYAnchor), + container.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) + ]) NotificationCenter.default.addObserver(self, selector: #selector(badgeValueDidChange), name: TabBarItem.badgeValueDidChangeNotification, object: item) + NotificationCenter.default.addObserver(self, + selector: #selector(isUnreadValueDidChange), + name: TabBarItem.isUnreadValueDidChangeNotification, + object: item) + badgeValue = item.badgeValue updateLayout() @@ -185,6 +194,15 @@ class TabBarItemView: UIControl, TokenizedControlInternal { } } + @objc private func isUnreadValueDidChange() { + isUnreadDotVisible = item.isUnreadDotVisible + updateBadgeView() + updateAccessibilityLabel() + setNeedsLayout() + } + + private var isUnreadDotVisible: Bool = false + private let container: UIStackView = { let container = UIStackView(frame: .zero) container.alignment = .center @@ -285,57 +303,89 @@ class TabBarItemView: UIControl, TokenizedControlInternal { } private func updateBadgeView() { - badgeView.text = badgeValue - badgeView.isHidden = badgeValue == nil + isUnreadDotVisible = item.isUnreadDotVisible && badgeValue == nil - if badgeValue != nil { - let maskLayer = CAShapeLayer() - maskLayer.fillRule = .evenOdd + // If nothing to display, remove mask and return + if badgeValue == nil && !isUnreadDotVisible { + badgeView.isHidden = true + imageView.layer.mask = nil + return + } + + // Otherwise, show either the badgeValue or an unreadDot + badgeView.isHidden = false + let maskLayer = CAShapeLayer() + maskLayer.fillRule = .evenOdd - let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: imageView.frame.size.width, height: imageView.frame.size.height)) + let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: imageView.frame.size.width, height: imageView.frame.size.height)) + + if isUnreadDotVisible { + // Badge with empty string and round corners is a dot + badgeView.text = "" + let badgeVerticalOffset = !titleLabel.isHidden && isInPortraitMode ? tokenSet[.unreadDotPortraitOffsetX].float : tokenSet[.unreadDotOffsetX].float + + createCircularBadgeFrame(labelView: badgeView, + path: path, + horizontalOffset: tokenSet[.unreadDotOffsetY].float, + verticalOffset: badgeVerticalOffset, + frameWidth: tokenSet[.unreadDotSize].float, + frameHeight: tokenSet[.unreadDotSize].float) + } else { + badgeView.text = badgeValue let badgeVerticalOffset = !titleLabel.isHidden && isInPortraitMode ? tokenSet[.badgePortraitTitleVerticalOffset].float : tokenSet[.badgeVerticalOffset].float if badgeView.text?.count ?? 1 > 1 { - let badgeWidth = min(max(badgeView.intrinsicContentSize.width + tokenSet[.badgeHorizontalPadding].float, tokenSet[.badgeMinWidth].float), maxBadgeWidth) + createRoundedRectBadgeFrame(labelView: badgeView, path: path, verticalOffset: badgeVerticalOffset) + } else { + createCircularBadgeFrame(labelView: badgeView, + path: path, + horizontalOffset: tokenSet[.singleDigitBadgeHorizontalOffset].float, + verticalOffset: badgeVerticalOffset, + frameWidth: tokenSet[.badgeMinWidth].float, + frameHeight: tokenSet[.badgeHeight].float) + } + } - badgeView.frame = CGRect(x: badgeFrameOriginX(offset: tokenSet[.multiDigitBadgeHorizontalOffset].float, frameWidth: badgeWidth), - y: imageView.frame.origin.y + badgeVerticalOffset, - width: badgeWidth, - height: tokenSet[.badgeHeight].float) + maskLayer.path = path.cgPath + imageView.layer.mask = maskLayer + } - let layer = CAShapeLayer() - layer.path = UIBezierPath(roundedRect: badgeView.bounds, - byRoundingCorners: .allCorners, - cornerRadii: CGSize(width: tokenSet[.badgeCornerRadii].float, height: tokenSet[.badgeCornerRadii].float)).cgPath + private func createRoundedRectBadgeFrame(labelView: UILabel, path: UIBezierPath, verticalOffset: CGFloat) { + let width = min(max(labelView.intrinsicContentSize.width + tokenSet[.badgeHorizontalPadding].float, tokenSet[.badgeMinWidth].float), maxBadgeWidth) - path.append(UIBezierPath(roundedRect: badgeBorderRect(badgeViewFrame: badgeView.frame), - byRoundingCorners: .allCorners, - cornerRadii: CGSize(width: tokenSet[.badgeCornerRadii].float, height: tokenSet[.badgeCornerRadii].float))) + labelView.frame = CGRect(x: frameOriginX(offset: tokenSet[.multiDigitBadgeHorizontalOffset].float, frameWidth: width), + y: imageView.frame.origin.y + verticalOffset, + width: width, + height: tokenSet[.badgeHeight].float) - badgeView.layer.mask = layer - badgeView.layer.cornerRadius = 0 - } else { - let badgeWidth = max(badgeView.intrinsicContentSize.width, tokenSet[.badgeMinWidth].float) + let layer = CAShapeLayer() + layer.path = UIBezierPath(roundedRect: labelView.bounds, + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: tokenSet[.badgeCornerRadii].float, height: tokenSet[.badgeCornerRadii].float)).cgPath - badgeView.frame = CGRect(x: badgeFrameOriginX(offset: tokenSet[.singleDigitBadgeHorizontalOffset].float, frameWidth: badgeWidth), - y: imageView.frame.origin.y + badgeVerticalOffset, - width: badgeWidth, - height: tokenSet[.badgeHeight].float) + path.append(UIBezierPath(roundedRect: badgeBorderRect(badgeViewFrame: labelView.frame), + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: tokenSet[.badgeCornerRadii].float, height: tokenSet[.badgeCornerRadii].float))) - path.append(UIBezierPath(ovalIn: badgeBorderRect(badgeViewFrame: badgeView.frame))) + labelView.layer.mask = layer + labelView.layer.cornerRadius = 0 + } - badgeView.layer.mask = nil - badgeView.layer.cornerRadius = badgeWidth / 2 - } + private func createCircularBadgeFrame(labelView: UILabel, path: UIBezierPath, horizontalOffset: CGFloat, verticalOffset: CGFloat, frameWidth: CGFloat, frameHeight: CGFloat) { + let width = max(labelView.intrinsicContentSize.width, frameWidth) - maskLayer.path = path.cgPath - imageView.layer.mask = maskLayer - } else { - imageView.layer.mask = nil - } + labelView.frame = CGRect(x: frameOriginX(offset: horizontalOffset, frameWidth: width), + y: imageView.frame.origin.y + verticalOffset, + width: width, + height: frameHeight) + + path.append(UIBezierPath(ovalIn: badgeBorderRect(badgeViewFrame: labelView.frame))) + + labelView.layer.mask = nil + labelView.layer.cornerRadius = width / 2 } - private func badgeFrameOriginX(offset: CGFloat, frameWidth: CGFloat) -> CGFloat { + private func frameOriginX(offset: CGFloat, frameWidth: CGFloat) -> CGFloat { var xOrigin: CGFloat = 0 if effectiveUserInterfaceLayoutDirection == .leftToRight { xOrigin = imageView.frame.origin.x + offset @@ -358,6 +408,12 @@ class TabBarItemView: UIControl, TokenizedControlInternal { badgeValue = item.badgeValue } + // The priority logic for accessibility label is: + // If the badge is visible: + // 1. Use the badge format string supplied by the caller if available + // 2. If not, use the default localized badge label format + // If the unread dot is visible, use the localized "unread" label + // If neither, then use the item's title, as supplied by the caller private func updateAccessibilityLabel() { if let badgeValue = badgeValue { if let accessibilityLabelBadgeFormatString = item.accessibilityLabelBadgeFormatString { @@ -366,7 +422,11 @@ class TabBarItemView: UIControl, TokenizedControlInternal { accessibilityLabel = String(format: "Accessibility.TabBarItemView.LabelFormat".localized, item.title, badgeValue) } } else { - accessibilityLabel = item.title + if isUnreadDotVisible { + accessibilityLabel = String(format: "Accessibility.TabBarItemView.UnreadFormat".localized, item.title) + } else { + accessibilityLabel = item.title + } } } } diff --git a/ios/FluentUI/Table View/TableViewCell.swift b/ios/FluentUI/Table View/TableViewCell.swift index c281415dbb..37f7081bc7 100644 --- a/ios/FluentUI/Table View/TableViewCell.swift +++ b/ios/FluentUI/Table View/TableViewCell.swift @@ -182,7 +182,9 @@ open class TableViewCell: UITableViewCell, TokenizedControlInternal { class var labelVerticalMarginForOneAndThreeLines: CGFloat { return TableViewCellTokenSet.defaultLabelVerticalMarginForOneAndThreeLines } public typealias TokenSetKeyType = TableViewCellTokenSet.Tokens - public lazy var tokenSet: TableViewCellTokenSet = .init(customViewSize: { self.customViewSize }) + public lazy var tokenSet: TableViewCellTokenSet = .init(customViewSize: { [weak self] in + return self?.customViewSize ?? .default + }) var tokenSetSink: AnyCancellable? diff --git a/scripts/xcode_select_current_version.sh b/scripts/xcode_select_current_version.sh index eb124c0659..1f2e6076cc 100755 --- a/scripts/xcode_select_current_version.sh +++ b/scripts/xcode_select_current_version.sh @@ -3,7 +3,7 @@ if [ -n "$XCODE_PATH_OVERRIDE" ]; then # If someone calls this with the XCODE_PATH_OVERRIDE variable set to a path to a developer dir, use it instead XCODE_PATH="$XCODE_PATH_OVERRIDE" else - XCODE_PATH='/Applications/Xcode_13.4.app/Contents/Developer' + XCODE_PATH='/Applications/Xcode_14.1.app/Contents/Developer' fi echo "Running command: sudo xcode-select --switch $XCODE_PATH"