From a35c4d1445f2ba716dea20735b5c9d1ad35e4777 Mon Sep 17 00:00:00 2001 From: Daniel Saidi Date: Wed, 24 Apr 2024 13:03:22 +0200 Subject: [PATCH] Add predefined message styles and types --- README.md | 2 +- RELEASE_NOTES.md | 12 ++ .../Articles/Getting Started.md | 26 ++- .../SystemNotification.swift | 25 +++ .../SystemNotificationContext.swift | 8 + ...SystemNotificationMessage+Predefined.swift | 165 ++++++++++++++++++ .../SystemNotificationMessage.swift | 47 +++-- 7 files changed, 260 insertions(+), 25 deletions(-) create mode 100644 Sources/SystemNotification/SystemNotificationMessage+Predefined.swift diff --git a/README.md b/README.md index fee7b4f..92dd494 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ struct MyView: View { The `SystemNotificationMessage` view lets you easily mimic a native notification view, with an icon, title and text, but you can use any custom view as the notification body. -For more information about how to configure and style your notifications, please see the [getting started guide][Getting-Started]. +For more information about how to configure and style your notifications, predefined message types and styles, and how to create your own custom message types, please see the [getting started guide][Getting-Started]. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9dd7291..f4899e9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,18 @@ Until then, breaking changes can happen in any version, and deprecated features +## 1.1 + +This version adds predefined system notification messages and styles and makes it easier to present a message. + +### ✨ New features + +* `SystemNotificationContext` has a new `presentMessage` function. +* `SystemNotificationMessage` has new, predefined `error`, `success`, `warning` and `silentMode` messages. +* `SystemNotificationMessageStyle` has new, predefined `prominent`, `error`, `success` and `warning` styles. + + + ## 1.0 This version bumps the deployment targets and moves styling and configuration to view modifiers. diff --git a/Sources/SystemNotification/SystemNotification.docc/Articles/Getting Started.md b/Sources/SystemNotification/SystemNotification.docc/Articles/Getting Started.md index ebf864d..759bad0 100644 --- a/Sources/SystemNotification/SystemNotification.docc/Articles/Getting Started.md +++ b/Sources/SystemNotification/SystemNotification.docc/Articles/Getting Started.md @@ -47,7 +47,7 @@ struct MyView: View { } ``` -Context-based notifications just take a ``SystemNotificationContext`` instance and can then show many different notifications with a single modifier: +Context-based notifications take a ``SystemNotificationContext`` and can then show different notifications with a single modifier: ```swift import SystemNotification @@ -83,14 +83,28 @@ struct MyView: View { The ``SystemNotificationMessage`` view lets you easily mimic a native notification view, with an icon, an optional title and a text, but you can use any custom view as the notification content view. -You can use the ``SwiftUI/View/systemNotificationConfiguration(_:)`` and ``SwiftUI/View/systemNotificationStyle(_:)`` view modifiers to apply custom configurations and styles, and the ``SwiftUI/View/systemNotificationMessageStyle(_:)`` to style the message. +You can use the ``SwiftUI/View/systemNotificationConfiguration(_:)`` and ``SwiftUI/View/systemNotificationStyle(_:)`` view modifiers to apply custom configurations and styles. -## Styling and configuration -You can style system notifications with ``SwiftUI/View/systemNotificationStyle(_:)`` and ``SwiftUI/View/systemNotificationConfiguration(_:)``, which must be applied after the ``SwiftUI/View/systemNotification(_:)`` view modifier. +## How to create custom notification messages -You can style system notification message views with ``SwiftUI/View/systemNotificationMessageStyle(_:)``. This lets you style individual messages while keeping the global notification style for the corner radius, background material, etc. +The ``SystemNotificationMessage`` view lets you easily mimic a native notification message, with an icon, an optional title and a text, as well as an explicit style that overrides any environment style. +You can easily extend ``SystemNotificationMessage`` with your own custom messages, which can then be easily presented with the context's ``SystemNotificationContext/presentMessage(_:afterDelay:)`` function: -See ``SystemNotificationStyle``, ``SystemNotificationConfiguration`` and ``SystemNotificationMessageStyle`` for more information about how to style and configure these views. +```swift +extension SystemNotificationMessage where IconView == Image { + + static var itemCreated: Self { + .init( + icon: Image(systemName: "checkmark"), + title: "Item created!", + text: "A new item was created", + style: .... + ) + } +} +``` + +You can also use the ``SwiftUI/View/systemNotificationMessageStyle(_:)`` view modifier to provide a standard style for all other messages. diff --git a/Sources/SystemNotification/SystemNotification.swift b/Sources/SystemNotification/SystemNotification.swift index 18acc10..7c949ec 100644 --- a/Sources/SystemNotification/SystemNotification.swift +++ b/Sources/SystemNotification/SystemNotification.swift @@ -273,3 +273,28 @@ private extension SystemNotification { return MyView() } + + +#Preview("README #3") { + + struct MyView: View { + + @State + var isSilentModeEnabled = false + + @StateObject + var notification = SystemNotificationContext() + + var body: some View { + List { + Toggle("Silent Mode", isOn: $isSilentModeEnabled) + } + .systemNotification(notification) + .onChange(of: isSilentModeEnabled) { value in + notification.presentMessage(.silentMode(on: value)) + } + } + } + + return MyView() +} diff --git a/Sources/SystemNotification/SystemNotificationContext.swift b/Sources/SystemNotification/SystemNotificationContext.swift index 0afdfbe..fc7923d 100644 --- a/Sources/SystemNotification/SystemNotificationContext.swift +++ b/Sources/SystemNotification/SystemNotificationContext.swift @@ -62,6 +62,14 @@ public class SystemNotificationContext: ObservableObject { ) { present(content(), afterDelay: delay) } + + /// Present a system notification message. + public func presentMessage( + _ message: SystemNotificationMessage, + afterDelay delay: TimeInterval = 0 + ) { + present(message, afterDelay: delay) + } } private extension SystemNotificationContext { diff --git a/Sources/SystemNotification/SystemNotificationMessage+Predefined.swift b/Sources/SystemNotification/SystemNotificationMessage+Predefined.swift new file mode 100644 index 0000000..4612047 --- /dev/null +++ b/Sources/SystemNotification/SystemNotificationMessage+Predefined.swift @@ -0,0 +1,165 @@ +// +// SystemNotificationMessage+Predefined.swift +// SystemNotification +// +// Created by Daniel Saidi on 2024-04-24. +// Copyright © 2024 Daniel Saidi. All rights reserved. +// + +import SwiftUI + +extension SystemNotificationMessageStyle { + + static var error: Self { + prominent(backgroundColor: .red) + } + + static var success: Self { + prominent(backgroundColor: .green) + } + + static var warning: Self { + prominent(backgroundColor: .orange) + } + + static func prominent( + backgroundColor: Color + ) -> Self { + .init( + backgroundColor: backgroundColor, + iconColor: .white, + textColor: .white.opacity(0.8), + titleColor: .white + ) + } +} + +extension SystemNotificationMessage where IconView == Image { + + static func error( + icon: Image = .init(systemName: "exclamationmark.triangle"), + title: LocalizedStringKey? = nil, + text: LocalizedStringKey + ) -> Self { + .init( + icon: icon, + title: title, + text: text, + style: .error + ) + } + + static func success( + icon: Image = .init(systemName: "checkmark"), + title: LocalizedStringKey? = nil, + text: LocalizedStringKey + ) -> Self { + .init( + icon: icon, + title: title, + text: text, + style: .success + ) + } + + static func warning( + icon: Image = .init(systemName: "exclamationmark.triangle"), + title: LocalizedStringKey? = nil, + text: LocalizedStringKey + ) -> Self { + .init( + icon: icon, + title: title, + text: text, + style: .warning + ) + } +} + +public extension SystemNotificationMessage where IconView == AnyView { + + /// This message mimics a native iOS silent mode message. + static func silentMode( + on: Bool, + title: LocalizedStringKey? = nil + ) -> Self { + .init( + icon: AnyView(SilentModeBell(isSilentModeOn: on)), + text: title ?? "Silent Mode \(on ? "On" : "Off")" + ) + } +} + +private struct SilentModeBell: View { + + var isSilentModeOn = false + + @State + private var isRotated: Bool = false + + @State + private var isAnimated: Bool = false + + var body: some View { + Image(systemName: iconName) + .rotationEffect( + .degrees(isRotated ? -45 : 0), + anchor: .top + ) + .animation( + .interpolatingSpring( + mass: 0.5, + stiffness: animationStiffness, + damping: animationDamping, + initialVelocity: 0 + ), + value: isAnimated) + .foregroundColor(iconColor) + .onAppear(perform: animate) + } +} + +private extension SilentModeBell { + + func animate() { + withAnimation { isRotated = true } + perform(after: 0.1) { + isRotated = false + isAnimated = true + } + } + + func perform(after: Double, action: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: action) + } + + var animationDamping: Double { + isSilentModeOn ? 4 : 1.5 + } + + var animationStiffness: Double { + isSilentModeOn ? 129 : 179 + } + + var iconName: String { + isSilentModeOn ? "bell.slash.fill" : "bell.fill" + } + + var iconColor: Color { + isSilentModeOn ? .red : .gray + } +} + +#Preview { + + VStack { + SystemNotificationMessage.silentMode(on: true) + SystemNotificationMessage.silentMode(on: false) + SystemNotificationMessage.error(title: "Error!", text: "Something failed!") + SystemNotificationMessage.success(title: "Success!", text: "You did it!") + SystemNotificationMessage.warning(title: "Warning!", text: "Danger ahead!") + } + .padding() + .background(Color.black.opacity(0.1)) + .clipShape(.rect(cornerRadius: 10)) +} diff --git a/Sources/SystemNotification/SystemNotificationMessage.swift b/Sources/SystemNotification/SystemNotificationMessage.swift index 767e56e..f472671 100644 --- a/Sources/SystemNotification/SystemNotificationMessage.swift +++ b/Sources/SystemNotification/SystemNotificationMessage.swift @@ -13,6 +13,19 @@ import SwiftUI /// /// You can provide a custom icon view, title, and text, and /// e.g. animate the icon when it's presented. +/// +/// You can easily create custom messages, by extending this +/// type with static message builders, for instance: +/// +/// ```swift +/// extension SystemNotificationMessage where IconView == Image { +/// +/// static func silentMode(on: Bool) -> Self { +/// +/// } +/// } +/// +/// ``` public struct SystemNotificationMessage: View { /// Create a system notification message view. @@ -21,15 +34,17 @@ public struct SystemNotificationMessage: View { /// - icon: The leading icon view. /// - title: The bold title text, by default `nil`. /// - text: The plain message text. + /// - style: An optional, explicit style to apply. public init( icon: IconView, title: LocalizedStringKey? = nil, - text: LocalizedStringKey + text: LocalizedStringKey, + style: SystemNotificationMessageStyle? = nil ) { self.icon = icon self.title = title self.text = text - self.initStyle = nil + self.initStyle = style } /// Create a system notification message view. @@ -38,15 +53,17 @@ public struct SystemNotificationMessage: View { /// - icon: The leading icon image. /// - title: The bold title text, by default `nil`. /// - text: The plain message text. + /// - style: An optional, explicit style to apply. public init( icon: Image, title: LocalizedStringKey? = nil, - text: LocalizedStringKey + text: LocalizedStringKey, + style: SystemNotificationMessageStyle? = nil ) where IconView == Image { self.icon = icon self.title = title self.text = text - self.initStyle = nil + self.initStyle = style } /// Create a system notification message view. @@ -54,14 +71,16 @@ public struct SystemNotificationMessage: View { /// - Parameters: /// - title: The bold title text, by default `nil`. /// - text: The plain message text. + /// - style: An optional, explicit style to apply. public init( title: LocalizedStringKey? = nil, - text: LocalizedStringKey + text: LocalizedStringKey, + style: SystemNotificationMessageStyle? = nil ) where IconView == EmptyView { self.icon = EmptyView() self.title = title self.text = text - self.initStyle = nil + self.initStyle = style } let icon: IconView @@ -70,7 +89,7 @@ public struct SystemNotificationMessage: View { let initStyle: SystemNotificationMessageStyle? @Environment(\.systemNotificationMessageStyle) - private var envStyle + private var environmentStyle public var body: some View { HStack(spacing: style.iconTextSpacing) { @@ -88,7 +107,7 @@ public struct SystemNotificationMessage: View { private extension SystemNotificationMessage { var style: SystemNotificationMessageStyle { - initStyle ?? envStyle + initStyle ?? environmentStyle } func foregroundColor( @@ -156,17 +175,9 @@ private extension SystemNotificationMessage { SystemNotificationMessage( icon: Image(systemName: "exclamationmark.triangle"), title: "Warning", - text: "This is a long message to demonstrate multiline messages." - ) - .systemNotificationMessageStyle( - .init( - iconColor: .orange, - iconFont: .headline, - textColor: .orange, - titleColor: .orange, - titleFont: .headline - ) + text: "This is a long warning message to demonstrate multiline messages." ) + .systemNotificationMessageStyle(.warning) } .background(Color.white) .cornerRadius(5)