Remove legacy notification view (#980)

* Transition UIKit Demo Controller to use SwiftUI Notification

* Let UIKit size itself independently

* Remove UIKit implementation and resolve comments

* Move xcflielist and update podspec

* Move xcfilelist into Notification folder

* Move override tokens to modifiers, reorder files to alphabetize, and add comments

(cherry picked from commit 1d9b1c7dcd)
This commit is contained in:
Jeanie Huynh 2022-05-11 14:23:41 -07:00 коммит произвёл huwilkes
Родитель 13d4cc35b4
Коммит 9f30175d1a
10 изменённых файлов: 223 добавлений и 711 удалений

Просмотреть файл

@ -191,9 +191,8 @@ fi', :execution_position => :before_compile }
notification_ios.platform = :ios
notification_ios.dependency 'MicrosoftFluentUI/Obscurable_ios'
notification_ios.dependency 'MicrosoftFluentUI/Label_ios'
notification_ios.dependency 'MicrosoftFluentUI/Separator_ios'
notification_ios.preserve_paths = ["ios/FluentUI/Notification/Notification.resources.xcfilelist"]
notification_ios.source_files = ["ios/FluentUI/Notification/**/*.{swift,h}"]
notification_ios.preserve_paths = ["ios/FluentUI/Vnext/Notification/Notification.resources.xcfilelist"]
notification_ios.source_files = ["ios/FluentUI/Vnext/Notification/**/*.{swift,h}"]
end
s.subspec 'Obscurable_ios' do |obscurable_ios|

Просмотреть файл

@ -44,15 +44,6 @@ class NotificationViewDemoController: DemoController {
}
}
var delayForHiding: TimeInterval {
switch self {
case .primaryToast, .primaryToastWithImageAndTitle, .primaryBar, .primaryOutlineBar, .neutralBar:
return 2
default:
return .infinity
}
}
}
override func viewDidLoad() {
@ -72,59 +63,97 @@ class NotificationViewDemoController: DemoController {
container.addArrangedSubview(UIView())
}
addTitle(text: variant.displayText)
container.addArrangedSubview(createNotificationView(forVariant: variant))
container.addArrangedSubview(createButton(title: "Show", action: #selector(showNotificationView)))
let showButton = MSFButton(style: .secondary, size: .small, action: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.createNotificationView(forVariant: variant).showNotification(in: strongSelf.view) {
$0.hide(after: 3.0)
}
})
showButton.state.text = "Show"
container.addArrangedSubview(showButton)
container.alignment = .leading
}
}
private func createNotificationView(forVariant variant: Variant) -> NotificationView {
private func createNotificationView(forVariant variant: Variant) -> MSFNotification {
switch variant {
case .primaryToast:
return NotificationView(style: .primaryToast).setup(style: .primaryToast,
message: "Mail Archived",
actionTitle: "Undo",
action: { [weak self] in self?.showMessage("`Undo` tapped") })
let notification = MSFNotification(style: .primaryToast, message: "Mail Archived")
notification.state.actionButtonTitle = "Undo"
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Undo` tapped")
notification.hide()
}
return notification
case .primaryToastWithImageAndTitle:
return NotificationView(style: .primaryToast).setup(style: .primaryToast,
title: "Kat's iPhoneX",
message: "Listen to Emails • 7 mins",
image: UIImage(named: "play-in-circle-24x24"),
action: { [weak self] in self?.showMessage("`Dismiss` tapped") },
messageAction: { [weak self] in self?.showMessage("`Listen to emails` tapped") })
let notification = MSFNotification(style: .primaryToast, message: "Listen to Emails • 7 mins")
notification.state.title = "Kat's iPhoneX"
notification.state.image = UIImage(named: "play-in-circle-24x24")
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Dismiss` tapped")
notification.hide()
}
notification.state.messageButtonAction = { [weak self] in
self?.showMessage("`Listen to emails` tapped")
notification.hide()
}
return notification
case .neutralToast:
return NotificationView(style: .neutralToast).setup(style: .neutralToast,
message: "Some items require you to sign in to view them",
actionTitle: "Sign in",
action: { [weak self] in self?.showMessage("`Sign in` tapped") })
let notification = MSFNotification(style: .neutralToast, message: "Some items require you to sign in to view them")
notification.state.actionButtonTitle = "Sign in"
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Sign in` tapped")
notification.hide()
}
return notification
case .dangerToast:
return NotificationView(style: .dangerToast).setup(style: .dangerToast,
message: "There was a problem, and your recent changes may not have saved",
actionTitle: "Retry",
action: { [weak self] in self?.showMessage("`Retry` tapped") })
let notification = MSFNotification(style: .dangerToast, message: "There was a problem, and your recent changes may not have saved")
notification.state.actionButtonTitle = "Retry"
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Retry` tapped")
notification.hide()
}
return notification
case .warningToast:
return NotificationView(style: .warningToast).setup(style: .warningToast,
message: "Read Only",
action: { [weak self] in self?.showMessage("`Dismiss` tapped") })
let notification = MSFNotification(style: .warningToast, message: "Read Only")
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Dismiss` tapped")
notification.hide()
}
return notification
case .primaryBar:
return NotificationView(style: .primaryBar).setup(style: .primaryBar,
message: "Updating...")
let notification = MSFNotification(style: .primaryBar, message: "Updating...")
return notification
case .primaryOutlineBar:
return NotificationView(style: .primaryOutlineBar).setup(style: .primaryOutlineBar,
message: "Mail Sent")
let notification = MSFNotification(style: .primaryOutlineBar, message: "Mail Sent")
return notification
case .neutralBar:
return NotificationView(style: .neutralBar).setup(style: .neutralBar,
message: "No internet connection")
let notification = MSFNotification(style: .neutralBar, message: "No internet connection")
return notification
case .persistentBarWithAction:
return NotificationView(style: .neutralBar).setup(style: .neutralBar,
message: "This error can be taken action on with the action on the right.",
actionTitle: "Action",
action: { [weak self] in self?.showMessage("`Action` tapped") })
let notification = MSFNotification(style: .neutralBar, message: "This error can be taken action on with the action on the right.")
notification.state.actionButtonTitle = "Action"
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Action` tapped")
notification.hide()
}
return notification
case .persistentBarWithCancel:
return NotificationView(style: .neutralBar).setup(style: .neutralBar,
message: "This error can be tapped or dismissed with the icon to the right.",
action: { [weak self] in self?.showMessage("`Dismiss` tapped") },
messageAction: { [weak self] in self?.showMessage("`Dismiss` tapped") })
let notification = MSFNotification(style: .neutralBar, message: "This error can be tapped or dismissed with the icon to the right.")
notification.state.actionButtonAction = { [weak self] in
self?.showMessage("`Dismiss` tapped")
notification.hide()
}
notification.state.messageButtonAction = { [weak self] in
self?.showMessage("`Dismiss` tapped")
notification.hide()
}
return notification
}
}

Просмотреть файл

@ -36,16 +36,13 @@ struct NotificationDemoView: View {
let image = showImage ? UIImage(named: "play-in-circle-24x24") : nil
let actionButtonAction = hasActionButtonAction ? { showAlert = true } : nil
let messageButtonAction = hasMessageAction ? { showAlert = true } : nil
let dismissAction = { showAlert = true }
let notification = NotificationViewSwiftUI(style: style,
message: message,
title: title,
image: image,
actionButtonTitle: actionButtonTitle,
actionButtonAction: actionButtonAction,
messageButtonAction: messageButtonAction,
dismissAction: dismissAction)
let notification = FluentNotification(style: style,
message: message,
title: title,
image: image,
actionButtonTitle: actionButtonTitle,
actionButtonAction: actionButtonAction,
messageButtonAction: messageButtonAction)
VStack {
notification
@ -138,7 +135,6 @@ struct NotificationDemoView: View {
image: image,
actionButtonTitle: actionButtonTitle,
actionButtonAction: actionButtonAction,
messageButtonAction: messageButtonAction,
dismissAction: dismissAction)
messageButtonAction: messageButtonAction)
}
}

Просмотреть файл

@ -58,7 +58,7 @@
5314E0E625F012C00099271A /* UIViewController+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DA7B4232C33A80013E41B /* UIViewController+Navigation.swift */; };
5314E0E725F012C00099271A /* UINavigationItem+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD41C8BD22DD47120086F899 /* UINavigationItem+Navigation.swift */; };
5314E0EC25F012C40099271A /* NavigationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD41C88022DD13230086F899 /* NavigationAnimator.swift */; };
43488C46270FAD1300124C71 /* NotificationView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43488C44270FAD0200124C71 /* NotificationView_SwiftUI.swift */; };
43488C46270FAD1300124C71 /* FluentNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43488C44270FAD0200124C71 /* FluentNotification.swift */; };
4B53505F27F63E3F0033B47F /* NotificationModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B53505E27F63E3F0033B47F /* NotificationModifiers.swift */; };
436F6B4226F4926B00D18073 /* NotificationTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436F6B4026F4926B00D18073 /* MSFNotificationTokens.swift */; };
4BF01DA027B3A862005B32F2 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF01D9F27B3A861005B32F2 /* UIApplication+Extensions.swift */; };
@ -67,7 +67,6 @@
5314E0F225F012C80099271A /* ShyHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD41C87122DD13230086F899 /* ShyHeaderView.swift */; };
5314E0F325F012C80099271A /* ShyHeaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD41C87022DD13230086F899 /* ShyHeaderController.swift */; };
5314E0F825F012CB0099271A /* LargeTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD41C87A22DD13230086F899 /* LargeTitleView.swift */; };
5314E10125F012E60099271A /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B6617223A41E2900E801DD /* NotificationView.swift */; };
5314E10A25F014600099271A /* Obscurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53BCB0CD253A4E8C00620960 /* Obscurable.swift */; };
5314E11625F015EA0099271A /* PersonaBadgeViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BA27872319DC0D0001563C /* PersonaBadgeViewDataSource.swift */; };
5314E11725F015EA0099271A /* PersonaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46D3F922151D95F0029772C /* PersonaCell.swift */; };
@ -212,7 +211,7 @@
1168630222E131CF0088B302 /* TabBarItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarItemView.swift; sourceTree = "<group>"; };
1168630322E131CF0088B302 /* TabBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = "<group>"; };
118D9847230BBA2300BC0B72 /* TabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarItem.swift; sourceTree = "<group>"; };
43488C44270FAD0200124C71 /* NotificationView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView_SwiftUI.swift; sourceTree = "<group>"; };
43488C44270FAD0200124C71 /* FluentNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluentNotification.swift; sourceTree = "<group>"; };
497DC2D724185885008D86F8 /* PillButtonBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillButtonBar.swift; sourceTree = "<group>"; };
497DC2D824185885008D86F8 /* PillButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
4B53505E27F63E3F0033B47F /* NotificationModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModifiers.swift; sourceTree = "<group>"; };
@ -299,7 +298,6 @@
A5961FA2218A25D100E2A506 /* PopupMenuItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupMenuItemCell.swift; sourceTree = "<group>"; };
A5961FA4218A260500E2A506 /* PopupMenuSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupMenuSectionHeaderView.swift; sourceTree = "<group>"; };
A5961FA6218A2E4500E2A506 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
A5B6617223A41E2900E801DD /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
A5B87AF0211BD4380038C37C /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; };
A5B87AF3211E16360038C37C /* DrawerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawerController.swift; sourceTree = "<group>"; };
A5B87AF4211E16360038C37C /* DrawerTransitionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawerTransitionAnimator.swift; sourceTree = "<group>"; };
@ -515,9 +513,10 @@
4BA9B8B5279F2940007536F5 /* Notification */ = {
isa = PBXGroup;
children = (
43488C44270FAD0200124C71 /* FluentNotification.swift */,
4BBD651E2755FD9500A8B09E /* MSFNotificationView.swift */,
4B53505E27F63E3F0033B47F /* NotificationModifiers.swift */,
43488C44270FAD0200124C71 /* NotificationView_SwiftUI.swift */,
4BF01D9927B37CF8005B32F2 /* NotificationTokens.swift */,
);
path = Notification;
sourceTree = "<group>";
@ -759,15 +758,6 @@
path = "Popup Menu";
sourceTree = "<group>";
};
A5B6617123A41DEC00E801DD /* Notification */ = {
isa = PBXGroup;
children = (
A5B6617223A41E2900E801DD /* NotificationView.swift */,
4BF01D9927B37CF8005B32F2 /* NotificationTokens.swift */,
);
path = Notification;
sourceTree = "<group>";
};
A5B87AED211BCA4A0038C37C /* Extensions */ = {
isa = PBXGroup;
children = (
@ -847,7 +837,6 @@
5314DFF425F0069C0099271A /* IndeterminateProgressBar */,
5314DFF025F0042E0099271A /* Label */,
FD41C86D22DD12A20086F899 /* Navigation */,
A5B6617123A41DEC00E801DD /* Notification */,
5314DFF925F008F10099271A /* Obscurable */,
C77A04EB25F0469C001B3EB6 /* Other Cells */,
B426613C214731AC00E25423 /* People Picker */,
@ -1456,7 +1445,6 @@
5314E25D25F0238E0099271A /* UIFont+Extension.swift in Sources */,
C77A04B825F03DD1001B3EB6 /* String+Date.swift in Sources */,
4B53505F27F63E3F0033B47F /* NotificationModifiers.swift in Sources */,
5314E10125F012E60099271A /* NotificationView.swift in Sources */,
5314E14425F016860099271A /* PageCardPresenterController.swift in Sources */,
5314E11B25F015EA0099271A /* PersonaListView.swift in Sources */,
925D462026FD18B200179583 /* AliasTokens.swift in Sources */,
@ -1496,7 +1484,7 @@
5314E1A125F01A7C0099271A /* TableViewCellFileAccessoryView.swift in Sources */,
5314E1D625F01E4A0099271A /* SearchBar.swift in Sources */,
5314E0A825F010070099271A /* DrawerPresentationController.swift in Sources */,
43488C46270FAD1300124C71 /* NotificationView_SwiftUI.swift in Sources */,
43488C46270FAD1300124C71 /* FluentNotification.swift in Sources */,
5314E06425F00EFD0099271A /* CalendarViewMonthBannerView.swift in Sources */,
5314E18E25F0195C0099271A /* ShimmerLinesView.swift in Sources */,
80AECC21263339E3005AF2F3 /* BottomSheetController.swift in Sources */,

Просмотреть файл

@ -1,511 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
import UIKit
// MARK: - NotificationView
/**
`NotificationView` can be used to present a toast (`.primaryToast` and `.neutralToast` styles) or a notification bar (`.primaryBar`, `.primaryOutlineBar`, and `.neutralBar` styles) with information and actions at the bottom of the screen.
This view can be inserted into layout manually, if needed, or by using the `show` and `hide` methods which implement default presentation (with or without animation). Positioning is done using constraints.
By default only one visible toast is allowed in the app. When a new toast is shown, the previous one is hidden. This behavior can be changed via `allowsMultipleToasts` static property.
When `action` exists but no `actionTitle` is provided, a "cross" (X) image will be used for action button.
When used as a notification bar some functionality like `title`, `image` and actions are not supported. A convenience method `setupAsBar` can be used to initialize notification bar and assign only supported properties.
*/
@objc(MSFNotificationView)
open class NotificationView: UIView, TokenizedControlInternal, ControlConfiguration {
@objc public static var allowsMultipleToasts: Bool = false
private static var currentToast: NotificationView? {
didSet {
if allowsMultipleToasts {
currentToast = nil
}
}
}
@objc open private(set) var isShown: Bool = false
private var isHiding: Bool = false
private var completionsForHide: [() -> Void] = []
private var action: (() -> Void)?
private var messageAction: (() -> Void)?
private lazy var container: UIStackView = {
let container = UIStackView()
container.distribution = .fill
container.alignment = .center
container.axis = .horizontal
container.isAccessibilityElement = true
container.spacing = tokens.horizontalSpacing
container.layoutMargins = UIEdgeInsets(top: tokens.verticalPadding, left: 0, bottom: tokens.verticalPadding, right: 0)
return container
}()
private let textContainer: UIStackView = {
let textContainer = UIStackView()
textContainer.axis = .vertical
textContainer.setContentCompressionResistancePriority(.required, for: .vertical)
textContainer.setContentHuggingPriority(.required, for: .vertical)
return textContainer
}()
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
imageView.setContentHuggingPriority(.required, for: .horizontal)
return imageView
}()
private let titleLabel: Label = {
let titleLabel = Label(style: .button1)
titleLabel.numberOfLines = 0
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
titleLabel.setContentHuggingPriority(.required, for: .vertical)
return titleLabel
}()
private let messageLabel: Label = {
let messageLabel = Label(style: .subhead)
messageLabel.numberOfLines = 0
messageLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
messageLabel.setContentCompressionResistancePriority(.required, for: .vertical)
messageLabel.setContentHuggingPriority(.required, for: .vertical)
return messageLabel
}()
private let actionButton: UIButton = {
let actionButton = UIButton(type: .system)
actionButton.setContentCompressionResistancePriority(.required, for: .horizontal)
actionButton.setContentHuggingPriority(.required, for: .horizontal)
actionButton.titleLabel?.font = TextStyle.button1.font
actionButton.titleLabel?.adjustsFontForContentSizeCategory = true
return actionButton
}()
private let separator = Separator(style: .shadow, orientation: .horizontal)
private var hasSingleLineLayout: Bool {
return titleLabel.text?.isEmpty == true && messageLabel.frame.height == messageLabel.font.deviceLineHeight
}
private var constraintWhenHidden: NSLayoutConstraint!
private var constraintWhenShown: NSLayoutConstraint!
private var backgroundLayer = CALayer()
private var perimeterShadow = CALayer()
private var ambientShadow = CALayer()
@objc public init(style: MSFNotificationStyle) {
self.style = style
let tokens = NotificationTokens()
tokens.style = style
self.tokens = tokens
super.init(frame: .zero)
initialize()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func removeFromSuperview() {
super.removeFromSuperview()
isShown = false
if NotificationView.currentToast == self {
NotificationView.currentToast = nil
}
}
open override func layoutSubviews() {
super.layoutSubviews()
perimeterShadow.frame = bounds
perimeterShadow.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: tokens.cornerRadius).cgPath
ambientShadow.frame = bounds
ambientShadow.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: tokens.cornerRadius).cgPath
backgroundLayer.frame = bounds
}
@objc open func initialize() {
addSubview(separator)
separator.translatesAutoresizingMaskIntoConstraints = false
addSubview(container)
container.translatesAutoresizingMaskIntoConstraints = false
container.addArrangedSubview(imageView)
container.addArrangedSubview(textContainer)
textContainer.addArrangedSubview(titleLabel)
textContainer.addArrangedSubview(messageLabel)
container.addArrangedSubview(actionButton)
let horizontalPadding: CGFloat! = tokens.horizontalPadding
NSLayoutConstraint.activate([
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
separator.bottomAnchor.constraint(equalTo: topAnchor),
container.topAnchor.constraint(equalTo: topAnchor),
container.bottomAnchor.constraint(equalTo: bottomAnchor),
container.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: horizontalPadding),
container.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -horizontalPadding)
])
updateForStyle()
accessibilityElements = [container, actionButton]
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleMessageTap)))
actionButton.addTarget(self, action: #selector(handleActionButtonTap), for: .touchUpInside)
layer.insertSublayer(backgroundLayer, at: 0)
layer.insertSublayer(perimeterShadow, below: backgroundLayer)
layer.insertSublayer(ambientShadow, below: perimeterShadow)
}
/// `setup` is used to initialize the view before showing.
/// - Parameters:
/// - style: The style that defines presentation and functionality of the view.
/// - title: The title text that is shown on the top of the view (only supported in toasts).
/// - message: The message text that is shown below title (if present) or vertically centered in the view.
/// - image: The image that is shown at the leading edge of the view (only supported in toasts).
/// - actionTitle: The title for action on the trailing edge of the view.
/// - action: The closure to be called when action button is tapped by a user.
/// - messageAction: The closure to be called when the body of the view (except action button) is tapped by a user (only supported in toasts).
/// - Returns: Reference to this view that can be used for "chained" calling of `show`. Can be ignored.
@discardableResult
@objc open func setup(style: MSFNotificationStyle,
title: String = "",
message: String,
image: UIImage? = nil,
actionTitle: String = "",
action: (() -> Void)? = nil,
messageAction: (() -> Void)? = nil) -> Self {
let title = style.supportsTitle ? title : ""
let isTitleEmpty = title.isEmpty
let image = style.supportsImage ? image : nil
titleLabel.text = title
titleLabel.isHidden = isTitleEmpty
messageLabel.text = message
messageLabel.style = !isTitleEmpty ? .subhead : tokens.style.isToast ? .button1 : .subhead
imageView.image = image?.renderingMode == .automatic ? image?.withRenderingMode(.alwaysTemplate) : image
imageView.isHidden = image == nil
if action != nil || style.shouldAlwaysShowActionButton {
if actionTitle.isEmpty {
let actionImage = UIImage.staticImageNamed("dismiss-20x20")
actionImage?.accessibilityLabel = "Accessibility.Dismiss.Label".localized
actionButton.setImage(actionImage, for: .normal)
actionButton.setTitle(nil, for: .normal)
} else {
actionButton.setImage(nil, for: .normal)
actionButton.setTitle(actionTitle, for: .normal)
}
actionButton.isHidden = false
messageLabel.textAlignment = .natural
} else {
actionButton.isHidden = true
messageLabel.textAlignment = .center
}
self.action = action
self.messageAction = messageAction
updateAccessibility(title: title, message: message, hasMessageAction: messageAction != nil)
return self
}
/// `show` is used to present the view inside a container view: insert into layout and show with optional animation. Constraints are used for the view positioning.
/// - Parameters:
/// - view: The container view where this view will be presented.
/// - anchorView: The view used as the bottom anchor for presentation (notification view is always presented up from the anchor). When no anchor view is provided the bottom anchor of the container's safe area is used.
/// - animated: Indicates whether to use animation during presentation or not.
/// - completion: The closure to be called after presentation is completed. Can be used to call `hide` with a delay.
@objc open func show(in view: UIView, from anchorView: UIView? = nil, animated: Bool = true, completion: ((NotificationView) -> Void)? = nil) {
if isShown {
return
}
let style = tokens.style
let presentationOffset: CGFloat! = tokens.presentationOffset
if style.isToast, let currentToast = NotificationView.currentToast {
currentToast.hide(animated: animated) {
self.show(in: view, from: anchorView, animated: animated, completion: completion)
}
return
}
translatesAutoresizingMaskIntoConstraints = false
if let anchorView = anchorView, anchorView.superview == view {
view.insertSubview(self, belowSubview: anchorView)
} else {
view.addSubview(self)
}
let anchor = anchorView?.topAnchor ?? view.safeAreaLayoutGuide.bottomAnchor
constraintWhenHidden = topAnchor.constraint(equalTo: anchor)
constraintWhenShown = bottomAnchor.constraint(equalTo: anchor, constant: -presentationOffset)
var constraints = [NSLayoutConstraint]()
constraints.append(animated ? constraintWhenHidden : constraintWhenShown)
if style.needsFullWidth {
constraints.append(leadingAnchor.constraint(equalTo: view.leadingAnchor))
constraints.append(trailingAnchor.constraint(equalTo: view.trailingAnchor))
} else {
let offset = ((view.safeAreaLayoutGuide.layoutFrame.width - self.intrinsicContentSize.width) / 2) + presentationOffset
constraints.append(leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: offset))
constraints.append(trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -offset))
}
NSLayoutConstraint.activate(constraints)
isShown = true
if style.isToast {
NotificationView.currentToast = self
}
let completionForShow = { (_: Bool) in
UIAccessibility.post(notification: .layoutChanged, argument: self)
completion?(self)
}
if animated {
view.layoutIfNeeded()
UIView.animate(withDuration: style.animationDurationForShow, delay: 0, usingSpringWithDamping: style.animationDampingRatio, initialSpringVelocity: 0, animations: {
self.constraintWhenHidden.isActive = false
self.constraintWhenShown.isActive = true
view.layoutIfNeeded()
}, completion: completionForShow)
} else {
completionForShow(true)
}
}
/// `show` is used to present the view inside a container controller: insert into controller's view layout and show with optional animation. When container is a `UINavigationController` then its toolbar (if visible) is used as the bottom anchor for presentation. When container is `UITabBarController`, its tab bar is used as the anchor. Constraints are used for the view positioning.
/// - Parameters:
/// - controller: The container controller whose view will be used for this view's presentation.
/// - animated: Indicates whether to use animation during presentation or not.
/// - completion: The closure to be called after presentation is completed. Can be used to call `hide` with a delay.
@objc open func show(from controller: UIViewController, animated: Bool = true, completion: ((NotificationView) -> Void)? = nil) {
if isShown {
return
}
var anchorView: UIView?
if let controller = controller as? UINavigationController, !controller.isToolbarHidden {
anchorView = controller.toolbar
}
if let controller = controller as? UITabBarController {
anchorView = controller.tabBar
}
show(in: controller.view, from: anchorView, animated: animated, completion: completion)
}
/// `hide` is used to dismiss the presented view: hide with optional animation and remove from the container.
/// - Parameters:
/// - delay: The delay used for the start of dismissal. Default is 0.
/// - animated: Indicates whether to use animation during dismissal or not.
/// - completion: The closure to be called after dismissal is completed.
@objc open func hide(after delay: TimeInterval = 0, animated: Bool = true, completion: (() -> Void)? = nil) {
if !isShown || delay == .infinity {
return
}
if delay > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.hide(animated: animated, completion: completion)
}
return
}
if let completion = completion {
completionsForHide.append(completion)
}
let completionForHide = {
self.removeFromSuperview()
UIAccessibility.post(notification: .layoutChanged, argument: nil)
self.completionsForHide.forEach { $0() }
self.completionsForHide.removeAll()
}
if animated {
if !isHiding {
isHiding = true
UIView.animate(withDuration: tokens.style.animationDurationForHide, animations: {
self.constraintWhenShown.isActive = false
self.constraintWhenHidden.isActive = true
self.superview?.layoutIfNeeded()
}, completion: { _ in
self.isHiding = false
completionForHide()
})
}
} else {
completionForHide()
}
}
open override func didMoveToWindow() {
super.didMoveToWindow()
updateNotificationTokens()
updateWindowSpecificColors()
}
// MARK: - TokenizedControl
public typealias TokenType = NotificationTokens
public func overrideTokens(_ tokens: NotificationTokens?) -> Self {
overrideTokens = tokens
return self
}
var state: NotificationView { self }
/// Design token set for this control, to use in place of the control's default Fluent tokens.
var overrideTokens: NotificationTokens?
/// Style to draw the control.
public var style: MSFNotificationStyle {
didSet {
tokens.style = style
}
}
var tokens: NotificationTokens {
didSet {
if tokens.style != oldValue.style {
tokens.style = style
}
}
}
open override func sizeThatFits(_ size: CGSize) -> CGSize {
var suggestedWidth: CGFloat = size.width
var availableLabelWidth = suggestedWidth
if tokens.style.needsFullWidth {
if let windowWidth = window?.safeAreaLayoutGuide.layoutFrame.width {
availableLabelWidth = windowWidth
}
} else {
if let windowWidth = window?.frame.width {
suggestedWidth = windowWidth
}
// for iPad regular width size, notification toast might look too wide
if traitCollection.userInterfaceIdiom == .pad &&
traitCollection.horizontalSizeClass == .regular &&
traitCollection.preferredContentSizeCategory < .accessibilityMedium {
suggestedWidth = max(suggestedWidth / 2, 375.0)
} else {
suggestedWidth -= (safeAreaInsets.left + safeAreaInsets.right + 2 * tokens.presentationOffset)
}
suggestedWidth = ceil(suggestedWidth)
availableLabelWidth = suggestedWidth
}
availableLabelWidth -= (2 * tokens.horizontalPadding)
if !actionButton.isHidden {
let actionButtonSize = actionButton.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
availableLabelWidth -= (actionButtonSize.width + tokens.horizontalSpacing)
}
if !imageView.isHidden {
let imageSize = imageView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
availableLabelWidth -= (imageSize.width + tokens.horizontalSpacing)
}
var suggestedHeight: CGFloat
let messagelabelSize = messageLabel.systemLayoutSizeFitting(CGSize(width: availableLabelWidth, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultHigh)
suggestedHeight = messagelabelSize.height
// there are different veritcal padding depending on the text we show
// `tokens.verticalPaddingForOneLine` is used when only messagelabel is shown and all the text fits in oneline
// Otherwise, use `tokens.veritcalPadding` for top and bottom
var hasSingleLineLayout = false
if titleLabel.text?.isEmpty == true {
hasSingleLineLayout = (messagelabelSize.height == messageLabel.font.deviceLineHeight)
} else {
let titleLabelSize = titleLabel.systemLayoutSizeFitting(CGSize(width: availableLabelWidth, height: 0), withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultHigh)
suggestedHeight += titleLabelSize.height
}
let suggestedVerticalPadding: CGFloat! = hasSingleLineLayout ? tokens.verticalPaddingForOneLine : tokens.verticalPadding
suggestedHeight += 2 * suggestedVerticalPadding
suggestedHeight = ceil(max(suggestedHeight, hasSingleLineLayout ? tokens.minimumHeightForOneLine : tokens.minimumHeight))
container.layoutMargins = UIEdgeInsets(top: suggestedVerticalPadding, left: 0, bottom: suggestedVerticalPadding, right: 0)
return CGSize(width: suggestedWidth, height: suggestedHeight)
}
open override var intrinsicContentSize: CGSize {
return sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
}
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
self.updateWindowSpecificColors()
self.setNeedsDisplay()
}
private func updateForStyle() {
clipsToBounds = !tokens.style.needsSeparator
layer.masksToBounds = false
backgroundLayer.cornerRadius = tokens.cornerRadius
backgroundLayer.cornerCurve = .continuous
perimeterShadow.shadowColor = UIColor(dynamicColor: tokens.perimeterShadowColor).cgColor
perimeterShadow.shadowRadius = tokens.perimeterShadowBlur
perimeterShadow.shadowOffset = CGSize(width: tokens.perimeterShadowOffsetX, height: tokens.perimeterShadowOffsetY)
perimeterShadow.shadowOpacity = 1.0
ambientShadow.shadowColor = UIColor(dynamicColor: tokens.ambientShadowColor).cgColor
ambientShadow.shadowRadius = tokens.ambientShadowBlur
ambientShadow.shadowOffset = CGSize(width: tokens.ambientShadowOffsetX, height: tokens.ambientShadowOffsetY)
ambientShadow.shadowOpacity = 1.0
separator.isHidden = !tokens.style.needsSeparator
updateWindowSpecificColors()
}
private func updateNotificationTokens() {
let tokens = TokenResolver.tokens(for: self, fluentTheme: fluentTheme)
self.tokens = tokens
}
private func updateWindowSpecificColors() {
backgroundLayer.backgroundColor = UIColor(dynamicColor: tokens.backgroundColor).cgColor
let foregroundColor = UIColor(dynamicColor: tokens.foregroundColor)
imageView.tintColor = foregroundColor
titleLabel.textColor = foregroundColor
messageLabel.textColor = foregroundColor
actionButton.tintColor = foregroundColor
}
private func updateAccessibility(title: String, message: String, hasMessageAction: Bool) {
container.accessibilityLabel = "\(title), \(message)"
container.accessibilityTraits = hasMessageAction ? .button : .staticText
}
@objc private func handleActionButtonTap() {
hide(animated: true)
action?()
}
@objc private func handleMessageTap() {
guard let messageAction = messageAction else {
return
}
hide(animated: true)
messageAction()
}
}

Просмотреть файл

@ -32,50 +32,40 @@ import SwiftUI
/// Action to be dispatched by tapping on the toast/bar notification.
var messageButtonAction: (() -> Void)? { get set }
/// Action to be dispatched when dismissing toast/bar notification.
var dismissAction: (() -> Void)? { get set }
/// Design token set for this control, to use in place of the control's default Fluent tokens.
var overrideTokens: NotificationTokens? { get set }
}
/// View that represents the Notification.
public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
@Environment(\.fluentTheme) var fluentTheme: FluentTheme
@ObservedObject var state: MSFNotificationStateImpl
@Binding var isPresented: Bool
@State private var bottomOffsetForDismissedState: CGFloat = 0
@State private var bottomOffset: CGFloat = 0
let defaultTokens: NotificationTokens = .init()
var tokens: NotificationTokens {
let tokens = resolvedTokens
tokens.style = state.style
return tokens
}
public func overrideTokens(_ tokens: NotificationTokens?) -> NotificationViewSwiftUI {
state.overrideTokens = tokens
return self
}
public struct FluentNotification: View, ConfigurableTokenizedControl {
/// Creates the FluentNotification
/// - Parameters:
/// - style: `MSFNotificationStyle` enum value that defines the style of the Notification being presented.
/// - shouldSelfPresent: Whether the notification should present itself (SwiftUI environment) or externally (UIKit environment)
/// - message: Text for the main title area of the control. If there is a title, the message becomes subtext.
/// - isPresented: Controls whether the Notification is being presented.
/// - title: Optional text to draw above the message area.
/// - image: Optional icon to draw at the leading edge of the control.
/// - actionButtonTitle:Title to display in the action button on the trailing edge of the control.
/// - actionButtonAction: Action to be dispatched by the action button on the trailing edge of the control.
/// - messageButtonAction: Action to be dispatched by tapping on the toast/bar notification.
public init(style: MSFNotificationStyle,
shouldSelfPresent: Bool = true,
message: String,
isPresented: Binding<Bool>? = nil,
title: String = "",
image: UIImage? = nil,
actionButtonTitle: String = "",
actionButtonAction: (() -> Void)? = nil,
messageButtonAction: (() -> Void)? = nil,
dismissAction: (() -> Void)? = nil) {
messageButtonAction: (() -> Void)? = nil) {
let state = MSFNotificationStateImpl(style: style, message: message)
state.title = title
state.image = image
state.actionButtonTitle = actionButtonTitle
state.actionButtonAction = actionButtonAction
state.messageButtonAction = messageButtonAction
state.dismissAction = dismissAction
self.state = state
self.shouldSelfPresent = shouldSelfPresent
if let isPresented = isPresented {
_isPresented = isPresented
@ -85,64 +75,76 @@ public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
}
public var body: some View {
GeometryReader { geometryReader in
let width: CGFloat = {
let geometryReaderWidth = geometryReader.size.width
let isFullLength = state.style.isToast && horizontalSizeClass == .regular
return isFullLength ? geometryReaderWidth / 2 : geometryReaderWidth - (2 * tokens.presentationOffset)
}()
let cornerRadius = tokens.cornerRadius
let ambientShadowOffsetY = tokens.ambientShadowOffsetY
let geometryReaderLocalFrame = geometryReader.frame(in: .local)
if !shouldSelfPresent {
notification
} else {
GeometryReader { proxy in
let proposedSize = proxy.size
let proposedWidth = proposedSize.width
let calculatedNotificationWidth: CGFloat = {
let isHalfLength = state.style.isToast && horizontalSizeClass == .regular
return isHalfLength ? proposedWidth / 2 : proposedWidth - (2 * tokens.presentationOffset)
}()
innerContents
.onTapGesture {
if let messageAction = state.messageButtonAction, let dismissAction = state.dismissAction {
isPresented = false
dismissAction()
messageAction()
notification
.frame(width: calculatedNotificationWidth, alignment: .center)
.onChange(of: isPresented, perform: { present in
if present {
presentAnimated()
} else {
dismissAnimated()
}
})
.padding(.bottom, tokens.bottomPresentationPadding)
.onSizeChange { newSize in
bottomOffsetForDismissedState = newSize.height + (tokens.ambientShadowOffsetY / 2)
// Bottom offset is only updated when the notification isn't presented to account for the new notification height (if presented, offset doesn't need to be updated since it grows upward vertically)
if !isPresented {
bottomOffset = bottomOffsetForDismissedState
}
}
}
.frame(width: width, alignment: .bottom)
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.strokeBorder(Color(dynamicColor: tokens.outlineColor), lineWidth: tokens.outlineWidth)
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.fill(Color(dynamicColor: tokens.backgroundColor))
)
.shadow(color: Color(dynamicColor: tokens.ambientShadowColor),
radius: tokens.ambientShadowBlur,
x: tokens.ambientShadowOffsetX,
y: ambientShadowOffsetY)
.shadow(color: Color(dynamicColor: tokens.perimeterShadowColor),
radius: tokens.perimeterShadowBlur,
x: tokens.perimeterShadowOffsetX,
y: tokens.perimeterShadowOffsetY)
)
.onChange(of: isPresented, perform: { present in
if present {
presentAnimated()
} else {
dismissAnimated()
}
})
.padding(.bottom, tokens.bottomPresentationPadding)
.onSizeChange { newSize in
bottomOffsetForDismissedState = newSize.height + (ambientShadowOffsetY / 2)
// Bottom offset is only updated when the notification isn't presented to account for the new notification height (if presented, offset doesn't need to be updated since it grows upward vertically)
if !isPresented {
bottomOffset = bottomOffsetForDismissedState
}
}
.frame(maxHeight: .infinity,
alignment: .bottom)
.offset(y: bottomOffset)
.position(x: geometryReaderLocalFrame.midX,
y: geometryReaderLocalFrame.midY)
.offset(y: bottomOffset)
.frame(width: proposedWidth, height: proposedSize.height, alignment: .bottom)
}
}
}
@Environment(\.fluentTheme) var fluentTheme: FluentTheme
@ObservedObject var state: MSFNotificationStateImpl
let defaultTokens: NotificationTokens = .init()
var tokens: NotificationTokens {
let tokens = resolvedTokens
tokens.style = state.style
return tokens
}
@ViewBuilder
private var notification: some View {
innerContents
.background(
RoundedRectangle(cornerRadius: tokens.cornerRadius)
.strokeBorder(Color(dynamicColor: tokens.outlineColor), lineWidth: tokens.outlineWidth)
.background(
RoundedRectangle(cornerRadius: tokens.cornerRadius)
.fill(Color(dynamicColor: tokens.backgroundColor))
)
.shadow(color: Color(dynamicColor: tokens.ambientShadowColor),
radius: tokens.ambientShadowBlur,
x: tokens.ambientShadowOffsetX,
y: tokens.ambientShadowOffsetY)
.shadow(color: Color(dynamicColor: tokens.perimeterShadowColor),
radius: tokens.perimeterShadowBlur,
x: tokens.perimeterShadowOffsetX,
y: tokens.perimeterShadowOffsetY)
)
.onTapGesture {
if let messageAction = state.messageButtonAction {
isPresented = false
messageAction()
}
}
}
@ViewBuilder
private var image: some View {
if state.style.isToast {
@ -193,7 +195,7 @@ public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
@ViewBuilder
private var button: some View {
if let buttonAction = state.actionButtonAction, let actionTitle = state.actionButtonTitle, let dismissAction = state.dismissAction {
if let buttonAction = state.actionButtonAction, let actionTitle = state.actionButtonTitle {
let foregroundColor = tokens.foregroundColor
let horizontalPadding = tokens.horizontalPadding
let verticalPadding = tokens.verticalPadding
@ -201,7 +203,6 @@ public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
if actionTitle.isEmpty {
SwiftUI.Button(action: {
isPresented = false
dismissAction()
buttonAction()
}, label: {
Image("dismiss-20x20", bundle: FluentUIFramework.resourceBundle)
@ -213,7 +214,6 @@ public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
} else {
SwiftUI.Button(actionTitle) {
isPresented = false
dismissAction()
buttonAction()
}
.lineLimit(1)
@ -228,7 +228,12 @@ public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
@ViewBuilder
private var innerContents: some View {
if hasCenteredText {
textContainer
HStack {
Spacer()
textContainer
Spacer()
}
.frame(minHeight: tokens.minimumHeight)
} else {
HStack(spacing: tokens.horizontalSpacing) {
image
@ -266,6 +271,19 @@ public struct NotificationViewSwiftUI: View, ConfigurableTokenizedControl {
bottomOffset = bottomOffsetForDismissedState
}
}
@Environment(\.horizontalSizeClass) private var horizontalSizeClass: UserInterfaceSizeClass?
@Binding private var isPresented: Bool
@State private var bottomOffsetForDismissedState: CGFloat = 0
@State private var bottomOffset: CGFloat = 0
// When true, the notification view will take up all proposed space
// and automatically position itself within it.
// isPresented only works when shouldSelfPresent is true.
//
// When false, the view will have a fitting, flexible width and self-sized height.
// In this mode the notification should be positioned and presented externally.
private let shouldSelfPresent: Bool
}
class MSFNotificationStateImpl: NSObject, ControlConfiguration, MSFNotificationState {
@ -286,9 +304,6 @@ class MSFNotificationStateImpl: NSObject, ControlConfiguration, MSFNotificationS
/// Action to be dispatched by tapping on the toast/bar notification.
@Published public var messageButtonAction: (() -> Void)?
/// Action to be dispatched when dismissing toast/bar notification.
@Published public var dismissAction: (() -> Void)?
/// Design token set for this control, to use in place of the control's default Fluent tokens.
@Published var overrideTokens: NotificationTokens?
@ -308,8 +323,7 @@ class MSFNotificationStateImpl: NSObject, ControlConfiguration, MSFNotificationS
image: UIImage? = nil,
actionButtonTitle: String? = nil,
actionButtonAction: (() -> Void)? = nil,
messageButtonAction: (() -> Void)? = nil,
dismissAction: (() -> Void)? = nil) {
messageButtonAction: (() -> Void)? = nil) {
self.init(style: style, message: message)
self.title = title
@ -317,6 +331,5 @@ class MSFNotificationStateImpl: NSObject, ControlConfiguration, MSFNotificationS
self.actionButtonTitle = actionButtonTitle
self.actionButtonAction = actionButtonAction
self.messageButtonAction = messageButtonAction
self.dismissAction = dismissAction
}
}

Просмотреть файл

@ -15,7 +15,9 @@ import UIKit
/// - message: The primary text to display in the Notification.
@objc public init(style: MSFNotificationStyle,
message: String) {
notification = NotificationViewSwiftUI(style: style, message: message)
notification = FluentNotification(style: style,
shouldSelfPresent: false,
message: message)
super.init(AnyView(notification))
}
@ -24,7 +26,6 @@ import UIKit
}
// MARK: - Show/Hide Methods
public func showNotification(in view: UIView, completion: ((MSFNotification) -> Void)? = nil) {
guard self.view.window == nil else {
return
@ -52,12 +53,14 @@ import UIKit
var constraints = [NSLayoutConstraint]()
constraints.append(animated ? constraintWhenHidden : constraintWhenShown)
if style.needsFullWidth {
constraints.append(self.view.leadingAnchor.constraint(equalTo: view.leadingAnchor))
constraints.append(self.view.trailingAnchor.constraint(equalTo: view.trailingAnchor))
constraints.append(self.centerXAnchor.constraint(equalTo: view.centerXAnchor))
let isHalfLength = state.style.isToast && traitCollection.horizontalSizeClass == .regular
if isHalfLength {
constraints.append(self.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5))
} else {
constraints.append(self.view.centerXAnchor.constraint(equalTo: view.centerXAnchor))
constraints.append(self.view.widthAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.widthAnchor, constant: -2 * presentationOffset))
let padding = notification.tokens.presentationOffset
constraints.append(self.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -2 * padding))
}
NSLayoutConstraint.activate(constraints)
@ -71,7 +74,7 @@ import UIKit
}
if animated {
view.layoutIfNeeded()
view.layoutIfNeeded()
UIView.animate(withDuration: style.animationDurationForShow,
delay: 0,
usingSpringWithDamping: style.animationDampingRatio,
@ -87,21 +90,12 @@ import UIKit
}
@objc public func hide(after delay: TimeInterval = 0, completion: (() -> Void)? = nil) {
let hideDelay: TimeInterval = {
switch state.style {
case .primaryToast, .primaryBar, .primaryOutlineBar, .neutralBar:
return delay
case .neutralToast, .dangerToast, .warningToast:
return (delay == 0) ? delay : .infinity
}
}()
guard self.view.window != nil && constraintWhenHidden != nil && hideDelay != .infinity else {
guard self.window != nil && constraintWhenHidden != nil else {
return
}
guard hideDelay == 0 else {
DispatchQueue.main.asyncAfter(deadline: .now() + hideDelay) { [weak self] in
guard delay == 0 else {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.hide(completion: completion)
}
return
@ -134,7 +128,6 @@ import UIKit
}
// MARK: - Private variables
private static var allowsMultipleToasts: Bool = false
private static var currentToast: MSFNotification? {
didSet {
@ -149,6 +142,6 @@ import UIKit
private var completionsForHide: [() -> Void] = []
private var constraintWhenHidden: NSLayoutConstraint!
private var constraintWhenShown: NSLayoutConstraint!
private var notification: NotificationViewSwiftUI!
private var notification: FluentNotification!
private var isHiding: Bool = false
}

Просмотреть файл

@ -6,7 +6,6 @@
import SwiftUI
public extension View {
/// Presents a Notification on top of the modified View.
/// - Parameters:
/// - style: `MSFNotificationStyle` enum value that defines the style of the Notification being presented.
@ -18,7 +17,6 @@ public extension View {
/// - actionButtonTitle:Title to display in the action button on the trailing edge of the control.
/// - actionButtonAction: Action to be dispatched by the action button on the trailing edge of the control.
/// - messageButtonAction: Action to be dispatched by tapping on the toast/bar notification.
/// - dismissAction: Action to be dispatched when dismissing toast/bar notification.
/// - Returns: The modified view with the capability of presenting a Notification.
func presentNotification(style: MSFNotificationStyle,
message: String,
@ -32,15 +30,22 @@ public extension View {
dismissAction: (() -> Void)? = nil) -> some View {
self.presentingView(isPresented: isPresented,
isBlocking: isBlocking) {
NotificationViewSwiftUI(style: style,
message: message,
isPresented: isPresented,
title: title,
image: image,
actionButtonTitle: actionButtonTitle,
actionButtonAction: actionButtonAction,
messageButtonAction: messageButtonAction,
dismissAction: dismissAction)
FluentNotification(style: style,
message: message,
isPresented: isPresented,
title: title,
image: image,
actionButtonTitle: actionButtonTitle,
actionButtonAction: actionButtonAction,
messageButtonAction: messageButtonAction)
}
}
}
public extension FluentNotification {
/// Provides a custom design token set to be used when drawing this control.
func overrideTokens(_ tokens: NotificationTokens?) -> FluentNotification {
state.overrideTokens = tokens
return self
}
}