diff --git a/Apps/Examples/Examples/FioriSwiftUICore/BannerMessage/BannerMultiMessageCustomInitExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/BannerMessage/BannerMultiMessageCustomInitExample.swift index c7784841e..b5f3b6fcf 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/BannerMessage/BannerMultiMessageCustomInitExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/BannerMessage/BannerMultiMessageCustomInitExample.swift @@ -6,6 +6,7 @@ // Copyright © 2024 SAP. All rights reserved. // import FioriSwiftUICore +import FioriThemeManager import RegexBuilder import SwiftUI @@ -61,11 +62,20 @@ struct BannerMultiMessageCustomInitExample: View { Spacer() } .popover(isPresented: self.$showingMessageDetail) { - BannerMultiMessageSheet(closeAction: { + BannerMultiMessageSheet(title: { + Text("CustomMessagesCount") + }, closeAction: { + FioriButton(isSelectionPersistent: false, action: { _ in + self.showingMessageDetail = false + }, image: { _ in + FioriIcon.tables.circleTaskFill + }) + .fioriButtonStyle(FioriTertiaryButtonStyle(colorStyle: .normal)) + }, dismissAction: { self.showingMessageDetail = false }, removeAction: { category, _ in self.removeCategory(category: category) - }, turnOnSectionHeader: self.turnOnSectionHeader, bannerMultiMessages: self.$bannerMultiMessages) { id in + }, turnOnSectionHeader: self.turnOnSectionHeader, messageItemView: { id in if let (message, category) = getItemData(with: id) { BannerMessage(icon: { message.icon @@ -103,8 +113,8 @@ struct BannerMultiMessageCustomInitExample: View { } else { EmptyView() } - } - .presentationDetents([.medium, .large]) + }, bannerMultiMessages: self.$bannerMultiMessages) + .presentationDetents([.medium, .large]) } } .listRowSeparator(.hidden) diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index f6ff5c10f..263dc95e6 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -636,6 +636,27 @@ protocol _ToastMessageComponent: _IconComponent, _TitleComponent { var duration: Double { get } } +// sourcery: CompositeComponent +protocol _BannerMultiMessageSheet: _TitleComponent, _CloseActionComponent { + /// callback when this component want to dismiss itself + var dismissAction: (() -> Void)? { get } + /// callback when category or single item is removed + var removeAction: ((String, UUID?) -> Void)? { get } + /// callback when the link button is clicked + var viewDetailAction: ((UUID) -> Void)? { get } + // sourcery: defaultValue = true + /// the mark to turn on section header or not + var turnOnSectionHeader: Bool { get } + // sourcery: defaultValue = "{ _ in EmptyView() }" + // sourcery: resultBuilder.defaultValue = "{ _ in EmptyView() }" + /// view for each item under the category + @ViewBuilder + var messageItemView: (UUID) -> any View { get } + // sourcery: @Binding + /// the data source for banner multi-message sheet + var bannerMultiMessages: [BannerMessageListModel] { get } +} + // sourcery: CompositeComponent protocol _LoadingIndicatorViewComponent: _TitleComponent { // sourcery: defaultValue = 0 diff --git a/Sources/FioriSwiftUICore/_FioriStyles/BannerMessageStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/BannerMessageStyle.fiori.swift index ad093978c..213b0152e 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/BannerMessageStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/BannerMessageStyle.fiori.swift @@ -20,7 +20,7 @@ public enum BannerMultiMessageType: Int { public struct BannerMessageBaseStyle: BannerMessageStyle { public func makeBody(_ configuration: BannerMessageConfiguration) -> some View { VStack(spacing: 0) { - configuration.topDivider.background(BannerMessageFioriStyle.titleForegroundColor(type: configuration.messageType)).frame(height: 4) + configuration.topDivider.frame(height: 4) HStack { HStack(spacing: 6, content: { switch configuration.alignment { @@ -42,7 +42,6 @@ public struct BannerMessageBaseStyle: BannerMessageStyle { .padding([.top, .bottom], 13) } }) - .foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: configuration.messageType)) .padding(.leading, configuration.alignment == .center ? 44 : 16) .padding(.trailing, configuration.alignment == .center ? 0 : 16) .onTapGesture { @@ -63,6 +62,63 @@ public struct BannerMessageBaseStyle: BannerMessageStyle { } } +struct BannerMessageNeutralStyle: BannerMessageStyle { + public func makeBody(_ configuration: BannerMessageConfiguration) -> some View { + BannerMessage(configuration) + .iconStyle { c in + c.icon.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .neutral)) + } + } +} + +struct BannerMessageNegativeStyle: BannerMessageStyle { + public func makeBody(_ configuration: BannerMessageConfiguration) -> some View { + BannerMessage(configuration) + .iconStyle { c in + c.icon.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .negative)) + } + .titleStyle(content: { c in + c.title.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .negative)) + }) + } +} + +struct BannerMessageCriticalStyle: BannerMessageStyle { + public func makeBody(_ configuration: BannerMessageConfiguration) -> some View { + BannerMessage(configuration) + .iconStyle { c in + c.icon.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .critical)) + } + .titleStyle(content: { c in + c.title.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .critical)) + }) + } +} + +struct BannerMessagePositiveStyle: BannerMessageStyle { + public func makeBody(_ configuration: BannerMessageConfiguration) -> some View { + BannerMessage(configuration) + .iconStyle { c in + c.icon.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .positive)) + } + .titleStyle(content: { c in + c.title.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .positive)) + }) + } +} + +struct BannerMessageInformativeStyle: BannerMessageStyle { + public func makeBody(_ configuration: BannerMessageConfiguration) -> some View { + BannerMessage(configuration) + .iconStyle { c in + c.icon.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .informative)) + } + .titleStyle(content: { c in + c.title.foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: .informative)) + }) + } +} + // Default fiori styles extension BannerMessageFioriStyle { static func titleForegroundColor(type: BannerMultiMessageType) -> Color { @@ -92,6 +148,7 @@ extension BannerMessageFioriStyle { func makeBody(_ configuration: IconConfiguration) -> some View { Icon(configuration) + .foregroundStyle(BannerMessageFioriStyle.titleForegroundColor(type: self.bannerMessageConfiguration.messageType)) } } @@ -118,6 +175,7 @@ extension BannerMessageFioriStyle { func makeBody(_ configuration: TopDividerConfiguration) -> some View { TopDivider(configuration) + .background(BannerMessageFioriStyle.titleForegroundColor(type: self.bannerMessageConfiguration.messageType)) } } } @@ -366,7 +424,11 @@ struct BannerMessageModifier: ViewModifier { })) .frame(minWidth: (UIDevice.current.userInterfaceIdiom != .phone && self.alignment != .center) ? 393 : nil, alignment: .leading) .popover(isPresented: self.$showingMessageDetail) { - BannerMultiMessageSheet(closeAction: { + BannerMultiMessageSheet(title: { + EmptyView() + }, closeAction: { + EmptyView() + }, dismissAction: { self.showingMessageDetail = false }, removeAction: { _, _ in if self.bannerMultiMessages.isEmpty { diff --git a/Sources/FioriSwiftUICore/_FioriStyles/BannerMultiMessageSheet.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/BannerMultiMessageSheetStyle.fiori.swift similarity index 61% rename from Sources/FioriSwiftUICore/_FioriStyles/BannerMultiMessageSheet.fiori.swift rename to Sources/FioriSwiftUICore/_FioriStyles/BannerMultiMessageSheetStyle.fiori.swift index e6096bf2b..fd238011f 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/BannerMultiMessageSheet.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/BannerMultiMessageSheetStyle.fiori.swift @@ -1,7 +1,17 @@ import Combine import FioriThemeManager +import Foundation import SwiftUI +/** + This file provides default fiori style for the component. + + 1. Uncomment fhe following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + /// Single Banner Message Model public struct BannerMessageItemModel: Identifiable { public var id: UUID @@ -35,6 +45,14 @@ public struct BannerMessageItemModel: Identifiable { } } +class CategorySelect: ObservableObject { + @Published var categorySelectedIndex = 0 + + init(categorySelectedIndex: Int = 0) { + self.categorySelectedIndex = categorySelectedIndex + } +} + public struct BannerMessageListModel: Identifiable, Equatable { public static func == (lhs: BannerMessageListModel, rhs: BannerMessageListModel) -> Bool { lhs.id == rhs.id @@ -57,26 +75,8 @@ public struct BannerMessageListModel: Identifiable, Equatable { } } -class CategorySelect: ObservableObject { - @Published var categorySelectedIndex = 0 - - init(categorySelectedIndex: Int = 0) { - self.categorySelectedIndex = categorySelectedIndex - } -} - -public struct BannerMultiMessageSheet: View { - // The callback when click the close button. - private var closeAction: (() -> Void)? = nil - // Remove item action, First parameter is category, and the secondary is the item's id. When the secondary is nil, the entire category was removed. - private var removeAction: ((String, UUID?) -> Void)? = nil - // View the message detail callback, the parameter is message id, developer can use the id to scroll to the relative item - private var viewDetailAction: ((UUID) -> Void)? = nil - // Turn on category section header or not - private var turnOnSectionHeader = true - - @Binding private var bannerMultiMessages: [BannerMessageListModel] - +// Base Layout style +public struct BannerMultiMessageSheetBaseStyle: BannerMultiMessageSheetStyle { @StateObject private var categorySelect = CategorySelect() @State private var dimensionSelector: DimensionSelector = { let all = NSLocalizedString("All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") @@ -86,56 +86,9 @@ public struct BannerMultiMessageSheet: View { @State private var timer: Timer? @State private var cancellableSet: Set = [] - /// Public initializer for banner multi-message sheet - /// - Parameters: - /// - closeAction: callback when close button is clicked - /// - removeAction: callback when category or single item is removed - /// - viewDetailAction: callback when the link button is clicked - /// - turnOnSectionHeader: the mark to turn on section header or not - /// - bannerMultiMessages: the data source for banner multi-message sheet - public init(closeAction: (() -> Void)? = nil, - removeAction: ((String, UUID?) -> Void)? = nil, - viewDetailAction: ((UUID) -> Void)? = nil, - turnOnSectionHeader: Bool = true, - bannerMultiMessages: Binding<[BannerMessageListModel]>) - { - self.closeAction = closeAction - self.removeAction = removeAction - self.viewDetailAction = viewDetailAction - self.turnOnSectionHeader = turnOnSectionHeader - - _bannerMultiMessages = bannerMultiMessages - - self.resetDimensionSelector() - } - - private var messageItemView: ((UUID) -> any View)? = nil - - /// Public initializer for banner multi-message sheet - /// - Parameters: - /// - closeAction: callback when close button is clicked - /// - removeAction: callback when category or single item is removed - /// - turnOnSectionHeader: the mark to turn on section header or not - /// - bannerMultiMessages: the data source for banner multi-message sheet - /// - messageItemView: view for each item under the category - public init(closeAction: (() -> Void)? = nil, - removeAction: ((String, UUID?) -> Void)? = nil, - turnOnSectionHeader: Bool = true, - bannerMultiMessages: Binding<[BannerMessageListModel]>, - @ViewBuilder messageItemView: @escaping ((UUID) -> any View)) - { - self.closeAction = closeAction - self.removeAction = removeAction - self.turnOnSectionHeader = turnOnSectionHeader - _bannerMultiMessages = bannerMultiMessages - self.messageItemView = messageItemView - - self.resetDimensionSelector() - } - - private func resetDimensionSelector() { + private func resetDimensionSelector(_ configuration: BannerMultiMessageSheetConfiguration) { var titles: [String] = [] - for element in self.bannerMultiMessages { + for element in configuration.bannerMultiMessages { titles.append(element.category) } let all = NSLocalizedString("All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") @@ -156,9 +109,9 @@ public struct BannerMultiMessageSheet: View { UIDevice.current.userInterfaceIdiom == .phone } - private var filteredBannerMultiMessages: [BannerMessageListModel] { + private func filteredBannerMultiMessages(_ configuration: BannerMultiMessageSheetConfiguration) -> [BannerMessageListModel] { let selectedCategory = self.dimensionSelector.titles[self.categorySelect.categorySelectedIndex] - let filteredBannerMultiMessages = self.bannerMultiMessages.filter { model in + let filteredBannerMultiMessages = configuration.bannerMultiMessages.filter { model in if self.categorySelect.categorySelectedIndex == 0 { return true } else { @@ -168,30 +121,110 @@ public struct BannerMultiMessageSheet: View { return filteredBannerMultiMessages } - private var messageCountStr: String { + private func messageCountStr(_ configuration: BannerMultiMessageSheetConfiguration) -> String { var count = 0 - for element in self.bannerMultiMessages { + for element in configuration.bannerMultiMessages { count += element.items.count } return String(format: NSLocalizedString("Messages (%d)", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), count) } - public var body: some View { + private func attributedMessageTitle(title: String, typeDesc: String) -> AttributedString { + let attributedString = NSMutableAttributedString(string: title) + + let viewDetailStr = String(format: NSLocalizedString("View %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), typeDesc) + let viewDetail = NSAttributedString(string: " \(viewDetailStr)", attributes: [.foregroundColor: UIColor(Color.preferredColor(.tintColor))]) + attributedString.append(viewDetail) + return AttributedString(attributedString) + } + + private func removeItem(_ configuration: BannerMultiMessageSheetConfiguration, category: String, at id: UUID) { + for i in 0 ..< configuration.bannerMultiMessages.count { + var element = configuration.bannerMultiMessages[i] + if element.category == category { + for index in 0 ..< element.items.count where element.items[index].id == id { + element.items.remove(at: index) + break + } + configuration.bannerMultiMessages.remove(at: i) + configuration.bannerMultiMessages.insert(element, at: i) + + if element.items.isEmpty { + self.handleRemoveCategory(configuration, category: category) + } + break + } + } + configuration.removeAction?(category, id) + } + + private func removeCategoryAction(_ configuration: BannerMultiMessageSheetConfiguration, category: String) { + self.handleRemoveCategory(configuration, category: category) + configuration.removeAction?(category, nil) + } + + private func handleRemoveCategory(_ configuration: BannerMultiMessageSheetConfiguration, category: String) { + for i in 0 ..< configuration.bannerMultiMessages.count { + let element = configuration.bannerMultiMessages[i] + if element.category == category { + configuration.bannerMultiMessages.remove(at: i) + break + } + } + } + + private func showItemDetail(_ configuration: BannerMultiMessageSheetConfiguration, category: String, at id: UUID) { + configuration.viewDetailAction?(id) + self.dismiss(configuration) + } + + private func dismiss(_ configuration: BannerMultiMessageSheetConfiguration) { + self.timer?.invalidate() + self.timer = nil + + configuration.dismissAction?() + } + + private func bannerMessageStyle(_ messageType: BannerMultiMessageType) -> any BannerMessageStyle { + switch messageType { + case .neutral: + return BannerMessageNeutralStyle() + case .negative: + return BannerMessageNegativeStyle() + case .critical: + return BannerMessageCriticalStyle() + case .positive: + return BannerMessagePositiveStyle() + case .informative: + return BannerMessageInformativeStyle() + } + } + + // swiftlint:disable:next function_body_length + public func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { VStack(spacing: 0, content: { HStack { - Text(self.messageCountStr) - .foregroundStyle(Color.preferredColor(.primaryLabel)) - .font(.fiori(forTextStyle: .headline, weight: .bold)) + if !configuration.title.isEmpty { + configuration.title + } else { + Text(self.messageCountStr(configuration)) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + .font(.fiori(forTextStyle: .headline, weight: .bold)) + } if self.isPhone { Spacer() - FioriButton(isSelectionPersistent: false, action: { _ in - self.dismiss() - }, image: { _ in - Image(fioriName: "fiori.error") - }) - .fioriButtonStyle(FioriTertiaryButtonStyle(colorStyle: .normal)) + if !configuration.closeAction.isEmpty { + configuration.closeAction + } else { + FioriButton(isSelectionPersistent: false, action: { _ in + self.dismiss(configuration) + }, image: { _ in + Image(fioriName: "fiori.error") + }) + .fioriButtonStyle(FioriTertiaryButtonStyle(colorStyle: .normal)) + } } } .padding(.leading, self.isPhone ? 16 : 0) @@ -207,9 +240,9 @@ public struct BannerMultiMessageSheet: View { } List { - ForEach(self.filteredBannerMultiMessages, id: \.id) { element in + ForEach(self.filteredBannerMultiMessages(configuration), id: \.id) { element in Section { - if self.turnOnSectionHeader { + if configuration.turnOnSectionHeader { HStack { Text("\(element.category) (\(element.items.count))") .font(.fiori(forTextStyle: .subheadline)) @@ -217,7 +250,7 @@ public struct BannerMultiMessageSheet: View { Spacer() _Action(actionText: _ClearActionDefault().actionText, didSelectAction: { - self.removeCategoryAction(category: element.category) + self.removeCategoryAction(configuration, category: element.category) }) .font(.fiori(forTextStyle: .subheadline)) .foregroundStyle(Color.preferredColor(.tintColor)) @@ -228,8 +261,8 @@ public struct BannerMultiMessageSheet: View { ForEach(0 ..< element.items.count, id: \.self) { index in let message = element.items[index] - if let item = self.messageItemView { - AnyView(item(message.id)) + if !configuration.messageItemView(message.id).isEmpty { + AnyView(configuration.messageItemView(message.id)) } else { BannerMessage(icon: { message.icon @@ -238,7 +271,7 @@ public struct BannerMultiMessageSheet: View { }, closeAction: { FioriButton { state in if state == .normal { - self.removeItem(category: element.category, at: message.id) + self.removeItem(configuration, category: element.category, at: message.id) } } label: { _ in Image(fioriName: "fiori.decline") @@ -246,11 +279,13 @@ public struct BannerMultiMessageSheet: View { }, topDivider: { EmptyView() }, bannerTapAction: { - self.showItemDetail(category: element.category, at: message.id) + self.showItemDetail(configuration, category: element.category, at: message.id) }, alignment: .leading, hideSeparator: true, messageType: message.messageType) + .bannerMessageStyle(self.bannerMessageStyle(message.messageType)) + .typeErased .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { - self.removeItem(category: element.category, at: message.id) + self.removeItem(configuration, category: element.category, at: message.id) } label: { Image(fioriName: "fiori.delete") } @@ -259,12 +294,12 @@ public struct BannerMultiMessageSheet: View { } } footer: { - if self.turnOnSectionHeader { + if configuration.turnOnSectionHeader { Rectangle().fill(Color.preferredColor(.primaryGroupedBackground)) .frame(height: 30) } } - .listSectionSeparator(self.turnOnSectionHeader ? .hidden : .visible, edges: .bottom) + .listSectionSeparator(configuration.turnOnSectionHeader ? .hidden : .visible, edges: .bottom) .listRowInsets(EdgeInsets()) .alignmentGuide(.listRowSeparatorLeading, computeValue: { _ in 0 @@ -292,16 +327,17 @@ public struct BannerMultiMessageSheet: View { .frame(height: self.popoverHeight) .animation(self.scrollContentHeight <= 40.0 ? nil : .spring) // .animation(.spring, value: self.popoverHeight) - .onChange(of: self.bannerMultiMessages) { _ in + .onChange(of: configuration.bannerMultiMessages) { _ in // when datasource is empty, dismiss in 2 seconds - if self.bannerMultiMessages.isEmpty { + if configuration.bannerMultiMessages.isEmpty { self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in - self.dismiss() + self.dismiss(configuration) }) } - self.resetDimensionSelector() + self.resetDimensionSelector(configuration) } .onAppear { + self.resetDimensionSelector(configuration) self.dimensionSelector.selectionDidChangePublisher .sink(receiveValue: { index in self.categorySelect.categorySelectedIndex = index ?? 0 @@ -309,68 +345,37 @@ public struct BannerMultiMessageSheet: View { .store(in: &self.cancellableSet) } } - - private func attributedMessageTitle(title: String, typeDesc: String) -> AttributedString { - let attributedString = NSMutableAttributedString(string: title) - - let viewDetailStr = String(format: NSLocalizedString("View %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), typeDesc) - let viewDetail = NSAttributedString(string: " \(viewDetailStr)", attributes: [.foregroundColor: UIColor(Color.preferredColor(.tintColor))]) - attributedString.append(viewDetail) - return AttributedString(attributedString) - } - - private func removeItem(category: String, at id: UUID) { - for i in 0 ..< self.bannerMultiMessages.count { - var element = self.bannerMultiMessages[i] - if element.category == category { - for index in 0 ..< element.items.count where element.items[index].id == id { - element.items.remove(at: index) - break - } - self.bannerMultiMessages.remove(at: i) - self.bannerMultiMessages.insert(element, at: i) - - if element.items.isEmpty { - self.handleRemoveCategory(category) - } - break - } +} + +// Default fiori styles +extension BannerMultiMessageSheetFioriStyle { + struct ContentFioriStyle: BannerMultiMessageSheetStyle { + func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { + BannerMultiMessageSheet(configuration) + // Add default style for its content + // .background() } - self.removeAction?(category, id) - } - - private func removeCategoryAction(category: String) { - self.handleRemoveCategory(category) - self.removeAction?(category, nil) } - - private func handleRemoveCategory(_ category: String) { - for i in 0 ..< self.bannerMultiMessages.count { - let element = self.bannerMultiMessages[i] - if element.category == category { - self.bannerMultiMessages.remove(at: i) - break - } + + struct TitleFioriStyle: TitleStyle { + let bannerMultiMessageSheetConfiguration: BannerMultiMessageSheetConfiguration + + func makeBody(_ configuration: TitleConfiguration) -> some View { + Title(configuration) + // Add default style for Title + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) } } - - private func showItemDetail(category: String, at id: UUID) { - self.viewDetailAction?(id) - self.dismiss() - } - - private func dismiss() { - self.timer?.invalidate() - self.timer = nil - - self.closeAction?() - } -} -#Preview { - BannerMultiMessageSheet(bannerMultiMessages: Binding<[BannerMessageListModel]>.constant([ - BannerMessageListModel(category: "Errors", items: [ - BannerMessageItemModel(icon: Image(fioriName: "fiori.notification.3"), title: "Single-line text for banner.", messageType: .negative) - ]) - ])) + struct CloseActionFioriStyle: CloseActionStyle { + let bannerMultiMessageSheetConfiguration: BannerMultiMessageSheetConfiguration + + func makeBody(_ configuration: CloseActionConfiguration) -> some View { + CloseAction(configuration) + // Add default style for CloseAction + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } + } } diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/BannerMultiMessageSheet/BannerMultiMessageSheet.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/BannerMultiMessageSheet/BannerMultiMessageSheet.generated.swift new file mode 100644 index 000000000..977254332 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/BannerMultiMessageSheet/BannerMultiMessageSheet.generated.swift @@ -0,0 +1,106 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct BannerMultiMessageSheet { + let title: any View + let closeAction: any View + /// callback when this component want to dismiss itself + let dismissAction: (() -> Void)? + /// callback when category or single item is removed + let removeAction: ((String, UUID?) -> Void)? + /// callback when the link button is clicked + let viewDetailAction: ((UUID) -> Void)? + /// the mark to turn on section header or not + let turnOnSectionHeader: Bool + /// view for each item under the category + let messageItemView: (UUID) -> any View + /// the data source for banner multi-message sheet + @Binding var bannerMultiMessages: [BannerMessageListModel] + + @Environment(\.bannerMultiMessageSheetStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder title: () -> any View, + @ViewBuilder closeAction: () -> any View = { FioriButton { _ in Image(systemName: "xmark") } }, + dismissAction: (() -> Void)? = nil, + removeAction: ((String, UUID?) -> Void)? = nil, + viewDetailAction: ((UUID) -> Void)? = nil, + turnOnSectionHeader: Bool = true, + @ViewBuilder messageItemView: @escaping (UUID) -> any View = { _ in EmptyView() }, + bannerMultiMessages: Binding<[BannerMessageListModel]>) + { + self.title = Title(title: title) + self.closeAction = CloseAction(closeAction: closeAction) + self.dismissAction = dismissAction + self.removeAction = removeAction + self.viewDetailAction = viewDetailAction + self.turnOnSectionHeader = turnOnSectionHeader + self.messageItemView = messageItemView + self._bannerMultiMessages = bannerMultiMessages + } +} + +public extension BannerMultiMessageSheet { + init(title: AttributedString, + closeAction: FioriButton? = FioriButton { _ in Image(systemName: "xmark") }, + dismissAction: (() -> Void)? = nil, + removeAction: ((String, UUID?) -> Void)? = nil, + viewDetailAction: ((UUID) -> Void)? = nil, + turnOnSectionHeader: Bool = true, + @ViewBuilder messageItemView: @escaping (UUID) -> any View = { _ in EmptyView() }, + bannerMultiMessages: Binding<[BannerMessageListModel]>) + { + self.init(title: { Text(title) }, closeAction: { closeAction }, dismissAction: dismissAction, removeAction: removeAction, viewDetailAction: viewDetailAction, turnOnSectionHeader: turnOnSectionHeader, messageItemView: messageItemView, bannerMultiMessages: bannerMultiMessages) + } +} + +public extension BannerMultiMessageSheet { + init(_ configuration: BannerMultiMessageSheetConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: BannerMultiMessageSheetConfiguration, shouldApplyDefaultStyle: Bool) { + self.title = configuration.title + self.closeAction = configuration.closeAction + self.dismissAction = configuration.dismissAction + self.removeAction = configuration.removeAction + self.viewDetailAction = configuration.viewDetailAction + self.turnOnSectionHeader = configuration.turnOnSectionHeader + self.messageItemView = configuration.messageItemView + self._bannerMultiMessages = configuration.$bannerMultiMessages + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension BannerMultiMessageSheet: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(title: .init(self.title), closeAction: .init(self.closeAction), dismissAction: self.dismissAction, removeAction: self.removeAction, viewDetailAction: self.viewDetailAction, turnOnSectionHeader: self.turnOnSectionHeader, messageItemView: self.messageItemView, bannerMultiMessages: self.$bannerMultiMessages)).typeErased + .transformEnvironment(\.bannerMultiMessageSheetStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension BannerMultiMessageSheet { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + BannerMultiMessageSheet(.init(title: .init(self.title), closeAction: .init(self.closeAction), dismissAction: self.dismissAction, removeAction: self.removeAction, viewDetailAction: self.viewDetailAction, turnOnSectionHeader: self.turnOnSectionHeader, messageItemView: self.messageItemView, bannerMultiMessages: self.$bannerMultiMessages)) + .shouldApplyDefaultStyle(false) + .bannerMultiMessageSheetStyle(BannerMultiMessageSheetFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/BannerMultiMessageSheet/BannerMultiMessageSheetStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/BannerMultiMessageSheet/BannerMultiMessageSheetStyle.generated.swift new file mode 100644 index 000000000..3e54304db --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/BannerMultiMessageSheet/BannerMultiMessageSheetStyle.generated.swift @@ -0,0 +1,44 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol BannerMultiMessageSheetStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> Body +} + +struct AnyBannerMultiMessageSheetStyle: BannerMultiMessageSheetStyle { + let content: (BannerMultiMessageSheetConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (BannerMultiMessageSheetConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct BannerMultiMessageSheetConfiguration { + public let title: Title + public let closeAction: CloseAction + public let dismissAction: (() -> Void)? + public let removeAction: ((String, UUID?) -> Void)? + public let viewDetailAction: ((UUID) -> Void)? + public let turnOnSectionHeader: Bool + public let messageItemView: (UUID) -> any View + @Binding public var bannerMultiMessages: [BannerMessageListModel] + + public typealias Title = ConfigurationViewWrapper + public typealias CloseAction = ConfigurationViewWrapper +} + +public struct BannerMultiMessageSheetFioriStyle: BannerMultiMessageSheetStyle { + public func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { + BannerMultiMessageSheet(configuration) + .titleStyle(TitleFioriStyle(bannerMultiMessageSheetConfiguration: configuration)) + .closeActionStyle(CloseActionFioriStyle(bannerMultiMessageSheetConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift index 902da86a7..91bba2878 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift @@ -255,6 +255,62 @@ public extension BannerMessageStyle where Self == BannerMessageTopDividerStyle { } } +// MARK: BannerMultiMessageSheetStyle + +public extension BannerMultiMessageSheetStyle where Self == BannerMultiMessageSheetBaseStyle { + static var base: BannerMultiMessageSheetBaseStyle { + BannerMultiMessageSheetBaseStyle() + } +} + +public extension BannerMultiMessageSheetStyle where Self == BannerMultiMessageSheetFioriStyle { + static var fiori: BannerMultiMessageSheetFioriStyle { + BannerMultiMessageSheetFioriStyle() + } +} + +public struct BannerMultiMessageSheetTitleStyle: BannerMultiMessageSheetStyle { + let style: any TitleStyle + + public func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { + BannerMultiMessageSheet(configuration) + .titleStyle(self.style) + .typeErased + } +} + +public extension BannerMultiMessageSheetStyle where Self == BannerMultiMessageSheetTitleStyle { + static func titleStyle(_ style: some TitleStyle) -> BannerMultiMessageSheetTitleStyle { + BannerMultiMessageSheetTitleStyle(style: style) + } + + static func titleStyle(@ViewBuilder content: @escaping (TitleConfiguration) -> some View) -> BannerMultiMessageSheetTitleStyle { + let style = AnyTitleStyle(content) + return BannerMultiMessageSheetTitleStyle(style: style) + } +} + +public struct BannerMultiMessageSheetCloseActionStyle: BannerMultiMessageSheetStyle { + let style: any CloseActionStyle + + public func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { + BannerMultiMessageSheet(configuration) + .closeActionStyle(self.style) + .typeErased + } +} + +public extension BannerMultiMessageSheetStyle where Self == BannerMultiMessageSheetCloseActionStyle { + static func closeActionStyle(_ style: some CloseActionStyle) -> BannerMultiMessageSheetCloseActionStyle { + BannerMultiMessageSheetCloseActionStyle(style: style) + } + + static func closeActionStyle(@ViewBuilder content: @escaping (CloseActionConfiguration) -> some View) -> BannerMultiMessageSheetCloseActionStyle { + let style = AnyCloseActionStyle(content) + return BannerMultiMessageSheetCloseActionStyle(style: style) + } +} + // MARK: CancelActionStyle public extension CancelActionStyle where Self == CancelActionBaseStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift index d066bd450..7774702db 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift @@ -192,6 +192,27 @@ extension EnvironmentValues { } } +// MARK: BannerMultiMessageSheetStyle + +struct BannerMultiMessageSheetStyleStackKey: EnvironmentKey { + static let defaultValue: [any BannerMultiMessageSheetStyle] = [] +} + +extension EnvironmentValues { + var bannerMultiMessageSheetStyle: any BannerMultiMessageSheetStyle { + self.bannerMultiMessageSheetStyleStack.last ?? .base.concat(.fiori) + } + + var bannerMultiMessageSheetStyleStack: [any BannerMultiMessageSheetStyle] { + get { + self[BannerMultiMessageSheetStyleStackKey.self] + } + set { + self[BannerMultiMessageSheetStyleStackKey.self] = newValue + } + } +} + // MARK: CancelActionStyle struct CancelActionStyleStackKey: EnvironmentKey { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift index eb1822176..d87ca8ace 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift @@ -260,6 +260,34 @@ public extension BannerMessageStyle { } } +// MARK: BannerMultiMessageSheetStyle + +extension ModifiedStyle: BannerMultiMessageSheetStyle where Style: BannerMultiMessageSheetStyle { + public func makeBody(_ configuration: BannerMultiMessageSheetConfiguration) -> some View { + BannerMultiMessageSheet(configuration) + .bannerMultiMessageSheetStyle(self.style) + .modifier(self.modifier) + } +} + +public struct BannerMultiMessageSheetStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.bannerMultiMessageSheetStyle(self.style) + } +} + +public extension BannerMultiMessageSheetStyle { + func modifier(_ modifier: some ViewModifier) -> some BannerMultiMessageSheetStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some BannerMultiMessageSheetStyle) -> some BannerMultiMessageSheetStyle { + style.modifier(BannerMultiMessageSheetStyleModifier(style: self)) + } +} + // MARK: CancelActionStyle extension ModifiedStyle: CancelActionStyle where Style: CancelActionStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift index 727da7d40..847181efd 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift @@ -147,6 +147,22 @@ extension BannerMessageStyle { } } +// MARK: BannerMultiMessageSheetStyle + +struct ResolvedBannerMultiMessageSheetStyle: View { + let style: Style + let configuration: BannerMultiMessageSheetConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension BannerMultiMessageSheetStyle { + func resolve(configuration: BannerMultiMessageSheetConfiguration) -> some View { + ResolvedBannerMultiMessageSheetStyle(style: self, configuration: configuration) + } +} + // MARK: CancelActionStyle struct ResolvedCancelActionStyle: View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift index f17e19292..8c813fb05 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift @@ -156,6 +156,23 @@ public extension View { } } +// MARK: BannerMultiMessageSheetStyle + +public extension View { + func bannerMultiMessageSheetStyle(_ style: some BannerMultiMessageSheetStyle) -> some View { + self.transformEnvironment(\.bannerMultiMessageSheetStyleStack) { stack in + stack.append(style) + } + } + + func bannerMultiMessageSheetStyle(@ViewBuilder content: @escaping (BannerMultiMessageSheetConfiguration) -> some View) -> some View { + self.transformEnvironment(\.bannerMultiMessageSheetStyleStack) { stack in + let style = AnyBannerMultiMessageSheetStyle(content) + stack.append(style) + } + } +} + // MARK: CancelActionStyle public extension View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift index a5837e718..1df38f5c4 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift @@ -61,6 +61,13 @@ extension BannerMessage: _ViewEmptyChecking { } } +extension BannerMultiMessageSheet: _ViewEmptyChecking { + public var isEmpty: Bool { + title.isEmpty && + closeAction.isEmpty + } +} + extension CancelAction: _ViewEmptyChecking { public var isEmpty: Bool { cancelAction.isEmpty diff --git a/sourcery/.lib/Sources/utils/ForStyleableComponent/Array+Extension.swift b/sourcery/.lib/Sources/utils/ForStyleableComponent/Array+Extension.swift index b5c253075..3be2519d7 100644 --- a/sourcery/.lib/Sources/utils/ForStyleableComponent/Array+Extension.swift +++ b/sourcery/.lib/Sources/utils/ForStyleableComponent/Array+Extension.swift @@ -10,9 +10,13 @@ extension [Variable] { var propertyListDecl: String { map { variable in var varDecl = variable.documentation.isEmpty ? "" : variable.docText + "\n" - if let (_, returnType, _, _) = variable.resultBuilderAttrs { + if let (_, returnType, _, _) = variable.resultBuilderAttrs, + variable.closureParameters.isEmpty + { varDecl += "let \(variable.name): \(returnType)" - } else if variable.hasResultBuilderAttribute { + } else if variable.hasResultBuilderAttribute, + variable.closureParameters.isEmpty + { let type = variable.closureReturnType ?? variable.typeName.name varDecl += "let \(variable.name): \(type)" } else if variable.isBinding { @@ -30,7 +34,14 @@ extension [Variable] { var viewBuilderInitParams: String { map { variable in if let (name, returnType, defaultValue, _) = variable.resultBuilderAttrs { - return "\(name) \(variable.name): () -> \(returnType)\(defaultValue.prependAssignmentIfNeeded())" + if variable.closureParameters.isEmpty { + return "\(name) \(variable.name): () -> \(returnType)\(defaultValue.prependAssignmentIfNeeded())" + } else { + let escapingAttr = variable.typeName.isClosure && + !variable.typeName.isOptional + ? "@escaping " : "" + return "\(name) \(variable.name): \(escapingAttr)\(variable.typeName)\(defaultValue.prependAssignmentIfNeeded())" + } } else if variable.hasResultBuilderAttribute { return variable.resultBuilderInitParamDecl } else if variable.isBinding { @@ -50,8 +61,12 @@ extension [Variable] { map { variable in let name = variable.name if variable.isResultBuilder { - let assignment = isBaseComponent || !variable.isStyleable ? "\(name)()" : "\(name.capitalizingFirst())(\(name): \(name))" - return "self.\(name) = \(assignment)" + if !variable.closureParameters.isEmpty { + return "self.\(name) = \(name)" + } else { + let assignment = isBaseComponent || !variable.isStyleable ? "\(name)()" : "\(name.capitalizingFirst())(\(name): \(name))" + return "self.\(name) = \(assignment)" + } } else if variable.isBinding { return "self._\(name) = \(name)" } else { @@ -64,7 +79,14 @@ extension [Variable] { var dataInitParams: String { map { variable in let decl: String - if variable.isBinding { + if let (name, returnType, defaultValue, _) = variable.resultBuilderAttrs, + !variable.closureParameters.isEmpty + { + let escapingAttr = variable.typeName.isClosure && + !variable.typeName.isOptional + ? "@escaping " : "" + return "\(name) \(variable.name): \(escapingAttr)\(variable.typeName)\(defaultValue.prependAssignmentIfNeeded())" + } else if variable.isBinding { return "\(variable.name): Binding<\(variable.typeName)>" } else if variable.hasResultBuilderAttribute { return variable.resultBuilderInitParamDecl @@ -78,7 +100,9 @@ extension [Variable] { var dataInitBody: String { let initArgs = map { variable in let name = variable.name - if let (_, _, _, backingComponent) = variable.resultBuilderAttrs { + if let (_, _, _, backingComponent) = variable.resultBuilderAttrs, + variable.closureParameters.isEmpty + { let arg = backingComponent.isEmpty ? "\(name)" : "\(backingComponent)(\(name))" return "\(name): { \(arg) }" } else { @@ -105,7 +129,10 @@ extension [Variable] { var configurationInitArgs: String { map { variable in let name = variable.name - if variable.isResultBuilder, variable.annotations.resultBuilderReturnType == nil { + if variable.isResultBuilder, + variable.annotations.resultBuilderReturnType == nil, + variable.closureParameters.isEmpty + { return "\(name): .init(self.\(name))" } else if variable.isBinding { return "\(name): self.$\(name)" @@ -121,7 +148,9 @@ extension [Variable] { variable.isResultBuilder }.compactMap { variable in let name = variable.name - if variable.isResultBuilder { + if variable.isResultBuilder, + variable.closureParameters.isEmpty + { return "\(name).isEmpty" } else if variable.isOptional { return "\(name) == nil" @@ -139,7 +168,9 @@ extension [Variable] { var `typealias`: [String] = [] for variable in self { let name = variable.name - if variable.isResultBuilder { + if variable.isResultBuilder, + variable.closureParameters.isEmpty + { props.append("public let \(name): \(name.capitalizingFirst())") var type = "ConfigurationViewWrapper" if let returnType = variable.annotations.resultBuilderReturnType { diff --git a/sourcery/.lib/Sources/utils/ForStyleableComponent/Variable+Extension.swift b/sourcery/.lib/Sources/utils/ForStyleableComponent/Variable+Extension.swift index a5f1e5a58..d4f4edd8a 100644 --- a/sourcery/.lib/Sources/utils/ForStyleableComponent/Variable+Extension.swift +++ b/sourcery/.lib/Sources/utils/ForStyleableComponent/Variable+Extension.swift @@ -131,7 +131,20 @@ extension Variable { var resultBuilderInitParamDecl: String { var ret = self.attributes.string + " " - ret += "\(self.name): \(self.resultBuilderTypeName ?? self.typeName.name)\(self.defaultValue.prependAssignmentIfNeeded())" + + if self.closureParameters.isEmpty { + ret += "\(self.name): \(self.resultBuilderTypeName ?? self.typeName.name)\(self.defaultValue.prependAssignmentIfNeeded())" + } else { + let escapingAttr = self.typeName.isClosure && + !self.typeName.isOptional + ? "@escaping " : "" + if ret.contains("@ViewBuilder") { + ret += "\(self.name): \(escapingAttr)\(self.typeName)\(self.defaultValue.prependAssignmentIfNeeded())" + } else { + ret += "@ViewBuilder \(self.name): \(escapingAttr)\(self.typeName)\(self.defaultValue.prependAssignmentIfNeeded())" + } + } + return ret } @@ -182,6 +195,10 @@ extension Variable { var closureReturnType: String? { self.typeName.closure?.actualReturnTypeName.name } + + var closureParameters: [ClosureParameter] { + self.typeName.closure?.parameters ?? [] + } } extension Variable { @@ -190,6 +207,7 @@ extension Variable { typeName.isClosure && !typeName.isOptional ? "@escaping " : "" + return "\(name): \(escapingAttr)\(typeName)\(self.defaultValue.prependAssignmentIfNeeded())" } }