Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 HCPSDKFIORIUIKIT-2920 Add position parameter to Toast Message #975

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import FioriSwiftUICore
import SwiftUI

struct ToastMessageExample: View {
var body: some View {
List {
NavigationLink("Basic Example",
destination: ToastMessageBasicExample())
NavigationLink("Position",
destination: ToastMessagePositionExample())
}
.navigationTitle("Toast Message")
}
}

struct ToastMessageBasicExample: View {
@State var show: Bool = false

var body: some View {
Expand All @@ -26,3 +38,48 @@ struct ToastMessageExample: View {
.navigationTitle("Toast Message")
}
}

struct ToastMessagePositionExample: View {
@State var selectedPosition: ToastMessagePosition = .above
@State var spacing: CGFloat = 0
@State var showIcon = false

var body: some View {
VStack {
HStack {}
.frame(maxWidth: 300, maxHeight: 300)
.border(.blue, width: 1)
.toastMessage(isPresented: .constant(true),
icon: {
self.showIcon ? Image(systemName: "info.circle") : nil
},
title: {
Text("Toast Message Title")
},
duration: .infinity,
position: self.selectedPosition,
spacing: self.spacing)
.padding(30)
VStack {
Picker("Position", selection: self.$selectedPosition) {
ForEach(ToastMessagePosition.allCases) { position in

Text(position.rawValue)
}
}
Text("Spacing: \(self.spacing)")
Slider(value: self.$spacing, in: -50.0 ... 50.0, step: 5)
Toggle("Show Icon", isOn: self.$showIcon)
}
.frame(maxWidth: 300)
}
.navigationTitle("Toast Message")
}
}

struct DetailView_Previews1: PreviewProvider {
static var previews: some View {
ToastMessagePositionExample()
.environment(\.locale, .init(identifier: "ar"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,14 @@ protocol _ListPickerDestinationComponent: _CancelActionComponent, _ApplyActionCo
// sourcery: CompositeComponent
protocol _ToastMessageComponent: _IconComponent, _TitleComponent {
// sourcery: defaultValue = 1
/// The duration in seconds for which the toast message is shown. The default is `1`.
/// The duration in seconds for which the toast message is shown. The default value is `1`.
var duration: Double { get }
// sourcery: defaultValue = .center
/// The position of the toast message relative to its parent view. `.center` puts the toast message in the center of its parent view, `.above` aligns it above the view, and `.below` aligns it below the view. The default value is `.center`.
var position: ToastMessagePosition { get }
// sourcery: defaultValue = 0
/// The amount of spacing to put in between the toast message and the frame of its parent view. This only applies to the `.above` and `.below` positions, and negative values are converted to `0`. The default value is `0`.
var spacing: CGFloat { get }
}

// sourcery: CompositeComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,28 @@ import SwiftUI
4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`.
*/

public enum ToastMessagePosition: String, CaseIterable, Identifiable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add API doc: what these position alignment mean visually, relative to the reference view.

case above
case center
case below
public var id: Self { self }
}

// Base Layout style
public struct ToastMessageBaseStyle: ToastMessageStyle {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State private var size: CGSize = .zero

public func makeBody(_ configuration: ToastMessageConfiguration) -> some View {
GeometryReader { reader in
VStack {
Spacer()
HStack {
Spacer()
self.makeMessageBody(configuration: configuration, size: reader.size)
Spacer()
}
Spacer()
}
self.makeMessageBody(configuration: configuration, size: reader.size)
.position(getPositionOffset(position: configuration.position, spacing: configuration.spacing, viewSize: self.size, parentViewSize: reader.size))
}
}

func makeMessageBody(configuration: ToastMessageConfiguration, size: CGSize) -> some View {
HStack(alignment: .center, spacing: 8) {
if configuration.icon.isEmpty {
Image(systemName: "checkmark.circle")
.foregroundColor(Color.preferredColor(.primaryLabel))
} else {
configuration.icon
}
configuration.icon
configuration.title
.font(Font.fiori(forTextStyle: .subheadline))
.foregroundColor(Color.preferredColor(.primaryLabel))
Expand All @@ -50,12 +46,38 @@ public struct ToastMessageBaseStyle: ToastMessageStyle {
.inset(by: 0.33)
.stroke(Color.preferredColor(.separator), lineWidth: 0.33)
)
.sizeReader { size in
self.size = size
}
.shadow(color: Color.preferredColor(.sectionShadow), radius: 2)
.shadow(color: Color.preferredColor(.cardShadow), radius: 16, x: 0, y: 8)
.shadow(color: Color.preferredColor(.cardShadow), radius: 32, x: 0, y: 16)
}
}

private func getPositionOffset(position: ToastMessagePosition, spacing: CGFloat, viewSize: CGSize, parentViewSize: CGSize) -> CGPoint {
var correctedSpacing: CGFloat
var viewCoordinates = CGPoint()
viewCoordinates.x = parentViewSize.width / 2

if spacing < 0 {
correctedSpacing = 0
} else {
correctedSpacing = spacing
}

switch position {
case .above:
viewCoordinates.y = -1 * (viewSize.height / 2 + correctedSpacing)
case .center:
viewCoordinates.y = parentViewSize.height / 2
case .below:
viewCoordinates.y = parentViewSize.height + viewSize.height / 2 + correctedSpacing
}

return viewCoordinates
}

// Default fiori styles
extension ToastMessageFioriStyle {
struct ContentFioriStyle: ToastMessageStyle {
Expand Down Expand Up @@ -95,16 +117,22 @@ public extension View {
/// - isPresented: A binding to a Boolean value that determines whether to present the banner message.
/// - icon: Icon image in front of the text. The default is a checkmark icon.
/// - title: The message to display.
/// - duration: The duration in seconds for which the toast message is shown. The default is `1`.
/// - duration: The duration in seconds for which the toast message is shown. The default value is `1`.
/// - position: The position of the toast message relative to its parent view. `.center` puts the toast message in the center of its parent view, `.above` aligns it above the view, and `.below` aligns it below the view. The default value is `.center`.
/// - spacing: The amount of spacing to put in between the toast message and the frame of its parent view. This only applies to the `.above` and `.below` positions, and negative values are converted to `0`. The default value is `0`.
/// - Returns: A new `View` with the toast message.
func toastMessage(isPresented: Binding<Bool>,
@ViewBuilder icon: () -> any View = { EmptyView() },
title: AttributedString,
duration: Double = 1) -> some View
duration: Double = 1,
position: ToastMessagePosition = .center,
spacing: CGFloat = 0) -> some View
{
self.modifier(ToastMessageModifier(icon: icon(),
title: Text(title),
duration: duration,
position: position,
spacing: spacing,
isPresented: isPresented))
}

Expand All @@ -113,16 +141,22 @@ public extension View {
/// - isPresented: A binding to a Boolean value that determines whether to present the banner message.
/// - icon: Icon image in front of the text. The default is a checkmark icon.
/// - title: The message to display.
/// - duration: The duration in seconds for which the toast message is shown. The default is `1`.
/// - duration: The duration in seconds for which the toast message is shown. The default value is `1`.
/// - position: The position of the toast message relative to its parent view. `.center` puts the toast message in the center of its parent view, `.above` aligns it above the view, and `.below` aligns it below the view. The default value is `.center`.
/// - spacing: The amount of spacing to put in between the toast message and the frame of its parent view. This only applies to the `.above` and `.below` positions, and negative values are converted to `0`. The default value is `0`.
/// - Returns: A new `View` with the toast message.
func toastMessage(isPresented: Binding<Bool>,
@ViewBuilder icon: () -> any View = { EmptyView() },
title: String,
duration: Double = 1) -> some View
duration: Double = 1,
position: ToastMessagePosition = .center,
spacing: CGFloat = 0) -> some View
{
self.modifier(ToastMessageModifier(icon: icon(),
title: Text(title),
duration: duration,
position: position,
spacing: spacing,
isPresented: isPresented))
}

Expand All @@ -131,16 +165,22 @@ public extension View {
/// - isPresented: A binding to a Boolean value that determines whether to present the banner message.
/// - icon: Icon image in front of the text. The default is a checkmark icon.
/// - title: The message to display.
/// - duration: The duration in seconds for which the toast message is shown. The default is `1`.
/// - duration: The duration in seconds for which the toast message is shown. The default value is `1`.
/// - position: The position of the toast message relative to its parent view. `.center` puts the toast message in the center of its parent view, `.above` aligns it above the view, and `.below` aligns it below the view. The default value is `.center`.
/// - spacing: The amount of spacing to put in between the toast message and the frame of its parent view. This only applies to the `.above` and `.below` positions, and negative values are converted to `0`. The default value is `0`.
/// - Returns: A new `View` with the toast message.
func toastMessage(isPresented: Binding<Bool>,
@ViewBuilder icon: () -> any View = { EmptyView() },
@ViewBuilder title: () -> any View,
duration: Double = 1) -> some View
duration: Double = 1,
position: ToastMessagePosition = .center,
spacing: CGFloat = 0) -> some View
{
self.modifier(ToastMessageModifier(icon: icon(),
title: title(),
duration: duration,
position: position,
spacing: spacing,
isPresented: isPresented))
}
}
Expand All @@ -149,6 +189,9 @@ struct ToastMessageModifier: ViewModifier {
let icon: any View
var title: any View
var duration: Double
var position: ToastMessagePosition
var spacing: CGFloat

@Binding var isPresented: Bool
@State private var workItem: DispatchWorkItem?

Expand All @@ -160,7 +203,8 @@ struct ToastMessageModifier: ViewModifier {
self.icon
}, title: {
self.title
})
}, position: self.position,
spacing: self.spacing)
}
})
.setOnChange(of: self.isPresented) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.2.6 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
import Foundation
import SwiftUI

public struct ToastMessage {
let icon: any View
let title: any View
/// The duration in seconds for which the toast message is shown. The default is `1`.
/// The duration in seconds for which the toast message is shown. The default value is `1`.
let duration: Double
/// The position of the toast message relative to its parent view. `.center` puts the toast message in the center of its parent view, `.above` aligns it above the view, and `.below` aligns it below the view. The default value is `.center`.
let position: ToastMessagePosition
/// The amount of spacing to put in between the toast message and the frame of its parent view. This only applies to the `.above` and `.below` positions, and negative values are converted to `0`. The default value is `0`.
let spacing: CGFloat

@Environment(\.toastMessageStyle) var style

Expand All @@ -18,11 +22,15 @@ public struct ToastMessage {
public init(@ViewBuilder icon: () -> any View = { EmptyView() },
@ViewBuilder title: () -> any View,
duration: Double = 1,
position: ToastMessagePosition = .center,
spacing: CGFloat = 0,
componentIdentifier: String? = ToastMessage.identifier)
{
self.icon = Icon(icon: icon, componentIdentifier: componentIdentifier)
self.title = Title(title: title, componentIdentifier: componentIdentifier)
self.duration = duration
self.position = position
self.spacing = spacing
self.componentIdentifier = componentIdentifier ?? ToastMessage.identifier
}
}
Expand All @@ -34,9 +42,11 @@ public extension ToastMessage {
public extension ToastMessage {
init(icon: Image? = nil,
title: AttributedString,
duration: Double = 1)
duration: Double = 1,
position: ToastMessagePosition = .center,
spacing: CGFloat = 0)
{
self.init(icon: { icon }, title: { Text(title) }, duration: duration)
self.init(icon: { icon }, title: { Text(title) }, duration: duration, position: position, spacing: spacing)
}
}

Expand All @@ -49,6 +59,8 @@ public extension ToastMessage {
self.icon = configuration.icon
self.title = configuration.title
self.duration = configuration.duration
self.position = configuration.position
self.spacing = configuration.spacing
self._shouldApplyDefaultStyle = shouldApplyDefaultStyle
self.componentIdentifier = configuration.componentIdentifier
}
Expand All @@ -59,7 +71,7 @@ extension ToastMessage: View {
if self._shouldApplyDefaultStyle {
self.defaultStyle()
} else {
self.style.resolve(configuration: .init(componentIdentifier: self.componentIdentifier, icon: .init(self.icon), title: .init(self.title), duration: self.duration)).typeErased
self.style.resolve(configuration: .init(componentIdentifier: self.componentIdentifier, icon: .init(self.icon), title: .init(self.title), duration: self.duration, position: self.position, spacing: self.spacing)).typeErased
.transformEnvironment(\.toastMessageStyleStack) { stack in
if !stack.isEmpty {
stack.removeLast()
Expand All @@ -77,7 +89,7 @@ private extension ToastMessage {
}

func defaultStyle() -> some View {
ToastMessage(.init(componentIdentifier: self.componentIdentifier, icon: .init(self.icon), title: .init(self.title), duration: self.duration))
ToastMessage(.init(componentIdentifier: self.componentIdentifier, icon: .init(self.icon), title: .init(self.title), duration: self.duration, position: self.position, spacing: self.spacing))
.shouldApplyDefaultStyle(false)
.toastMessageStyle(ToastMessageFioriStyle.ContentFioriStyle())
.typeErased
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.2.6 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
import Foundation
import SwiftUI
Expand Down Expand Up @@ -26,6 +26,8 @@ public struct ToastMessageConfiguration {
public let icon: Icon
public let title: Title
public let duration: Double
public let position: ToastMessagePosition
public let spacing: CGFloat

public typealias Icon = ConfigurationViewWrapper
public typealias Title = ConfigurationViewWrapper
Expand Down
Loading