diff --git a/Package.swift b/Package.swift index c0840afeb8..db4600650b 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,7 @@ let package = Package( platforms: [ .iOS(.v15), .macOS(.v12), + .visionOS(.v1), ], products: [ .library( @@ -22,7 +23,7 @@ let package = Package( .target( name: "FluentUI", dependencies: [ - .target(name: "FluentUI_ios", condition: .when(platforms: [.iOS])), + .target(name: "FluentUI_ios", condition: .when(platforms: [.iOS, .visionOS])), .target(name: "FluentUI_macos", condition: .when(platforms: [.macOS])) ], path: "public" diff --git a/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj b/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj index 70f1fd106a..259af263ce 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj +++ b/ios/FluentUI.Demo/FluentUI.Demo.xcodeproj/project.pbxproj @@ -82,9 +82,9 @@ 807E8B4528F9F8B8002B8F84 /* PillButtonDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807E8B4428F9F8B8002B8F84 /* PillButtonDemoController.swift */; }; 80AECC0C2630F1BB005AF2F3 /* BottomCommandingDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AECC0B2630F1BB005AF2F3 /* BottomCommandingDemoController.swift */; }; 80B1F7012628D8BB004DFEE5 /* BottomSheetDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B1F7002628D8BB004DFEE5 /* BottomSheetDemoController.swift */; }; - 8F0B81122670200300463726 /* AppCenterDistribute in Frameworks */ = {isa = PBXBuildFile; productRef = 8F0B81112670200300463726 /* AppCenterDistribute */; }; - 8F0B8114267021A700463726 /* AppCenterAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 8F0B8113267021A700463726 /* AppCenterAnalytics */; }; - 8F0B8116267021A700463726 /* AppCenterCrashes in Frameworks */ = {isa = PBXBuildFile; productRef = 8F0B8115267021A700463726 /* AppCenterCrashes */; }; + 8F0B81122670200300463726 /* AppCenterDistribute in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 8F0B81112670200300463726 /* AppCenterDistribute */; }; + 8F0B8114267021A700463726 /* AppCenterAnalytics in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 8F0B8113267021A700463726 /* AppCenterAnalytics */; }; + 8F0B8116267021A700463726 /* AppCenterCrashes in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 8F0B8115267021A700463726 /* AppCenterCrashes */; }; 923DF2DB271158C900637646 /* libFluentUI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 923DF2DA271158C900637646 /* libFluentUI.a */; }; 923DF2DF27115B4700637646 /* FluentUIResources-ios.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 923DF2DC271158CD00637646 /* FluentUIResources-ios.bundle */; }; 9245E1F927BECDBB007616F3 /* GlobalColorTokensDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9245E1F827BECDBB007616F3 /* GlobalColorTokensDemoController.swift */; }; @@ -956,7 +956,7 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; TEST_TARGET_NAME = FluentUI.Demo; }; name = Debug; @@ -983,7 +983,7 @@ PROVISIONING_PROFILE = "63d62159-2691-4b44-9553-b668cc1746c1"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; TEST_TARGET_NAME = FluentUI.Demo; }; name = Release; @@ -1010,7 +1010,7 @@ PROVISIONING_PROFILE = "63d62159-2691-4b44-9553-b668cc1746c1"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; TEST_TARGET_NAME = FluentUI.Demo; }; name = Dogfood; @@ -1099,9 +1099,12 @@ PRODUCT_NAME = FluentUI.Demo; PROVISIONING_PROFILE = "4596e7d8-5232-4b9f-82bf-63883e38cd5c"; PROVISIONING_PROFILE_SPECIFIER = "Office Fabric Demo Dogfood development"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "FluentUI.Demo/FluentUI.Demo-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Dogfood; }; @@ -1249,10 +1252,13 @@ PRODUCT_NAME = FluentUI.Demo; PROVISIONING_PROFILE = "63d62159-2691-4b44-9553-b668cc1746c1"; PROVISIONING_PROFILE_SPECIFIER = "iOS Team Provisioning Profile"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "FluentUI.Demo/FluentUI.Demo-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; @@ -1278,9 +1284,12 @@ PRODUCT_NAME = FluentUI.Demo; PROVISIONING_PROFILE = "63d62159-2691-4b44-9553-b668cc1746c1"; PROVISIONING_PROFILE_SPECIFIER = "iOS Team Provisioning Profile"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "FluentUI.Demo/FluentUI.Demo-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; }; diff --git a/ios/FluentUI.Demo/FluentUI.Demo/DemoListViewController.swift b/ios/FluentUI.Demo/FluentUI.Demo/DemoListViewController.swift index 4dc5a991de..7601e5938f 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/DemoListViewController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/DemoListViewController.swift @@ -30,7 +30,7 @@ class DemoListViewController: DemoTableViewController { func updateColorProviderFor(window: UIWindow, theme: DemoColorTheme) { self.theme = theme if let provider = self.provider { - window.setColorProvider(provider) + FluentTheme.setSharedThemeColorProvider(provider) let fluentTheme = self.view.fluentTheme let primaryColor = fluentTheme.color(.brandBackground1) FluentUIFramework.initializeAppearance(with: primaryColor, whenContainedInInstancesOf: [type(of: window)]) @@ -113,6 +113,9 @@ class DemoListViewController: DemoTableViewController { } cell.titleNumberOfLinesForLargerDynamicType = 2 cell.backgroundStyleType = .grouped +#if os(visionOS) + cell.isEnabled = demo.supportsVisionOS +#endif if indexPath.row == DemoControllerSection.allCases[indexPath.section].rows.count - 1 { cell.bottomSeparatorType = .none diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift index e76c4e3e99..04244b2d6d 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift @@ -9,68 +9,70 @@ import UIKit struct DemoDescriptor: Identifiable { let title: String let controllerClass: UIViewController.Type + let supportsVisionOS: Bool let id = UUID() - init(_ title: String, _ controllerClass: UIViewController.Type, debugOnly: Bool = false) { + init(_ title: String, _ controllerClass: UIViewController.Type, supportsVisionOS: Bool) { self.title = title self.controllerClass = controllerClass + self.supportsVisionOS = supportsVisionOS } } struct Demos { static let fluent2: [DemoDescriptor] = [ - DemoDescriptor("ActivityIndicator", ActivityIndicatorDemoController.self), - DemoDescriptor("Avatar", AvatarDemoController.self), - DemoDescriptor("AvatarGroup", AvatarGroupDemoController.self), - DemoDescriptor("BadgeField", BadgeFieldDemoController.self), - DemoDescriptor("BadgeView", BadgeViewDemoController.self), - DemoDescriptor("BottomCommandingController", BottomCommandingDemoController.self), - DemoDescriptor("BottomSheetController", BottomSheetDemoController.self), - DemoDescriptor("Button", ButtonDemoController.self), - DemoDescriptor("CardNudge", CardNudgeDemoController.self), - DemoDescriptor("CommandBar", CommandBarDemoController.self), - DemoDescriptor("DrawerController", DrawerDemoController.self), - DemoDescriptor("HUD", HUDDemoController.self), - DemoDescriptor("IndeterminateProgressBar", IndeterminateProgressBarDemoController.self), - DemoDescriptor("Label", LabelDemoController.self), - DemoDescriptor("ListActionItem", ListActionItemDemoController.self), - DemoDescriptor("ListItem", ListItemDemoController.self), - DemoDescriptor("MultilineCommandBar", MultilineCommandBarDemoController.self), - DemoDescriptor("NavigationController", NavigationControllerDemoController.self), - DemoDescriptor("NotificationView", NotificationViewDemoController.self), - DemoDescriptor("Other cells", OtherCellsDemoController.self), - DemoDescriptor("PeoplePicker", PeoplePickerDemoController.self), - DemoDescriptor("PersonaButtonCarousel", PersonaButtonCarouselDemoController.self), - DemoDescriptor("PillButton", PillButtonDemoController.self), - DemoDescriptor("PillButtonBar", PillButtonBarDemoController.self), - DemoDescriptor("PopupMenuController", PopupMenuDemoController.self), - DemoDescriptor("SearchBar", SearchBarDemoController.self), - DemoDescriptor("SegmentedControl", SegmentedControlDemoController.self), - DemoDescriptor("ShimmerView", ShimmerViewDemoController.self), - DemoDescriptor("SideTabBar", SideTabBarDemoController.self), - DemoDescriptor("TabBarView", TabBarViewDemoController.self), - DemoDescriptor("TableViewCell", TableViewCellDemoController.self), - DemoDescriptor("TableViewHeaderFooterView", TableViewHeaderFooterViewDemoController.self), - DemoDescriptor("Text Field", TextFieldDemoController.self), - DemoDescriptor("Tooltip", TooltipDemoController.self), - DemoDescriptor("TwoLineTitleView", TwoLineTitleViewDemoController.self) + DemoDescriptor("ActivityIndicator", ActivityIndicatorDemoController.self, supportsVisionOS: false), + DemoDescriptor("Avatar", AvatarDemoController.self, supportsVisionOS: true), + DemoDescriptor("AvatarGroup", AvatarGroupDemoController.self, supportsVisionOS: true), + DemoDescriptor("BadgeField", BadgeFieldDemoController.self, supportsVisionOS: true), + DemoDescriptor("BadgeView", BadgeViewDemoController.self, supportsVisionOS: false), + DemoDescriptor("BottomCommandingController", BottomCommandingDemoController.self, supportsVisionOS: true), + DemoDescriptor("BottomSheetController", BottomSheetDemoController.self, supportsVisionOS: false), + DemoDescriptor("Button", ButtonDemoController.self, supportsVisionOS: true), + DemoDescriptor("CardNudge", CardNudgeDemoController.self, supportsVisionOS: false), + DemoDescriptor("CommandBar", CommandBarDemoController.self, supportsVisionOS: false), + DemoDescriptor("DrawerController", DrawerDemoController.self, supportsVisionOS: true), + DemoDescriptor("HUD", HUDDemoController.self, supportsVisionOS: true), + DemoDescriptor("IndeterminateProgressBar", IndeterminateProgressBarDemoController.self, supportsVisionOS: false), + DemoDescriptor("Label", LabelDemoController.self, supportsVisionOS: true), + DemoDescriptor("ListActionItem", ListActionItemDemoController.self, supportsVisionOS: false), + DemoDescriptor("ListItem", ListItemDemoController.self, supportsVisionOS: false), + DemoDescriptor("MultilineCommandBar", MultilineCommandBarDemoController.self, supportsVisionOS: false), + DemoDescriptor("NavigationController", NavigationControllerDemoController.self, supportsVisionOS: true), + DemoDescriptor("NotificationView", NotificationViewDemoController.self, supportsVisionOS: true), + DemoDescriptor("Other cells", OtherCellsDemoController.self, supportsVisionOS: false), + DemoDescriptor("PeoplePicker", PeoplePickerDemoController.self, supportsVisionOS: false), + DemoDescriptor("PersonaButtonCarousel", PersonaButtonCarouselDemoController.self, supportsVisionOS: false), + DemoDescriptor("PillButton", PillButtonDemoController.self, supportsVisionOS: true), + DemoDescriptor("PillButtonBar", PillButtonBarDemoController.self, supportsVisionOS: true), + DemoDescriptor("PopupMenuController", PopupMenuDemoController.self, supportsVisionOS: false), + DemoDescriptor("SearchBar", SearchBarDemoController.self, supportsVisionOS: true), + DemoDescriptor("SegmentedControl", SegmentedControlDemoController.self, supportsVisionOS: false), + DemoDescriptor("ShimmerView", ShimmerViewDemoController.self, supportsVisionOS: false), + DemoDescriptor("SideTabBar", SideTabBarDemoController.self, supportsVisionOS: false), + DemoDescriptor("TabBarView", TabBarViewDemoController.self, supportsVisionOS: true), + DemoDescriptor("TableViewCell", TableViewCellDemoController.self, supportsVisionOS: true), + DemoDescriptor("TableViewHeaderFooterView", TableViewHeaderFooterViewDemoController.self, supportsVisionOS: true), + DemoDescriptor("Text Field", TextFieldDemoController.self, supportsVisionOS: false), + DemoDescriptor("Tooltip", TooltipDemoController.self, supportsVisionOS: false), + DemoDescriptor("TwoLineTitleView", TwoLineTitleViewDemoController.self, supportsVisionOS: false) ] static let fluent2DesignTokens: [DemoDescriptor] = [ - DemoDescriptor("Global Color Tokens", GlobalColorTokensDemoController.self), - DemoDescriptor("Alias Color Tokens", AliasColorTokensDemoController.self), - DemoDescriptor("Shadow Tokens", ShadowTokensDemoController.self), - DemoDescriptor("Typography Tokens", TypographyTokensDemoController.self) + DemoDescriptor("Global Color Tokens", GlobalColorTokensDemoController.self, supportsVisionOS: true), + DemoDescriptor("Alias Color Tokens", AliasColorTokensDemoController.self, supportsVisionOS: true), + DemoDescriptor("Shadow Tokens", ShadowTokensDemoController.self, supportsVisionOS: true), + DemoDescriptor("Typography Tokens", TypographyTokensDemoController.self, supportsVisionOS: true) ] static let controls: [DemoDescriptor] = [ - DemoDescriptor("Card", CardViewDemoController.self), - DemoDescriptor("DateTimePicker", DateTimePickerDemoController.self), - DemoDescriptor("PersonaListView", PersonaListViewDemoController.self), - DemoDescriptor("TableViewCellShimmer", TableViewCellShimmerDemoController.self) + DemoDescriptor("Card", CardViewDemoController.self, supportsVisionOS: false), + DemoDescriptor("DateTimePicker", DateTimePickerDemoController.self, supportsVisionOS: false), + DemoDescriptor("PersonaListView", PersonaListViewDemoController.self, supportsVisionOS: false), + DemoDescriptor("TableViewCellShimmer", TableViewCellShimmerDemoController.self, supportsVisionOS: false) ] static let debug: [DemoDescriptor] = [ - DemoDescriptor("DEBUG: Objective-C Demos", ObjectiveCDemoController.self) + DemoDescriptor("DEBUG: Objective-C Demos", ObjectiveCDemoController.self, supportsVisionOS: false) ] } diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/CommandBarDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/CommandBarDemoController.swift index cdc73f33d1..8c0fbade14 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/CommandBarDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/CommandBarDemoController.swift @@ -297,7 +297,9 @@ class CommandBarDemoController: DemoController { let accessoryCommandBar = CommandBar(itemGroups: createItemGroups(), trailingItemGroups: [[newItem(for: .keyboard)]]) accessoryCommandBar.translatesAutoresizingMaskIntoConstraints = false +#if os(iOS) textField.inputAccessoryView = accessoryCommandBar +#endif } func createItemGroups() -> [CommandBarItemGroup] { diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/DrawerDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/DrawerDemoController.swift index 62548f0e2e..2c55b39f46 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/DrawerDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/DrawerDemoController.swift @@ -60,6 +60,7 @@ class DrawerDemoController: DemoController { // Screen edge gestures to interactively present side drawers +#if os(iOS) let leadingEdgeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleScreenEdgePan)) leadingEdgeGesture.edges = view.effectiveUserInterfaceLayoutDirection == .leftToRight ? .left : .right view.addGestureRecognizer(leadingEdgeGesture) @@ -68,6 +69,7 @@ class DrawerDemoController: DemoController { let trailingEdgeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleScreenEdgePan)) trailingEdgeGesture.edges = view.effectiveUserInterfaceLayoutDirection == .leftToRight ? .right : .left view.addGestureRecognizer(trailingEdgeGesture) +#endif } @discardableResult @@ -318,6 +320,7 @@ class DrawerDemoController: DemoController { textField.becomeFirstResponder() } +#if os(iOS) @objc private func handleScreenEdgePan(gesture: UIScreenEdgePanGestureRecognizer) { guard gesture.state == .began else { return @@ -327,6 +330,7 @@ class DrawerDemoController: DemoController { presentDrawer(sourceView: view, presentationDirection: gesture.edges == leadingEdge ? .fromLeading : .fromTrailing, presentingGesture: gesture, contentView: containerForActionViews(drawerHasFlexibleHeight: false), resizingBehavior: .dismiss) } +#endif @objc private func changeContentHeightButtonTapped(sender: UIButton) { if let spacer = (sender.superview as? UIStackView)?.arrangedSubviews.last, diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift index 997a181a65..8a79d2fae9 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift @@ -252,12 +252,14 @@ class NavigationControllerDemoController: DemoController { } controller.modalPresentationStyle = .fullScreen +#if os(iOS) if titleStyle.usesLeadingAlignment { let leadingEdgeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleScreenEdgePan)) leadingEdgeGesture.edges = view.effectiveUserInterfaceLayoutDirection == .leftToRight ? .left : .right leadingEdgeGesture.delegate = self controller.view.addGestureRecognizer(leadingEdgeGesture) } +#endif present(controller, animated: false) @@ -279,7 +281,9 @@ class NavigationControllerDemoController: DemoController { } navigationItem.navigationBarStyle = newStyle +#if os(iOS) self.setNeedsStatusBarAppearanceUpdate() +#endif self.changeStyleContinuously(in: navigationItem) } } @@ -327,13 +331,16 @@ class NavigationControllerDemoController: DemoController { presentSideDrawer() } +#if os(iOS) @objc private func handleScreenEdgePan(gesture: UIScreenEdgePanGestureRecognizer) { if gesture.state == .began { presentSideDrawer(presentingGesture: gesture) } } +#endif } +#if os(iOS) // MARK: - NavigationControllerDemoController: UIGestureRecognizerDelegate extension NavigationControllerDemoController: UIGestureRecognizerDelegate { @@ -346,6 +353,7 @@ extension NavigationControllerDemoController: UIGestureRecognizerDelegate { return true } } +#endif // MARK: - RootViewController @@ -815,7 +823,9 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe func navigationBarDidTapOnTitle(_ sender: NavigationBar) { if let topItem = sender.topItem { topItem.navigationBarStyle = topItem.navigationBarStyle == .primary ? .system : .primary +#if os(iOS) setNeedsStatusBarAppearanceUpdate() +#endif } } } diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/TableViewHeaderFooterViewDemoController.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/TableViewHeaderFooterViewDemoController.swift index c8c0139900..79bd0dfce8 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/TableViewHeaderFooterViewDemoController.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/TableViewHeaderFooterViewDemoController.swift @@ -133,9 +133,11 @@ extension TableViewHeaderFooterViewDemoController { } footer?.setup(style: .footer, attributedTitle: title) +#if os(iOS) if section.hasCustomLinkHandler { footer?.delegate = self } +#endif } footer?.titleNumberOfLines = section.numberOfLines footer?.tokenSet.replaceAllOverrides(with: overrideTokens) @@ -163,6 +165,7 @@ extension TableViewHeaderFooterViewDemoController { // MARK: - TableViewHeaderFooterViewDemoController: TableViewHeaderFooterViewDelegate +#if os(iOS) extension TableViewHeaderFooterViewDemoController: TableViewHeaderFooterViewDelegate { func headerFooterView(_ headerFooterView: TableViewHeaderFooterView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { let alertController = UIAlertController(title: "Link tapped", message: nil, preferredStyle: .alert) @@ -171,6 +174,7 @@ extension TableViewHeaderFooterViewDemoController: TableViewHeaderFooterViewDele return false } } +#endif extension TableViewHeaderFooterViewDemoController: DemoAppearanceDelegate { func themeWideOverrideDidChange(isOverrideEnabled: Bool) { diff --git a/ios/FluentUI.xcodeproj/project.pbxproj b/ios/FluentUI.xcodeproj/project.pbxproj index e5a1caefac..3a284ac817 100644 --- a/ios/FluentUI.xcodeproj/project.pbxproj +++ b/ios/FluentUI.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ 925728F9276D6B5800EE1019 /* FontInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925728F8276D6B5800EE1019 /* FontInfo.swift */; }; 925D461D26FD133600179583 /* GlobalTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925D461B26FD133600179583 /* GlobalTokens.swift */; }; 925D462026FD18B200179583 /* AliasTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925D461E26FD18B200179583 /* AliasTokens.swift */; }; + 926FEEAA2B45A8B4002C61D0 /* Compatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 926FEEA92B45A8B4002C61D0 /* Compatibility.swift */; }; 9275105626815A7100F12730 /* MSFPersonaButtonCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9275105426815A7100F12730 /* MSFPersonaButtonCarousel.swift */; }; 927EB2BD278627440069753D /* PersonaButtonModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927EB2BC278627440069753D /* PersonaButtonModifiers.swift */; }; 9298798B2669A875002B1EB4 /* PersonaButtonTokenSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927E34C62668350800998031 /* PersonaButtonTokenSet.swift */; }; @@ -354,6 +355,7 @@ 925728F8276D6B5800EE1019 /* FontInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontInfo.swift; sourceTree = ""; }; 925D461B26FD133600179583 /* GlobalTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalTokens.swift; sourceTree = ""; }; 925D461E26FD18B200179583 /* AliasTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasTokens.swift; sourceTree = ""; }; + 926FEEA92B45A8B4002C61D0 /* Compatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compatibility.swift; sourceTree = ""; }; 9275105426815A7100F12730 /* MSFPersonaButtonCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSFPersonaButtonCarousel.swift; sourceTree = ""; }; 927E34C62668350800998031 /* PersonaButtonTokenSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonaButtonTokenSet.swift; sourceTree = ""; }; 927EB2BC278627440069753D /* PersonaButtonModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonaButtonModifiers.swift; sourceTree = ""; }; @@ -995,6 +997,7 @@ children = ( 925D461926FD124B00179583 /* Theme */, D6296DAD295B7C9F002E8EB6 /* ColorProviding.swift */, + 926FEEA92B45A8B4002C61D0 /* Compatibility.swift */, 923DB9D6274CB66D00D8E58A /* ControlHostingView.swift */, A559BB82212B7D870055E107 /* FluentUIFramework.swift */, 535559E22711411E0094A871 /* FluentUIHostingController.swift */, @@ -1728,6 +1731,7 @@ F3F1138D2A705B6900DA852A /* ListItemModifiers.swift in Sources */, 5314E07D25F00F1A0099271A /* DateTimePickerViewComponentTableView.swift in Sources */, 0AE3041D29F721B2003CDDD9 /* TableViewHeaderFooterViewTokenSet.swift in Sources */, + 926FEEAA2B45A8B4002C61D0 /* Compatibility.swift in Sources */, 5314E03125F00DDD0099271A /* CardView.swift in Sources */, 5314E08925F00F2D0099271A /* CommandBarButtonGroupView.swift in Sources */, 5314E08C25F00F2D0099271A /* CommandBarButton.swift in Sources */, diff --git a/ios/FluentUI/Badge Field/BadgeField.swift b/ios/FluentUI/Badge Field/BadgeField.swift index 788774623c..4a822f22ae 100644 --- a/ios/FluentUI/Badge Field/BadgeField.swift +++ b/ios/FluentUI/Badge Field/BadgeField.swift @@ -140,10 +140,16 @@ open class BadgeField: UIView, TokenizedControlInternal { public var tokenSet: BadgeFieldTokenSet = .init() var deviceOrientationIsChanging: Bool { +#if os(iOS) originalDeviceOrientation != UIDevice.current.orientation +#elseif os(visionOS) + false +#endif // os(visionOS) } +#if os(iOS) private var originalDeviceOrientation: UIDeviceOrientation = UIDevice.current.orientation +#endif // os(iOS) private var cachedContentHeight: CGFloat = 0 { didSet { @@ -183,8 +189,10 @@ open class BadgeField: UIView, TokenizedControlInternal { textField.addTarget(self, action: #selector(textFieldTextChanged), for: .editingChanged) +#if os(iOS) UIDevice.current.beginGeneratingDeviceOrientationNotifications() NotificationCenter.default.addObserver(self, selector: #selector(handleOrientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil) +#endif // os(iOS) NotificationCenter.default.addObserver(self, selector: #selector(handleContentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) @@ -219,9 +227,11 @@ open class BadgeField: UIView, TokenizedControlInternal { preconditionFailure("init(coder:) has not been implemented") } +#if os(iOS) deinit { UIDevice.current.endGeneratingDeviceOrientationNotifications() } +#endif // os(iOS) /** Sets up the view using the badge data sources. @@ -252,6 +262,9 @@ open class BadgeField: UIView, TokenizedControlInternal { textField.autocorrectionType = .no textField.keyboardType = .emailAddress textField.delegate = self + if #available(iOS 17, *) { + textField.hoverStyle = nil + } } private func setupDraggingWindow() { @@ -798,11 +811,13 @@ open class BadgeField: UIView, TokenizedControlInternal { textField.isPaste = false } +#if os(iOS) @objc private func handleOrientationChanged() { // Hack: to avoid properly handling rotations for the dragging window (which is annoying and overkill for this feature), let's just reset the dragging window resetDraggingWindow() originalDeviceOrientation = UIDevice.current.orientation } +#endif // os(iOS) @objc private func handleContentSizeCategoryDidChange() { invalidateIntrinsicContentSize() diff --git a/ios/FluentUI/Bottom Sheet/BottomSheetController.swift b/ios/FluentUI/Bottom Sheet/BottomSheetController.swift index 1ea70aa09e..801bce9ee9 100644 --- a/ios/FluentUI/Bottom Sheet/BottomSheetController.swift +++ b/ios/FluentUI/Bottom Sheet/BottomSheetController.swift @@ -1003,7 +1003,9 @@ public class BottomSheetController: UIViewController, Shadowable, TokenizedContr let oldContext = lastCollapsedSheetHeightResolutionContext let newContext = ContentHeightResolutionContext(maximumHeight: maxSheetHeight - view.safeAreaInsets.bottom, containerTraitCollection: view.traitCollection) - if oldContext?.maximumHeight != newContext.maximumHeight || !(oldContext?.containerTraitCollection.containsTraits(in: newContext.containerTraitCollection) ?? false) { + if oldContext?.maximumHeight != newContext.maximumHeight + || oldContext?.containerTraitCollection.horizontalSizeClass != newContext.containerTraitCollection.horizontalSizeClass + || oldContext?.containerTraitCollection.verticalSizeClass != newContext.containerTraitCollection.verticalSizeClass { lastResolvedCollapsedSheetHeight = collapsedHeightResolver?(newContext) ?? 0 lastCollapsedSheetHeightResolutionContext = newContext } diff --git a/ios/FluentUI/Button/ButtonTokenSet.swift b/ios/FluentUI/Button/ButtonTokenSet.swift index 90d766dc9b..569a8fda8a 100644 --- a/ios/FluentUI/Button/ButtonTokenSet.swift +++ b/ios/FluentUI/Button/ButtonTokenSet.swift @@ -235,6 +235,9 @@ public class ButtonTokenSet: ControlTokenSet { } case .cornerRadius: return .float { + if Compatibility.isDeviceIdiomVision() { + return ButtonTokenSet.minContainerHeight(style: style(), size: size()) / 2 + } switch size() { case .large: return GlobalTokens.corner(.radius120) diff --git a/ios/FluentUI/Core/ColorProviding.swift b/ios/FluentUI/Core/ColorProviding.swift index 88b59842a3..44c0c7d292 100644 --- a/ios/FluentUI/Core/ColorProviding.swift +++ b/ios/FluentUI/Core/ColorProviding.swift @@ -69,6 +69,12 @@ private func brandColorOverrides(provider: ColorProviding) -> [FluentTheme.Color brandColors[.brandGradient3] = brandGradient3 } +#if os(visionOS) + // Remove the dark values from all our brand colors on visionOS. + // We only want the light variants. + brandColors = brandColors.mapValues({ $0.light }) +#endif + return brandColors } diff --git a/ios/FluentUI/Core/Compatibility.swift b/ios/FluentUI/Core/Compatibility.swift new file mode 100644 index 0000000000..dccb9c7bda --- /dev/null +++ b/ios/FluentUI/Core/Compatibility.swift @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import UIKit + +struct Compatibility { + /// A cross-version way to check if the current device is running visionOS. + /// + /// - Returns: `UIDevice.current.userInterfaceIdiom == .vision` if the current OS is >= iOS 17, + /// and `false` otherwise. + static func isDeviceIdiomVision() -> Bool { + if #available(iOS 17, *) { + return UIDevice.current.userInterfaceIdiom == .vision + } + return false + } +} diff --git a/ios/FluentUI/Core/Theme/FluentTheme+Tokens.swift b/ios/FluentUI/Core/Theme/FluentTheme+Tokens.swift index e7142b9110..e4d33e6c23 100644 --- a/ios/FluentUI/Core/Theme/FluentTheme+Tokens.swift +++ b/ios/FluentUI/Core/Theme/FluentTheme+Tokens.swift @@ -204,22 +204,22 @@ extension FluentTheme { switch token { case .foreground1: return UIColor(light: GlobalTokens.neutralColor(.grey14), - dark: GlobalTokens.neutralColor(.white)) + dark: Compatibility.isDeviceIdiomVision() ? .white : GlobalTokens.neutralColor(.white)) case .foreground2: return UIColor(light: GlobalTokens.neutralColor(.grey38), - dark: GlobalTokens.neutralColor(.grey84)) + dark: Compatibility.isDeviceIdiomVision() ? .white : GlobalTokens.neutralColor(.grey84)) case .foreground3: return UIColor(light: GlobalTokens.neutralColor(.grey50), - dark: GlobalTokens.neutralColor(.grey68)) + dark: Compatibility.isDeviceIdiomVision() ? .white : GlobalTokens.neutralColor(.grey68)) case .foregroundDisabled1: return UIColor(light: GlobalTokens.neutralColor(.grey74), - dark: GlobalTokens.neutralColor(.grey36)) + dark: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.8) : GlobalTokens.neutralColor(.grey36)) case .foregroundDisabled2: return UIColor(light: GlobalTokens.neutralColor(.white), dark: GlobalTokens.neutralColor(.grey18)) case .foregroundOnColor: return UIColor(light: GlobalTokens.neutralColor(.white), - dark: GlobalTokens.neutralColor(.black)) + dark: Compatibility.isDeviceIdiomVision() ? .white : GlobalTokens.neutralColor(.black)) case .brandForegroundTint: return UIColor(light: GlobalTokens.brandColor(.comm60), dark: GlobalTokens.brandColor(.comm130)) @@ -254,32 +254,32 @@ extension FluentTheme { dark: GlobalTokens.neutralColor(.white)) case .background1: return UIColor(light: GlobalTokens.neutralColor(.white), - dark: GlobalTokens.neutralColor(.black), - darkElevated: GlobalTokens.neutralColor(.grey4)) + dark: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.black), + darkElevated: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey4)) case .background1Pressed: return UIColor(light: GlobalTokens.neutralColor(.grey88), - dark: GlobalTokens.neutralColor(.grey18), - darkElevated: GlobalTokens.neutralColor(.grey18)) + dark: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey18), + darkElevated: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey18)) case .background1Selected: return UIColor(light: GlobalTokens.neutralColor(.grey92), dark: GlobalTokens.neutralColor(.grey14), darkElevated: GlobalTokens.neutralColor(.grey14)) case .background2: return UIColor(light: GlobalTokens.neutralColor(.white), - dark: GlobalTokens.neutralColor(.grey12), - darkElevated: GlobalTokens.neutralColor(.grey16)) + dark: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey12), + darkElevated: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey16)) case .background2Pressed: return UIColor(light: GlobalTokens.neutralColor(.grey88), - dark: GlobalTokens.neutralColor(.grey30), - darkElevated: GlobalTokens.neutralColor(.grey30)) + dark: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey30), + darkElevated: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey30)) case .background2Selected: return UIColor(light: GlobalTokens.neutralColor(.grey92), dark: GlobalTokens.neutralColor(.grey26), darkElevated: GlobalTokens.neutralColor(.grey26)) case .background3: return UIColor(light: GlobalTokens.neutralColor(.white), - dark: GlobalTokens.neutralColor(.grey16), - darkElevated: GlobalTokens.neutralColor(.grey20)) + dark: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey16), + darkElevated: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey20)) case .background3Pressed: return UIColor(light: GlobalTokens.neutralColor(.grey88), dark: GlobalTokens.neutralColor(.grey34), @@ -290,8 +290,8 @@ extension FluentTheme { darkElevated: GlobalTokens.neutralColor(.grey30)) case .background4: return UIColor(light: GlobalTokens.neutralColor(.grey98), - dark: GlobalTokens.neutralColor(.grey20), - darkElevated: GlobalTokens.neutralColor(.grey24)) + dark: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey20), + darkElevated: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey24)) case .background4Pressed: return UIColor(light: GlobalTokens.neutralColor(.grey86), dark: GlobalTokens.neutralColor(.grey38), @@ -302,12 +302,12 @@ extension FluentTheme { darkElevated: GlobalTokens.neutralColor(.grey34)) case .background5: return UIColor(light: GlobalTokens.neutralColor(.grey94), - dark: GlobalTokens.neutralColor(.grey24), - darkElevated: GlobalTokens.neutralColor(.grey28)) + dark: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.2) : GlobalTokens.neutralColor(.grey24), + darkElevated: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.2) : GlobalTokens.neutralColor(.grey28)) case .background5Pressed: return UIColor(light: GlobalTokens.neutralColor(.grey82), - dark: GlobalTokens.neutralColor(.grey42), - darkElevated: GlobalTokens.neutralColor(.grey42)) + dark: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey42), + darkElevated: Compatibility.isDeviceIdiomVision() ? .black.withAlphaComponent(0.1) : GlobalTokens.neutralColor(.grey42)) case .background5Selected: return UIColor(light: GlobalTokens.neutralColor(.grey86), dark: GlobalTokens.neutralColor(.grey38), @@ -352,8 +352,8 @@ extension FluentTheme { dark: GlobalTokens.neutralColor(.grey20)) case .backgroundCanvas: return UIColor(light: GlobalTokens.neutralColor(.grey96), - dark: GlobalTokens.neutralColor(.grey8), - darkElevated: GlobalTokens.neutralColor(.grey14)) + dark: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey8), + darkElevated: Compatibility.isDeviceIdiomVision() ? .clear : GlobalTokens.neutralColor(.grey14)) case .backgroundDarkStatic: return UIColor(light: GlobalTokens.neutralColor(.grey14), dark: GlobalTokens.neutralColor(.grey24), @@ -372,15 +372,15 @@ extension FluentTheme { darkElevated: GlobalTokens.neutralColor(.grey42)) case .stroke1: return UIColor(light: GlobalTokens.neutralColor(.grey82), - dark: GlobalTokens.neutralColor(.grey30), - darkElevated: GlobalTokens.neutralColor(.grey36)) + dark: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.4) : GlobalTokens.neutralColor(.grey30), + darkElevated: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.4) : GlobalTokens.neutralColor(.grey36)) case .stroke1Pressed: return UIColor(light: GlobalTokens.neutralColor(.grey70), dark: GlobalTokens.neutralColor(.grey48)) case .stroke2: return UIColor(light: GlobalTokens.neutralColor(.grey88), - dark: GlobalTokens.neutralColor(.grey24), - darkElevated: GlobalTokens.neutralColor(.grey30)) + dark: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.5) : GlobalTokens.neutralColor(.grey24), + darkElevated: Compatibility.isDeviceIdiomVision() ? .white.withAlphaComponent(0.5) : GlobalTokens.neutralColor(.grey30)) case .strokeAccessible: return UIColor(light: GlobalTokens.neutralColor(.grey38), dark: GlobalTokens.neutralColor(.grey62), diff --git a/ios/FluentUI/Drawer/DrawerController.swift b/ios/FluentUI/Drawer/DrawerController.swift index d99035e023..fbc93401aa 100644 --- a/ios/FluentUI/Drawer/DrawerController.swift +++ b/ios/FluentUI/Drawer/DrawerController.swift @@ -633,7 +633,7 @@ open class DrawerController: UIViewController, TokenizedControlInternal { return } - let screenHeight: CGFloat = window.screen.bounds.height + let screenHeight: CGFloat = window.bounds.height if preferredMaximumExpansionHeight != -1 && preferredMaximumExpansionHeight < screenHeight && preferredMaximumExpansionHeight >= originalDrawerHeight { @@ -1005,7 +1005,9 @@ extension DrawerController: UIViewControllerTransitioningDelegate { return drawerPresentationController case .popover: let presentationController = UIPopoverPresentationController(presentedViewController: presented, presenting: presenting) +#if os(iOS) presentationController.backgroundColor = backgroundColor +#endif // os(iOS) presentationController.permittedArrowDirections = permittedArrowDirections presentationController.delegate = self diff --git a/ios/FluentUI/Drawer/DrawerPresentationController.swift b/ios/FluentUI/Drawer/DrawerPresentationController.swift index 4affeb871b..54f83a9b5f 100644 --- a/ios/FluentUI/Drawer/DrawerPresentationController.swift +++ b/ios/FluentUI/Drawer/DrawerPresentationController.swift @@ -193,7 +193,7 @@ class DrawerPresentationController: UIPresentationController { return presentationOrigin } - let containerBounds = containerView?.bounds ?? (sourceViewController.view.window?.screen.bounds ?? .zero) + let containerBounds = containerView?.bounds ?? (sourceViewController.view.window?.bounds ?? .zero) switch presentationDirection { case .down: var controller = sourceViewController diff --git a/ios/FluentUI/HUD/HeadsUpDisplay.swift b/ios/FluentUI/HUD/HeadsUpDisplay.swift index 50025563de..599492edbf 100644 --- a/ios/FluentUI/HUD/HeadsUpDisplay.swift +++ b/ios/FluentUI/HUD/HeadsUpDisplay.swift @@ -100,13 +100,13 @@ public struct HeadsUpDisplay: View, TokenizedControlView { .cornerRadius(tokenSet[.cornerRadius].float) ) .contentShape(Rectangle()) - .onChange(of: isPresented, perform: { present in + .onChange_iOS17(of: isPresented) { present in if present { presentAnimated() } else { dismissAnimated() } - }) + } .onAnimationComplete(for: presentationScaleFactor) { resetScaleFactor() } diff --git a/ios/FluentUI/Navigation/NavigationBar.swift b/ios/FluentUI/Navigation/NavigationBar.swift index 19735531a9..14a5c1fcb4 100644 --- a/ios/FluentUI/Navigation/NavigationBar.swift +++ b/ios/FluentUI/Navigation/NavigationBar.swift @@ -626,7 +626,7 @@ open class NavigationBar: UINavigationBar, TokenizedControlInternal, TwoLineTitl func updateColors(for navigationItem: UINavigationItem?) { let color = navigationItem?.navigationBarColor(fluentTheme: tokenSet.fluentTheme) - let shouldHideRegularTitle: Bool = (style == .gradient) && usesLeadingTitle + let shouldHideRegularTitle: Bool = (style == .gradient || color?.resolvedColor(with: traitCollection) == .clear) && usesLeadingTitle switch style { case .primary, .default, .custom: diff --git a/ios/FluentUI/Navigation/NavigationController.swift b/ios/FluentUI/Navigation/NavigationController.swift index 56d0640b83..1808421049 100644 --- a/ios/FluentUI/Navigation/NavigationController.swift +++ b/ios/FluentUI/Navigation/NavigationController.swift @@ -70,12 +70,14 @@ open class NavigationController: UINavigationController { open override func viewDidLoad() { super.viewDidLoad() +#if os(iOS) // We tap into the NavController's pop gesture to coordinate our own NavBar's content alongside the native transition if let popGesture = interactivePopGestureRecognizer { popGesture.delegate = nil popGesture.removeTarget(nil, action: nil) popGesture.addTarget(self, action: #selector(navigationPopScreenPanGestureRecognizerRecognized)) } +#endif // os(iOS) super.delegate = self @@ -140,7 +142,9 @@ open class NavigationController: UINavigationController { func updateNavigationBar(for viewController: UIViewController) { msfNavigationBar.update(with: viewController.navigationItem) viewController.navigationItem.accessorySearchBar?.navigationController = self +#if os(iOS) setNeedsStatusBarAppearanceUpdate() +#endif // os(iOS) if let backgroundColor = msfNavigationBar.backgroundView.backgroundColor { transitionAnimator.tintColor = backgroundColor } @@ -175,6 +179,7 @@ open class NavigationController: UINavigationController { return viewController?.navigationItem.accessorySearchBar?.isActive == true } +#if os(iOS) /// Secondary target for the default InteractivePopGestureRecognizer /// Used to handle the case of a cancelled pop gesture /// @@ -207,6 +212,7 @@ open class NavigationController: UINavigationController { return } } +#endif // os(iOS) } // MARK: - NavigationController: UINavigationControllerDelegate diff --git a/ios/FluentUI/Navigation/SearchBar/SearchBar.swift b/ios/FluentUI/Navigation/SearchBar/SearchBar.swift index 104dc0fc8a..72871f98fa 100644 --- a/ios/FluentUI/Navigation/SearchBar/SearchBar.swift +++ b/ios/FluentUI/Navigation/SearchBar/SearchBar.swift @@ -89,6 +89,9 @@ open class SearchBar: UIView, TokenizedControlInternal { textField.accessibilityTraits = .searchField textField.addTarget(self, action: #selector(searchTextFieldValueDidChange(_:)), for: .editingChanged) textField.showsLargeContentViewer = true + if #available(iOS 17, *) { + textField.hoverStyle = nil + } return textField }() @@ -104,6 +107,9 @@ open class SearchBar: UIView, TokenizedControlInternal { let backgroundView = UIView() backgroundView.backgroundColor = tokenSet[.backgroundColor].uiColor backgroundView.layer.cornerRadius = tokenSet[.searchTextFieldCornerRadius].float + if #available(iOS 17, *) { + backgroundView.hoverStyle = UIHoverStyle(shape: .capsule) + } return backgroundView }() @@ -119,6 +125,9 @@ open class SearchBar: UIView, TokenizedControlInternal { let preview = UITargetedPreview(view: button) return UIPointerStyle(effect: .lift(preview)) } + if #available(iOS 17, *) { + clearButton.hoverStyle = UIHoverStyle(shape: .circle) + } return clearButton }() @@ -131,6 +140,9 @@ open class SearchBar: UIView, TokenizedControlInternal { button.alpha = 0.0 button.showsLargeContentViewer = true button.isPointerInteractionEnabled = true + if #available(iOS 17, *) { + button.hoverStyle = UIHoverStyle(shape: .capsule) + } return button }() diff --git a/ios/FluentUI/Navigation/SearchBar/SearchBarTokenSet.swift b/ios/FluentUI/Navigation/SearchBar/SearchBarTokenSet.swift index 867c98e3f7..b15df6f448 100644 --- a/ios/FluentUI/Navigation/SearchBar/SearchBarTokenSet.swift +++ b/ios/FluentUI/Navigation/SearchBar/SearchBarTokenSet.swift @@ -141,7 +141,7 @@ public class SearchBarTokenSet: ControlTokenSet { } }) case .searchTextFieldCornerRadius: - return .float({ 10.0 }) + return .float({ Compatibility.isDeviceIdiomVision() ? SearchBarTokenSet.searchTextFieldBackgroundHeight / 2 : 10.0 }) case .font: return .uiFont({ theme.typography(.body1) }) } @@ -154,7 +154,7 @@ public class SearchBarTokenSet: ControlTokenSet { // MARK: Constants extension SearchBarTokenSet { - static let searchTextFieldBackgroundHeight: CGFloat = GlobalTokens.spacing(.size360) + static let searchTextFieldBackgroundHeight: CGFloat = Compatibility.isDeviceIdiomVision() ? 44 : GlobalTokens.spacing(.size360) static let searchIconImageViewDimension: CGFloat = GlobalTokens.spacing(.size200) static let searchIconInset: CGFloat = GlobalTokens.spacing(.size100) static let searchTextFieldLeadingInset: CGFloat = GlobalTokens.spacing(.size100) diff --git a/ios/FluentUI/Navigation/Shy Header/ShyHeaderController.swift b/ios/FluentUI/Navigation/Shy Header/ShyHeaderController.swift index 8e87bab50a..b64b8b892a 100644 --- a/ios/FluentUI/Navigation/Shy Header/ShyHeaderController.swift +++ b/ios/FluentUI/Navigation/Shy Header/ShyHeaderController.swift @@ -24,7 +24,7 @@ class ShyHeaderController: UIViewController { private let contentContainerView: UIView = { // within the gesture-based configuration, houses the contentViewController.view let contentContainerView = UIView() - contentContainerView.backgroundColor = UIColor(red: 0.20, green: 0.20, blue: 0.20, alpha: 1.00) + contentContainerView.backgroundColor = Compatibility.isDeviceIdiomVision() ? .clear : UIColor(red: 0.20, green: 0.20, blue: 0.20, alpha: 1.00) return contentContainerView }() diff --git a/ios/FluentUI/Navigation/Views/AvatarTitleView.swift b/ios/FluentUI/Navigation/Views/AvatarTitleView.swift index cf4ec12c7d..835bb5744c 100644 --- a/ios/FluentUI/Navigation/Views/AvatarTitleView.swift +++ b/ios/FluentUI/Navigation/Views/AvatarTitleView.swift @@ -238,6 +238,10 @@ class AvatarTitleView: UIView, TokenizedControlInternal, TwoLineTitleViewDelegat titleButton.showsLargeContentViewer = true + if #available(iOS 17, *) { + titleButton.hoverStyle = nil + } + updateAvatarViewPointerInteraction() } diff --git a/ios/FluentUI/Notification/FluentNotification.swift b/ios/FluentUI/Notification/FluentNotification.swift index 96f94c86fa..bb4d6ba061 100644 --- a/ios/FluentUI/Notification/FluentNotification.swift +++ b/ios/FluentUI/Notification/FluentNotification.swift @@ -246,6 +246,9 @@ public struct FluentNotification: View, TokenizedControlView { .hoverEffect() }) button +#if os(visionOS) + .buttonStyle(.borderless) +#endif // os(visionOS) .layoutPriority(1) } .onSizeChange { newSize in diff --git a/ios/FluentUI/Pill Button Bar/PillButton.swift b/ios/FluentUI/Pill Button Bar/PillButton.swift index c7631eb230..58bb113506 100644 --- a/ios/FluentUI/Pill Button Bar/PillButton.swift +++ b/ios/FluentUI/Pill Button Bar/PillButton.swift @@ -124,7 +124,11 @@ open class PillButton: UIButton, TokenizedControlInternal { self?.updateAppearance() } +#if os(iOS) + // On vision, let the parent set the radius because the pill button height is not self-determined. + // The proper solution may be to keep the corner radius updated to current height * 0.5 in layoutSubviews. layer.cornerRadius = PillButton.cornerRadius +#endif // os(iOS) clipsToBounds = true layer.cornerCurve = .continuous diff --git a/ios/FluentUI/Tab Bar/SideTabBarTokenSet.swift b/ios/FluentUI/Tab Bar/SideTabBarTokenSet.swift index 5e8a534f35..6fb31def3e 100644 --- a/ios/FluentUI/Tab Bar/SideTabBarTokenSet.swift +++ b/ios/FluentUI/Tab Bar/SideTabBarTokenSet.swift @@ -69,7 +69,7 @@ extension SideTabBarTokenSet { static let avatarViewSafeTopSpacing: CGFloat = 18.0 /// The minimum spacing for the avatar. - static let avatarViewMinTopSpacing: CGFloat = GlobalTokens.spacing(.size360) + static let avatarViewMinTopSpacing: CGFloat = Compatibility.isDeviceIdiomVision() ? 34 : GlobalTokens.spacing(.size360) /// The spacing for the avatar StackView. static let avatarViewTopStackViewSpacing: CGFloat = GlobalTokens.spacing(.size360) @@ -78,7 +78,7 @@ extension SideTabBarTokenSet { static let bottomStackViewSafeSpacing: CGFloat = 14.0 /// The min spacing for the bottom StackView. - static let bottomStackViewMinSpacing: CGFloat = GlobalTokens.spacing(.size240) + static let bottomStackViewMinSpacing: CGFloat = Compatibility.isDeviceIdiomVision() ? GlobalTokens.spacing(.size360) : GlobalTokens.spacing(.size240) /// The padding for the badges on the top set of TabBarItems. static let badgeTopSectionPadding: CGFloat = GlobalTokens.spacing(.size20) diff --git a/ios/FluentUI/Tab Bar/TabBarItemView.swift b/ios/FluentUI/Tab Bar/TabBarItemView.swift index 5decb889bd..f095c1dcbe 100644 --- a/ios/FluentUI/Tab Bar/TabBarItemView.swift +++ b/ios/FluentUI/Tab Bar/TabBarItemView.swift @@ -99,6 +99,10 @@ class TabBarItemView: UIControl, TokenizedControlInternal { let pointerInteraction = UIPointerInteraction(delegate: self) addInteraction(pointerInteraction) + if #available(iOS 17, *) { + self.hoverStyle = UIHoverStyle(shape: showsTitle ? .capsule : .circle) + } + isAccessibilityElement = true updateAccessibilityLabel() accessibilityIdentifier = item.accessibilityIdentifier diff --git a/ios/FluentUI/Tab Bar/TabBarView.swift b/ios/FluentUI/Tab Bar/TabBarView.swift index 05ba1584b2..8025444b7f 100644 --- a/ios/FluentUI/Tab Bar/TabBarView.swift +++ b/ios/FluentUI/Tab Bar/TabBarView.swift @@ -96,7 +96,9 @@ open class TabBarView: UIView, TokenizedControlInternal { contain(view: backgroundView) stackView.translatesAutoresizingMaskIntoConstraints = false - contain(view: stackView, withInsets: .zero, respectingSafeAreaInsets: true) + + let stackViewInsets = Compatibility.isDeviceIdiomVision() ? UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0) : .zero + contain(view: stackView, withInsets: stackViewInsets, respectingSafeAreaInsets: true) topBorderLine.translatesAutoresizingMaskIntoConstraints = false addSubview(topBorderLine) diff --git a/ios/FluentUI/Table View/TableViewCell.swift b/ios/FluentUI/Table View/TableViewCell.swift index bfdb997a64..61294cc2b0 100644 --- a/ios/FluentUI/Table View/TableViewCell.swift +++ b/ios/FluentUI/Table View/TableViewCell.swift @@ -997,7 +997,7 @@ open class TableViewCell: UITableViewCell, TokenizedControlInternal { } } /// Style describing whether or not the cell's bottom separator should be visible and how wide it should extend - @objc open var bottomSeparatorType: SeparatorType = .inset { + @objc open var bottomSeparatorType: SeparatorType = Compatibility.isDeviceIdiomVision() ? .full : .inset { didSet { if bottomSeparatorType != oldValue { updateSeparator(bottomSeparator, with: bottomSeparatorType) @@ -1764,7 +1764,7 @@ open class TableViewCell: UITableViewCell, TokenizedControlInternal { customAccessoryViewExtendsToEdge = false topSeparatorType = .none - bottomSeparatorType = .inset + bottomSeparatorType = Compatibility.isDeviceIdiomVision() ? .full : .inset isEnabled = true isInSelectionMode = false diff --git a/ios/FluentUI/Table View/TableViewCellTokenSet.swift b/ios/FluentUI/Table View/TableViewCellTokenSet.swift index 8a10e41cb1..0405d46f0f 100644 --- a/ios/FluentUI/Table View/TableViewCellTokenSet.swift +++ b/ios/FluentUI/Table View/TableViewCellTokenSet.swift @@ -193,7 +193,7 @@ public class TableViewCellTokenSet: ControlTokenSet { extension TableViewCellTokenSet { /// The minimum TableViewCell height; the height of a TableViewCell with one line of text. - static let oneLineMinHeight: CGFloat = GlobalTokens.spacing(.size480) + static let oneLineMinHeight: CGFloat = Compatibility.isDeviceIdiomVision() ? 60.0 : GlobalTokens.spacing(.size480) /// The height of a TableViewCell with two lines of text. static let twoLineMinHeight: CGFloat = 64.0 @@ -250,10 +250,10 @@ extension TableViewCellTokenSet { static let customAccessoryViewMinVerticalMargin: CGFloat = 6.0 /// The vertical margin for the label when it has one or three lines. - static let defaultLabelVerticalMarginForOneAndThreeLines: CGFloat = 11.0 + static let defaultLabelVerticalMarginForOneAndThreeLines: CGFloat = Compatibility.isDeviceIdiomVision() ? 19.0 : 11.0 /// The vertical margin for the label when it has two lines. - static let labelVerticalMarginForTwoLines: CGFloat = GlobalTokens.spacing(.size120) + static let labelVerticalMarginForTwoLines: CGFloat = Compatibility.isDeviceIdiomVision() ? GlobalTokens.spacing(.size200) : GlobalTokens.spacing(.size120) /// The vertical spacing for the label. static let labelVerticalSpacing: CGFloat = GlobalTokens.spacing(.sizeNone) diff --git a/ios/FluentUI/Table View/TableViewHeaderFooterView.swift b/ios/FluentUI/Table View/TableViewHeaderFooterView.swift index 5884d817da..9a6fe02c77 100644 --- a/ios/FluentUI/Table View/TableViewHeaderFooterView.swift +++ b/ios/FluentUI/Table View/TableViewHeaderFooterView.swift @@ -7,11 +7,13 @@ import UIKit // MARK: TableViewHeaderFooterViewDelegate +#if os(iOS) @objc(MSFTableViewHeaderFooterViewDelegate) public protocol TableViewHeaderFooterViewDelegate: AnyObject { /// Returns: true if the interaction with the header view should be allowed; false if the interaction should not be allowed. @objc optional func headerFooterView(_ headerFooterView: TableViewHeaderFooterView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool } +#endif // os(iOS) // MARK: - TableViewHeaderFooterView @@ -123,7 +125,9 @@ open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedCont } } +#if os(iOS) @objc public weak var delegate: TableViewHeaderFooterViewDelegate? +#endif // os(iOS) open override var intrinsicContentSize: CGSize { return CGSize( @@ -386,7 +390,9 @@ open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedCont open override func prepareForReuse() { super.prepareForReuse() +#if os(iOS) delegate = nil +#endif // os(iOS) accessoryButtonStyle = .regular titleNumberOfLines = 1 @@ -433,7 +439,11 @@ open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedCont let titleFont = tokenSet[.textFont].uiFont titleView.font = titleFont // offset text container to center its content +#if os(iOS) let scale = window.rootViewController?.view.contentScaleFactor ?? window.screen.scale +#elseif os(visionOS) + let scale: CGFloat = 2.0 +#endif // os(visionOS) let offset = (floor((abs(titleFont.leading) / 2) * scale) / scale) / 2 titleView.textContainerInset.top = offset titleView.textContainerInset.bottom = -offset @@ -472,6 +482,9 @@ open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedCont let button = UIButton(type: .system) button.setTitle(title, for: .normal) button.addTarget(self, action: #selector(handleAccessoryButtonTapped), for: .touchUpInside) + if #available(iOS 17, *) { + button.hoverStyle = UIHoverStyle(shape: .capsule) + } return button } @@ -492,10 +505,12 @@ open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedCont // MARK: - TableViewHeaderFooterView: UITextViewDelegate extension TableViewHeaderFooterView: UITextViewDelegate { +#if os(iOS) public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { // If the delegate function is not set, return `true` to let the default interaction handle this return delegate?.headerFooterView?(self, shouldInteractWith: URL, in: characterRange, interaction: interaction) ?? true } +#endif // os(iOS) } // MARK: - TableViewHeaderFooterTitleView diff --git a/ios/xcode/FluentUITests.xcconfig b/ios/xcode/FluentUITests.xcconfig index 11921efb31..32c8108368 100644 --- a/ios/xcode/FluentUITests.xcconfig +++ b/ios/xcode/FluentUITests.xcconfig @@ -10,5 +10,7 @@ INFOPLIST_FILE = FluentUI.Tests/Info.plist LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.FluentUITests PRODUCT_NAME = $(TARGET_NAME) +SUPPORTED_PLATFORMS = iphoneos iphonesimulator xros xrsimulator +SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO SWIFT_VERSION = 5.0 -TARGETED_DEVICE_FAMILY = 1,2 +TARGETED_DEVICE_FAMILY = 1,2,7 diff --git a/ios/xcode/FluentUI_common.xcconfig b/ios/xcode/FluentUI_common.xcconfig index 7d2b87f083..9ab0929fd3 100644 --- a/ios/xcode/FluentUI_common.xcconfig +++ b/ios/xcode/FluentUI_common.xcconfig @@ -48,7 +48,9 @@ GCC_WARN_UNUSED_FUNCTION = YES GCC_WARN_UNUSED_VARIABLE = YES IPHONEOS_DEPLOYMENT_TARGET = 15.0 SDKROOT = iphoneos +SUPPORTED_PLATFORMS = iphoneos iphonesimulator xros xrsimulator +SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO SWIFT_TREAT_WARNINGS_AS_ERRORS = YES -TARGETED_DEVICE_FAMILY = 1,2 +TARGETED_DEVICE_FAMILY = 1,2,7 VERSIONING_SYSTEM = apple-generic VERSION_INFO_PREFIX =