Extracted the in-app notifications from the Sample app target into its own Notifying package target.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
/// A transient in-app notification, posted and dismissed by a ``Notifier``.
|
||||
public struct AppNotification: Equatable, Identifiable, Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The unique identifier of the notification.
|
||||
public let id: UUID
|
||||
|
||||
/// The kind of the notification, driving its visual weight.
|
||||
public let kind: Kind
|
||||
|
||||
/// The localized message of the notification.
|
||||
public let message: String
|
||||
|
||||
/// The system symbol of the notification's icon.
|
||||
public let symbol: String
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a notification of a given kind, with a localized message and a system symbol.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - id: The unique identifier of the notification. Defaults to a newly generated identifier.
|
||||
/// - kind: The kind of the notification.
|
||||
/// - message: The localized message of the notification.
|
||||
/// - symbol: The system symbol of the notification's icon.
|
||||
public init(
|
||||
id: UUID = .init(),
|
||||
kind: Kind,
|
||||
message: String,
|
||||
symbol: String
|
||||
) {
|
||||
self.id = id
|
||||
self.kind = kind
|
||||
self.message = message
|
||||
self.symbol = symbol
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Enumerations
|
||||
|
||||
extension AppNotification {
|
||||
/// The kind of a notification, driving its visual weight.
|
||||
public enum Kind: Sendable {
|
||||
/// The notification reports an error.
|
||||
case error
|
||||
/// The notification reports an informational event.
|
||||
case info
|
||||
/// The notification reports a warning.
|
||||
case warning
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The label style for the transient in-app notification banners.
|
||||
///
|
||||
/// The style renders the label's icon next to its title, over a padded, capsule Liquid Glass background. The icon is tinted with a color
|
||||
/// matching the kind of notification given at initialization — green for an informational one, yellow for a warning, and red for an
|
||||
/// error — and the title wears a medium-weight callout font.
|
||||
public struct NotificationLabelStyle: LabelStyle {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The kind of notification whose matching color tints the label's icon.
|
||||
private let kind: AppNotification.Kind
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a notification label style for a given kind of notification.
|
||||
///
|
||||
/// - Parameter kind: The kind of notification whose matching color tints the label's icon.
|
||||
public init(
|
||||
kind: AppNotification.Kind
|
||||
) {
|
||||
self.kind = kind
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Creates the styled body of the label.
|
||||
///
|
||||
/// - Parameter configuration: The properties of the label being styled.
|
||||
public func makeBody(
|
||||
configuration: Configuration
|
||||
) -> some View {
|
||||
HStack(
|
||||
spacing: Constant.Spacing.stack
|
||||
) {
|
||||
configuration.icon
|
||||
.font(.title3)
|
||||
.foregroundStyle(kind.imageStyle)
|
||||
|
||||
configuration.title
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
.padding(.vertical, Constant.Padding.vertical)
|
||||
.padding(.horizontal, Constant.Padding.horizontal)
|
||||
.glassEffect(
|
||||
.regular,
|
||||
in: .capsule
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Styles
|
||||
|
||||
public extension LabelStyle where Self == NotificationLabelStyle {
|
||||
|
||||
/// The label style for the transient in-app notification banners.
|
||||
///
|
||||
/// - Parameter kind: The kind of notification whose matching color tints the label's icon.
|
||||
/// - Returns: A notification label style for the given kind of notification.
|
||||
static func notification(
|
||||
kind: AppNotification.Kind
|
||||
) -> NotificationLabelStyle {
|
||||
.init(kind: kind)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AppNotification.Kind+Properties
|
||||
|
||||
private extension AppNotification.Kind {
|
||||
|
||||
/// The color that tints the icon of a notification label: green for an informational notification, yellow for a warning, and red
|
||||
/// for an error.
|
||||
var imageStyle: Color {
|
||||
switch self {
|
||||
case .error: .red
|
||||
case .info: .green
|
||||
case .warning: .yellow
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// The constant values used across the label style.
|
||||
private enum Constant {
|
||||
/// The padding constants.
|
||||
enum Padding {
|
||||
/// The horizontal padding between a label's content and its background.
|
||||
static let horizontal: CGFloat = 16
|
||||
/// The vertical padding between a label's content and its background.
|
||||
static let vertical: CGFloat = 8
|
||||
}
|
||||
|
||||
/// The spacing constants.
|
||||
enum Spacing {
|
||||
/// The spacing between the elements of a stack.
|
||||
static let stack: CGFloat = 16
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Notification label style"
|
||||
) {
|
||||
VStack(
|
||||
alignment: .leading,
|
||||
spacing: 16
|
||||
) {
|
||||
Label {
|
||||
Text(verbatim: "Downloading the speech model for Dutch (Netherlands)…")
|
||||
} icon: {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
}
|
||||
.labelStyle(.notification(
|
||||
kind: .info
|
||||
))
|
||||
|
||||
Label {
|
||||
Text(verbatim: "The speech model download for Dutch (Netherlands) was cancelled.")
|
||||
} icon: {
|
||||
Image(systemName: "xmark.circle")
|
||||
}
|
||||
.labelStyle(.notification(
|
||||
kind: .warning
|
||||
))
|
||||
|
||||
Label {
|
||||
Text(verbatim: "The speech model for German (Germany) could not be downloaded.")
|
||||
} icon: {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
}
|
||||
.labelStyle(.notification(
|
||||
kind: .error
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Accessibility
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// The observable model that owns the lifecycle of the transient in-app notifications.
|
||||
///
|
||||
/// Posting a notification appends it to ``notifications``, announces its message to assistive technologies — since the visual
|
||||
/// banners are transient — and schedules its automatic dismissal a few seconds later, which is the only way a notification is
|
||||
/// dismissed.
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class Notifier {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The notifications currently presented, in post order.
|
||||
public private(set) var notifications: [AppNotification] = []
|
||||
|
||||
/// The time a posted notification stays visible before its automatic dismissal.
|
||||
@ObservationIgnored
|
||||
private let delayDismissal: Duration
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a notifier.
|
||||
///
|
||||
/// - Parameter delayDismissal: The time a posted notification stays visible before its automatic dismissal.
|
||||
/// Defaults to four seconds.
|
||||
public init(
|
||||
delayDismissal: Duration = .seconds(4)
|
||||
) {
|
||||
self.delayDismissal = delayDismissal
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Posts a notification, announcing its message to assistive technologies and scheduling its automatic dismissal.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - kind: The kind of the notification.
|
||||
/// - message: The localized message of the notification.
|
||||
/// - symbol: The system symbol of the notification's icon.
|
||||
public func post(
|
||||
_ kind: AppNotification.Kind,
|
||||
message: String,
|
||||
symbol: String
|
||||
) {
|
||||
let notification = AppNotification(
|
||||
kind: kind,
|
||||
message: message,
|
||||
symbol: symbol
|
||||
)
|
||||
|
||||
notifications.append(notification)
|
||||
|
||||
AccessibilityNotification.Announcement(message).post()
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(for: delayDismissal)
|
||||
|
||||
notifications.removeAll { $0.id == notification.id }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user