diff --git a/Apps/Examples/Examples/FioriSwiftUICore/ToastMessage/ToastMessageExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/ToastMessage/ToastMessageExample.swift index 387dabb6f..3d608f65f 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/ToastMessage/ToastMessageExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/ToastMessage/ToastMessageExample.swift @@ -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 { @@ -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")) + } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index 5d0610c3d..72ebcb818 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -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 diff --git a/Sources/FioriSwiftUICore/_FioriStyles/ToastMessageStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/ToastMessageStyle.fiori.swift index f02dc7716..043483e03 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/ToastMessageStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/ToastMessageStyle.fiori.swift @@ -11,32 +11,28 @@ import SwiftUI 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. */ +public enum ToastMessagePosition: String, CaseIterable, Identifiable { + 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)) @@ -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 { @@ -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, @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)) } @@ -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, @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)) } @@ -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, @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)) } } @@ -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? @@ -160,7 +203,8 @@ struct ToastMessageModifier: ViewModifier { self.icon }, title: { self.title - }) + }, position: self.position, + spacing: self.spacing) } }) .setOnChange(of: self.isPresented) { diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessage.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessage.generated.swift index 493c39443..04ff2397c 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessage.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessage.generated.swift @@ -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 @@ -6,8 +6,12 @@ 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 @@ -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 } } @@ -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) } } @@ -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 } @@ -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() @@ -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 diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessageStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessageStyle.generated.swift index 82f38d288..56ad567a2 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessageStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ToastMessage/ToastMessageStyle.generated.swift @@ -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 @@ -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