diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift index f61d63934..0f191de20 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift @@ -49,6 +49,7 @@ struct NotificationDemoView: View { @State var showFromBottom: Bool = true @State var showBackgroundGradient: Bool = false @State var useCustomTheme: Bool = false + @State var verticalOffset: CGFloat = 0.0 @ObservedObject var fluentTheme: FluentTheme = .shared let customTheme: FluentTheme = { let foregroundColor = UIColor(light: GlobalTokens.sharedColor(.lavender, .shade30), @@ -175,7 +176,7 @@ struct NotificationDemoView: View { Alert(title: Text("Button tapped")) }) - Button("Show") { + Button("Show Notification") { if isPresented == false { isPresented = true DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { @@ -183,99 +184,30 @@ struct NotificationDemoView: View { } } } - .fixedSize() - .padding() + .buttonStyle(FluentButtonStyle(style: .accent)) + .fixedSize() + .padding() - ScrollView { - Group { - Group { - VStack(spacing: 0) { - Text("Content") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.title) - Divider() - } - - TextField("Title", text: $title) - .autocapitalization(.none) - .disableAutocorrection(true) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - TextField("Message", text: $message) - .autocapitalization(.none) - .disableAutocorrection(true) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - TextField("Action Button Title", text: $actionButtonTitle) - .autocapitalization(.none) - .disableAutocorrection(true) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - FluentUIDemoToggle(titleKey: "Has Attributed Text: Strikethrough", isOn: $hasBlueStrikethroughAttribute) - FluentUIDemoToggle(titleKey: "Has Attributed Text: Large Red Papyrus Font", isOn: $hasLargeRedPapyrusFontAttribute) - FluentUIDemoToggle(titleKey: "Set image", isOn: $showImage) - FluentUIDemoToggle(titleKey: "Set trailing image", isOn: $showTrailingImage) - } - - Group { - VStack(spacing: 0) { - Text("Action") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.title) - Divider() - } - FluentUIDemoToggle(titleKey: "Has Action Button Action", isOn: $hasActionButtonAction) - FluentUIDemoToggle(titleKey: "Show Default Dismiss Button", isOn: $showDefaultDismissActionButton) - FluentUIDemoToggle(titleKey: "Has Message Action", isOn: $hasMessageAction) - } - - Group { - VStack(spacing: 0) { - Text("Style") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.title) - Divider() - } - - Picker(selection: $style, label: EmptyView()) { - Text(".primaryToast").tag(MSFNotificationStyle.primaryToast) - Text(".neutralToast").tag(MSFNotificationStyle.neutralToast) - Text(".primaryBar").tag(MSFNotificationStyle.primaryBar) - Text(".primaryOutlineBar").tag(MSFNotificationStyle.primaryOutlineBar) - Text(".neutralBar").tag(MSFNotificationStyle.neutralBar) - Text(".dangerToast").tag(MSFNotificationStyle.dangerToast) - Text(".warningToast").tag(MSFNotificationStyle.warningToast) - } - .labelsHidden() - .frame(maxWidth: .infinity, alignment: .leading) - - FluentUIDemoToggle(titleKey: "Override Tokens (Image Color and Horizontal Spacing)", isOn: $overrideTokens) - FluentUIDemoToggle(titleKey: "Flexible Width Toast", isOn: $isFlexibleWidthToast) - FluentUIDemoToggle(titleKey: "Present From Bottom", isOn: $showFromBottom) - FluentUIDemoToggle(titleKey: "Background Gradient", isOn: $showBackgroundGradient) - FluentUIDemoToggle(titleKey: "Custom theme", isOn: $useCustomTheme) - } - } - .padding() - } + notificationSettings } .presentNotification(isPresented: $isPresented, isBlocking: false) { FluentNotification(style: style, - isFlexibleWidthToast: $isFlexibleWidthToast.wrappedValue, - message: hasMessage ? message : nil, - attributedMessage: hasAttribute && hasMessage ? attributedMessage : nil, - isPresented: $isPresented, - title: hasTitle ? title : nil, - attributedTitle: hasAttribute && hasTitle ? attributedTitle : nil, - image: image, - trailingImage: trailingImage, - trailingImageAccessibilityLabel: trailingImageLabel, - actionButtonTitle: actionButtonTitle, - actionButtonAction: actionButtonAction, - showDefaultDismissActionButton: showDefaultDismissActionButton, - messageButtonAction: messageButtonAction, - showFromBottom: showFromBottom) + isFlexibleWidthToast: $isFlexibleWidthToast.wrappedValue, + message: hasMessage ? message : nil, + attributedMessage: hasAttribute && hasMessage ? attributedMessage : nil, + isPresented: $isPresented, + title: hasTitle ? title : nil, + attributedTitle: hasAttribute && hasTitle ? attributedTitle : nil, + image: image, + trailingImage: trailingImage, + trailingImageAccessibilityLabel: trailingImageLabel, + actionButtonTitle: actionButtonTitle, + actionButtonAction: actionButtonAction, + showDefaultDismissActionButton: showDefaultDismissActionButton, + messageButtonAction: messageButtonAction, + showFromBottom: showFromBottom, + verticalOffset: verticalOffset) .backgroundGradient(showBackgroundGradient ? backgroundGradient : nil) .overrideTokens($overrideTokens.wrappedValue ? notificationOverrideTokens : nil) } @@ -283,6 +215,81 @@ struct NotificationDemoView: View { .tint(Color(theme.color(.brandForeground1))) } + @ViewBuilder + var notificationSettings: some View { + FluentList { + FluentListSection("Content") { + LabeledContent { + TextField("Title", text: $title) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.trailing) + } label: { + Text("Title") + } + + LabeledContent { + TextField("Message", text: $message) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.trailing) + } label: { + Text("Message") + } + + LabeledContent { + TextField("Action Button Title", text: $actionButtonTitle) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.trailing) + } label: { + Text("Action Button Title") + } + + LabeledContent { + TextField("Offset", value: $verticalOffset, format: FloatingPointFormatStyle()) + .keyboardType(.numberPad) + .multilineTextAlignment(.trailing) + } label: { + Text("Vertical Offset") + } + + Toggle("Has Attributed Text: Strikethrough", isOn: $hasBlueStrikethroughAttribute) + Toggle("Has Attributed Text: Large Red Papyrus Font", isOn: $hasLargeRedPapyrusFontAttribute) + Toggle("Set image", isOn: $showImage) + Toggle("Set trailing image", isOn: $showTrailingImage) + } + + FluentListSection("Action") { + Toggle("Has Action Button Action", isOn: $hasActionButtonAction) + Toggle("Show Default Dismiss Button", isOn: $showDefaultDismissActionButton) + Toggle("Has Message Action", isOn: $hasMessageAction) + } + + FluentListSection("Style") { + + Picker(selection: $style, label: EmptyView()) { + Text(".primaryToast").tag(MSFNotificationStyle.primaryToast) + Text(".neutralToast").tag(MSFNotificationStyle.neutralToast) + Text(".primaryBar").tag(MSFNotificationStyle.primaryBar) + Text(".primaryOutlineBar").tag(MSFNotificationStyle.primaryOutlineBar) + Text(".neutralBar").tag(MSFNotificationStyle.neutralBar) + Text(".dangerToast").tag(MSFNotificationStyle.dangerToast) + Text(".warningToast").tag(MSFNotificationStyle.warningToast) + } + .labelsHidden() + .frame(maxWidth: .infinity, alignment: .leading) + + Toggle("Override Tokens (Image Color and Horizontal Spacing)", isOn: $overrideTokens) + Toggle("Flexible Width Toast", isOn: $isFlexibleWidthToast) + Toggle("Present From Bottom", isOn: $showFromBottom) + Toggle("Background Gradient", isOn: $showBackgroundGradient) + Toggle("Custom theme", isOn: $useCustomTheme) + } + } + .fluentListStyle(.insetGrouped) + } + private var backgroundGradient: LinearGradientInfo { // It's a lovely blue-to-pink gradient let colors: [UIColor] = [UIColor(light: GlobalTokens.sharedColor(.pink, .tint50), diff --git a/ios/FluentUI/Notification/FluentNotification.swift b/ios/FluentUI/Notification/FluentNotification.swift index 9b73060e7..5b3420e0b 100644 --- a/ios/FluentUI/Notification/FluentNotification.swift +++ b/ios/FluentUI/Notification/FluentNotification.swift @@ -80,6 +80,7 @@ public struct FluentNotification: View, TokenizedControlView { /// - showDefaultDismissActionButton: Bool to control if the Notification has a dismiss action by default. /// - messageButtonAction: Action to be dispatched by tapping on the toast/bar notification. /// - showFromBottom: Defines whether the notification shows from the bottom of the presenting view or the top. + /// - verticalOffset: How much to vertically offset the notification from its default position. public init(style: MSFNotificationStyle, shouldSelfPresent: Bool = true, isFlexibleWidthToast: Bool = false, @@ -95,7 +96,8 @@ public struct FluentNotification: View, TokenizedControlView { actionButtonAction: (() -> Void)? = nil, showDefaultDismissActionButton: Bool? = nil, messageButtonAction: (() -> Void)? = nil, - showFromBottom: Bool = true) { + showFromBottom: Bool = true, + verticalOffset: CGFloat = 0.0) { let state = MSFNotificationStateImpl(style: style, message: message, attributedMessage: attributedMessage, @@ -108,7 +110,8 @@ public struct FluentNotification: View, TokenizedControlView { actionButtonAction: actionButtonAction, showDefaultDismissActionButton: showDefaultDismissActionButton, messageButtonAction: messageButtonAction, - showFromBottom: showFromBottom) + showFromBottom: showFromBottom, + verticalOffset: verticalOffset) self.state = state self.shouldSelfPresent = shouldSelfPresent self.isFlexibleWidthToast = isFlexibleWidthToast && style.isToast @@ -355,7 +358,7 @@ public struct FluentNotification: View, TokenizedControlView { withAnimation(.spring(response: state.style.animationDurationForShow / 2.0, dampingFraction: state.style.animationDampingRatio, blendDuration: 0)) { - bottomOffset = 0 + bottomOffset = -state.verticalOffset opacity = 1 } } @@ -400,6 +403,7 @@ class MSFNotificationStateImpl: ControlState, MSFNotificationState { @Published var showDefaultDismissActionButton: Bool @Published var showFromBottom: Bool @Published var backgroundGradient: LinearGradientInfo? + @Published var verticalOffset: CGFloat /// Title to display in the action button on the trailing edge of the control. /// @@ -430,7 +434,8 @@ class MSFNotificationStateImpl: ControlState, MSFNotificationState { actionButtonAction: nil, showDefaultDismissActionButton: nil, messageButtonAction: nil, - showFromBottom: true) + showFromBottom: true, + verticalOffset: 0.0) } init(style: MSFNotificationStyle, @@ -445,7 +450,8 @@ class MSFNotificationStateImpl: ControlState, MSFNotificationState { actionButtonAction: (() -> Void)? = nil, showDefaultDismissActionButton: Bool? = nil, messageButtonAction: (() -> Void)? = nil, - showFromBottom: Bool = true) { + showFromBottom: Bool = true, + verticalOffset: CGFloat) { self.style = style self.message = message self.attributedMessage = attributedMessage @@ -459,6 +465,7 @@ class MSFNotificationStateImpl: ControlState, MSFNotificationState { self.messageButtonAction = messageButtonAction self.showFromBottom = showFromBottom self.showDefaultDismissActionButton = showDefaultDismissActionButton ?? style.isToast + self.verticalOffset = verticalOffset super.init() }