diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 91e3bc977..e0f2e4511 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 1FC30412270540FB004BEE00 /* 72-Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC30411270540FB004BEE00 /* 72-Fonts.swift */; }; 1FC30414270541BF004BEE00 /* FioriThemeManagerContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC30413270541BF004BEE00 /* FioriThemeManagerContentView.swift */; }; 1FF3662E264C662A00AB8BD8 /* DimensionSelector+Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF3662D264C662A00AB8BD8 /* DimensionSelector+Chart.swift */; }; + 3B62AB7E2C0EE257003262EB /* EditableSideBarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */; }; 3C180C282B858CF6007CE79A /* IllustratedMessageExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C180C272B858CF6007CE79A /* IllustratedMessageExample.swift */; }; 691DE21925F2A30B00094D4A /* KPIViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691DE21825F2A30B00094D4A /* KPIViewExample.swift */; }; 692F338B26556A6A009B98DA /* SideBarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692F338A26556A6A009B98DA /* SideBarExample.swift */; }; @@ -194,6 +195,7 @@ 1FC30411270540FB004BEE00 /* 72-Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "72-Fonts.swift"; sourceTree = ""; }; 1FC30413270541BF004BEE00 /* FioriThemeManagerContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriThemeManagerContentView.swift; sourceTree = ""; }; 1FF3662D264C662A00AB8BD8 /* DimensionSelector+Chart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DimensionSelector+Chart.swift"; sourceTree = ""; }; + 3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditableSideBarExample.swift; sourceTree = ""; }; 3C180C272B858CF6007CE79A /* IllustratedMessageExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IllustratedMessageExample.swift; sourceTree = ""; }; 691DE21825F2A30B00094D4A /* KPIViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KPIViewExample.swift; sourceTree = ""; }; 692F338A26556A6A009B98DA /* SideBarExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarExample.swift; sourceTree = ""; }; @@ -422,6 +424,7 @@ 692F338926556A34009B98DA /* SideBar */ = { isa = PBXGroup; children = ( + 3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */, 692F338A26556A6A009B98DA /* SideBarExample.swift */, ); path = SideBar; @@ -892,6 +895,7 @@ 993B55BE29DF7EC70002B065 /* IconLibraryExample.swift in Sources */, B80DA9BE260C1CC200C0B2E9 /* ListDataProtocol.swift in Sources */, B1DD86532B0758F000D7EDFD /* NavigationBarPopover.swift in Sources */, + 3B62AB7E2C0EE257003262EB /* EditableSideBarExample.swift in Sources */, B1DD86552B0759DD00D7EDFD /* NavigationBarCustomItem.swift in Sources */, B84D24EE2652F343007F2373 /* ObjectHeaderViewScenarios.swift in Sources */, 9D0B26082B9BA5C0004278A5 /* FormViewExamples.swift in Sources */, diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SideBar/EditableSideBarExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/SideBar/EditableSideBarExample.swift new file mode 100644 index 000000000..fb69702f3 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/SideBar/EditableSideBarExample.swift @@ -0,0 +1,266 @@ +import CoreLocation +import FioriSwiftUICore +import FioriThemeManager +import MapKit +import SwiftUI + +public struct SideBarExample: View { + public var body: some View { + List { + NavigationLink { + OutdatedSideBarExample() + } label: { + Text("Outdated Sidebar") + } + NavigationLink { + List { + NavigationLink { + EditableSideBarExample(allowEdit: false) + } label: { + Text("Readonly Sidebar") + } + NavigationLink { + EditableSideBarExample() + } label: { + Text("Editable Sidebar") + } + NavigationLink { + EditableSideBarExample(isCustom: true) + } label: { + Text("Customized Sidebar") + } + } + .navigationBarHidden(false) + } label: { + Text("Sidebar") + } + } + } +} + +struct EditableSideBarExample: View { + var allowEdit: Bool = true + var isCustom: Bool = false + let isPad: Bool = UIDevice.current.userInterfaceIdiom == .pad + + var deviceModelObject = DeviceExampleModelObject() + let destinationView = DeviceDetail() + + @Environment(\.presentationMode) var presentationMode + @State private var isEditing = false + @State private var listItems: [SideBarItemModel] = loadItemModelData() + @State private var queryString: String? + @State private var selection: SideBarItemModel? + + public var body: some View { + let footer = UIDevice.current.userInterfaceIdiom == .pad ? ObjectItem(title: "Title", subtitle: "SubTitle", detailImage: Image(systemName: "person")) + .objectItemStyle(content: { configuration in + ObjectItem(configuration) + .background(Color.preferredColor(.quaternaryFill)) + }) : nil + + let view = NavigationSplitView { + SideBar( + isEditing: self.$isEditing, + queryString: self.$queryString, + data: self.$listItems, + selection: self.$selection, + title: "Devices", + footer: { self.isPad && !self.isCustom ? footer : nil }, + editButton: { self.allowEdit ? Button(action: { + if !self.isEditing { + // Check the listItems + for (_, item) in self.listItems.enumerated() { + if !item.isSection { + print("BarItem: '" + item.title + "' was hidden? --- " + String(item.isInvisible)) + } else { + print("Bar Section: '" + item.title + "' has following children:") + if let children = item.children { + for (index, child) in children.enumerated() { + print(String(index + 1) + " : '" + child.title + "' was hidden? --- " + String(child.isInvisible)) + } + } + } + } + } + }, label: { Text(self.isEditing ? "Done" : "Edit") }) : nil }, +// editButton: { EditButton() }, // Also can use system EditButton here if you don't want to check the updated data or customize the button's label + destination: { model in + if let device = getDevice(item: model) { + DispatchQueue.main.async { + self.deviceModelObject.device = device + } + } + return self.destinationView.environmentObject(self.deviceModelObject) + }, + item: { item in + self.makeItem(item) + } + ) + .navigationBarItems(leading: Button(action: { + self.presentationMode.wrappedValue.dismiss() + }, label: { Text("Back") })) + } detail: { + DevDetailView(title: "Home Page - Starting From Here") + } + .navigationBarHidden(true) + + if !self.isCustom { + view.searchable(text: Binding(get: { self.queryString ?? "" }, set: { newValue in self.queryString = newValue }), prompt: "Search") + .onAppear { + let searchImage = UIImage(systemName: "magnifyingglass")? + .withTintColor(UIColor(Color.preferredColor(.tertiaryLabel)), renderingMode: .alwaysOriginal) + .applyingSymbolConfiguration(UIImage.SymbolConfiguration(weight: .semibold)) + UISearchBar.appearance().setImage(searchImage, for: .search, state: .normal) + } + .onDisappear { + UISearchBar.appearance().setImage(nil, for: .search, state: .normal) + } + } else { + view + } + } + + func makeItem(_ item: Binding) -> any View { + let filledIcon = item.wrappedValue.filledIcon != nil ? item.wrappedValue.filledIcon : item.wrappedValue.icon + let barItem = SideBarListItem(icon: item.wrappedValue.icon, filledIcon: filledIcon, title: AttributedString(item.wrappedValue.title), subtitle: AttributedString(item.wrappedValue.subtitle ?? ""), accessoryIcon: item.wrappedValue.accessoryIcon, isOn: Binding(get: { !item.wrappedValue.isInvisible }, set: { item.wrappedValue.isInvisible = !$0 }), data: item.wrappedValue, isSelected: Binding(get: { self.selection == item.wrappedValue }, set: { if $0 { self.selection = item.wrappedValue } })) + + if self.isCustom { + return barItem.sideBarListItemStyle { configuration in + if self.selection == configuration.data, !self.isEditing { + SideBarListItem(configuration) + .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.preferredColor(.chart3))) + } else { + SideBarListItem(configuration) + } + } + .titleStyle { configuration in + configuration.title.foregroundColor(self.isEditing ? .green : .indigo) + .font(.fiori(forTextStyle: .title3, weight: .regular)) + } + .iconStyle { configuration in + configuration.icon.foregroundColor(!self.isEditing ? .red : .pink) + } + .filledIconStyle { configuration in + configuration.filledIcon.foregroundColor(.brown) + } + .subtitleStyle { configuration in + configuration.subtitle.foregroundColor(.indigo) + .font(.fiori(forTextStyle: .footnote, weight: .regular)) + } + .accessoryIconStyle { configuration in + configuration.accessoryIcon.foregroundColor(.accentColor) + } + } else { + return barItem + } + } +} + +func loadItemModelData() -> [SideBarItemModel] { + var barItems: [SideBarItemModel] = [] + var groupdedItems: [DeviceCategory: [SideBarItemModel]] = Dictionary() + for item in devices { + var sideBarItem = SideBarItemModel(title: item.name, icon: Image(systemName: "square.dashed"), filledIcon: Image(systemName: "square.dashed.inset.filled"), subtitle: item.description, accessoryIcon: Image(systemName: "chevron.right")) + sideBarItem.id = item.id + + if let category = item.category { + if groupdedItems.index(forKey: category) != nil { + groupdedItems[category]?.append(sideBarItem) + } else { + groupdedItems[category] = [sideBarItem] + } + } else { + barItems.append(sideBarItem) + } + } + + for category in groupdedItems.keys { + var section = SideBarItemModel(title: category.rawValue) + section.children = groupdedItems[category] + barItems.append(section) + } + + return barItems +} + +func getDevice(item: SideBarItemModel) -> Device? { + for device in devices { + if item.id == device.id { + return device + } + } + return nil +} + +let devices: [Device] = [ + Device("Apple Watch", "Mental health"), + Device("AirPods Max", "connection"), + Device("iPad Pro", "Ultra Retina XDR"), + Device("iPhone 14", "closer look", .iPhone14), + Device("iPhone 14 Plus", "Dynamic Island", .iPhone14), + Device("iPhone 14 Pro", nil, .iPhone14), + Device("iPhone 14 Pro Max", "48MP", .iPhone14), + Device("iPhone 15", nil, .iPhone15), + Device("iPhone 15 Plus", "$799", .iPhone15), + Device("iPhone 15 Pro", nil, .iPhone15), + Device("iPhone 15 Pro Max", "Dynamic Island", .iPhone15) +] + +class DeviceExampleModelObject: ObservableObject { + @Published var device: Device? +} + +struct DeviceDetail: View { + @EnvironmentObject private var modelObject: DeviceExampleModelObject + + var body: some View { + if let device = modelObject.device { + ScrollView { + VStack(alignment: .leading) { + HStack { + Text(device.name) + .font(.title).fontWeight(.bold) + } + + if let dec = device.description { + HStack { + Text(dec) + } + .font(.title3).fontWeight(.bold) + .foregroundStyle(.secondary) + } + + Divider() + + Text("\(device.about) ") + .font(.title2) + } + .padding() + } + .navigationTitle(device.name) + .navigationBarTitleDisplayMode(.inline) + } else { + Group {} + } + } +} + +struct Device: Hashable, Identifiable { + var id = UUID() + var name: String + var description: String? + var category: DeviceCategory? + var about: String + + init(_ name: String, _ description: String? = nil, _ category: DeviceCategory? = nil) { + self.name = name + self.description = description + self.category = category + self.about = "Check out our official YouTube channel to help you get the most from your Apple devices and services." + } +} + +enum DeviceCategory: String, CaseIterable { + case iPhone12, iPhone13, iPhone14, iPhone15 +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SideBar/SideBarExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/SideBar/SideBarExample.swift index 6bdf352da..89dd4d783 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/SideBar/SideBarExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/SideBar/SideBarExample.swift @@ -1,7 +1,7 @@ import FioriSwiftUICore import SwiftUI -public struct SideBarExample: View { +public struct OutdatedSideBarExample: View { public var body: some View { NavigationView { SideBarView() @@ -41,7 +41,7 @@ struct BarItem: Identifiable, Hashable { } public struct SideBarView: View { - struct DevRowModel: Identifiable, SideBarListItemModel { + struct DevRowModel: Identifiable, _SideBarListItemModel { var id = UUID() var accessoryIcon: Image? @@ -114,17 +114,17 @@ public struct SideBarView: View { public init() {} public var body: some View { - SideBar(footerModel: DevObjectItemModel(title: "Title", subtitle: "Subtitle", detailImage: Image(systemName: "person")), - list: ExpandableList(data: self.items, - children: \.children, - selection: self.$selectedItem, - rowModel: { item in - DevRowModel(icon: item.icon, title: item.title, subtitle: item.subtitle, accessory: item.status) - }, - destination: { item in - DevDetailView(title: item.title) - })) - .background(Color.preferredColor(.header)) + _SideBar(footerModel: DevObjectItemModel(title: "Title", subtitle: "Subtitle", detailImage: Image(systemName: "person")), + list: ExpandableList(data: self.items, + children: \.children, + selection: self.$selectedItem, + rowModel: { item in + DevRowModel(icon: item.icon, title: item.title, subtitle: item.subtitle, accessory: item.status) + }, + destination: { item in + DevDetailView(title: item.title) + })) + .background(Color.preferredColor(.header)) } } diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index d7f352288..6d7618f20 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -115,17 +115,27 @@ public protocol ListPickerItemModel: KeyComponent, ValueComponent {} // sourcery: generated_component_not_configurable public protocol ProgressIndicatorModel: ProgressIndicatorComponent {} +/// Deprecated SideBarListItem // sourcery: add_env_props = "sideBarListItemConfigMode" // sourcery: add_env_props = "sizeCategory" // sourcery: virtualPropSidebarIconScaleMetric = "@ScaledMetric var scale: CGFloat = 1" // sourcery: generated_component -public protocol SideBarListItemModel: IconComponent, TitleComponent, SubtitleComponent, AccessoryIconComponent {} +public protocol _SideBarListItemModel: IconComponent, TitleComponent, SubtitleComponent, AccessoryIconComponent {} +/// Deprecated SideBarListItem +@available(*, unavailable, renamed: "_SideBarListItemModel", message: "Will be removed in the future release. Please create SideBarListItem with other initializers instead.") +public protocol SideBarListItemModel {} + +/// Deprecated SideBar // sourcery: availableAttributeContent = "iOS 14, *" // sourcery: add_view_builder_params = "detail" // sourcery: add_view_builder_params = "footer" // sourcery: generated_component -public protocol SideBarModel: SubtitleComponent {} +public protocol _SideBarModel: SubtitleComponent {} + +/// Deprecated SideBar +@available(*, unavailable, renamed: "_SideBarModel", message: "Will be removed in the future release. Please create SideBar with other initializers instead.") +public protocol SideBarModel {} // sourcery: add_env_props = "horizontalSizeClass" // sourcery: add_env_props = "sizeCategory" diff --git a/Sources/FioriSwiftUICore/Views/SideBar+View.swift b/Sources/FioriSwiftUICore/Views/_SideBar+View.swift similarity index 95% rename from Sources/FioriSwiftUICore/Views/SideBar+View.swift rename to Sources/FioriSwiftUICore/Views/_SideBar+View.swift index 8c7926875..f6bc1933a 100644 --- a/Sources/FioriSwiftUICore/Views/SideBar+View.swift +++ b/Sources/FioriSwiftUICore/Views/_SideBar+View.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI extension Fiori { - enum SideBar { + enum _SideBar { struct Subtitle: ViewModifier { func body(content: Content) -> some View { content @@ -23,7 +23,7 @@ extension Fiori { } @available(iOS 14, *) -extension SideBar: View { +extension _SideBar: View { public var body: some View { VStack(spacing: 0) { subtitle @@ -39,7 +39,7 @@ extension SideBar: View { } @available(iOS 14, *) -public extension SideBar where Detail == AnyView { +public extension _SideBar where Detail == AnyView { /// Returns a side bar view with given configuration. /// - Parameters: /// - subtitle: The view builder which returns the subtitle view. @@ -55,7 +55,7 @@ public extension SideBar where Detail == AnyView { } @available(iOS 14, *) -public extension SideBar where Subtitle == _ConditionalContent, Detail == AnyView { +public extension _SideBar where Subtitle == _ConditionalContent, Detail == AnyView { /// Returns a side bar view with given configuration. /// - Parameters: /// - subtitle: The subtitle string. @@ -73,7 +73,7 @@ public extension SideBar where Subtitle == _ConditionalContent, } @available(iOS 14, *) -public extension SideBar where Subtitle == _ConditionalContent, +public extension _SideBar where Subtitle == _ConditionalContent, Footer == _ConditionalContent, Detail == _ConditionalContent { @@ -164,7 +164,7 @@ public struct ExpandableList: View where Data: RandomAcc } @available(iOS 14, *) -public extension ExpandableList where Row == SideBarListItem<_ConditionalContent, Text, _ConditionalContent, _ConditionalContent> { +public extension ExpandableList where Row == _SideBarListItem<_ConditionalContent, Text, _ConditionalContent, _ConditionalContent> { /// Creates an expandable list from a collection of data which supports multi-level hierarchy with the ability to select a single item. /// - Parameters: /// - data: The data for constructing the list. @@ -175,7 +175,7 @@ public extension ExpandableList where Row == SideBarListItem<_ConditionalContent init(data: Data, children: KeyPath, selection: Binding, - rowModel: @escaping (Data.Element) -> SideBarListItemModel, + rowModel: @escaping (Data.Element) -> _SideBarListItemModel, destination: @escaping (Data.Element) -> Destination) { self.contentView = ScrollView(.vertical, showsIndicators: false, content: { @@ -197,7 +197,7 @@ public extension ExpandableList where Row == SideBarListItem<_ConditionalContent }, isModelInit: true) } else { if item == selection.wrappedValue { - SideBarListItem(model: rowModel(item)) + _SideBarListItem(model: rowModel(item)) .modifier(ListItemBackgroundSelectionStyle()) .environment(\.sideBarListItemConfigMode, SideBarListItemConfig(isSelected: true, isHeaderContent: false)) .overlay(NavigationLink(destination: destination(item), @@ -210,7 +210,7 @@ public extension ExpandableList where Row == SideBarListItem<_ConditionalContent selection.wrappedValue = item } } else { - SideBarListItem(model: rowModel(item)) + _SideBarListItem(model: rowModel(item)) .environment(\.sideBarListItemConfigMode, SideBarListItemConfig(isSelected: false, isHeaderContent: false)) .contentShape(Rectangle()) .onTapGesture { diff --git a/Sources/FioriSwiftUICore/Views/SideBarListItem+View.swift b/Sources/FioriSwiftUICore/Views/_SideBarListItem+View.swift similarity index 98% rename from Sources/FioriSwiftUICore/Views/SideBarListItem+View.swift rename to Sources/FioriSwiftUICore/Views/_SideBarListItem+View.swift index b1291210d..f8c24a83d 100644 --- a/Sources/FioriSwiftUICore/Views/SideBarListItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/_SideBarListItem+View.swift @@ -1,7 +1,7 @@ import SwiftUI extension Fiori { - enum SideBarListItem { + enum _SideBarListItem { struct Title: ViewModifier { func body(content: Content) -> some View { content @@ -36,7 +36,7 @@ extension Fiori { } } -extension SideBarListItem: View { +extension _SideBarListItem: View { public var body: some View { Group { if sizeCategory.isAccessibilityCategory { diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift index c04ddca1e..5af7744dd 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift @@ -59,6 +59,18 @@ protocol _IconsComponent { var icons: [TextOrIcon] { get } } +// sourcery: BaseComponent +protocol _FilledIconComponent { + // sourcery: @ViewBuilder + var filledIcon: Image? { get } +} + +// sourcery: BaseComponent +protocol _AccessoryIconComponent { + // sourcery: @ViewBuilder + var accessoryIcon: Image? { get } +} + // sourcery: BaseComponent protocol _FootnoteIconsComponent { // sourcery: resultBuilder.name = @FootnoteIconsBuilder, resultBuilder.backingComponent = FootnoteIconStack diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index 5c683cee1..0c4bc4bc9 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -132,3 +132,131 @@ protocol _StepperFieldComponent: _DecrementActionComponent, _TextInputFieldCompo // sourcery: CompositeComponent protocol _StepperViewComponent: _TitleComponent, _StepperFieldComponent, _InformationViewComponent {} + +/// SideBar: SwiftUI View +/// +/// The `SideBar` SwiftUI view presents a expandable list of items using the `SideBarListItem` SwiftUI view. It has support for both edit and view modes. In edit mode, the listed data can be rearranged, and each item can be toggled as hidden. The hidden items are not shown in view mode. +/// +/// This View should be used in `NavigationSplitView` as side bar. How, in case you are NOT using the `NavigationSplitView` for the SideBar, you should observe the change of selected item by property 'selection' of SideBar and handle follow-up logic by youself. Also, you should set 'isUsedInSplitView' of SideBar to true and return EmptyView in 'destination' callback. +/// +/// ## Usage +/// +/// ### Initialization: +/// +/// Construct the data,, array of `SideBarItemModel`, for the expandable list that will be displayed in side bar. +/// +/// ```swift +/// @State private var data: [SideBarItemModel] = [ +/// SideBarItemModel(title: "Root Item 0.1", icon: Image(systemName: "square.dashed"), filledIcon: Image(systemName: "square.dashed.inset.filled"), subtitle: +/// "9,999+", accessoryIcon: Image(systemName: "clock"), children: nil), +/// SideBarItemModel(title: "Root Item 0.4", icon: Image(systemName: "cloud.snow"), children: nil), +/// SideBarItemModel(title: "Group 1", children: [ +/// SideBarItemModel(title: "Child Item 1.1", icon: Image(systemName: "square.and.pencil"), subtitle: "66", accessoryIcon: Image(systemName: "circle"), +/// children: nil), +/// SideBarItemModel(title: "Child Item 1.2", icon: Image(systemName: "square.and.pencil"), accessoryIcon: Image(systemName: "circle"), children: nil) +/// ]), +/// SideBarItemModel(title: "Group 2", children: [ +/// SideBarItemModel(title: "Child Item 2.1", icon: Image(systemName: "folder"), subtitle: "5", accessoryIcon: Image(systemName: "mail"), children: nil) +/// ]) +/// ] +/// ``` +/// Initialize a `SideBar` with title, edit button, selected item destination view, the binding edit mode indicator, search query string, data, selected item and row item content +/// +/// ```swift +/// @State private var isEditing = false +/// @State private var queryString: String? +/// @State private var selection: SideBarItemModel? +/// +/// SideBar( +/// isEditing: $isEditing, +/// queryString: $queryString, +/// data: $data, +/// selection: $selection, +/// title: "SideBar", +/// editButton: { +/// // Or use SWiftUI EditButton() here directly if you don't need to check the changed data or customize the label for edit button: EditButton() +/// Button(action: { +/// if !self.isEditing { +/// // Check the listItems +/// for(_, item) in listItems.enumerated() { +/// +/// } +/// } +/// }, label: {Text(isEditing ? "Done" : "Edit")}) }, +/// destination: { item in +/// DevDetailView(item) +/// }, +/// item: { item in +/// SideBarListItem(icon: item.wrappedValue.icon, filledIcon: item.wrappedValue.filledIcon, title: AttributedString(item.wrappedValue.title), subtitle: +/// AttributedString(item.wrappedValue.subtitle ?? ""), accessoryIcon: item.wrappedValue.accessoryIcon, isOn: Binding(get: { +/// !item.wrappedValue.isInvisible }, set: { newValue in item.wrappedValue.isInvisible = !newValue}), selection: $selection, data: item.wrappedValue) +/// } +/// ) +/// ``` +/// ### Handle Search: +/// +/// The binding property `queryString` was used to trigger the searching on SideBar. The `.searchable` modifier on `NavigationSplieView` can be used to bind the @State variable `queryString` which will bind to SideBar. As the same time, an `UISearchBar` can inititalized in `onAppear` modifier and dismissed in `onDisappear` modifier +/// +/// ```swift +/// NavigationSplitView { +/// sideBar +/// } +/// .searchable(text: Binding(get: { self.queryString ?? "" }, set: { newValue in self.queryString = newValue}), prompt: "Search") +/// .onAppear { +/// let searchImage = UIImage(systemName: "magnifyingglass")? +/// .withTintColor(UIColor(Color.preferredColor(.tertiaryLabel)), renderingMode: .alwaysOriginal) +/// .applyingSymbolConfiguration(UIImage.SymbolConfiguration(weight: .semibold)) +/// UISearchBar.appearance().setImage(searchImage, for: .search, state: .normal) +/// } +/// .onDisappear { +/// UISearchBar.appearance().setImage(nil, for: .search, state: .normal) +/// } +/// +/// ``` +/// +// sourcery: CompositeComponent +protocol _SideBarComponent { + // sourcery: @Binding + /// Indicate whether the edit mode of the side bar is active. + var isEditing: Bool { get } + // sourcery: @Binding + /// The query string for side bar + var queryString: String? { get } + // sourcery: @Binding + /// The data for the expandable list that will be displayed in side bar + var data: [SideBarItemModel] { get } + // sourcery: @Binding + /// The selected item of the side bar + var selection: SideBarItemModel? { get } + /// The title of the side bar. + var title: AttributedString? { get } + @ViewBuilder + /// The footer for the Side bar + var footer: (() -> any View)? { get } + @ViewBuilder + /// The edit button for the Side bar + var editButton: (() -> any View)? { get } + /// the destination for the selected item. It only can work when the SideBar was used in NavigationSplitView. For other case, you need observe the 'selection' property to perform the follow-up logic by yourself + var destination: (SideBarItemModel) -> any View { get } + /// Construct the row content according to the give SideBarItemModel + var item: (Binding) -> any View { get } + /// The callback event utilized to monitor data changes when the SideBar is in edit mode. This callback should be used if the editButton didn't provided for the SideBar. E,g, the case to wrapper this SwiftUI control in the UIkit. + var onDataChange: (([SideBarItemModel]) -> Void)? { get } + // sourcery: defaultValue = "true" + /// Indicate whether the Side bar was used in NavigationSplitView. The default was true + var isUsedInSplitView: Bool { get } +} + +/// SideBarListItem: SwiftUI View +/// +/// A SwiftUI View that displays a row with title, icon, subtitle and accessory icon in `SideBar` SwiftUI View +/// +/// +// sourcery: CompositeComponent +protocol _SideBarListItemComponent: _IconComponent, _FilledIconComponent, _TitleComponent, _SubtitleComponent, _AccessoryIconComponent, _SwitchComponent { + /// The data of the side bar item + var data: SideBarItemModel { get } + // sourcery: @Binding + /// Whether the item is selected or not + var isSelected: Bool { get } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/AccessoryIconStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/AccessoryIconStyle.fiori.swift new file mode 100644 index 000000000..18868f62a --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/AccessoryIconStyle.fiori.swift @@ -0,0 +1,30 @@ +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`. + */ + +// Base Layout style +public struct AccessoryIconBaseStyle: AccessoryIconStyle { + @ViewBuilder + public func makeBody(_ configuration: AccessoryIconConfiguration) -> some View { + // Add default layout here + configuration.accessoryIcon + } +} + +// Default fiori styles +public struct AccessoryIconFioriStyle: AccessoryIconStyle { + @ViewBuilder + public func makeBody(_ configuration: AccessoryIconConfiguration) -> some View { + AccessoryIcon(configuration) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/FilledIconStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/FilledIconStyle.fiori.swift new file mode 100644 index 000000000..62ca7124b --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/FilledIconStyle.fiori.swift @@ -0,0 +1,30 @@ +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`. + */ + +// Base Layout style +public struct FilledIconBaseStyle: FilledIconStyle { + @ViewBuilder + public func makeBody(_ configuration: FilledIconConfiguration) -> some View { + // Add default layout here + configuration.filledIcon + } +} + +// Default fiori styles +public struct FilledIconFioriStyle: FilledIconStyle { + @ViewBuilder + public func makeBody(_ configuration: FilledIconConfiguration) -> some View { + FilledIcon(configuration) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/SideBarListItemStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/SideBarListItemStyle.fiori.swift new file mode 100644 index 000000000..a59b6bfaa --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/SideBarListItemStyle.fiori.swift @@ -0,0 +1,177 @@ +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`. + */ + +// Base Layout style +public struct SideBarListItemBaseStyle: SideBarListItemStyle { + @Environment(\.editMode) private var editMode + @EnvironmentObject private var modelObject: SideBarModelObject + @Environment(\.sizeCategory) private var sizeCategory + @ScaledMetric var scale: CGFloat = 1 + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + Group { + let dragImage = Image(systemName: "line.horizontal.3") + .frame(width: 22 * self.scale, height: 22 * self.scale) + + if self.sizeCategory.isAccessibilityCategory { + VStack { + HStack(spacing: 11) { + if configuration.isSelected, self.editMode?.wrappedValue == .inactive { + configuration.filledIcon + .frame(width: 22 * self.scale, height: 22 * self.scale) + } else { + configuration.icon + .frame(width: 22 * self.scale, height: 22 * self.scale) + } + configuration.title + Spacer() + } + + HStack(spacing: 11) { + Spacer() + if self.editMode?.wrappedValue == .inactive { + configuration.subtitle + configuration.accessoryIcon + .frame(width: 22 * self.scale, height: 22 * self.scale) + } else if self.editMode?.wrappedValue == .active { + configuration._switch + .frame(width: 60 * self.scale, height: 22 * self.scale) + dragImage + } + } + } + } else { + HStack(spacing: 11) { + if configuration.isSelected, self.editMode?.wrappedValue == .inactive { + configuration.filledIcon + .frame(width: 22 * self.scale, height: 22 * self.scale) + } else { + configuration.icon + .frame(width: 22 * self.scale, height: 22 * self.scale) + } + + configuration.title.frame(height: 44, alignment: .leading) + Spacer() + if self.editMode?.wrappedValue == .inactive { + configuration.subtitle.frame(height: 44, alignment: .leading) + configuration.accessoryIcon + .frame(width: 22 * self.scale, height: 22 * self.scale) + } else if self.editMode?.wrappedValue == .active { + configuration._switch + .frame(width: 60 * self.scale, height: 22 * self.scale) + dragImage + } + } + } + } + .padding(EdgeInsets(top: 11, leading: 11, bottom: 11, trailing: 11)) + .cornerRadius(8, antialiased: true) + .frame(width: self.sizeCategory.isAccessibilityCategory ? nil : (UIDevice.current.userInterfaceIdiom != .pad ? nil : 288), height: self.sizeCategory.isAccessibilityCategory ? nil : 44) + .accessibilityElement(children: .ignore) + .accessibilityAddTraits(.isButton) + .accessibilityRemoveTraits(.isSelected) + .accessibilityLabel(configuration.data.title) + } +} + +// Default fiori styles +extension SideBarListItemFioriStyle { + struct ContentFioriStyle: SideBarListItemStyle { + @Environment(\.editMode) private var editMode + @EnvironmentObject private var modelObject: SideBarModelObject + + func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + Group { + if !configuration.isSelected || self.editMode?.wrappedValue == .active { // Present normal style for item in edit mode or it was NOT selected + SideBarListItem(configuration) + } else { // Present active style for item when it was selected in view mode + SideBarListItem(configuration) + .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.preferredColor(.tintColor))) + .titleStyle { configuration in + configuration.title.foregroundStyle(Color.preferredColor(.quinaryLabel)) + .font(.fiori(forTextStyle: .body, weight: .bold)) + } + .subtitleStyle { configuration in + configuration.subtitle.foregroundStyle(Color.preferredColor(.quinaryLabel)) + .font(.fiori(forTextStyle: .subheadline, weight: .bold)) + } + .iconStyle { configuration in + configuration.icon.foregroundStyle(Color.preferredColor(.quinaryLabel)) + .fontWeight(.bold) + } + .accessoryIconStyle { configuration in + configuration.accessoryIcon.foregroundStyle(Color.preferredColor(.quinaryLabel)) + .fontWeight(.bold) + } + .accessibilityAddTraits(.isSelected) + } + } + } + } + + struct IconFioriStyle: IconStyle { + let sideBarListItemConfiguration: SideBarListItemConfiguration + + func makeBody(_ configuration: IconConfiguration) -> some View { + Icon(configuration) + .foregroundStyle(Color.preferredColor(.tintColor)) + } + } + + struct FilledIconFioriStyle: FilledIconStyle { + let sideBarListItemConfiguration: SideBarListItemConfiguration + + func makeBody(_ configuration: FilledIconConfiguration) -> some View { + FilledIcon(configuration) + .foregroundStyle(Color.preferredColor(.quinaryLabel)) + } + } + + struct TitleFioriStyle: TitleStyle { + let sideBarListItemConfiguration: SideBarListItemConfiguration + + func makeBody(_ configuration: TitleConfiguration) -> some View { + Title(configuration) + .font(.fiori(forTextStyle: .body, weight: .regular)) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + } + } + + struct SubtitleFioriStyle: SubtitleStyle { + let sideBarListItemConfiguration: SideBarListItemConfiguration + + func makeBody(_ configuration: SubtitleConfiguration) -> some View { + Subtitle(configuration) + .font(.fiori(forTextStyle: .subheadline, weight: .regular)) + .foregroundStyle(Color.preferredColor(.tertiaryLabel)) + } + } + + struct AccessoryIconFioriStyle: AccessoryIconStyle { + let sideBarListItemConfiguration: SideBarListItemConfiguration + + func makeBody(_ configuration: AccessoryIconConfiguration) -> some View { + AccessoryIcon(configuration) + .foregroundStyle(Color.preferredColor(.tertiaryLabel)) + } + } + + struct SwitchFioriStyle: SwitchStyle { + let sideBarListItemConfiguration: SideBarListItemConfiguration + + func makeBody(_ configuration: SwitchConfiguration) -> some View { + Switch(configuration) + } + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/SideBarStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/SideBarStyle.fiori.swift new file mode 100644 index 000000000..7ca79337e --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/SideBarStyle.fiori.swift @@ -0,0 +1,487 @@ +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`. + */ + +// Base Layout style +public struct SideBarBaseStyle: SideBarStyle { + @Environment(\.editMode) private var editMode + @EnvironmentObject private var modelObject: SideBarModelObject + @State private var collapsedSections: [UUID] = [] // To keep the collasped section ID + + public func makeBody(_ configuration: SideBarConfiguration) -> some View { + Group { + if !configuration.isUsedInSplitView { + VStack(spacing: 0, content: { + ScrollView(.vertical, showsIndicators: false, content: { + LazyVStack(spacing: 0) { + self.buildSideBarList(configuration) + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + }) + + configuration.footer.typeErased + }) + .environment(\.editMode, .constant(configuration.isEditing ? EditMode.active : EditMode.inactive)) + } else { + let onEditButtonClicked = { + configuration.isEditing.toggle() + if !configuration.isEditing { + // to conver the flat list data(for drag&drop supporting) to tree structed list data. + configuration.data.removeAll() + configuration.data.append(contentsOf: self.modelObject.refreshItems()) + } + } + + let destinationCallback: (SideBarItemModel, SideBarConfiguration) -> any View = { item, configuration in + if configuration.isUsedInSplitView { + // With NavigationLink and navigationDestination modifier under NavigationStack/NavigationSplitView, need to set selected item here + DispatchQueue.main.async { + configuration.selection = item + } + } + return configuration.destination(item) + } + + NavigationStack { + ScrollView(.vertical, showsIndicators: false, content: { + LazyVStack(spacing: 0) { + self.buildSideBarList(configuration) + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + }) + + configuration.footer.typeErased + } + .navigationDestination(for: SideBarItemModel.self) { item in + destinationCallback(item, configuration).typeErased + } + .navigationTitle(String(configuration.title?.characters ?? AttributedString("").characters)) + .environment(\.editMode, .constant(configuration.isEditing ? EditMode.active : EditMode.inactive)) + .navigationBarItems(trailing: configuration.editButton + .simultaneousGesture(TapGesture().onEnded { + onEditButtonClicked() + }) + .accessibilityAction { + onEditButtonClicked() + } + ) + } + } + } + + func buildSideBarList(_ configuration: SideBarConfiguration) -> some View { + ForEach(Array(self.modelObject.filterItems().enumerated()), id: \.element.id) { _, item in + if item.isSection { // Section header + let onDisclosureGroupToggled = { + // handle the section expand/collapse + if !self.collapsedSections.contains(where: { $0 == item.id }) { + self.collapsedSections.append(item.id) + } else { + self.collapsedSections.removeAll(where: { $0 == item.id }) + } + } + DisclosureGroup(item.title, + isExpanded: Binding( + get: { !self.collapsedSections.contains(where: { $0 == item.id }) }, + set: { isExpanded in + if isExpanded { self.collapsedSections.removeAll(where: { $0 == item.id }) } else { self.collapsedSections.append(item.id) } + } + )) { + if let sectionItems = self.modelObject.fetchChildrenItems(item) { // Get all children item of the section + ForEach(sectionItems) { childrenItem in + self.buildSideBarItem(configuration, childrenItem) + } + } + } + .disclosureGroupStyle(SideBarListSectionDisclosureStyle(onDisclosureGroupToggled: onDisclosureGroupToggled)) + .onTapGesture { + onDisclosureGroupToggled() + } + .onDrop(of: [.text], delegate: SideBarDropDelegate(toItem: item, modelObject: self.modelObject)) // The section header can't be dragged but it was allow to put a item under it. E,g, Drag a item to empty section + } else if !self.modelObject.isChildrenItem(item) { // The item is not section header and its children + self.buildSideBarItem(configuration, item) + } + } + } + + /** + * Build the row item for side bar item. The isFlatList and parentIndex parameter was used for non-flat list source during drag and drop for method buildNestedSideBarList + */ + func buildSideBarItem(_ configuration: SideBarConfiguration, _ item: SideBarItemModel) -> some View { + Group { + if let index = self.modelObject.flatListItems.firstIndex(of: item) { + let bindableItem = Binding(get: { + self.modelObject.flatListItems[index] + }, set: { newItem in + self.modelObject.flatListItems[index] = newItem + }) + + if !item.isInvisible, !configuration.isEditing { // For view mode + if configuration.isUsedInSplitView { + // In NavigationSplieView, the 'select' standard accessibility action can trigger the navigationDestination modifer for the NavigationLink when enter key pressed or double-tapping with VoiceOver. + // However, the custom accessibility action can't execute in the standard accessibility action here. Also, the 'select' standard accessibility action can not let NavigationLink work if add a nother customer accessibility action here. So, we just use 'select' standard accessibility action to tigger navigationDestination modifer and set the selected item in navigationDestination callback. + NavigationLink(value: item) { + configuration.item(bindableItem).typeErased + }.accessibilityAction(named: "Select") {} + } else { + // in case the navigationDestination modifer of the NavigationLink can't work, to set the selection item when the NavigationLink was clicked or enter key pressed or double-tapping with VoiceOver + configuration.item(bindableItem).typeErased + .simultaneousGesture(TapGesture().onEnded { + configuration.selection = item + }) + .accessibilityAction { + configuration.selection = item + } + } + } else if configuration.isEditing { // For edit-mode + configuration.item(bindableItem).typeErased + .simultaneousGesture(TapGesture().onEnded {}) // To capture the Tap Gesture on the Item Row in Edit mode and to do nothing to avoid the Tap Gesture was captured by section expending logic + .background(Color.white) + .overlay( + DraggingItemCornerShape(radius: 8) + .stroke(Color("light-gray"), lineWidth: 4) + ) + .clipShape(DraggingItemCornerShape(radius: 8)) + .contentShape([.dragPreview], DraggingItemCornerShape(radius: 8)) + .overlay(self.modelObject.draggingItem == item && self.modelObject.isDragging ? .white : .clear) + .onDrag { + self.modelObject.draggingItem = item + return NSItemProvider(object: NSString(string: item.id.uuidString)) + } + .onDrop(of: [.text], delegate: SideBarDropDelegate(toItem: item, modelObject: self.modelObject)) + .accessibilityAction { + if configuration.isEditing { + bindableItem.wrappedValue.isInvisible.toggle() + } + } + } + } else { + EmptyView() + } + } + } +} + +extension SideBarFioriStyle { + struct ContentFioriStyle: SideBarStyle { + func makeBody(_ configuration: SideBarConfiguration) -> some View { + SideBar(configuration) + .environmentObject(SideBarModelObject(items: configuration.data, queryString: configuration.queryString ?? "", configuration: configuration)) + } + } +} + +/** + The `SideBarItemModel` struct is a data model that represents a side bar item . It conforms to the `Identifiable`, `Hashable`, and `Equatable` protocols, which allow it to be used in collections and compared for equality. + */ +public struct SideBarItemModel: Identifiable, Hashable, Equatable { + /// A unique identifier for each `SideBarItemModel` instance, generated using `UUID()` + public var id = UUID() + /// A `String` representing the title of the side bar item + public var title: String + /// An optional `Image` that represents the icon of the side bar item. + public var icon: Image? + /// An optional `Image` that represents a icon of the side bar item when it was selected. + public var filledIcon: Image? + /// An optional `String` representing the subtitle of the side bar item. + public var subtitle: String? + /// An optional `Image` that represents an accessory icon for the side bar item. + public var accessoryIcon: Image? + /// A `Bool` indicating whether the side bar item is invisible when the SideBar was in view mode. It is set to `false` by default. + public var isInvisible: Bool = false + /// An optional array of `SideBarItemModel` instances representing the children items of the side bar item. + public var children: [SideBarItemModel]? = nil { + didSet { + self.isSection = self.children != nil + } + } + + /// A `Bool` indicating whether the side bar item is a section or not. It is set to `false` by default and will set to true if children is not empty when initializing + public var isSection: Bool = false + + /// Public initializer for SideBarItemModel. + /// - Parameters: + /// - title: A `String` representing the title of the side bar item + /// - icon: An optional `Image` that represents the icon of the side bar item. + /// - filledIcon: An optional `Image` that represents a icon of the side bar item when it was selected. + /// - subtitle: An optional `String` representing the subtitle of the side bar item. + /// - accessoryIcon: An optional `Image` that represents an accessory icon for the side bar item. + /// - children: An optional array of `SideBarItemModel` instances representing the children items of the side bar item. + /// - isSection: A `Bool` indicating whether the side bar item is a section or not. It is set to `false` by default and will set to true if children is not empty and don't set it explicitly. + /// - isInvisible: A `Bool` indicating whether the side bar item is hidden or not in view mode. It is set to `false` by default. + public init(title: String, icon: Image? = nil, filledIcon: Image? = nil, subtitle: String? = nil, accessoryIcon: Image? = nil, children: [SideBarItemModel]? = nil, isSection: Bool = false, isInvisible: Bool = false) { + self.title = title + self.icon = icon + self.filledIcon = filledIcon + self.subtitle = subtitle + self.accessoryIcon = accessoryIcon + self.children = children + self.children != nil ? (self.isSection = true) : (self.isSection = isSection) + if self.isSection { + self.isInvisible = false + } else { + self.isInvisible = isInvisible + } + } + + /// A method that allows the `SideBarItemModel` instances to be used in hash-based collections. It combines the `id` and `title` properties to generate a hash value. + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + hasher.combine(self.title) + } + + /// An equality operator method that compares two `SideBarItemModel` instances based on their `id` property. + public static func == (lhs: SideBarItemModel, rhs: SideBarItemModel) -> Bool { + lhs.id == rhs.id + } +} + +class SideBarModelObject: ObservableObject { + private var itemCount: Int = 0 + + // The tree list structure don't support drag&drop, so, to use the variable to keep the flat list data + @Published var flatListItems: [SideBarItemModel] = [] { + didSet { + if let onChanged = configuration.onDataChange { + if self.configuration.isEditing, self.flatListItems.count == self.itemCount { // Only fire the data change event when the data items were changed in edit model after they were initilized + onChanged(self.inflateItemModels(flatItemModels: self.flatListItems)) + } + } + } + } + + @Published var selection: SideBarItemModel? + + @Published var queryString: String + + @Published var isDragging = false + var draggingItem: SideBarItemModel? + + var configuration: SideBarConfiguration + + public init(items: [SideBarItemModel], queryString: String, configuration: SideBarConfiguration) { + self.queryString = queryString + self.configuration = configuration + // To parse the consumer's tree list structure data and put them in a flat list for drag&drop reordering + // E,g, Convert items = [A, B, C[a,b,c], D] to flatListItems = [A,B,D,C,a,b,c] + var sections: [SideBarItemModel] = [] + for item in items { + if item.isSection { + sections.append(item) // Put the item to the flat list directly if it has no children + } else { + self.flatListItems.append(item) // Keep the section header item for later using + self.itemCount += 1 + } + } + for section in sections { // Loop the possibile sections and add it and its children to the flat list + self.flatListItems.append(section) + self.itemCount += 1 + if let children = section.children, !children.isEmpty { + self.flatListItems.append(contentsOf: children) + self.itemCount += children.count + } + } + self.selection = configuration.selection + } + + public func refreshItems() -> [SideBarItemModel] { + self.inflateItemModels(flatItemModels: self.flatListItems) + } + + // swiftlint:disable cyclomatic_complexity + private func inflateItemModels(flatItemModels: [SideBarItemModel]) -> [SideBarItemModel] { + var targetItemModels: [SideBarItemModel] = [] + var processingSection: SideBarItemModel? = nil + var children: [SideBarItemModel] = [] + var sourceFlatItemModels = flatItemModels // The flatItemModels has values like:[item1, item2, Section1, item1-1. item1-2, Section2, item2-1, item2-2] + + for item in sourceFlatItemModels { + if !item.isSection, processingSection == nil { // the item is not in any gourp if it is not setion and there is no processing section. + targetItemModels.append(item) + } else if item.isSection { // current item is section + if let process = processingSection { + if let index = sourceFlatItemModels.firstIndex(of: process) { // Find the section and append its children + if !children.isEmpty { + sourceFlatItemModels[index].children = children + children.removeAll() + } else { + sourceFlatItemModels[index].children?.removeAll() + } + + targetItemModels.append(sourceFlatItemModels[index]) + } + } + + processingSection = item // Means to handle the section now + if let index = sourceFlatItemModels.firstIndex(of: item), index == sourceFlatItemModels.count - 1 { // handle the case that the section no child and is the latest node in the flat list. + sourceFlatItemModels[index].children?.removeAll() + targetItemModels.append(sourceFlatItemModels[index]) + } + } else { // Handle the item was a child + children.append(item) + if let index = sourceFlatItemModels.firstIndex(of: item), index == sourceFlatItemModels.count - 1 { // when the item was latest one in the flat list + if let process = processingSection { + if let secIndex = sourceFlatItemModels.firstIndex(of: process) { + if !children.isEmpty { + sourceFlatItemModels[secIndex].children = children + children.removeAll() + } else { + sourceFlatItemModels[index].children?.removeAll() + } + + targetItemModels.append(sourceFlatItemModels[secIndex]) + } + } + } + } + } + return targetItemModels + } + + func filterItems() -> [SideBarItemModel] { + if self.queryString.isEmpty { + return self.flatListItems + } else { + return self.flatListItems.filter { item in item.isSection || item.title.localizedCaseInsensitiveContains(self.queryString) } + } + } + + /** + * Fetch all children items of the given section header item from the flat list + */ + func fetchChildrenItems(_ section: SideBarItemModel) -> [SideBarItemModel]? { + if let sectionIndex = self.filterItems().firstIndex(of: section) { + var items: [SideBarItemModel] = [] + if (sectionIndex + 1) <= self.filterItems().count - 1 { + for index in sectionIndex + 1 ... self.filterItems().count - 1 { // Loop all items after the section until find another section + let item = self.filterItems()[index] + if !item.isSection { // The item belongs to the current section if it has no any child, otherwise, it is another section header item + items.append(item) + } else { + break + } + } + } + + if items.isEmpty { + return nil + } else { + return items + } + } + return nil + } + + /** + * Check if the given item is a child of section header in flat list + */ + func isChildrenItem(_ item: SideBarItemModel) -> Bool { + if let index = self.filterItems().firstIndex(of: item) { + if index > 0 { + for n in 0 ... index - 1 { // Loop all items before the item until find section + if self.filterItems()[n].isSection { + return true + } + } + } + } + return false + } +} + +private struct SideBarListSectionDisclosureStyle: DisclosureGroupStyle { + @Environment(\.sizeCategory) private var sizeCategory + @ScaledMetric var scale: CGFloat = 1 + var onDisclosureGroupToggled: () -> Void + + func makeBody(configuration: Configuration) -> some View { + VStack(spacing: 0) { + HStack { + configuration.label + .font(.fiori(forTextStyle: .title3, weight: .regular)) + Spacer() + Image(systemName: configuration.isExpanded ? "chevron.down" : "chevron.right") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18 * self.scale, height: 18 * self.scale) + .font(.fiori(fixedSize: 17, weight: .semibold)) + .foregroundColor(.preferredColor(.tintColor)) + } + + if !configuration.isExpanded { + Rectangle() + .fill(Color.gray) + .frame(height: 0.5) + } + } + .frame(width: self.sizeCategory.isAccessibilityCategory ? nil : (UIDevice.current.userInterfaceIdiom != .pad ? nil : 288), height: self.sizeCategory.isAccessibilityCategory ? nil : 44) + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .accessibilityAction { + self.onDisclosureGroupToggled() + } + + if configuration.isExpanded { + configuration.content + .padding(.leading, 0) + .disclosureGroupStyle(self) + } + } +} + +private struct SideBarDropDelegate: DropDelegate { + let toItem: SideBarItemModel + var modelObject: SideBarModelObject + + /// Drop finished work + func performDrop(info: DropInfo) -> Bool { + self.modelObject.draggingItem = nil + self.modelObject.isDragging = false + return true + } + + /// Moving style without "+" icon + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + /// Object is dragged off of the onDrop view. + func dropExited(info: DropInfo) { + self.modelObject.isDragging = false + } + + /// Object is dragged over the onDrop view. + func dropEntered(info: DropInfo) { + self.modelObject.isDragging = true + + guard let dragItem = modelObject.draggingItem, dragItem != toItem else { return } + + guard let fromIndex = modelObject.flatListItems.firstIndex(of: dragItem), + let toIndex = modelObject.flatListItems.firstIndex(of: toItem) else { return } + withAnimation { + self.modelObject.flatListItems.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex) + } + } +} + +private struct DraggingItemCornerShape: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/AccessoryIcon/AccessoryIcon.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AccessoryIcon/AccessoryIcon.generated.swift new file mode 100644 index 000000000..28fd9a50f --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AccessoryIcon/AccessoryIcon.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct AccessoryIcon { + let accessoryIcon: any View + + @Environment(\.accessoryIconStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder accessoryIcon: () -> any View = { EmptyView() }) { + self.accessoryIcon = accessoryIcon() + } +} + +public extension AccessoryIcon { + init(accessoryIcon: Image? = nil) { + self.init(accessoryIcon: { accessoryIcon }) + } +} + +public extension AccessoryIcon { + init(_ configuration: AccessoryIconConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: AccessoryIconConfiguration, shouldApplyDefaultStyle: Bool) { + self.accessoryIcon = configuration.accessoryIcon + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension AccessoryIcon: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(accessoryIcon: .init(self.accessoryIcon))).typeErased + .transformEnvironment(\.accessoryIconStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension AccessoryIcon { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + AccessoryIcon(accessoryIcon: { self.accessoryIcon }) + .shouldApplyDefaultStyle(false) + .accessoryIconStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/AccessoryIcon/AccessoryIconStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AccessoryIcon/AccessoryIconStyle.generated.swift new file mode 100644 index 000000000..1482110f3 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AccessoryIcon/AccessoryIconStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol AccessoryIconStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: AccessoryIconConfiguration) -> Body +} + +struct AnyAccessoryIconStyle: AccessoryIconStyle { + let content: (AccessoryIconConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (AccessoryIconConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: AccessoryIconConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct AccessoryIconConfiguration { + public let accessoryIcon: AccessoryIcon + + public typealias AccessoryIcon = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/FilledIcon/FilledIcon.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FilledIcon/FilledIcon.generated.swift new file mode 100644 index 000000000..e27c78cb0 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FilledIcon/FilledIcon.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct FilledIcon { + let filledIcon: any View + + @Environment(\.filledIconStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder filledIcon: () -> any View = { EmptyView() }) { + self.filledIcon = filledIcon() + } +} + +public extension FilledIcon { + init(filledIcon: Image? = nil) { + self.init(filledIcon: { filledIcon }) + } +} + +public extension FilledIcon { + init(_ configuration: FilledIconConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: FilledIconConfiguration, shouldApplyDefaultStyle: Bool) { + self.filledIcon = configuration.filledIcon + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension FilledIcon: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(filledIcon: .init(self.filledIcon))).typeErased + .transformEnvironment(\.filledIconStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension FilledIcon { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + FilledIcon(filledIcon: { self.filledIcon }) + .shouldApplyDefaultStyle(false) + .filledIconStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/FilledIcon/FilledIconStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FilledIcon/FilledIconStyle.generated.swift new file mode 100644 index 000000000..8fb4fd837 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FilledIcon/FilledIconStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol FilledIconStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: FilledIconConfiguration) -> Body +} + +struct AnyFilledIconStyle: FilledIconStyle { + let content: (FilledIconConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (FilledIconConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: FilledIconConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct FilledIconConfiguration { + public let filledIcon: FilledIcon + + public typealias FilledIcon = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBar/SideBar.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBar/SideBar.generated.swift new file mode 100644 index 000000000..29795b34c --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBar/SideBar.generated.swift @@ -0,0 +1,190 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/// SideBar: SwiftUI View +/// +/// The `SideBar` SwiftUI view presents a expandable list of items using the `SideBarListItem` SwiftUI view. It has support for both edit and view modes. In edit mode, the listed data can be rearranged, and each item can be toggled as hidden. The hidden items are not shown in view mode. +/// +/// This View should be used in `NavigationSplitView` as side bar. How, in case you are NOT using the `NavigationSplitView` for the SideBar, you should observe the change of selected item by property 'selection' of SideBar and handle follow-up logic by youself. Also, you should set 'isUsedInSplitView' of SideBar to true and return EmptyView in 'destination' callback. +/// +/// ## Usage +/// +/// ### Initialization: +/// +/// Construct the data,, array of `SideBarItemModel`, for the expandable list that will be displayed in side bar. +/// +/// ```swift +/// @State private var data: [SideBarItemModel] = [ +/// SideBarItemModel(title: "Root Item 0.1", icon: Image(systemName: "square.dashed"), filledIcon: Image(systemName: "square.dashed.inset.filled"), subtitle: +/// "9,999+", accessoryIcon: Image(systemName: "clock"), children: nil), +/// SideBarItemModel(title: "Root Item 0.4", icon: Image(systemName: "cloud.snow"), children: nil), +/// SideBarItemModel(title: "Group 1", children: [ +/// SideBarItemModel(title: "Child Item 1.1", icon: Image(systemName: "square.and.pencil"), subtitle: "66", accessoryIcon: Image(systemName: "circle"), +/// children: nil), +/// SideBarItemModel(title: "Child Item 1.2", icon: Image(systemName: "square.and.pencil"), accessoryIcon: Image(systemName: "circle"), children: nil) +/// ]), +/// SideBarItemModel(title: "Group 2", children: [ +/// SideBarItemModel(title: "Child Item 2.1", icon: Image(systemName: "folder"), subtitle: "5", accessoryIcon: Image(systemName: "mail"), children: nil) +/// ]) +/// ] +/// ``` +/// Initialize a `SideBar` with title, edit button, selected item destination view, the binding edit mode indicator, search query string, data, selected item and row item content +/// +/// ```swift +/// @State private var isEditing = false +/// @State private var queryString: String? +/// @State private var selection: SideBarItemModel? +/// +/// SideBar( +/// isEditing: $isEditing, +/// queryString: $queryString, +/// data: $data, +/// selection: $selection, +/// title: "SideBar", +/// editButton: { +/// // Or use SWiftUI EditButton() here directly if you don't need to check the changed data or customize the label for edit button: EditButton() +/// Button(action: { +/// if !self.isEditing { +/// // Check the listItems +/// for(_, item) in listItems.enumerated() { +/// +/// } +/// } +/// }, label: {Text(isEditing ? "Done" : "Edit")}) }, +/// destination: { item in +/// DevDetailView(item) +/// }, +/// item: { item in +/// SideBarListItem(icon: item.wrappedValue.icon, filledIcon: item.wrappedValue.filledIcon, title: AttributedString(item.wrappedValue.title), subtitle: +/// AttributedString(item.wrappedValue.subtitle ?? ""), accessoryIcon: item.wrappedValue.accessoryIcon, isOn: Binding(get: { +/// !item.wrappedValue.isInvisible }, set: { newValue in item.wrappedValue.isInvisible = !newValue}), selection: $selection, data: item.wrappedValue) +/// } +/// ) +/// ``` +/// ### Handle Search: +/// +/// The binding property `queryString` was used to trigger the searching on SideBar. The `.searchable` modifier on `NavigationSplieView` can be used to bind the @State variable `queryString` which will bind to SideBar. As the same time, an `UISearchBar` can inititalized in `onAppear` modifier and dismissed in `onDisappear` modifier +/// +/// ```swift +/// NavigationSplitView { +/// sideBar +/// } +/// .searchable(text: Binding(get: { self.queryString ?? "" }, set: { newValue in self.queryString = newValue}), prompt: "Search") +/// .onAppear { +/// let searchImage = UIImage(systemName: "magnifyingglass")? +/// .withTintColor(UIColor(Color.preferredColor(.tertiaryLabel)), renderingMode: .alwaysOriginal) +/// .applyingSymbolConfiguration(UIImage.SymbolConfiguration(weight: .semibold)) +/// UISearchBar.appearance().setImage(searchImage, for: .search, state: .normal) +/// } +/// .onDisappear { +/// UISearchBar.appearance().setImage(nil, for: .search, state: .normal) +/// } +/// +/// ``` +/// +public struct SideBar { + /// Indicate whether the edit mode of the side bar is active. + @Binding var isEditing: Bool + /// The query string for side bar + @Binding var queryString: String? + /// The data for the expandable list that will be displayed in side bar + @Binding var data: [SideBarItemModel] + /// The selected item of the side bar + @Binding var selection: SideBarItemModel? + /// The title of the side bar. + let title: AttributedString? + /// The footer for the Side bar + let footer: any View + /// The edit button for the Side bar + let editButton: any View + /// the destination for the selected item. It only can work when the SideBar was used in NavigationSplitView. For other case, you need observe the 'selection' property to perform the follow-up logic by yourself + let destination: (SideBarItemModel) -> any View + /// Construct the row content according to the give SideBarItemModel + let item: (Binding) -> any View + /// The callback event utilized to monitor data changes when the SideBar is in edit mode. This callback should be used if the editButton didn't provided for the SideBar. E,g, the case to wrapper this SwiftUI control in the UIkit. + let onDataChange: (([SideBarItemModel]) -> Void)? + /// Indicate whether the Side bar was used in NavigationSplitView. The default was true + let isUsedInSplitView: Bool + + @Environment(\.sideBarStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(isEditing: Binding, + queryString: Binding, + data: Binding<[SideBarItemModel]>, + selection: Binding, + title: AttributedString? = nil, + @ViewBuilder footer: () -> any View = { EmptyView() }, + @ViewBuilder editButton: () -> any View = { EmptyView() }, + destination: @escaping (SideBarItemModel) -> any View, + item: @escaping (Binding) -> any View, + onDataChange: (([SideBarItemModel]) -> Void)? = nil, + isUsedInSplitView: Bool = true) + { + self._isEditing = isEditing + self._queryString = queryString + self._data = data + self._selection = selection + self.title = title + self.footer = footer() + self.editButton = editButton() + self.destination = destination + self.item = item + self.onDataChange = onDataChange + self.isUsedInSplitView = isUsedInSplitView + } +} + +public extension SideBar { + init(_ configuration: SideBarConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: SideBarConfiguration, shouldApplyDefaultStyle: Bool) { + self._isEditing = configuration.$isEditing + self._queryString = configuration.$queryString + self._data = configuration.$data + self._selection = configuration.$selection + self.title = configuration.title + self.footer = configuration.footer + self.editButton = configuration.editButton + self.destination = configuration.destination + self.item = configuration.item + self.onDataChange = configuration.onDataChange + self.isUsedInSplitView = configuration.isUsedInSplitView + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension SideBar: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(isEditing: self.$isEditing, queryString: self.$queryString, data: self.$data, selection: self.$selection, title: self.title, footer: .init(self.footer), editButton: .init(self.editButton), destination: self.destination, item: self.item, onDataChange: self.onDataChange, isUsedInSplitView: self.isUsedInSplitView)).typeErased + .transformEnvironment(\.sideBarStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension SideBar { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + SideBar(.init(isEditing: self.$isEditing, queryString: self.$queryString, data: self.$data, selection: self.$selection, title: self.title, footer: .init(self.footer), editButton: .init(self.editButton), destination: self.destination, item: self.item, onDataChange: self.onDataChange, isUsedInSplitView: self.isUsedInSplitView)) + .shouldApplyDefaultStyle(false) + .sideBarStyle(SideBarFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBar/SideBarStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBar/SideBarStyle.generated.swift new file mode 100644 index 000000000..a6103b3aa --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBar/SideBarStyle.generated.swift @@ -0,0 +1,45 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol SideBarStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: SideBarConfiguration) -> Body +} + +struct AnySideBarStyle: SideBarStyle { + let content: (SideBarConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (SideBarConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: SideBarConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct SideBarConfiguration { + @Binding public var isEditing: Bool + @Binding public var queryString: String? + @Binding public var data: [SideBarItemModel] + @Binding public var selection: SideBarItemModel? + public let title: AttributedString? + public let footer: Footer + public let editButton: EditButton + public let destination: (SideBarItemModel) -> any View + public let item: (Binding) -> any View + public let onDataChange: (([SideBarItemModel]) -> Void)? + public let isUsedInSplitView: Bool + + public typealias Footer = ConfigurationViewWrapper + public typealias EditButton = ConfigurationViewWrapper +} + +public struct SideBarFioriStyle: SideBarStyle { + public func makeBody(_ configuration: SideBarConfiguration) -> some View { + SideBar(configuration) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBarListItem/SideBarListItem.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBarListItem/SideBarListItem.generated.swift new file mode 100644 index 000000000..cedd856d3 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBarListItem/SideBarListItem.generated.swift @@ -0,0 +1,107 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/// SideBarListItem: SwiftUI View +/// +/// A SwiftUI View that displays a row with title, icon, subtitle and accessory icon in `SideBar` SwiftUI View +/// +/// +public struct SideBarListItem { + let icon: any View + let filledIcon: any View + let title: any View + let subtitle: any View + let accessoryIcon: any View + @Binding var isOn: Bool + /// The data of the side bar item + let data: SideBarItemModel + /// Whether the item is selected or not + @Binding var isSelected: Bool + + @Environment(\.sideBarListItemStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder icon: () -> any View = { EmptyView() }, + @ViewBuilder filledIcon: () -> any View = { EmptyView() }, + @ViewBuilder title: () -> any View, + @ViewBuilder subtitle: () -> any View = { EmptyView() }, + @ViewBuilder accessoryIcon: () -> any View = { EmptyView() }, + isOn: Binding, + data: SideBarItemModel, + isSelected: Binding) + { + self.icon = Icon { icon() } + self.filledIcon = FilledIcon { filledIcon() } + self.title = Title { title() } + self.subtitle = Subtitle { subtitle() } + self.accessoryIcon = AccessoryIcon { accessoryIcon() } + self._isOn = isOn + self.data = data + self._isSelected = isSelected + } +} + +public extension SideBarListItem { + init(icon: Image? = nil, + filledIcon: Image? = nil, + title: AttributedString, + subtitle: AttributedString? = nil, + accessoryIcon: Image? = nil, + isOn: Binding, + data: SideBarItemModel, + isSelected: Binding) + { + self.init(icon: { icon }, filledIcon: { filledIcon }, title: { Text(title) }, subtitle: { OptionalText(subtitle) }, accessoryIcon: { accessoryIcon }, isOn: isOn, data: data, isSelected: isSelected) + } +} + +public extension SideBarListItem { + init(_ configuration: SideBarListItemConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: SideBarListItemConfiguration, shouldApplyDefaultStyle: Bool) { + self.icon = configuration.icon + self.filledIcon = configuration.filledIcon + self.title = configuration.title + self.subtitle = configuration.subtitle + self.accessoryIcon = configuration.accessoryIcon + self._isOn = configuration.$isOn + self.data = configuration.data + self._isSelected = configuration.$isSelected + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension SideBarListItem: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(icon: .init(self.icon), filledIcon: .init(self.filledIcon), title: .init(self.title), subtitle: .init(self.subtitle), accessoryIcon: .init(self.accessoryIcon), isOn: self.$isOn, data: self.data, isSelected: self.$isSelected)).typeErased + .transformEnvironment(\.sideBarListItemStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension SideBarListItem { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + SideBarListItem(.init(icon: .init(self.icon), filledIcon: .init(self.filledIcon), title: .init(self.title), subtitle: .init(self.subtitle), accessoryIcon: .init(self.accessoryIcon), isOn: self.$isOn, data: self.data, isSelected: self.$isSelected)) + .shouldApplyDefaultStyle(false) + .sideBarListItemStyle(SideBarListItemFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBarListItem/SideBarListItemStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBarListItem/SideBarListItemStyle.generated.swift new file mode 100644 index 000000000..cb40111b7 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/SideBarListItem/SideBarListItemStyle.generated.swift @@ -0,0 +1,51 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol SideBarListItemStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: SideBarListItemConfiguration) -> Body +} + +struct AnySideBarListItemStyle: SideBarListItemStyle { + let content: (SideBarListItemConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (SideBarListItemConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct SideBarListItemConfiguration { + public let icon: Icon + public let filledIcon: FilledIcon + public let title: Title + public let subtitle: Subtitle + public let accessoryIcon: AccessoryIcon + @Binding public var isOn: Bool + public let data: SideBarItemModel + @Binding public var isSelected: Bool + + public typealias Icon = ConfigurationViewWrapper + public typealias FilledIcon = ConfigurationViewWrapper + public typealias Title = ConfigurationViewWrapper + public typealias Subtitle = ConfigurationViewWrapper + public typealias AccessoryIcon = ConfigurationViewWrapper +} + +public struct SideBarListItemFioriStyle: SideBarListItemStyle { + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .iconStyle(IconFioriStyle(sideBarListItemConfiguration: configuration)) + .filledIconStyle(FilledIconFioriStyle(sideBarListItemConfiguration: configuration)) + .titleStyle(TitleFioriStyle(sideBarListItemConfiguration: configuration)) + .subtitleStyle(SubtitleFioriStyle(sideBarListItemConfiguration: configuration)) + .accessoryIconStyle(AccessoryIconFioriStyle(sideBarListItemConfiguration: configuration)) + .switchStyle(SwitchFioriStyle(sideBarListItemConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift index 6ead0b280..2a0f8f5f7 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift @@ -3,6 +3,20 @@ import Foundation import SwiftUI +// MARK: AccessoryIconStyle + +public extension AccessoryIconStyle where Self == AccessoryIconBaseStyle { + static var base: AccessoryIconBaseStyle { + AccessoryIconBaseStyle() + } +} + +public extension AccessoryIconStyle where Self == AccessoryIconFioriStyle { + static var fiori: AccessoryIconFioriStyle { + AccessoryIconFioriStyle() + } +} + // MARK: ActionStyle public extension ActionStyle where Self == ActionBaseStyle { @@ -1333,6 +1347,20 @@ public extension DetailImageStyle where Self == DetailImageFioriStyle { } } +// MARK: FilledIconStyle + +public extension FilledIconStyle where Self == FilledIconBaseStyle { + static var base: FilledIconBaseStyle { + FilledIconBaseStyle() + } +} + +public extension FilledIconStyle where Self == FilledIconFioriStyle { + static var fiori: FilledIconFioriStyle { + FilledIconFioriStyle() + } +} + // MARK: FootnoteStyle public extension FootnoteStyle where Self == FootnoteBaseStyle { @@ -2593,6 +2621,160 @@ public extension SecondaryActionStyle where Self == SecondaryActionFioriStyle { } } +// MARK: SideBarStyle + +public extension SideBarStyle where Self == SideBarBaseStyle { + static var base: SideBarBaseStyle { + SideBarBaseStyle() + } +} + +public extension SideBarStyle where Self == SideBarFioriStyle { + static var fiori: SideBarFioriStyle { + SideBarFioriStyle() + } +} + +// MARK: SideBarListItemStyle + +public extension SideBarListItemStyle where Self == SideBarListItemBaseStyle { + static var base: SideBarListItemBaseStyle { + SideBarListItemBaseStyle() + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemFioriStyle { + static var fiori: SideBarListItemFioriStyle { + SideBarListItemFioriStyle() + } +} + +public struct SideBarListItemIconStyle: SideBarListItemStyle { + let style: any IconStyle + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .iconStyle(self.style) + .typeErased + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemIconStyle { + static func iconStyle(_ style: some IconStyle) -> SideBarListItemIconStyle { + SideBarListItemIconStyle(style: style) + } + + static func iconStyle(@ViewBuilder content: @escaping (IconConfiguration) -> some View) -> SideBarListItemIconStyle { + let style = AnyIconStyle(content) + return SideBarListItemIconStyle(style: style) + } +} + +public struct SideBarListItemFilledIconStyle: SideBarListItemStyle { + let style: any FilledIconStyle + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .filledIconStyle(self.style) + .typeErased + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemFilledIconStyle { + static func filledIconStyle(_ style: some FilledIconStyle) -> SideBarListItemFilledIconStyle { + SideBarListItemFilledIconStyle(style: style) + } + + static func filledIconStyle(@ViewBuilder content: @escaping (FilledIconConfiguration) -> some View) -> SideBarListItemFilledIconStyle { + let style = AnyFilledIconStyle(content) + return SideBarListItemFilledIconStyle(style: style) + } +} + +public struct SideBarListItemTitleStyle: SideBarListItemStyle { + let style: any TitleStyle + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .titleStyle(self.style) + .typeErased + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemTitleStyle { + static func titleStyle(_ style: some TitleStyle) -> SideBarListItemTitleStyle { + SideBarListItemTitleStyle(style: style) + } + + static func titleStyle(@ViewBuilder content: @escaping (TitleConfiguration) -> some View) -> SideBarListItemTitleStyle { + let style = AnyTitleStyle(content) + return SideBarListItemTitleStyle(style: style) + } +} + +public struct SideBarListItemSubtitleStyle: SideBarListItemStyle { + let style: any SubtitleStyle + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .subtitleStyle(self.style) + .typeErased + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemSubtitleStyle { + static func subtitleStyle(_ style: some SubtitleStyle) -> SideBarListItemSubtitleStyle { + SideBarListItemSubtitleStyle(style: style) + } + + static func subtitleStyle(@ViewBuilder content: @escaping (SubtitleConfiguration) -> some View) -> SideBarListItemSubtitleStyle { + let style = AnySubtitleStyle(content) + return SideBarListItemSubtitleStyle(style: style) + } +} + +public struct SideBarListItemAccessoryIconStyle: SideBarListItemStyle { + let style: any AccessoryIconStyle + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .accessoryIconStyle(self.style) + .typeErased + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemAccessoryIconStyle { + static func accessoryIconStyle(_ style: some AccessoryIconStyle) -> SideBarListItemAccessoryIconStyle { + SideBarListItemAccessoryIconStyle(style: style) + } + + static func accessoryIconStyle(@ViewBuilder content: @escaping (AccessoryIconConfiguration) -> some View) -> SideBarListItemAccessoryIconStyle { + let style = AnyAccessoryIconStyle(content) + return SideBarListItemAccessoryIconStyle(style: style) + } +} + +public struct SideBarListItemSwitchStyle: SideBarListItemStyle { + let style: any SwitchStyle + + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .switchStyle(self.style) + .typeErased + } +} + +public extension SideBarListItemStyle where Self == SideBarListItemSwitchStyle { + static func switchStyle(_ style: some SwitchStyle) -> SideBarListItemSwitchStyle { + SideBarListItemSwitchStyle(style: style) + } + + static func switchStyle(@ViewBuilder content: @escaping (SwitchConfiguration) -> some View) -> SideBarListItemSwitchStyle { + let style = AnySwitchStyle(content) + return SideBarListItemSwitchStyle(style: style) + } +} + // MARK: StatusStyle public extension StatusStyle where Self == StatusBaseStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift index 21cd216b3..6eb50d671 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift @@ -3,6 +3,27 @@ import Foundation import SwiftUI +// MARK: AccessoryIconStyle + +struct AccessoryIconStyleStackKey: EnvironmentKey { + static let defaultValue: [any AccessoryIconStyle] = [] +} + +extension EnvironmentValues { + var accessoryIconStyle: any AccessoryIconStyle { + self.accessoryIconStyleStack.last ?? .base + } + + var accessoryIconStyleStack: [any AccessoryIconStyle] { + get { + self[AccessoryIconStyleStackKey.self] + } + set { + self[AccessoryIconStyleStackKey.self] = newValue + } + } +} + // MARK: ActionStyle struct ActionStyleStackKey: EnvironmentKey { @@ -297,6 +318,27 @@ extension EnvironmentValues { } } +// MARK: FilledIconStyle + +struct FilledIconStyleStackKey: EnvironmentKey { + static let defaultValue: [any FilledIconStyle] = [] +} + +extension EnvironmentValues { + var filledIconStyle: any FilledIconStyle { + self.filledIconStyleStack.last ?? .base + } + + var filledIconStyleStack: [any FilledIconStyle] { + get { + self[FilledIconStyleStackKey.self] + } + set { + self[FilledIconStyleStackKey.self] = newValue + } + } +} + // MARK: FootnoteStyle struct FootnoteStyleStackKey: EnvironmentKey { @@ -927,6 +969,48 @@ extension EnvironmentValues { } } +// MARK: SideBarStyle + +struct SideBarStyleStackKey: EnvironmentKey { + static let defaultValue: [any SideBarStyle] = [] +} + +extension EnvironmentValues { + var sideBarStyle: any SideBarStyle { + self.sideBarStyleStack.last ?? .base.concat(.fiori) + } + + var sideBarStyleStack: [any SideBarStyle] { + get { + self[SideBarStyleStackKey.self] + } + set { + self[SideBarStyleStackKey.self] = newValue + } + } +} + +// MARK: SideBarListItemStyle + +struct SideBarListItemStyleStackKey: EnvironmentKey { + static let defaultValue: [any SideBarListItemStyle] = [] +} + +extension EnvironmentValues { + var sideBarListItemStyle: any SideBarListItemStyle { + self.sideBarListItemStyleStack.last ?? .base.concat(.fiori) + } + + var sideBarListItemStyleStack: [any SideBarListItemStyle] { + get { + self[SideBarListItemStyleStackKey.self] + } + set { + self[SideBarListItemStyleStackKey.self] = newValue + } + } +} + // MARK: StatusStyle struct StatusStyleStackKey: EnvironmentKey { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift index 85f1ff8e2..b3b064c18 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift @@ -8,6 +8,34 @@ public struct ModifiedStyle: DynamicProperty { var modifier: Modifier } +// MARK: AccessoryIconStyle + +extension ModifiedStyle: AccessoryIconStyle where Style: AccessoryIconStyle { + public func makeBody(_ configuration: AccessoryIconConfiguration) -> some View { + AccessoryIcon(configuration) + .accessoryIconStyle(self.style) + .modifier(self.modifier) + } +} + +public struct AccessoryIconStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.accessoryIconStyle(self.style) + } +} + +public extension AccessoryIconStyle { + func modifier(_ modifier: some ViewModifier) -> some AccessoryIconStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some AccessoryIconStyle) -> some AccessoryIconStyle { + style.modifier(AccessoryIconStyleModifier(style: self)) + } +} + // MARK: ActionStyle extension ModifiedStyle: ActionStyle where Style: ActionStyle { @@ -400,6 +428,34 @@ public extension DetailImageStyle { } } +// MARK: FilledIconStyle + +extension ModifiedStyle: FilledIconStyle where Style: FilledIconStyle { + public func makeBody(_ configuration: FilledIconConfiguration) -> some View { + FilledIcon(configuration) + .filledIconStyle(self.style) + .modifier(self.modifier) + } +} + +public struct FilledIconStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.filledIconStyle(self.style) + } +} + +public extension FilledIconStyle { + func modifier(_ modifier: some ViewModifier) -> some FilledIconStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some FilledIconStyle) -> some FilledIconStyle { + style.modifier(FilledIconStyleModifier(style: self)) + } +} + // MARK: FootnoteStyle extension ModifiedStyle: FootnoteStyle where Style: FootnoteStyle { @@ -1240,6 +1296,62 @@ public extension SecondaryActionStyle { } } +// MARK: SideBarStyle + +extension ModifiedStyle: SideBarStyle where Style: SideBarStyle { + public func makeBody(_ configuration: SideBarConfiguration) -> some View { + SideBar(configuration) + .sideBarStyle(self.style) + .modifier(self.modifier) + } +} + +public struct SideBarStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.sideBarStyle(self.style) + } +} + +public extension SideBarStyle { + func modifier(_ modifier: some ViewModifier) -> some SideBarStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some SideBarStyle) -> some SideBarStyle { + style.modifier(SideBarStyleModifier(style: self)) + } +} + +// MARK: SideBarListItemStyle + +extension ModifiedStyle: SideBarListItemStyle where Style: SideBarListItemStyle { + public func makeBody(_ configuration: SideBarListItemConfiguration) -> some View { + SideBarListItem(configuration) + .sideBarListItemStyle(self.style) + .modifier(self.modifier) + } +} + +public struct SideBarListItemStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.sideBarListItemStyle(self.style) + } +} + +public extension SideBarListItemStyle { + func modifier(_ modifier: some ViewModifier) -> some SideBarListItemStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some SideBarListItemStyle) -> some SideBarListItemStyle { + style.modifier(SideBarListItemStyleModifier(style: self)) + } +} + // MARK: StatusStyle extension ModifiedStyle: StatusStyle where Style: StatusStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift index ab890d7b1..bc96ab7aa 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift @@ -3,6 +3,22 @@ import Foundation import SwiftUI +// MARK: AccessoryIconStyle + +struct ResolvedAccessoryIconStyle: View { + let style: Style + let configuration: AccessoryIconConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension AccessoryIconStyle { + func resolve(configuration: AccessoryIconConfiguration) -> some View { + ResolvedAccessoryIconStyle(style: self, configuration: configuration) + } +} + // MARK: ActionStyle struct ResolvedActionStyle: View { @@ -227,6 +243,22 @@ extension DetailImageStyle { } } +// MARK: FilledIconStyle + +struct ResolvedFilledIconStyle: View { + let style: Style + let configuration: FilledIconConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension FilledIconStyle { + func resolve(configuration: FilledIconConfiguration) -> some View { + ResolvedFilledIconStyle(style: self, configuration: configuration) + } +} + // MARK: FootnoteStyle struct ResolvedFootnoteStyle: View { @@ -707,6 +739,38 @@ extension SecondaryActionStyle { } } +// MARK: SideBarStyle + +struct ResolvedSideBarStyle: View { + let style: Style + let configuration: SideBarConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension SideBarStyle { + func resolve(configuration: SideBarConfiguration) -> some View { + ResolvedSideBarStyle(style: self, configuration: configuration) + } +} + +// MARK: SideBarListItemStyle + +struct ResolvedSideBarListItemStyle: View { + let style: Style + let configuration: SideBarListItemConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension SideBarListItemStyle { + func resolve(configuration: SideBarListItemConfiguration) -> some View { + ResolvedSideBarListItemStyle(style: self, configuration: configuration) + } +} + // MARK: StatusStyle struct ResolvedStatusStyle: View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift index eb336cb69..9c3469832 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift @@ -85,6 +85,14 @@ extension PlaceholderTextFieldConfiguration { } } +// MARK: SideBarListItemConfiguration + +extension SideBarListItemConfiguration { + var _switch: Switch { + Switch(.init(isOn: self.$isOn), shouldApplyDefaultStyle: true) + } +} + // MARK: StepperFieldConfiguration extension StepperFieldConfiguration { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift index 988fe263e..6ddfcf7c9 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift @@ -3,6 +3,23 @@ import Foundation import SwiftUI +// MARK: AccessoryIconStyle + +public extension View { + func accessoryIconStyle(_ style: some AccessoryIconStyle) -> some View { + self.transformEnvironment(\.accessoryIconStyleStack) { stack in + stack.append(style) + } + } + + func accessoryIconStyle(@ViewBuilder content: @escaping (AccessoryIconConfiguration) -> some View) -> some View { + self.transformEnvironment(\.accessoryIconStyleStack) { stack in + let style = AnyAccessoryIconStyle(content) + stack.append(style) + } + } +} + // MARK: ActionStyle public extension View { @@ -241,6 +258,23 @@ public extension View { } } +// MARK: FilledIconStyle + +public extension View { + func filledIconStyle(_ style: some FilledIconStyle) -> some View { + self.transformEnvironment(\.filledIconStyleStack) { stack in + stack.append(style) + } + } + + func filledIconStyle(@ViewBuilder content: @escaping (FilledIconConfiguration) -> some View) -> some View { + self.transformEnvironment(\.filledIconStyleStack) { stack in + let style = AnyFilledIconStyle(content) + stack.append(style) + } + } +} + // MARK: FootnoteStyle public extension View { @@ -751,6 +785,40 @@ public extension View { } } +// MARK: SideBarStyle + +public extension View { + func sideBarStyle(_ style: some SideBarStyle) -> some View { + self.transformEnvironment(\.sideBarStyleStack) { stack in + stack.append(style) + } + } + + func sideBarStyle(@ViewBuilder content: @escaping (SideBarConfiguration) -> some View) -> some View { + self.transformEnvironment(\.sideBarStyleStack) { stack in + let style = AnySideBarStyle(content) + stack.append(style) + } + } +} + +// MARK: SideBarListItemStyle + +public extension View { + func sideBarListItemStyle(_ style: some SideBarListItemStyle) -> some View { + self.transformEnvironment(\.sideBarListItemStyleStack) { stack in + stack.append(style) + } + } + + func sideBarListItemStyle(@ViewBuilder content: @escaping (SideBarListItemConfiguration) -> some View) -> some View { + self.transformEnvironment(\.sideBarListItemStyleStack) { stack in + let style = AnySideBarListItemStyle(content) + stack.append(style) + } + } +} + // MARK: StatusStyle public extension View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift index 4c40404a9..8efd37679 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift @@ -3,6 +3,12 @@ import Foundation import SwiftUI +extension AccessoryIcon: _ViewEmptyChecking { + public var isEmpty: Bool { + accessoryIcon.isEmpty + } +} + extension Action: _ViewEmptyChecking { public var isEmpty: Bool { action.isEmpty @@ -128,6 +134,12 @@ extension DetailImage: _ViewEmptyChecking { } } +extension FilledIcon: _ViewEmptyChecking { + public var isEmpty: Bool { + filledIcon.isEmpty + } +} + extension Footnote: _ViewEmptyChecking { public var isEmpty: Bool { footnote.isEmpty @@ -330,6 +342,23 @@ extension SecondaryAction: _ViewEmptyChecking { } } +extension SideBar: _ViewEmptyChecking { + public var isEmpty: Bool { + footer.isEmpty && + editButton.isEmpty + } +} + +extension SideBarListItem: _ViewEmptyChecking { + public var isEmpty: Bool { + icon.isEmpty && + filledIcon.isEmpty && + title.isEmpty && + subtitle.isEmpty && + accessoryIcon.isEmpty + } +} + extension Status: _ViewEmptyChecking { public var isEmpty: Bool { status.isEmpty diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SideBar+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SideBar+API.generated.swift similarity index 75% rename from Sources/FioriSwiftUICore/_generated/ViewModels/API/SideBar+API.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/API/_SideBar+API.generated.swift index ded818ff6..10d388dbd 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SideBar+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SideBar+API.generated.swift @@ -3,7 +3,7 @@ import SwiftUI @available(iOS 14, *) -public struct SideBar { +public struct _SideBar { @Environment(\.subtitleModifier) private var subtitleModifier let _subtitle: Subtitle @@ -25,9 +25,9 @@ public struct SideBar { @ViewBuilder var subtitle: some View { if isModelInit { - _subtitle.modifier(subtitleModifier.concat(Fiori.SideBar.subtitle).concat(Fiori.SideBar.subtitleCumulative)) + _subtitle.modifier(subtitleModifier.concat(Fiori._SideBar.subtitle).concat(Fiori._SideBar.subtitleCumulative)) } else { - _subtitle.modifier(subtitleModifier.concat(Fiori.SideBar.subtitle)) + _subtitle.modifier(subtitleModifier.concat(Fiori._SideBar.subtitle)) } } var footer: some View { @@ -42,9 +42,9 @@ public struct SideBar { } @available(iOS 14, *) -extension SideBar where Subtitle == _ConditionalContent { +extension _SideBar where Subtitle == _ConditionalContent { - public init(model: SideBarModel, @ViewBuilder footer: () -> Footer, @ViewBuilder detail: () -> Detail) { + public init(model: _SideBarModel, @ViewBuilder footer: () -> Footer, @ViewBuilder detail: () -> Detail) { self.init(subtitle: model.subtitle, footer: footer, detail: detail) } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SideBarListItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SideBarListItem+API.generated.swift similarity index 75% rename from Sources/FioriSwiftUICore/_generated/ViewModels/API/SideBarListItem+API.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/API/_SideBarListItem+API.generated.swift index 5b49f6bdd..ebf087574 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SideBarListItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SideBarListItem+API.generated.swift @@ -2,7 +2,7 @@ // DO NOT EDIT import SwiftUI -public struct SideBarListItem { +public struct _SideBarListItem { @Environment(\.iconModifier) private var iconModifier @Environment(\.titleModifier) private var titleModifier @Environment(\.subtitleModifier) private var subtitleModifier @@ -34,30 +34,30 @@ public struct SideBarListItem, +extension _SideBarListItem where Icon == _ConditionalContent, Title == Text, Subtitle == _ConditionalContent, AccessoryIcon == _ConditionalContent { - public init(model: SideBarListItemModel) { + public init(model: _SideBarListItemModel) { self.init(icon: model.icon, title: model.title, subtitle: model.subtitle, accessoryIcon: model.accessoryIcon) } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SideBar+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/_SideBar+View.generated.swift similarity index 81% rename from Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SideBar+View.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/_SideBar+View.generated.swift index f0eaec528..4ed31ed8a 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SideBar+View.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/_SideBar+View.generated.swift @@ -1,8 +1,8 @@ // Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT -//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SideBar+View.swift` +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/_SideBar+View.swift` //TODO: Implement default Fiori style definitions as `ViewModifier` -//TODO: Implement SideBar `View` body +//TODO: Implement _SideBar `View` body //TODO: Implement LibraryContentProvider /// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible @@ -16,7 +16,7 @@ import SwiftUI // FIXME: - Implement Fiori style definitions extension Fiori { - enum SideBar { + enum _SideBar { typealias Subtitle = EmptyModifier typealias SubtitleCumulative = EmptyModifier @@ -37,22 +37,22 @@ extension Fiori { } } -// FIXME: - Implement SideBar View body +// FIXME: - Implement _SideBar View body @available(iOS 14, *) -extension SideBar: View { +extension _SideBar: View { public var body: some View { <# View body #> } } -// FIXME: - Implement SideBar specific LibraryContentProvider +// FIXME: - Implement _SideBar specific LibraryContentProvider @available(iOS 14.0, macOS 11.0, *) -struct SideBarLibraryContent: LibraryContentProvider { +struct _SideBarLibraryContent: LibraryContentProvider { @LibraryContentBuilder var views: [LibraryItem] { - LibraryItem(SideBar(model: LibraryPreviewData.Person.laurelosborn), + LibraryItem(_SideBar(model: LibraryPreviewData.Person.laurelosborn), category: .control) } } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SideBarListItem+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/_SideBarListItem+View.generated.swift similarity index 83% rename from Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SideBarListItem+View.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/_SideBarListItem+View.generated.swift index 623209319..1600403b4 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SideBarListItem+View.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/_SideBarListItem+View.generated.swift @@ -1,8 +1,8 @@ // Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT -//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SideBarListItem+View.swift` +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/_SideBarListItem+View.swift` //TODO: Implement default Fiori style definitions as `ViewModifier` -//TODO: Implement SideBarListItem `View` body +//TODO: Implement _SideBarListItem `View` body //TODO: Implement LibraryContentProvider /// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible @@ -16,7 +16,7 @@ import SwiftUI // FIXME: - Implement Fiori style definitions extension Fiori { - enum SideBarListItem { + enum _SideBarListItem { typealias Icon = EmptyModifier typealias IconCumulative = EmptyModifier typealias Title = EmptyModifier @@ -49,21 +49,21 @@ extension Fiori { } } -// FIXME: - Implement SideBarListItem View body +// FIXME: - Implement _SideBarListItem View body -extension SideBarListItem: View { +extension _SideBarListItem: View { public var body: some View { <# View body #> } } -// FIXME: - Implement SideBarListItem specific LibraryContentProvider +// FIXME: - Implement _SideBarListItem specific LibraryContentProvider @available(iOS 14.0, macOS 11.0, *) -struct SideBarListItemLibraryContent: LibraryContentProvider { +struct _SideBarListItemLibraryContent: LibraryContentProvider { @LibraryContentBuilder var views: [LibraryItem] { - LibraryItem(SideBarListItem(model: LibraryPreviewData.Person.laurelosborn), + LibraryItem(_SideBarListItem(model: LibraryPreviewData.Person.laurelosborn), category: .control) } } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SideBar+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/_SideBar+Init.generated.swift similarity index 78% rename from Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SideBar+Init.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/_SideBar+Init.generated.swift index ce3cbefa0..ff840ae05 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SideBar+Init.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/_SideBar+Init.generated.swift @@ -3,7 +3,7 @@ import SwiftUI @available(iOS 14, *) -extension SideBar where Subtitle == EmptyView { +extension _SideBar where Subtitle == EmptyView { public init( @ViewBuilder footer: () -> Footer, @ViewBuilder detail: () -> Detail @@ -17,7 +17,7 @@ extension SideBar where Subtitle == EmptyView { } @available(iOS 14, *) -extension SideBar where Footer == EmptyView { +extension _SideBar where Footer == EmptyView { public init( @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder detail: () -> Detail @@ -31,7 +31,7 @@ extension SideBar where Footer == EmptyView { } @available(iOS 14, *) -extension SideBar where Detail == EmptyView { +extension _SideBar where Detail == EmptyView { public init( @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer @@ -45,7 +45,7 @@ extension SideBar where Detail == EmptyView { } @available(iOS 14, *) -extension SideBar where Subtitle == EmptyView, Footer == EmptyView { +extension _SideBar where Subtitle == EmptyView, Footer == EmptyView { public init( @ViewBuilder detail: () -> Detail ) { @@ -58,7 +58,7 @@ extension SideBar where Subtitle == EmptyView, Footer == EmptyView { } @available(iOS 14, *) -extension SideBar where Subtitle == EmptyView, Detail == EmptyView { +extension _SideBar where Subtitle == EmptyView, Detail == EmptyView { public init( @ViewBuilder footer: () -> Footer ) { @@ -71,7 +71,7 @@ extension SideBar where Subtitle == EmptyView, Detail == EmptyView { } @available(iOS 14, *) -extension SideBar where Footer == EmptyView, Detail == EmptyView { +extension _SideBar where Footer == EmptyView, Detail == EmptyView { public init( @ViewBuilder subtitle: () -> Subtitle ) { @@ -84,7 +84,7 @@ extension SideBar where Footer == EmptyView, Detail == EmptyView { } @available(iOS 14, *) -extension SideBar where Subtitle == EmptyView, Footer == EmptyView, Detail == EmptyView { +extension _SideBar where Subtitle == EmptyView, Footer == EmptyView, Detail == EmptyView { public init( ) { diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SideBarListItem+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/_SideBarListItem+Init.generated.swift similarity index 78% rename from Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SideBarListItem+Init.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/_SideBarListItem+Init.generated.swift index e6931325e..8a7a26816 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SideBarListItem+Init.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/_SideBarListItem+Init.generated.swift @@ -2,7 +2,7 @@ // DO NOT EDIT import SwiftUI -extension SideBarListItem where Icon == EmptyView { +extension _SideBarListItem where Icon == EmptyView { public init( @ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, @@ -17,7 +17,7 @@ extension SideBarListItem where Icon == EmptyView { } } -extension SideBarListItem where Subtitle == EmptyView { +extension _SideBarListItem where Subtitle == EmptyView { public init( @ViewBuilder icon: () -> Icon, @ViewBuilder title: () -> Title, @@ -32,7 +32,7 @@ extension SideBarListItem where Subtitle == EmptyView { } } -extension SideBarListItem where AccessoryIcon == EmptyView { +extension _SideBarListItem where AccessoryIcon == EmptyView { public init( @ViewBuilder icon: () -> Icon, @ViewBuilder title: () -> Title, @@ -47,7 +47,7 @@ extension SideBarListItem where AccessoryIcon == EmptyView { } } -extension SideBarListItem where Icon == EmptyView, Subtitle == EmptyView { +extension _SideBarListItem where Icon == EmptyView, Subtitle == EmptyView { public init( @ViewBuilder title: () -> Title, @ViewBuilder accessoryIcon: () -> AccessoryIcon @@ -61,7 +61,7 @@ extension SideBarListItem where Icon == EmptyView, Subtitle == EmptyView { } } -extension SideBarListItem where Icon == EmptyView, AccessoryIcon == EmptyView { +extension _SideBarListItem where Icon == EmptyView, AccessoryIcon == EmptyView { public init( @ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle @@ -75,7 +75,7 @@ extension SideBarListItem where Icon == EmptyView, AccessoryIcon == EmptyView { } } -extension SideBarListItem where Subtitle == EmptyView, AccessoryIcon == EmptyView { +extension _SideBarListItem where Subtitle == EmptyView, AccessoryIcon == EmptyView { public init( @ViewBuilder icon: () -> Icon, @ViewBuilder title: () -> Title @@ -89,7 +89,7 @@ extension SideBarListItem where Subtitle == EmptyView, AccessoryIcon == EmptyVie } } -extension SideBarListItem where Icon == EmptyView, Subtitle == EmptyView, AccessoryIcon == EmptyView { +extension _SideBarListItem where Icon == EmptyView, Subtitle == EmptyView, AccessoryIcon == EmptyView { public init( @ViewBuilder title: () -> Title ) { diff --git a/sourcery/.lib/Sources/utils/Type+Extensions.swift b/sourcery/.lib/Sources/utils/Type+Extensions.swift index a63ff7d6f..504fc1542 100644 --- a/sourcery/.lib/Sources/utils/Type+Extensions.swift +++ b/sourcery/.lib/Sources/utils/Type+Extensions.swift @@ -69,7 +69,7 @@ public extension Type { var componentName: String { var name = name - if name == "_ActionModel" || name == "_ObjectItemModel" { + if name == "_ActionModel" || name == "_ObjectItemModel" || name == "_SideBarModel" || name == "_SideBarListItemModel"{ return name.replacingOccurrences(of: "Model", with: "") }