From 84cf5501e42702dfdb6f54ef09a1562341ae1f51 Mon Sep 17 00:00:00 2001 From: Charles Xu Date: Thu, 19 Oct 2023 14:22:06 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20jira=202286=20sort=20&?= =?UTF-8?q?=20filter=20for=20SwiftUI=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A SwiftUI component for configuration criteria of performing sorting and filter. SortFilterMenu and SortFilterFullCFG are provided. ✅ Closes: 1 --- .../Examples.xcodeproj/project.pbxproj | 30 +- .../FioriSwiftUICore/CoreContentView.swift | 4 + .../SortFilter/SortFilterExample.swift | 101 ++++ .../SortFilter/View+Extensions.swift | 35 ++ .../iconfromapp.imageset/Contents.json | 21 + .../iconfromapp.imageset/icon.png | Bin 0 -> 934 bytes Package.swift | 2 +- .../CancellableResettableForm.swift | 92 ++++ .../Components/MultiPropertyComponents.swift | 49 ++ .../Components/SinglePropertyComponents.swift | 2 + .../DataTypes/SoftFilter+DataType.swift | 521 ++++++++++++++++++ .../Models/DefaultViewModels.swift | 16 + .../Models/ModelDefinitions.swift | 68 +++ .../Views/OptionChip+View.swift | 182 ++++++ .../Views/OptionListPicker+View.swift | 61 ++ .../Views/SliderPicker+View.swift | 191 +++++++ .../SortFilter/SortFilter+Environment.swift | 77 +++ .../Views/SortFilter/SortFilterContext.swift | 14 + .../SortFilter/SortFilterDialog+View.swift | 63 +++ .../SortFilter/SortFilterFullCFG+View.swift | 102 ++++ .../SortFilter/SortFilterItemTitle.swift | 29 + .../SortFilter/SortFilterMenu+View.swift | 30 + .../SortFilter/SortFilterMenuItem+Style.swift | 102 ++++ .../SortFilter/SortFilterMenuItem+View.swift | 464 ++++++++++++++++ .../Views/SortFilter/SortFilterStyle.swift | 26 + .../_SortFilterCFGItemContainer.swift | 173 ++++++ .../_SortFilterMenuItemContainer.swift | 136 +++++ .../Views/SwitchPicker+View.swift | 158 ++++++ .../Component+Protocols.generated.swift | 51 +- ...mponentProtocols+Extension.generated.swift | 56 +- .../EnvironmentKey+Styles.generated.swift | 30 +- .../EnvironmentValue+Styles.generated.swift | 72 ++- .../API/OptionChip+API.generated.swift | 63 +++ .../API/OptionListPicker+API.generated.swift | 23 + .../API/SliderPicker+API.generated.swift | 25 + .../API/SortFilterFullCFG+API.generated.swift | 116 ++++ .../API/SortFilterMenu+API.generated.swift | 47 ++ .../SortFilterMenuItem+API.generated.swift | 82 +++ .../API/SwitchPicker+API.generated.swift | 22 + .../OptionChip+View.generated.swift | 62 +++ .../OptionListPicker+View.generated.swift | 34 ++ .../SliderPicker+View.generated.swift | 34 ++ .../SortFilterFullCFG+View.generated.swift | 74 +++ .../SortFilterMenu+View.generated.swift | 58 ++ .../SortFilterMenuItem+View.generated.swift | 66 +++ .../SwitchPicker+View.generated.swift | 34 ++ .../OptionChip+Init.generated.swift | 16 + .../OptionListPicker+Init.generated.swift | 3 + .../SliderPicker+Init.generated.swift | 3 + .../SortFilterFullCFG+Init.generated.swift | 131 +++++ .../SortFilterMenu+Init.generated.swift | 3 + .../SortFilterMenuItem+Init.generated.swift | 47 ++ .../SwitchPicker+Init.generated.swift | 3 + ...ListPickerModel+Extensions.generated.swift | 9 + ...terFullCFGModel+Extensions.generated.swift | 21 + ...FilterMenuModel+Extensions.generated.swift | 9 + .../en.lproj/FioriSwiftUICore.strings | 3 + 57 files changed, 3920 insertions(+), 26 deletions(-) create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/SortFilter/View+Extensions.swift create mode 100644 Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/Contents.json create mode 100644 Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/icon.png create mode 100644 Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift create mode 100644 Sources/FioriSwiftUICore/DataTypes/SoftFilter+DataType.swift create mode 100644 Sources/FioriSwiftUICore/Views/OptionChip+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/OptionListPicker+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/SliderPicker+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilter+Environment.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterContext.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterDialog+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterFullCFG+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterItemTitle.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenu+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+Style.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+View.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/SortFilterStyle.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift create mode 100644 Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift create mode 100644 Sources/FioriSwiftUICore/Views/SwitchPicker+View.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionChip+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPicker+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/SliderPicker+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterFullCFG+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenu+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenuItem+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/SwitchPicker+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionChip+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionListPicker+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SliderPicker+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterFullCFG+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenu+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenuItem+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SwitchPicker+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionChip+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionListPicker+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SliderPicker+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterFullCFG+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenu+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenuItem+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SwitchPicker+Init.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerModel+Extensions.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterFullCFGModel+Extensions.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterMenuModel+Extensions.generated.swift diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 7f18def4a..64f224ac3 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -125,6 +125,8 @@ B8D4376F25F980340024EE7D /* ObjectCell_Spec_Jan2018.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D4376E25F980340024EE7D /* ObjectCell_Spec_Jan2018.swift */; }; B8D4377125F983730024EE7D /* ObjectCell_Rules_Alignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D4377025F983730024EE7D /* ObjectCell_Rules_Alignment.swift */; }; B8D437732609479E0024EE7D /* SingleActionFollowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D437722609479E0024EE7D /* SingleActionFollowButton.swift */; }; + C1A0FDB32AD893FA0001738E /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A0FDB22AD893FA0001738E /* View+Extensions.swift */; }; + C1C764882A818BEC00BCB0F7 /* SortFilterExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C764872A818BEC00BCB0F7 /* SortFilterExample.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -307,6 +309,8 @@ B8D4376E25F980340024EE7D /* ObjectCell_Spec_Jan2018.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectCell_Spec_Jan2018.swift; sourceTree = ""; }; B8D4377025F983730024EE7D /* ObjectCell_Rules_Alignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectCell_Rules_Alignment.swift; sourceTree = ""; }; B8D437722609479E0024EE7D /* SingleActionFollowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleActionFollowButton.swift; sourceTree = ""; }; + C1A0FDB22AD893FA0001738E /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; + C1C764872A818BEC00BCB0F7 /* SortFilterExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortFilterExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -501,6 +505,7 @@ 8A5579C824C1293C0098003A /* FioriSwiftUICore */ = { isa = PBXGroup; children = ( + C1C764862A818BD600BCB0F7 /* SortFilter */, B100639129C0623300AF0CA2 /* StepProgressIndicator */, 108E43D3292DAB3E006532F3 /* EmptyStateView */, B1D41B1E291A2D2E004E64A5 /* Picker */, @@ -700,6 +705,15 @@ path = ObjectItem; sourceTree = ""; }; + C1C764862A818BD600BCB0F7 /* SortFilter */ = { + isa = PBXGroup; + children = ( + C1C764872A818BEC00BCB0F7 /* SortFilterExample.swift */, + C1A0FDB22AD893FA0001738E /* View+Extensions.swift */, + ); + path = SortFilter; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -889,6 +903,7 @@ 8A557A2424C12F380098003A /* ChartDetailView.swift in Sources */, 8A5579D024C1293C0098003A /* SettingsLine.swift in Sources */, 1FC30412270540FB004BEE00 /* 72-Fonts.swift in Sources */, + C1A0FDB32AD893FA0001738E /* View+Extensions.swift in Sources */, B84D24ED2652F343007F2373 /* HeaderChartExample.swift in Sources */, B100639329C0624D00AF0CA2 /* StepProgressIndicatorExample.swift in Sources */, B846F94626815CC90085044B /* ContactItemExample.swift in Sources */, @@ -937,6 +952,7 @@ 8A5579D524C1293C0098003A /* SettingsSeries.swift in Sources */, 8A557A2224C12C9B0098003A /* CoreContentView.swift in Sources */, 8A5579D224C1293C0098003A /* Color+Extensions.swift in Sources */, + C1C764882A818BEC00BCB0F7 /* SortFilterExample.swift in Sources */, B84D24EF2652F343007F2373 /* ObjectHeaderTestApp.swift in Sources */, B84D24EC2652F343007F2373 /* ObjectHeaderSpecCompact.swift in Sources */, 8A5579CD24C1293C0098003A /* SettingsLabel.swift in Sources */, @@ -1116,7 +1132,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1171,7 +1187,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -1191,7 +1207,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Examples/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1213,7 +1229,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Examples/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1232,7 +1248,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = ExamplesTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1253,7 +1269,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = ExamplesTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift b/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift index fcf70c39e..91545232c 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift @@ -107,6 +107,10 @@ struct CoreContentView: View { destination: EmptyStateViewExample()) { Text("EmptyStateViewExample") } + + NavigationLink(destination: SortFilterExample()) { + Text("SortFilterExample") + } } }.navigationBarTitle("FioriSwiftUICore") } diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift new file mode 100644 index 000000000..2ba825e21 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift @@ -0,0 +1,101 @@ +import FioriSwiftUICore +import SwiftUI + +struct SortFilterExample: View { + @State private var items: [[SortFilterItem]] = [[ + .picker(item: .init(value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review", "Accepted", "Rejected"], name: "JIRA Status", allowsMultipleSelection: true, allowsEmptySelection: true, icon: "clock"), isShownOnMenu: true), + .picker(item: .init(value: [0], valueOptions: ["High", "Medium", "Low"], name: "Priority", allowsMultipleSelection: true, allowsEmptySelection: true, icon: "filemenu.and.cursorarrow"), isShownOnMenu: true), + .filterfeedback(item: .init(value: [0], valueOptions: ["Ascending", "Descending"], name: "Sort Order", allowsMultipleSelection: false, allowsEmptySelection: false, icon: "checkmark")), + .slider(item: .init(value: 10, minimumValue: 0, maximumValue: 100, name: "User Stories", formatter: "%2d Stories", icon: "number"), isShownOnMenu: true), + .slider(item: .init(value: nil, minimumValue: 0, maximumValue: 100, name: "Number of Tasks"), isShownOnMenu: true), + .datetime(item: .init(value: Date(), name: "Start Date", formatter: "yyyy-MM-dd HH:mm",icon: "calendar"), isShownOnMenu: true), + .datetime(item: .init(value: nil, name: "Completion Date"), isShownOnMenu: true), + .switch(item: .init(name: "Favorite", value: true, icon: "heart.fill"), isShownOnMenu: true), + .switch(item: .init(name: "Tagged", value: nil, icon: "tag"), isShownOnMenu: false) + ]] + + @State private var isShowingFullCFG: Bool = false + @State private var isCustomStyle: Bool = false + @State private var sortFilterList: [String] = [] + @State private var sortFilterButtonLabel: String = "Sort & Filter" + + var body: some View { + VStack { + if isCustomStyle { + SortFilterMenu(items: $items, onUpdate: performSortAndFilter) + .sortFilterMenuItemStyle(font: .subheadline, foregroundColorSelected: .red, strokeColorSelected: .red, cornerRadius: 25) + .optionChipStyle(font: .footnote, foregroundColorUnselected: .green, strokeColorSelected: .black) + // .trailingFullConfigurationMenuItem(icon: "command") + // .leadingFullConfigurationMenuItem(icon: "command") + // .leadingFullConfigurationMenuItem(name: "All") + } else { + SortFilterMenu(items: $items, onUpdate: performSortAndFilter) + } + + List { + ForEach(sortFilterList, id: \.self) { line in + Text(line) + } + } + .listStyle(PlainListStyle()) + + VStack { + Toggle("Custom Style", isOn: $isCustomStyle) + .toggleStyle(FioriToggleStyle()) + + Button("Print") { + for line in sortFilterList { + print(line) + } + } + } + } + .navigationTitle("Sort & Filter") + .toolbar { + Button(sortFilterButtonLabel) { + isShowingFullCFG.toggle() + } + .popover(isPresented: $isShowingFullCFG, arrowEdge: .leading) { + if isCustomStyle { + SortFilterFullCFG( + title: "Configuration", + items: $items, + onUpdate: performSortAndFilter + ) + .optionChipStyle(font: .footnote, foregroundColorUnselected: .green, strokeColorSelected: .black) + } else { + SortFilterFullCFG( + title: "Configuration", + items: $items, + onUpdate: performSortAndFilter + ) + } + } + } + .onAppear { + performSortAndFilter() + } + } + + func numberOfItems() -> Int { + // randomly padding result to mimic impact of filterring + for i in 0 ... Int.random(in: 0 ... 5) { + self.sortFilterList.append("Non-SortFilterCFG: element \(i + 1)") + } + return self.sortFilterList.count + } + + func performSortAndFilter() { + self.sortFilterList = self.items.joined().map { value(of: $0) } + self.sortFilterButtonLabel = "Sort & Filter (\(self.numberOfItems()))" + } +} + +#if DEBUG + @available(iOS 16.0, *) + struct SortFilterExample_Previews: PreviewProvider { + static var previews: some View { + SortFilterExample() + } + } +#endif diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/View+Extensions.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/View+Extensions.swift new file mode 100644 index 000000000..10686c555 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/View+Extensions.swift @@ -0,0 +1,35 @@ +import FioriSwiftUICore +import SwiftUI + +extension View { + func value(of item: SortFilterItem) -> String { + switch item { + case .picker(let v, _): + return self.json(item: v) + case .filterfeedback(let v): + return self.json(item: v) + case .slider(let v, _): + return self.json(item: v) + case .datetime(let v, _): + return self.json(item: v) + case .switch(let v, _): + return self.json(item: v) + } + } + + func json(item: PickerItem) -> String { + "SortFilterCFG: {name: \(item.name), value: \(item.value)}" + } + + func json(item: SliderItem) -> String { + "SortFilterCFG: {name: \(item.name), value: \(String(describing: item.value))}" + } + + func json(item: DateTimeItem) -> String { + "SortFilterCFG: {name: \(item.name), value: \(String(describing: item.value))}" + } + + func json(item: SwitchItem) -> String { + "SortFilterCFG: {name: \(item.name), value: \(String(describing: item.value))}" + } +} diff --git a/Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/Contents.json b/Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/Contents.json new file mode 100644 index 000000000..2945b36b9 --- /dev/null +++ b/Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/icon.png b/Apps/Examples/Examples/Preview Content/Preview Assets.xcassets/iconfromapp.imageset/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d91dd8e60c39253ab9753e4f1c0d137b4e06cbdb GIT binary patch literal 934 zcmV;X16lluP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR916rckD1ONa40RR916aWAK0J7ci{Qv+15J^NqR5%fpR7q=8K@k3`Ur#1# zP;(em)aY$okQ~I5H_?+4m-xnrzd%9sqT&YQilPTWe}v{G2K5p*@ZwD`t{6dF2^w)t z$YkE@F003;K0+Enn5^>MDW|pX^!4#1{#?F+J;N3Pft7AI1RK?4cu<-%$vqhUJOcTYeE!SPz4x-MpalaBV?OT2%N_V%~|lHDf<#OMsmekqr?EP$(w zd!|lVE4<@zm024s!|+I37O25D1C}Dsr?)Rb)pnxA-ayV2y)`>c>-S?O|KoT*FuRaz zkphwE@+;C}iBAJe_hvwqxI~Kuj8pYnoII3*5XK5Yx>O!l9;!|Z|MnGirpj{al`5B_ z7kqB>S9zP=vrX=m?6+X62R*ZvauB)PDtNaTEy*?7H}ENan=ds|jV6uq5p84g?w-)$ z=r)`kN|^3?m^*OQp;Siv95V}}4Up{KoMBxvXRJalpn}}d8V^^r(`@?GeI>UwJ`u{} zrgnTwn!Bd%J1HQQqftKFbysbS%Rj~hcOr4dX*yZ+j;d2OYXHl66k)q|c@GeplM2eVo?S6!>H~AIb7zEPL8C!x}yO*Kl~YoME5N zW0-$jCS+1&~()d5RhhJY&n zhtdI?g6V+WYo?d4z9%;8ka!v65>N;26qnkjQ{K5g#UR1@1r+>sK#p6*NdN!<07*qo IM6N<$g1&*0mH+?% literal 0 HcmV?d00001 diff --git a/Package.swift b/Package.swift index 0b4374da2..48162e04b 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "FioriSwiftUI", defaultLocalization: "en", - platforms: [.iOS(.v15), .watchOS(.v7)], + platforms: [.iOS(.v16), .watchOS(.v7)], products: [ .library( name: "FioriSwiftUI", diff --git a/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift b/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift new file mode 100644 index 000000000..114413063 --- /dev/null +++ b/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift @@ -0,0 +1,92 @@ +import Foundation +import SwiftUI + +struct CancellableResettableDialogForm: View { + let title: Title + + let components: Components + + var cancelAction: CancelAction + var resetAction: ResetAction + var applyAction: ApplyAction + + public init(@ViewBuilder title: () -> Title, + @ViewBuilder cancelAction: () -> CancelAction, + @ViewBuilder resetAction: () -> ResetAction, + @ViewBuilder applyAction: () -> ApplyAction, + @ViewBuilder components: () -> Components) + { + self.title = title() + self.cancelAction = cancelAction() + self.resetAction = resetAction() + self.applyAction = applyAction() + self.components = components() + } + + var body: some View { + VStack(spacing: UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) { + HStack { + cancelAction + Spacer() + title + Spacer() + resetAction + } + components + applyAction + } + .frame(minWidth: 375) + .padding(UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) + } +} + +struct ApplyButtonStyle: PrimitiveButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .frame(minWidth: UIDevice.current.userInterfaceIdiom == .pad ? 375 : 200, maxWidth: .infinity) + .padding(15) + .font(.body) + .fontWeight(.bold) + .foregroundStyle(Color.preferredColor(.base2)) + .background(RoundedRectangle(cornerRadius: 8).fill(Color.preferredColor(.tintColor))) + .onTapGesture { + configuration.trigger() + } + } +} + +struct CancelResetButtonStyle: PrimitiveButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.body) + .fontWeight(.bold) + .foregroundStyle(Color.preferredColor(.tintColor)) + .frame(minWidth: UIDevice.current.userInterfaceIdiom == .pad ? 375 : 200, maxWidth: .infinity) + .onTapGesture { + configuration.trigger() + } + } +} + +#Preview { + VStack { + Spacer() + CancellableResettableDialogForm { + Text("Date of Completion") + } cancelAction: { + Action(actionText: "Cancel", didSelectAction: nil) + } resetAction: { + Action(actionText: "Reset", didSelectAction: nil) + } applyAction: { + Action(actionText: "Apply", didSelectAction: nil) + .buttonStyle(ApplyButtonStyle()) + } components: { + DatePicker( + "date", + selection: Binding(get: { Date() }, set: { print($0) }), + displayedComponents: [.date] + ) + .datePickerStyle(.graphical) + } + } +} diff --git a/Sources/FioriSwiftUICore/Components/MultiPropertyComponents.swift b/Sources/FioriSwiftUICore/Components/MultiPropertyComponents.swift index 14215991d..a2d135c86 100644 --- a/Sources/FioriSwiftUICore/Components/MultiPropertyComponents.swift +++ b/Sources/FioriSwiftUICore/Components/MultiPropertyComponents.swift @@ -53,3 +53,52 @@ internal protocol _DurationPicker: _ComponentMultiPropGenerating, AnyObject { // sourcery: default.value = MeasurementFormatter() var measurementFormatter: MeasurementFormatter { get set } } + +internal protocol _SliderPicker: _ComponentMultiPropGenerating, AnyObject { + // sourcery: bindingProperty + // sourcery: no_view + var value: Int? { get set } + + // sourcery: no_view + // sourcery: default.value = nil + var formatter: String? { get } + + // sourcery: no_view + // sourcery: default.value = 0.0 + var minimumValue: Int { get } + + // sourcery: no_view + // sourcery: default.value = 100.0 + var maximumValue: Int { get } + + // sourcery: no_view + // sourcery: default.value = nil + var hint: String? { get } +} + +internal protocol _SwitchPicker: _ComponentMultiPropGenerating, AnyObject { + // sourcery: bindingProperty + // sourcery: no_view + var value: Bool? { get set } + + // sourcery: no_view + // sourcery: default.value = nil + var name: String? { get } + + // sourcery: no_view + // sourcery: default.value = nil + var hint: String? { get } +} + +internal protocol _OptionListPicker: _ComponentMultiPropGenerating, AnyObject { + // sourcery: bindingProperty + // sourcery: no_view + var value: [Int] { get set } + + // sourcery: no_view + var valueOptions: [String] { get } + + // sourcery: no_view + // sourcery: default.value = nil + var hint: String? { get } +} diff --git a/Sources/FioriSwiftUICore/Components/SinglePropertyComponents.swift b/Sources/FioriSwiftUICore/Components/SinglePropertyComponents.swift index e9731f208..2020a4b4b 100644 --- a/Sources/FioriSwiftUICore/Components/SinglePropertyComponents.swift +++ b/Sources/FioriSwiftUICore/Components/SinglePropertyComponents.swift @@ -51,4 +51,6 @@ internal struct _Component: _ComponentGenerating { // sourcery: backingComponent=FootnoteIconStack // sourcery: customFunctionBuilder=FootnoteIconsBuilder let footnoteIcons_: [TextOrIcon]? + let leftIcon_: Image? + let rightIcon_: Image? } diff --git a/Sources/FioriSwiftUICore/DataTypes/SoftFilter+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/SoftFilter+DataType.swift new file mode 100644 index 000000000..d4b455777 --- /dev/null +++ b/Sources/FioriSwiftUICore/DataTypes/SoftFilter+DataType.swift @@ -0,0 +1,521 @@ +import SwiftUI +import UIKit + +public enum SortFilterItem: Identifiable, Hashable { + public var id: String { + switch self { + case .picker(let item, _): + return item.id + case .filterfeedback(let item): + return item.id + case .switch(let item, _): + return item.id + case .slider(let item, _): + return item.id + case .datetime(let item, _): + return item.id + } + } + + case picker(item: PickerItem, isShownOnMenu: Bool) + case filterfeedback(item: PickerItem) + case `switch`(item: SwitchItem, isShownOnMenu: Bool) + case slider(item: SliderItem, isShownOnMenu: Bool) + case datetime(item: DateTimeItem, isShownOnMenu: Bool) + + public var isShownOnMenu: Bool { + switch self { + case .picker(_, let isShownOnMenu): + return isShownOnMenu + case .filterfeedback: + return true + case .switch(_, let isShownOnMenu): + return isShownOnMenu + case .slider(_, let isShownOnMenu): + return isShownOnMenu + case .datetime(_, let isShownOnMenu): + return isShownOnMenu + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .picker(let item, _): + hasher.combine(item.id) + hasher.combine(item.originalValue) + hasher.combine(item.workingValue) + hasher.combine(item.value) + case .filterfeedback(let item): + hasher.combine(item.id) + hasher.combine(item.originalValue) + hasher.combine(item.workingValue) + hasher.combine(item.value) + case .switch(let item, _): + hasher.combine(item.id) + hasher.combine(item.originalValue) + hasher.combine(item.workingValue) + hasher.combine(item.value) + case .slider(let item, _): + hasher.combine(item.id) + hasher.combine(item.originalValue) + hasher.combine(item.workingValue) + hasher.combine(item.value) + case .datetime(let item, _): + hasher.combine(item.id) + hasher.combine(item.originalValue) + hasher.combine(item.workingValue) + hasher.combine(item.value) + } + } +} + +/// (value: [Int], valueOptions: [String], keyName: String?, allowsMultipleSelection: Bool, allowsEmptySelection: Bool) +public struct PickerItem: Identifiable, Equatable { + public var id = UUID().uuidString + + public var name: String + + public var value: [Int] + public var workingValue: [Int] + let originalValue: [Int] + + var valueOptions: [String] + public let allowsMultipleSelection: Bool + public let allowsEmptySelection: Bool + public let icon: String? + + public init(value: [Int], valueOptions: [String], name: String, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, icon: String? = nil) { + self.value = value + self.workingValue = value + self.originalValue = value + self.valueOptions = valueOptions + self.name = name + self.allowsMultipleSelection = allowsMultipleSelection + self.allowsEmptySelection = allowsEmptySelection + self.icon = icon + } + + mutating func onTap(option: String) { + guard let index = valueOptions.firstIndex(of: option) else { return } + if self.workingValue.contains(index) { + if self.workingValue.count > 1 { + self.workingValue = self.workingValue.filter { $0 != index } + } else { + if self.allowsEmptySelection { + self.workingValue = [] + } else { + self.workingValue = index == 1 ? [0] : [1] + } + } + } else { + if self.allowsMultipleSelection { + self.workingValue.append(index) + } else { + self.workingValue = [index] + } + } + } + + mutating func optionOnTap(_ index: Int) { + if self.workingValue.contains(index) { + if self.workingValue.count > 1 { + self.workingValue = self.workingValue.filter { $0 != index } + } else { + if self.allowsEmptySelection { + self.workingValue = [] + } else { + self.workingValue = index == 1 ? [0] : [1] + } + } + } else { + if self.allowsMultipleSelection { + self.workingValue.append(index) + } else { + self.workingValue = [index] + } + } + } + + mutating func cancel() { + self.workingValue = self.value.map { $0 } + } + + mutating func reset() { + self.workingValue = self.originalValue.map { $0 } + } + + mutating func apply() { + self.value = self.workingValue.map { $0 } + } + + func isOptionSelected(_ option: String) -> Bool { + guard let idx = valueOptions.firstIndex(of: option) else { return false } + return self.workingValue.contains(idx) + } + + func isOptionSelected(index: Int) -> Bool { + self.workingValue.contains(index) + } + + var isChecked: Bool { + !self.value.isEmpty + } + + var label: String { + + if allowsMultipleSelection && self.value.count >= 1 { + if self.value.count == 1 { + return valueOptions[value[0]] + } else { + return "\(self.name) (\(self.value.count))" + } + } else { + return self.name + } + } + + var isChanged: Bool { + self.value != self.workingValue + } +} + +/// (value: Bool, keyName: String) +public struct SwitchItem: Identifiable, Equatable { + public var id = UUID().uuidString + + public var name: String + public var value: Bool? + var workingValue: Bool? + let originalValue: Bool? + public let icon: String? + public let hint: String? + + public init(id: String = UUID().uuidString, name: String, value: Bool?, icon: String? = nil, hint: String? = nil) { + self.id = id + self.name = name + self.value = value + self.workingValue = value + self.originalValue = value + self.icon = icon + self.hint = hint + } + + mutating func reset() { + self.workingValue = self.originalValue + } + + mutating func cancel() { + self.workingValue = self.value + } + + mutating func apply() { + self.value = self.workingValue + } + + var isChecked: Bool { + self.value ?? false + } + + var isChanged: Bool { + self.value != self.workingValue + } +} + +/// (value: Float, minimumValue: Float, maximumValue: Float, keyName: String?) +public struct SliderItem: Identifiable, Equatable { + public var id = UUID().uuidString + + public var name: String + + public var value: Int? + var workingValue: Int? + let originalValue: Int? + public let minimumValue: Int + public let maximumValue: Int + let formatter: String? + public let icon: String? + public let hint: String? + + public init(value: Int? = nil, minimumValue: Int, maximumValue: Int, name: String, formatter: String? = nil, icon: String? = nil, hint: String? = nil) { + self.value = value + self.workingValue = value + self.originalValue = value + self.minimumValue = minimumValue + self.maximumValue = maximumValue + self.name = name + self.formatter = formatter + self.icon = icon + self.hint = hint + } + + mutating func reset() { + self.workingValue = self.originalValue + } + + mutating func cancel() { + self.workingValue = self.value + } + + mutating func apply() { + self.value = self.workingValue + } + + var isChecked: Bool { + self.value != nil + } + + var label: String { + if let formatter = formatter, let value = value { + return String(format: formatter, value) + } + return name + } + + mutating func setValue(newValue: SliderItem) { + self.value = newValue.value + } + + var isChanged: Bool { + self.value != self.workingValue + } +} + +public struct DateTimeItem: Equatable, Hashable { + public let id = UUID().uuidString + + public var name: String + public var value: Date? + var workingValue: Date? + let originalValue: Date? + public var icon: String? + public let formatter: String? + + public init(value: Date?, name: String, formatter: String? = nil, icon: String? = nil) { + self.value = value + self.workingValue = value + self.originalValue = value + self.name = name + self.formatter = formatter + self.icon = icon + } + + mutating func reset() { + self.workingValue = self.originalValue + } + + mutating func apply() { + self.value = self.workingValue + } + + mutating func cancel() { + self.workingValue = self.value + } + + var isChecked: Bool { + self.value != nil + } + + var label: String { + if let value = self.value { + if let format = self.formatter { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.string(from: value) + } else { + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .short + return dateFormatter.string(from: value) + } + } else { + return self.name + } + } + + var isChanged: Bool { + self.value != self.workingValue + } +} + +extension SortFilterItem { + var picker: PickerItem { + get { + switch self { + case .picker(let item, _): + return item + default: + fatalError("Unexpected value \(self)") + } + } + + set { + switch self { + case .picker(_, let isShownOnMenu): + self = .picker(item: newValue, isShownOnMenu: isShownOnMenu) + default: + fatalError("Unexpected value \(self)") + } + } + } + + var filterfeedback: PickerItem { + get { + switch self { + case .filterfeedback(let item): + return item + default: + fatalError("Unexpected value \(self)") + } + } + + set { + switch self { + case .filterfeedback: + self = .filterfeedback(item: newValue) + default: + fatalError("Unexpected value \(self)") + } + } + } + + var slider: SliderItem { + get { + switch self { + case .slider(let item, _): + return item + default: + fatalError("Unexpected value \(self)") + } + } + + set { + switch self { + case .slider(_, let isShownOnMenu): + self = .slider(item: newValue, isShownOnMenu: isShownOnMenu) + default: + fatalError("Unexpected value \(self)") + } + } + } + + var datetime: DateTimeItem { + get { + switch self { + case .datetime(let item, _): + return item + default: + fatalError("Unexpected value \(self)") + } + } + + set { + switch self { + case .datetime(_, let isShownOnMenu): + self = .datetime(item: newValue, isShownOnMenu: isShownOnMenu) + default: + fatalError("Unexpected value \(self)") + } + } + } + + var `switch`: SwitchItem { + get { + switch self { + case .switch(let item, _): + return item + default: + fatalError("Unexpected value \(self)") + } + } + + set { + switch self { + case .switch(_, let isShownOnMenu): + self = .switch(item: newValue, isShownOnMenu: isShownOnMenu) + default: + fatalError("Unexpected value \(self)") + } + } + } + + public var isChanged: Bool { + switch self { + case .picker(let item, _): + return item.isChanged + case .filterfeedback(let item): + return item.isChanged + case .switch(let item, _): + return item.isChanged + case .datetime(let item, _): + return item.isChanged + case .slider(let item, _): + return item.isChanged + } + } + + public mutating func cancel() { + switch self { + case .picker(var item, _): + item.cancel() + self.picker = item + case .filterfeedback(var item): + item.cancel() + self.filterfeedback = item + case .switch(var item, _): + item.cancel() + self.switch = item + case .datetime(var item, _): + item.cancel() + self.datetime = item + case .slider(var item, _): + item.cancel() + self.slider = item + } + } + + public mutating func reset() { + switch self { + case .picker(var item, _): + item.reset() + self.picker = item + case .filterfeedback(var item): + item.reset() + self.filterfeedback = item + case .switch(var item, _): + item.reset() + self.switch = item + case .datetime(var item, _): + item.reset() + self.datetime = item + case .slider(var item, _): + item.reset() + self.slider = item + } + } + + public mutating func apply() { + switch self { + case .picker(var item, _): + item.apply() + self.picker = item + case .filterfeedback(var item): + item.apply() + self.filterfeedback = item + case .switch(var item, _): + item.apply() + self.switch = item + case .datetime(var item, _): + item.apply() + self.datetime = item + case .slider(var item, _): + item.apply() + self.slider = item + } + } +} + +/* + Notes: + c. to resolve: keyName should not be nillable for menu item, but it can be nil for sheet + e. make filter feedback configuraion a separate item type, instead of sharing FilterItem. ?? + */ diff --git a/Sources/FioriSwiftUICore/Models/DefaultViewModels.swift b/Sources/FioriSwiftUICore/Models/DefaultViewModels.swift index 420d1ad11..7c615eecb 100644 --- a/Sources/FioriSwiftUICore/Models/DefaultViewModels.swift +++ b/Sources/FioriSwiftUICore/Models/DefaultViewModels.swift @@ -24,6 +24,22 @@ public struct _CancelActionDefault: ActionModel { public init() {} } +public struct _ResetActionDefault: ActionModel { + public var actionText: String? { + NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: fioriSwiftUICoreBundle, comment: "") + } + + public init() {} +} + +public struct _ApplyActionDefault: ActionModel { + public var actionText: String? { + NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: fioriSwiftUICoreBundle, comment: "") + } + + public init() {} +} + public struct _AgreeActionDefault: ActionModel { public var actionText: String? { NSLocalizedString("Agree", tableName: "FioriSwiftUICore", bundle: fioriSwiftUICoreBundle, comment: "") diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index 41941e396..b625bb3f0 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -433,3 +433,71 @@ public protocol StepProgressIndicatorModel: AnyObject { // sourcery: default.value = _CancelActionDefault() var cancelAction: ActionModel? { get } } + +// sourcery: generated_component_composite +public protocol SortFilterMenuModel: AnyObject { + // sourcery: bindingProperty + // sourcery: backingComponent=_SortFilterMenuItemContainer + var items: [[SortFilterItem]] { get set } + + // sourcery: default.value = nil + // sourcery: no_view + var onUpdate: (() -> Void)? { get set } +} + +// sourcery: virtualPropActionHelper = "@State var context: SortFilterContext = SortFilterContext()" +// sourcery: add_env_props = "dismiss" +// sourcery: generated_component_composite +public protocol SortFilterFullCFGModel: AnyObject, TitleComponent { + // sourcery: bindingProperty + // sourcery: backingComponent=_SortFilterCFGItemContainer + var items: [[SortFilterItem]] { get set } + + // sourcery: genericParameter.name = CancelActionView + // sourcery: default.value = _CancelActionDefault() + var cancelAction: ActionModel? { get } + + // sourcery: genericParameter.name = ResetActionView + // sourcery: default.value = _ResetActionDefault() + var resetAction: ActionModel? { get } + + // sourcery: genericParameter.name = ApplyActionView + // sourcery: default.value = _ApplyActionDefault() + var applyAction: ActionModel? { get } + + // sourcery: default.value = nil + // sourcery: no_view + var onUpdate: (() -> Void)? { get } +} + +// sourcery: add_env_props = "sortFilterMenuItemStyle" +// sourcery: virtualPropActionHelper = "@State var context: SortFilterContext = SortFilterContext()" +// sourcery: generated_component_composite +public protocol SortFilterMenuItemModel: LeftIconComponent, TitleComponent, RightIconComponent { + // sourcery: no_view + var isSelected: Bool { get } +} + +// sourcery: add_env_props = "optionChipStyle" +// sourcery: generated_component_composite +public protocol OptionChipModel: LeftIconComponent, TitleComponent { + // sourcery: no_view + var isSelected: Bool { get } +} + +// sourcery: add_env_props = "sortFilterMenuItemStyle" +// sourcery: generated_component_not_configurable +public protocol OptionListPickerModel: OptionListPickerComponent { + // sourcery: default.value = nil + // sourcery: no_view + var onTap: ((_ index: Int) -> Void)? { get } +} + +// sourcery: add_env_props = "sortFilterMenuItemStyle" +// sourcery: generated_component_not_configurable +// sourcery: add_env_props = "fioriToggleStyle" +public protocol SwitchPickerModel: SwitchPickerComponent {} + +// sourcery: add_env_props = "sortFilterMenuItemStyle" +// sourcery: generated_component_not_configurable +public protocol SliderPickerModel: SliderPickerComponent {} diff --git a/Sources/FioriSwiftUICore/Views/OptionChip+View.swift b/Sources/FioriSwiftUICore/Views/OptionChip+View.swift new file mode 100644 index 000000000..82bcc02e9 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/OptionChip+View.swift @@ -0,0 +1,182 @@ +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +extension Fiori { + enum OptionChip { + typealias LeftIcon = EmptyModifier + typealias LeftIconCumulative = EmptyModifier + typealias Title = EmptyModifier + typealias TitleCumulative = EmptyModifier + + // TODO: - substitute type-specific ViewModifier for EmptyModifier + /* + // replace `typealias Subtitle = EmptyModifier` with: + + struct Subtitle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .foregroundColor(.preferredColor(.primary3)) + } + } + */ + static let leftIcon = LeftIcon() + static let title = Title() + static let leftIconCumulative = LeftIconCumulative() + static let titleCumulative = TitleCumulative() + } +} + +extension OptionChip: View { + public var body: some View { + optionChipStyle.makeBody(configuration: OptionChipConfiguration(leftIcon: AnyView(leftIcon), title: AnyView(title), isSelected: _isSelected)) + } +} + +/* + // FIXME: - Implement OptionChip specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct OptionChipLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(OptionChip(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ + +public struct OptionChipConfiguration { + let leftIcon: AnyView + let title: AnyView + let isSelected: Bool + + public init(leftIcon: AnyView, title: AnyView, isSelected: Bool) { + self.leftIcon = leftIcon + self.title = title + self.isSelected = isSelected + } +} + +public protocol OptionChipStyle { + associatedtype Body = View + + typealias Configuration = OptionChipConfiguration + + func makeBody(configuration: Self.Configuration) -> AnyView // Self.Body +} + +public struct DefaultOptionChipStyle: OptionChipStyle { + let font: Font + let foregroundColorSelected: Color + let foregroundColorUnselected: Color + let fillColorSelected: Color + let fillColorUnselected: Color + let strokeColorSelected: Color + let strokeColorUnselected: Color + let cornerRadius: CGFloat + let spacing: CGFloat + let padding: CGFloat + let borderWidth: CGFloat + let minHeight: CGFloat + + public init(font: Font = .system(.body), foregroundColorSelected: Color = .preferredColor(.tintColor), foregroundColorUnselected: Color = .preferredColor(.tertiaryLabel), fillColorSelected: Color = .preferredColor(.primaryFill), fillColorUnselected: Color = .preferredColor(.secondaryFill), strokeColorSelected: Color = .preferredColor(.tintColor), strokeColorUnselected: Color = .preferredColor(.separator), cornerRadius: CGFloat = 10, spacing: CGFloat = 6, padding: CGFloat = 8, borderWidth: CGFloat = 1, minHeight: CGFloat = 38) { + self.font = font + self.foregroundColorSelected = foregroundColorSelected + self.foregroundColorUnselected = foregroundColorUnselected + self.fillColorSelected = fillColorSelected + self.fillColorUnselected = fillColorUnselected + self.strokeColorSelected = strokeColorSelected + self.strokeColorUnselected = strokeColorUnselected + self.cornerRadius = cornerRadius + self.spacing = spacing + self.padding = padding + self.borderWidth = borderWidth + self.minHeight = minHeight + } + + public func makeBody(configuration: Configuration) -> AnyView { + AnyView( + HStack(spacing: self.spacing) { + configuration.leftIcon + configuration.title + } + .font(self.font) + .foregroundColor(configuration.isSelected ? self.foregroundColorSelected : self.foregroundColorUnselected) + .padding(self.padding) + .frame(maxWidth: .infinity) + .background( + ZStack { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(configuration.isSelected ? fillColorSelected : fillColorUnselected) + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(configuration.isSelected ? strokeColorSelected : strokeColorUnselected, lineWidth: borderWidth) + } + ) + ) + } +} + +struct OptionChipStyleKey: EnvironmentKey { + static var defaultValue: any OptionChipStyle = DefaultOptionChipStyle() +} + +extension EnvironmentValues { + var optionChipStyle: any OptionChipStyle { + get { + self[OptionChipStyleKey.self] + } + set { + self[OptionChipStyleKey.self] = newValue + } + } +} + +public extension View { + func optionChipStyle(_ style: S) -> some View where S: OptionChipStyle { + self.environment(\.optionChipStyle, style) + } + + func optionChipStyle(font: Font = .system(.body), foregroundColorSelected: Color = .preferredColor(.tintColor), foregroundColorUnselected: Color = .preferredColor(.tertiaryLabel), fillColorSelected: Color = .preferredColor(.primaryFill), fillColorUnselected: Color = .preferredColor(.secondaryFill), strokeColorSelected: Color = .preferredColor(.tintColor), strokeColorUnselected: Color = .preferredColor(.separator), cornerRadius: CGFloat = 10, spacing: CGFloat = 6, padding: CGFloat = 8, borderWidth: CGFloat = 1, minHeight: CGFloat = 38) -> some View { + self.environment(\.optionChipStyle, + DefaultOptionChipStyle(font: font, foregroundColorSelected: foregroundColorSelected, foregroundColorUnselected: foregroundColorUnselected, fillColorSelected: fillColorSelected, fillColorUnselected: fillColorUnselected, strokeColorSelected: strokeColorSelected, strokeColorUnselected: strokeColorUnselected, cornerRadius: cornerRadius, spacing: spacing, padding: padding, borderWidth: borderWidth, minHeight: minHeight)) + } +} + +#Preview { + VStack { + Spacer() + + OptionChip(leftIcon: Image(systemName: "airplane"), title: "Airplane", isSelected: true) + OptionChip(leftIcon: Image(systemName: "airplane"), title: "Airplane", isSelected: false) + OptionChip(title: "Ship", isSelected: true) + OptionChip(title: "Ship", isSelected: false) + OptionChip(leftIcon: Image(systemName: "bus"), title: "Bus", isSelected: true) + OptionChip(leftIcon: Image(systemName: "bus"), title: "Bus", isSelected: false) + + Spacer() + + OptionChip(leftIcon: Image(systemName: "airplane"), title: "Air Plane", isSelected: true) + .optionChipStyle(font: .largeTitle, foregroundColorSelected: .red, strokeColorSelected: .red, cornerRadius: 25) + OptionChip(leftIcon: Image(systemName: "airplane"), title: "Air Plane", isSelected: false) + .optionChipStyle(font: .footnote, foregroundColorUnselected: .green, strokeColorSelected: .black) + + .optionChipStyle(cornerRadius: 16) + OptionChip(title: "Ship", isSelected: true) + .optionChipStyle(fillColorSelected: .yellow) + OptionChip(title: "Ship", isSelected: false) + .optionChipStyle(fillColorUnselected: .gray) + OptionChip(leftIcon: Image(systemName: "bus"), title: "Blue Bus", isSelected: true) + .optionChipStyle(cornerRadius: 20) + OptionChip(leftIcon: Image(systemName: "bus"), title: "Gray Bus", isSelected: false) + .optionChipStyle(cornerRadius: 20) + + Spacer() + } +} diff --git a/Sources/FioriSwiftUICore/Views/OptionListPicker+View.swift b/Sources/FioriSwiftUICore/Views/OptionListPicker+View.swift new file mode 100644 index 000000000..f46c06e72 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/OptionListPicker+View.swift @@ -0,0 +1,61 @@ +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +import SwiftUI + +extension OptionListPicker: View { + public var body: some View { + Grid { + ForEach(0 ..< Int(ceil(Double(_valueOptions.count) / 2.0))) { rowIndex in + GridRow { + OptionChip( + leftIcon: _value.wrappedValue.contains(rowIndex * 2) ? Image(systemName: "checkmark") : nil, + title: _valueOptions[rowIndex * 2], + isSelected: _value.wrappedValue.contains(rowIndex * 2) + ) + .onTapGesture { + _onTap?(rowIndex * 2) + } + if rowIndex * 2 + 1 < _valueOptions.count { + OptionChip( + leftIcon: _value.wrappedValue.contains(rowIndex * 2 + 1) ? Image(systemName: "checkmark") : nil, + title: _valueOptions[rowIndex * 2 + 1], + isSelected: _value.wrappedValue.contains(rowIndex * 2 + 1) + ) + .onTapGesture { + _onTap?(rowIndex * 2 + 1) + } + } + } + } + } + } +} + +/* + // FIXME: - Implement OptionListPicker specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct OptionListPickerLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(OptionListPicker(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ + +#Preview { + VStack { + Spacer() + OptionListPicker(value: Binding<[Int]>(get: { [0, 1, 2] }, set: { print($0) }), valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review", "Accepted", "Rejected"], hint: nil) + .frame(width: 375) + Spacer() + OptionListPicker(value: Binding<[Int]>(get: { [0, 1, 2] }, set: { print($0) }), valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review", "Accepted", "Rejected"], hint: nil) + .optionChipStyle(font: .title, foregroundColorSelected: Color.red, strokeColorSelected: Color.red, cornerRadius: 25) + .frame(width: 375) + Spacer() + } +} diff --git a/Sources/FioriSwiftUICore/Views/SliderPicker+View.swift b/Sources/FioriSwiftUICore/Views/SliderPicker+View.swift new file mode 100644 index 000000000..f1578c7af --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SliderPicker+View.swift @@ -0,0 +1,191 @@ +import SwiftUI + +extension SliderPicker: View { + public var body: some View { + VStack { + if let formatter = self._formatter { + HStack { + Text(String(format: formatter, _value.wrappedValue ?? _minimumValue)) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + } + } + HStack { + Slider(value: .convert(from: _value, ifNilUse: _minimumValue), in: Float(_minimumValue) ... Float(_maximumValue), step: 1.0) + TextField("", value: Binding(get: { _value.wrappedValue ?? _minimumValue }, set: { _value.wrappedValue = $0 }), format: .number) + .frame(width: calcWidth(font: .body)) + .keyboardType(.numberPad) + .textFieldStyle(.roundedBorder) + } + if let hint = _hint { + HStack { + Text(hint) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + } + } + } + } +} + +private extension SliderPicker { + func calcWidth(font: Font) -> CGFloat { + var width: CGFloat = 0 + for i in 0 ... 9 { + let fontAttributes = [NSAttributedString.Key.font: self.preferredFont(from: font)] + let size = String(i).size(withAttributes: fontAttributes) + width = max(size.width, width) + } + return floor(log10(CGFloat(_maximumValue)) + 1) * width + 2 * 12 + } + + func preferredFont(from font: Font) -> UIFont { + let uiFont: UIFont + + switch font { + case .largeTitle: + uiFont = UIFont.preferredFont(forTextStyle: .largeTitle) + case .title: + uiFont = UIFont.preferredFont(forTextStyle: .title1) + case .title2: + uiFont = UIFont.preferredFont(forTextStyle: .title2) + case .title3: + uiFont = UIFont.preferredFont(forTextStyle: .title3) + case .headline: + uiFont = UIFont.preferredFont(forTextStyle: .headline) + case .subheadline: + uiFont = UIFont.preferredFont(forTextStyle: .subheadline) + case .callout: + uiFont = UIFont.preferredFont(forTextStyle: .callout) + case .caption: + uiFont = UIFont.preferredFont(forTextStyle: .caption1) + case .caption2: + uiFont = UIFont.preferredFont(forTextStyle: .caption2) + case .footnote: + uiFont = UIFont.preferredFont(forTextStyle: .footnote) + case .body: + fallthrough + default: + uiFont = UIFont.preferredFont(forTextStyle: .body) + } + + return uiFont + } +} + +struct RangeIntegerStyle: ParseableFormatStyle { + var parseStrategy: RangeIntegerStrategy = .init() + let range: ClosedRange + + func format(_ value: Int) -> String { + let constrainedValue = min(max(value, range.lowerBound), self.range.upperBound) + return "\(constrainedValue)" + } +} + +struct RangeIntegerStrategy: ParseStrategy { + func parse(_ value: String) throws -> Int { + Int(value) ?? 1 + } +} + +extension FormatStyle where Self == RangeIntegerStyle { + static func ranged(_ range: ClosedRange) -> RangeIntegerStyle { + RangeIntegerStyle(range: range) + } +} + +extension Binding { + static func convert(from intBinding: Binding, ifNilUse defaultValue: TInt) -> Binding where TInt: BinaryInteger, TFloat: BinaryFloatingPoint { + Binding( + get: { + TFloat(intBinding.wrappedValue ?? defaultValue) + }, + set: { + intBinding.wrappedValue = TInt($0) + } + ) + } +} + +/* + // FIXME: - Implement SliderPicker specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct SliderPickerLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SliderPicker(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ + +private struct SliderPickeTestView: View { + @State var value1: Int? = 10 + @State var value2: Int? = 20 + @State var value3: Int? = nil + + var body: some View { + VStack { + Spacer() + HStack { + Text("Value 1: \($value1.wrappedValue ?? 0)") + .font(.largeTitle) + .foregroundColor(value1 != nil ? .blue : .gray) + Spacer() + } + SliderPicker(value: Binding( + get: { + value1 + }, + set: { + value1 = $0 + } + ), minimumValue: 0, maximumValue: 1000, hint: nil) + Spacer() + HStack { + Text("Value 2: \(value2 ?? 0)") + .font(.largeTitle) + .foregroundColor(value2 != nil ? .blue : .gray) + + Spacer() + } + + SliderPicker(value: Binding( + get: { + value2 + }, + set: { + value2 = $0 + } + ), minimumValue: 0, maximumValue: 100, hint: "Pick an integer value") + Spacer() + HStack { + Text("Value 3: \(value3 ?? 0)") + .font(.largeTitle) + .foregroundColor(value3 != nil ? .blue : .gray) + + Spacer() + } + SliderPicker(value: Binding( + get: { + value3 + }, + set: { + value3 = $0 + } + ), minimumValue: 0, maximumValue: 100, hint: "Pick an integer value") + Spacer() + } + .frame(width: 375) + } +} + +struct SliderPickeTestView_Previews: PreviewProvider { + static var previews: some View { + SliderPickeTestView() + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilter+Environment.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilter+Environment.swift new file mode 100644 index 000000000..8312f55ba --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilter+Environment.swift @@ -0,0 +1,77 @@ +import SwiftUI + +struct SortFilterOnModelUpdateAppCallbackKey: EnvironmentKey { + static let defaultValue: () -> Void = { print("default empty callback") } +} + +//struct SortFilterMenuCancelActionKey: EnvironmentKey { +// static var defaultValue: any View = Button("TO BE REPLACED") {} +//} +// +//struct SortFilterMenuResetActionKey: EnvironmentKey { +// static var defaultValue: any View = Button("TO BE REPLACED") {} +//} +// +//struct SortFilterMenuApplyActionKey: EnvironmentKey { +// static var defaultValue: any View = Button("TO BE REPLACED") {} +//} +// +extension EnvironmentValues { + var onModelUpdateAppCallback: () -> Void { + get { + self[SortFilterOnModelUpdateAppCallbackKey.self] + } + + set { + self[SortFilterOnModelUpdateAppCallbackKey.self] = newValue + } + } +// +// var cancelActionView: any View { +// get { +// self[SortFilterMenuCancelActionKey.self] +// } +// +// set { +// self[SortFilterMenuCancelActionKey.self] = newValue +// } +// } +// +// var resetActionView: any View { +// get { +// self[SortFilterMenuResetActionKey.self] +// } +// +// set { +// self[SortFilterMenuResetActionKey.self] = newValue +// } +// } +// +// var applyActionView: any View { +// get { +// self[SortFilterMenuApplyActionKey.self] +// } +// +// set { +// self[SortFilterMenuApplyActionKey.self] = newValue +// } +// } +} +// +extension View { + func onModelUpdateAppCallback(_ closure: @escaping () -> Void) -> some View { + self.environment(\.onModelUpdateAppCallback, closure) + } +// +// func cancelActionView(_ view: any View) -> some View { +// self.environment(\.cancelActionView, view) +// } +// +// func resetActionView(_ view: any View) -> some View { +// self.environment(\.resetActionView, view) +// } +// +// func applyActionView(_ view: any View) -> some View { +// self.environment(\.applyActionView, view) +// } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterContext.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterContext.swift new file mode 100644 index 000000000..c87a7c066 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterContext.swift @@ -0,0 +1,14 @@ +import SwiftUI + +class SortFilterContext: ObservableObject { + @Published public var isResetButtonEnabled: Bool = false + @Published public var isApplyButtonEnabled: Bool = false + + @Published public var handleCancel: (() -> Void)? + @Published public var handleReset: (() -> Void)? + @Published public var handleApply: (() -> Void)? + + @Published public var handleDismiss: (() -> Void)? + + public init() {} +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterDialog+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterDialog+View.swift new file mode 100644 index 000000000..cc4939d47 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterDialog+View.swift @@ -0,0 +1,63 @@ +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* + import SwiftUI + + // FIXME: - Implement Fiori style definitions + + extension Fiori { + enum SortFilterDialog { + typealias Item = EmptyModifier + typealias ItemCumulative = EmptyModifier + typealias CancelAction = EmptyModifier + typealias CancelActionCumulative = EmptyModifier + typealias ResetAction = EmptyModifier + typealias ResetActionCumulative = EmptyModifier + typealias ApplyAction = EmptyModifier + typealias ApplyActionCumulative = EmptyModifier + + // TODO: - substitute type-specific ViewModifier for EmptyModifier + /* + // replace `typealias Subtitle = EmptyModifier` with: + + struct Subtitle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .foregroundColor(.preferredColor(.primary3)) + } + } + */ + static let item = Item() + static let cancelAction = CancelAction() + static let resetAction = ResetAction() + static let applyAction = ApplyAction() + static let itemCumulative = ItemCumulative() + static let cancelActionCumulative = CancelActionCumulative() + static let resetActionCumulative = ResetActionCumulative() + static let applyActionCumulative = ApplyActionCumulative() + } + } + + // FIXME: - Implement SortFilterDialog View body + + extension SortFilterDialog: View { + public var body: some View { + <# View body #> + } + } + + // FIXME: - Implement SortFilterDialog specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct SortFilterDialogLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterDialog(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterFullCFG+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterFullCFG+View.swift new file mode 100644 index 000000000..fb05dcfac --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterFullCFG+View.swift @@ -0,0 +1,102 @@ +import SwiftUI + +extension Fiori { + enum SortFilterFullCFG { +// typealias Title = EmptyModifier + struct Title: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + .multilineTextAlignment(.center) + } + } + typealias TitleCumulative = EmptyModifier + typealias Items = EmptyModifier + typealias ItemsCumulative = EmptyModifier + typealias CancelAction = EmptyModifier + typealias CancelActionCumulative = EmptyModifier + typealias ResetAction = EmptyModifier + typealias ResetActionCumulative = EmptyModifier + typealias ApplyAction = EmptyModifier + typealias ApplyActionCumulative = EmptyModifier + + + static let title = Title() + static let items = Items() + static let cancelAction = CancelAction() + static let resetAction = ResetAction() + static let applyAction = ApplyAction() + static let titleCumulative = TitleCumulative() + static let itemsCumulative = ItemsCumulative() + static let cancelActionCumulative = CancelActionCumulative() + static let resetActionCumulative = ResetActionCumulative() + static let applyActionCumulative = ApplyActionCumulative() + } +} + +extension SortFilterFullCFG: View { + public var body: some View { + CancellableResettableDialogForm { + title + } cancelAction: { + cancelAction + .simultaneousGesture( + TapGesture() + .onEnded { _ in + print("...Cancel...") + context.handleCancel?() + context.isApplyButtonEnabled = false + context.isResetButtonEnabled = false + dismiss() + } + ) + .buttonStyle(CancelResetButtonStyle()) + } resetAction: { + resetAction + .simultaneousGesture( + TapGesture() + .onEnded { _ in + print("...Reset...") + context.handleReset?() + context.isApplyButtonEnabled = false + context.isResetButtonEnabled = false + dismiss() + } + ) + .buttonStyle(CancelResetButtonStyle()) + } applyAction: { + applyAction + .simultaneousGesture( + TapGesture() + .onEnded { _ in + print("...Apply...") + context.handleApply?() + context.isApplyButtonEnabled = false + context.isResetButtonEnabled = false + _onUpdate?() + dismiss() + } + ) + + .buttonStyle(ApplyButtonStyle()) + } components: { + _items + .environmentObject(context) + } + } +} + +/* + // FIXME: - Implement SortFilterFullCFG specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct SortFilterFullCFGLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterFullCFG(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterItemTitle.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterItemTitle.swift new file mode 100644 index 000000000..965a2ca90 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterItemTitle.swift @@ -0,0 +1,29 @@ +// +// SortFilterItemTitle.swift +// +// +// Created by Xu, Charles on 10/24/23. +// + +import SwiftUI +import FioriThemeManager + +public struct SortFilterItemTitle: TitleComponent, View { + public let title: String + + public init(title: String) { + self.title = title + } + + public var body: some View { + Text(title) + .font(.body) + .fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + .multilineTextAlignment(.center) + } +} + +#Preview { + SortFilterItemTitle(title: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenu+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenu+View.swift new file mode 100644 index 000000000..fb4afcfc2 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenu+View.swift @@ -0,0 +1,30 @@ +import SwiftUI + +extension Fiori { + enum SortFilterMenu { + typealias Items = EmptyModifier + typealias ItemsCumulative = EmptyModifier + + static let items = Items() + static let itemsCumulative = ItemsCumulative() + } +} + +extension SortFilterMenu: View { + public var body: some View { + items + .onModelUpdateAppCallback(_onUpdate!) + } +} + +/* + // FIXME: - Implement SortFilterMenu specific LibraryContentProvider + @available(iOS 14.0, macOS 11.0, *) + struct SortFilterMenuLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterMenu(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+Style.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+Style.swift new file mode 100644 index 000000000..b843c7982 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+Style.swift @@ -0,0 +1,102 @@ +import FioriThemeManager +import SwiftUI + +public struct SortFilterMenuItemConfiguration { + let leftIcon: AnyView + let title: AnyView + let isSelected: Bool + let rightIcon: AnyView + + public init(leftIcon: AnyView, title: AnyView, isSelected: Bool, rightIcon: AnyView) { + self.leftIcon = leftIcon + self.title = title + self.isSelected = isSelected + self.rightIcon = rightIcon + } +} + +public protocol SortFilterMenuItemStyle { + associatedtype Body = View + + typealias Configuration = SortFilterMenuItemConfiguration + + func makeBody(configuration: Self.Configuration) -> AnyView +} + +public struct DefaultSortFilterMenuItemStyle: SortFilterMenuItemStyle { + let font: Font + let foregroundColorSelected: Color + let foregroundColorUnselected: Color + let fillColorSelected: Color + let fillColorUnselected: Color + let strokeColorSelected: Color + let strokeColorUnselected: Color + let cornerRadius: CGFloat + let spacing: CGFloat + let padding: CGFloat + let borderWidth: CGFloat + let minHeight: CGFloat + + public init(font: Font = .system(.body), foregroundColorSelected: Color = .preferredColor(.tintColor), foregroundColorUnselected: Color = .preferredColor(.tertiaryLabel), fillColorSelected: Color = .preferredColor(.primaryFill), fillColorUnselected: Color = .preferredColor(.secondaryFill), strokeColorSelected: Color = .preferredColor(.tintColor), strokeColorUnselected: Color = .preferredColor(.separator), cornerRadius: CGFloat = 10, spacing: CGFloat = 6, padding: CGFloat = 8, borderWidth: CGFloat = 1, minHeight: CGFloat = 38) { + self.font = font + self.foregroundColorSelected = foregroundColorSelected + self.foregroundColorUnselected = foregroundColorUnselected + self.fillColorSelected = fillColorSelected + self.fillColorUnselected = fillColorUnselected + self.strokeColorSelected = strokeColorSelected + self.strokeColorUnselected = strokeColorUnselected + self.cornerRadius = cornerRadius + self.spacing = spacing + self.padding = padding + self.borderWidth = borderWidth + self.minHeight = minHeight + } + + public func makeBody(configuration: Configuration) -> AnyView { + AnyView( + HStack(spacing: self.spacing) { + configuration.leftIcon + configuration.title + configuration.rightIcon + } + .font(self.font) + .foregroundColor(configuration.isSelected ? self.foregroundColorSelected : self.foregroundColorUnselected) + .padding(self.padding) + .frame(minHeight: self.minHeight) + .background( + ZStack { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(configuration.isSelected ? fillColorSelected : fillColorUnselected) + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(configuration.isSelected ? strokeColorSelected : strokeColorUnselected, lineWidth: borderWidth) + } + ) + ) + } +} + +struct SortFilterMenuItemStyleKey: EnvironmentKey { + static var defaultValue: any SortFilterMenuItemStyle = DefaultSortFilterMenuItemStyle() +} + +extension EnvironmentValues { + var sortFilterMenuItemStyle: any SortFilterMenuItemStyle { + get { + self[SortFilterMenuItemStyleKey.self] + } + set { + self[SortFilterMenuItemStyleKey.self] = newValue + } + } +} + +public extension View { + func sortFilterMenuItemStyle(_ style: S) -> some View where S: SortFilterMenuItemStyle { + self.environment(\.sortFilterMenuItemStyle, style) + } + + func sortFilterMenuItemStyle(font: Font = .system(.body), foregroundColorSelected: Color = .preferredColor(.tintColor), foregroundColorUnselected: Color = .preferredColor(.tertiaryLabel), fillColorSelected: Color = .preferredColor(.primaryFill), fillColorUnselected: Color = .preferredColor(.secondaryFill), strokeColorSelected: Color = .preferredColor(.tintColor), strokeColorUnselected: Color = .preferredColor(.separator), cornerRadius: CGFloat = 10, spacing: CGFloat = 6, padding: CGFloat = 8, borderWidth: CGFloat = 1, minHeight: CGFloat = 38) -> some View { + self.environment(\.sortFilterMenuItemStyle, + DefaultSortFilterMenuItemStyle(font: font, foregroundColorSelected: foregroundColorSelected, foregroundColorUnselected: foregroundColorUnselected, fillColorSelected: fillColorSelected, fillColorUnselected: fillColorUnselected, strokeColorSelected: strokeColorSelected, strokeColorUnselected: strokeColorUnselected, cornerRadius: cornerRadius, spacing: spacing, padding: padding, borderWidth: borderWidth, minHeight: minHeight)) + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+View.swift new file mode 100644 index 000000000..33d87d64d --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterMenuItem+View.swift @@ -0,0 +1,464 @@ +import SwiftUI + +extension Fiori { + enum SortFilterMenuItem { + typealias LeftIcon = EmptyModifier + typealias LeftIconCumulative = EmptyModifier + typealias Title = EmptyModifier + typealias TitleCumulative = EmptyModifier + typealias RightIcon = EmptyModifier + typealias RightIconCumulative = EmptyModifier + + static let leftIcon = LeftIcon() + static let title = Title() + static let rightIcon = RightIcon() + static let leftIconCumulative = LeftIconCumulative() + static let titleCumulative = TitleCumulative() + static let rightIconCumulative = RightIconCumulative() + } +} + +extension SortFilterMenuItem: View { + public var body: some View { + sortFilterMenuItemStyle.makeBody(configuration: SortFilterMenuItemConfiguration(leftIcon: AnyView(_leftIcon), title: AnyView(_title), isSelected: _isSelected, rightIcon: AnyView(_rightIcon))).typeErased + } +} + +/* + // FIXME: - Implement SortFilterMenuItem specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct SortFilterMenuItemLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterMenuItem(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ + +private extension View { + func icon(name: String?, isVisible: Bool) -> Image? { + if isVisible { + if let name = name { + return Image(systemName: name) + } + } + return nil + } +} + +struct FilterFeedbackMenuItem: View { + @Binding var item: PickerItem + var onUpdate: () -> Void + + public init(item: Binding, onUpdate: @escaping () -> Void) { + self._item = item + self.onUpdate = onUpdate + } + + var body: some View { + Group { + ForEach($item.valueOptions.wrappedValue, id: \.self) { opt in + SortFilterMenuItem(leftIcon: item.isOptionSelected(opt) ? icon(name: item.icon, isVisible: true) : nil, title: opt, isSelected: item.isOptionSelected(opt)) + .onTapGesture { + item.onTap(option: opt) + item.apply() + onUpdate() + } + } + } + } +} + +struct SliderMenuItem: View { + @Binding var item: SliderItem + + @State var isSheetVisible = false + + @State var detentHeight: CGFloat = 0 + + var onUpdate: () -> Void + + public init(item: Binding, onUpdate: @escaping () -> Void) { + self._item = item + self.onUpdate = onUpdate + } + + var body: some View { + SortFilterMenuItem(leftIcon: icon(name: item.icon, isVisible: true), title: item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: item.isChecked) + .onTapGesture { + isSheetVisible.toggle() + } + .popover(isPresented: $isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + CancellableResettableDialogForm { + SortFilterItemTitle(title: item.name) + } cancelAction: { + Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.cancel() + isSheetVisible.toggle() + }) + .buttonStyle(CancelResetButtonStyle()) + } resetAction: { + Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.reset() + }) + .buttonStyle(CancelResetButtonStyle()) + } applyAction: { + Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.apply() + onUpdate() + isSheetVisible.toggle() + }) + .buttonStyle(ApplyButtonStyle()) + + } components: { + SliderPicker(value: Binding(get: { item.workingValue }, set: { item.workingValue = $0 }), formatter: item.formatter, minimumValue: item.minimumValue, maximumValue: item.maximumValue) + } + .readHeight() + .onPreferenceChange(HeightPreferenceKey.self) { height in + if let height { + self.detentHeight = height + } + } + .presentationDetents([.height(self.detentHeight)]) + } + } +} + +struct PickerMenuItem: View { + @Binding var item: PickerItem + var onUpdate: () -> Void + + @State var isSheetVisible = false + + @State var detentHeight: CGFloat = 0 + + public init(item: Binding, onUpdate: @escaping () -> Void) { + self._item = item + self.onUpdate = onUpdate + } + + var body: some View { + if item.valueOptions.count > 4 { + button + } else { + menu + } + } + + @ViewBuilder + var button: some View { + SortFilterMenuItem(leftIcon: icon(name: item.icon, isVisible: true), title: item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: item.isChecked) + .onTapGesture { + isSheetVisible.toggle() + } + .popover(isPresented: $isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + CancellableResettableDialogForm { + SortFilterItemTitle(title: item.name) + } cancelAction: { + Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.cancel() + isSheetVisible.toggle() + }) + .buttonStyle(CancelResetButtonStyle()) + } resetAction: { + Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.reset() + }) + .buttonStyle(CancelResetButtonStyle()) + } applyAction: { + Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.apply() + onUpdate() + isSheetVisible.toggle() + }) + .buttonStyle(ApplyButtonStyle()) + } components: { + OptionListPicker(value: $item.workingValue, valueOptions: item.valueOptions, hint: nil) { index in + item.onTap(option: item.valueOptions[index]) + } + } + .readHeight() + .onPreferenceChange(HeightPreferenceKey.self) { height in + if let height { + self.detentHeight = height + } + } + .presentationDetents([.height(self.detentHeight)]) + } + } + + @ViewBuilder + var menu: some View { + HStack { + Menu { + ForEach(item.valueOptions.indices) { idx in + if item.isOptionSelected(index: idx) { + Button { + item.onTap(option: item.valueOptions[idx]) + item.apply() + onUpdate() + } label: { + Label { Text(item.valueOptions[idx]) } icon: { Image(fioriName: "fiori.accept") } + } + } else { + Button(item.valueOptions[idx]) { + item.onTap(option: item.valueOptions[idx]) + item.apply() + onUpdate() + } + } + } + } label: { + SortFilterMenuItem(leftIcon: icon(name: item.icon, isVisible: true), title: item.label, isSelected: item.isChecked) + } + } + } +} + +struct HeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat? + + static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { + guard let nextValue = nextValue() else { return } + value = nextValue + } +} + +private extension View { + func readHeight() -> some View { + self.modifier(ReadHeightModifier()) + } +} + +private struct ReadHeightModifier: ViewModifier { + private var sizeView: some View { + GeometryReader { geometry in + Color.clear.preference(key: HeightPreferenceKey.self, value: geometry.size.height) + } + } + + func body(content: Content) -> some View { + content.background(self.sizeView) + } +} + +struct DateTimeMenuItem: View { + @Binding private var item: DateTimeItem + + @State private var isSheetVisible: Bool = false + + @State var detentHeight: CGFloat = 0 + + var onUpdate: () -> Void + + public init(item: Binding, onUpdate: @escaping () -> Void) { + self._item = item + self.onUpdate = onUpdate + } + + var body: some View { + SortFilterMenuItem(leftIcon: icon(name: item.icon, isVisible: true), title: item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: item.isChecked) + .onTapGesture { + isSheetVisible.toggle() + } + .popover(isPresented: $isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + CancellableResettableDialogForm { + SortFilterItemTitle(title: item.name) + } cancelAction: { + Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.cancel() + isSheetVisible.toggle() + }) + .buttonStyle(CancelResetButtonStyle()) + } resetAction: { + Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.reset() + }) + .buttonStyle(CancelResetButtonStyle()) + } applyAction: { + Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + item.apply() + onUpdate() + isSheetVisible.toggle() + }) + .buttonStyle(ApplyButtonStyle()) + } components: { + VStack { + HStack { + Text(NSLocalizedString("Time", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + DatePicker( + "", + selection: Binding(get: { item.workingValue ?? Date() }, set: { item.workingValue = $0 }), + displayedComponents: [.hourAndMinute] + ) + .labelsHidden() + } + + DatePicker( + item.label, + selection: Binding(get: { item.workingValue ?? Date() }, set: { item.workingValue = $0 }), + displayedComponents: [.date] + ) + .datePickerStyle(.graphical) + } + } + .readHeight() + .onPreferenceChange(HeightPreferenceKey.self) { height in + if let height { + self.detentHeight = height + } + } + .presentationDetents([.height(self.detentHeight)]) + } + } +} + +struct SwitchMenuItem: View { + @Binding private var item: SwitchItem + +// @State var detentHeight: CGFloat = 0 + +// @State private var isSheetVisible: Bool = false + var onUpdate: () -> Void + + public init(item: Binding, onUpdate: @escaping () -> Void) { + self._item = item + self.onUpdate = onUpdate + } + + var body: some View { + SortFilterMenuItem(leftIcon: icon(name: item.icon, isVisible: true), title: item.name, isSelected: item.isChecked) + .onTapGesture { + if item.value != nil { + item.workingValue?.toggle() + item.apply() + onUpdate() + } else { + item.workingValue = true + item.apply() + onUpdate() + } +// isSheetVisible.toggle() + } +// .popover(isPresented: $isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { +// CancellableResettableDialogForm { +// Text(item.name) +// } cancelAction: { +// Action(actionText: "Cancel", didSelectAction: { +// item.cancel() +// isSheetVisible.toggle() +// }) +// .buttonStyle(CancelResetButtonStyle()) +// } resetAction: { +// Action(actionText: "Reset", didSelectAction: { +// item.reset() +// }) +// .buttonStyle(CancelResetButtonStyle()) +// } applyAction: { +// Action(actionText: "Apply", didSelectAction: { +// item.apply() +// onUpdate() +// isSheetVisible.toggle() +// }) +// .buttonStyle(ApplyButtonStyle()) +// } components: { +// SwitchPicker(value: $item.workingValue) +// } +// } + } +} + +struct FullCFGMenuItem: View { + @Environment(\.sortFilterMenuItemFullConfigurationButton) var fullCFGButton + + @Binding var items: [[SortFilterItem]] + + @State var isSheetVisible = false + + var onUpdate: () -> Void + + public init(items: Binding<[[SortFilterItem]]>, onUpdate: @escaping () -> Void) { + self._items = items + self.onUpdate = onUpdate + } + + var body: some View { + SortFilterMenuItem(leftIcon: icon(name: fullCFGButton.icon, isVisible: true), title: fullCFGButton.name ?? "", isSelected: true) + .onTapGesture { + isSheetVisible.toggle() + } + .popover(isPresented: $isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + SortFilterFullCFG( + title: { + if let title = fullCFGButton.name { + Text(title) + } else { + EmptyView() + } + }, + items: { + _SortFilterCFGItemContainer(items: $items) + }, + cancelAction: { + Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + // item.apply() + onUpdate() + isSheetVisible.toggle() + }) + .buttonStyle(CancelResetButtonStyle()) + }, + resetAction: { + Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + // item.cancel() + isSheetVisible.toggle() + }) + .buttonStyle(CancelResetButtonStyle()) + }, + applyAction: { + Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + // item.reset() + }) + .buttonStyle(ApplyButtonStyle()) + }, + onUpdate: {} + ) + } + } +} + +#Preview { + VStack { + Spacer() + + SortFilterMenuItem(leftIcon: Image(systemName: "airplane"), title: "Airplane", rightIcon: Image(systemName: "chevron.down"), isSelected: true) + SortFilterMenuItem(leftIcon: Image(systemName: "airplane"), title: "Airplane", rightIcon: Image(systemName: "chevron.down"), isSelected: false) + SortFilterMenuItem(title: "Ship", rightIcon: Image(systemName: "chevron.down"), isSelected: true) + SortFilterMenuItem(title: "Ship", rightIcon: Image(systemName: "chevron.down"), isSelected: false) + SortFilterMenuItem(leftIcon: Image(systemName: "bus"), title: "Bus", isSelected: true) + SortFilterMenuItem(leftIcon: Image(systemName: "bus"), title: "Bus", isSelected: false) + + Spacer() + + SortFilterMenuItem(leftIcon: Image(systemName: "airplane"), title: "Air Plane", rightIcon: Image(systemName: "chevron.down"), isSelected: true) + .sortFilterMenuItemStyle(font: .largeTitle, foregroundColorSelected: .red, strokeColorSelected: .red, cornerRadius: 25) + SortFilterMenuItem(leftIcon: Image(systemName: "airplane"), title: "Air Plane", rightIcon: Image(systemName: "chevron.down"), isSelected: false) + .sortFilterMenuItemStyle(font: .footnote, foregroundColorUnselected: .green, strokeColorSelected: .black) + + .sortFilterMenuItemStyle(cornerRadius: 16) + SortFilterMenuItem(title: "Ship", rightIcon: Image(systemName: "chevron.down"), isSelected: true) + .sortFilterMenuItemStyle(fillColorSelected: .yellow) + SortFilterMenuItem(title: "Ship", rightIcon: Image(systemName: "chevron.down"), isSelected: false) + .sortFilterMenuItemStyle(fillColorUnselected: .gray) + SortFilterMenuItem(leftIcon: Image(systemName: "bus"), title: "Blue Bus", isSelected: true) + .sortFilterMenuItemStyle(cornerRadius: 20) + SortFilterMenuItem(leftIcon: Image(systemName: "bus"), title: "Gray Bus", isSelected: false) + .sortFilterMenuItemStyle(cornerRadius: 20) + + Spacer() + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterStyle.swift b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterStyle.swift new file mode 100644 index 000000000..e4ca740a5 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/SortFilterStyle.swift @@ -0,0 +1,26 @@ +import FioriThemeManager +import SwiftUI + +public final class SortFilterStyle { + public static var instance = SortFilterStyle(iconForCheckedItem: Image(fioriName: "fiori.accept")) + + let iconForCheckedItem: Image? + let iconForMenuItem: Image + let foregroundColorForSelectedItem: Color + let backgroundColorForSelectedColor: Color + let foregroundColorForUnselectedItem: Color + let backgroundColorForUnselectedColor: Color + + public static var shared: SortFilterStyle { + instance + } + + public init(iconForCheckedItem: Image? = nil, iconForMenuItem: Image? = nil, foregroundColorForSelectedItem: Color? = nil, backgroundColorForSelectedColor: Color? = nil, foregroundColorForUnselectedItem: Color? = nil, backgroundColorForUnselectedColor: Color? = nil) { + self.iconForCheckedItem = iconForCheckedItem + self.iconForMenuItem = iconForMenuItem ?? Image(fioriName: "fiori.navigation.down.arrow")! + self.foregroundColorForSelectedItem = foregroundColorForSelectedItem ?? .preferredColor(.tintColor) + self.backgroundColorForSelectedColor = backgroundColorForSelectedColor ?? .preferredColor(.primaryBackground) + self.foregroundColorForUnselectedItem = foregroundColorForUnselectedItem ?? .preferredColor(.separator) + self.backgroundColorForUnselectedColor = foregroundColorForUnselectedItem ?? .preferredColor(.tertiaryFill) + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift new file mode 100644 index 000000000..2fe7b2218 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift @@ -0,0 +1,173 @@ +// +import FioriThemeManager +// _SortFilterMenuItemContainer.swift +// +// +// Created by Xu, Charles on 9/25/23. +// +import SwiftUI + +public struct _SortFilterCFGItemContainer { + @EnvironmentObject var context: SortFilterContext + + @Binding var _items: [[SortFilterItem]] + + public init(items: Binding<[[SortFilterItem]]>) { + self.__items = items + } +} + +extension _SortFilterCFGItemContainer: View { + public var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 20) { + ForEach(0 ..< _items.count) { r in + ForEach(0 ..< _items[r].count) { c in + switch _items[r][c] { + case .picker: + VStack { + HStack { + Text(_items[r][c].picker.name) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + } + OptionListPicker( + value: Binding<[Int]>(get: { _items[r][c].picker.workingValue }, set: { _items[r][c].picker.workingValue = $0 }), + valueOptions: _items[r][c].picker.valueOptions, + onTap: { index in + _items[r][c].picker.onTap(option: _items[r][c].picker.valueOptions[index]) + } + ) + } + .padding() + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.preferredColor(.separator), lineWidth: 2) + ) + case .filterfeedback: + VStack { + HStack { + Text(_items[r][c].filterfeedback.name) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + } + OptionListPicker( + value: Binding<[Int]>(get: { _items[r][c].filterfeedback.workingValue }, set: { _items[r][c].filterfeedback.workingValue = $0 }), + valueOptions: _items[r][c].filterfeedback.valueOptions, + onTap: { index in + _items[r][c].filterfeedback.onTap(option: _items[r][c].filterfeedback.valueOptions[index]) + } + ) + } + .padding() + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.preferredColor(.separator), lineWidth: 2) + ) + + case .switch: + VStack { +// Text(_items[r][c].switch.name) + SwitchPicker(value: Binding(get: { _items[r][c].switch.workingValue }, set: { _items[r][c].switch.workingValue = $0 }), name: _items[r][c].switch.name, hint: nil) + } + .padding() + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.preferredColor(.separator), lineWidth: 2) + ) + case .slider: + VStack { + HStack { + Text(_items[r][c].slider.name) + .font(.headline) + Spacer() + } + SliderPicker( + value: Binding(get: { _items[r][c].slider.workingValue }, set: { _items[r][c].slider.workingValue = $0 }), + formatter: _items[r][c].slider.formatter, + minimumValue: _items[r][c].slider.minimumValue, + maximumValue: _items[r][c].slider.maximumValue + ) + } + .padding() + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.preferredColor(.separator), lineWidth: 2) + ) + case .datetime: + VStack { + HStack { + Text(_items[r][c].datetime.name) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + } + + DatePicker( + NSLocalizedString("Time", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), + selection: Binding(get: { _items[r][c].datetime.workingValue ?? Date() }, set: { _items[r][c].datetime.workingValue = $0 }), + displayedComponents: [.hourAndMinute] + ) + + DatePicker( + _items[r][c].datetime.label, + selection: Binding(get: { _items[r][c].datetime.workingValue ?? Date() }, set: { _items[r][c].datetime.workingValue = $0 }), + displayedComponents: [.date] + ) + .datePickerStyle(.graphical) + } + .padding() + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.preferredColor(.separator), lineWidth: 2) + ) + } + } + } + } + } + .onChange(of: _items) { _ in + for item in _items.joined() { + if item.isChanged { + context.isResetButtonEnabled = true + context.isApplyButtonEnabled = true + return + } + } + context.isResetButtonEnabled = true + } + .onAppear { + context.handleCancel = { + print("....cancel in context...") + for r in 0 ..< _items.count { + for c in 0 ..< _items[r].count { + _items[r][c].cancel() + } + } + } + + context.handleReset = { + print("....reset in context...") + for r in 0 ..< _items.count { + for c in 0 ..< _items[r].count { + _items[r][c].reset() + } + } + } + + context.handleApply = { + print("....apply in context...") + for r in 0 ..< _items.count { + for c in 0 ..< _items[r].count { + _items[r][c].apply() + } + } + } + + context.isResetButtonEnabled = false + context.isApplyButtonEnabled = false + } + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift new file mode 100644 index 000000000..93941c514 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift @@ -0,0 +1,136 @@ +// +// _SortFilterMenuItemContainer.swift +// +// +// Created by Xu, Charles on 9/25/23. +// +import SwiftUI + +public struct _SortFilterMenuItemContainer { + @Environment(\.onModelUpdateAppCallback) var onUpdate: () -> Void +// @Environment(\.cancelActionView) var _cancelAction + @Environment(\.sortFilterMenuItemFullConfigurationButton) var fullCFGButton + @Binding var _items: [[SortFilterItem]] + + public init(items: Binding<[[SortFilterItem]]>) { + self.__items = items + } +} + +extension _SortFilterMenuItemContainer: View { + public var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + if fullCFGButton.positon == .leading { + FullCFGMenuItem(items: $_items, onUpdate: onUpdate) + } + ForEach(0 ..< _items.count) { r in + ForEach(0 ..< _items[r].count) { c in + if _items[r][c].isShownOnMenu { + switch _items[r][c] { + case .picker: + PickerMenuItem(item: Binding(get: { _items[r][c].picker }, set: { _items[r][c].picker = $0 }), onUpdate: onUpdate) + case .filterfeedback: + FilterFeedbackMenuItem(item: Binding(get: { _items[r][c].filterfeedback }, set: { _items[r][c].filterfeedback = $0 }), onUpdate: onUpdate) + case .switch: + SwitchMenuItem(item: Binding(get: { _items[r][c].switch }, set: { _items[r][c].switch = $0 }), onUpdate: onUpdate) + case .slider: + SliderMenuItem(item: Binding(get: { _items[r][c].slider }, set: { _items[r][c].slider = $0 }), onUpdate: onUpdate) + case .datetime: + DateTimeMenuItem(item: Binding(get: { _items[r][c].datetime }, set: { _items[r][c].datetime = $0 }), onUpdate: onUpdate) + } + } + } + } + if fullCFGButton.positon == .trailing { + FullCFGMenuItem(items: $_items, onUpdate: onUpdate) + } + } + } + .frame(minHeight: 44) + .padding(.leading, 5) + } +} + +public struct SortFilterMenuItemFullConfigurationButtonKey: EnvironmentKey { + public static var defaultValue: SortFilterMenuItemFullConfigurationButton = .none +} + +public struct SortFilterMenuItemFullConfigurationButton { + public let name: String? + public let icon: String? + public let positon: Position + + public enum Position { + case leading, trailing, none + } + + private init(name: String? = nil, icon: String? = nil, positon: Position) { + self.name = name + self.icon = icon + self.positon = positon + } + + public static func leading(name: String) -> SortFilterMenuItemFullConfigurationButton { + SortFilterMenuItemFullConfigurationButton(name: name, positon: .leading) + } + + public static func leading(icon: String) -> SortFilterMenuItemFullConfigurationButton { + SortFilterMenuItemFullConfigurationButton(icon: icon, positon: .leading) + } + + public static func leading(name: String, icon: String) -> SortFilterMenuItemFullConfigurationButton { + SortFilterMenuItemFullConfigurationButton(name: name, icon: icon, positon: .leading) + } + + public static func trailing(name: String) -> SortFilterMenuItemFullConfigurationButton { + SortFilterMenuItemFullConfigurationButton(name: name, positon: .trailing) + } + + public static func trailing(icon: String) -> SortFilterMenuItemFullConfigurationButton { + SortFilterMenuItemFullConfigurationButton(icon: icon, positon: .trailing) + } + + public static func trailing(name: String, icon: String) -> SortFilterMenuItemFullConfigurationButton { + SortFilterMenuItemFullConfigurationButton(name: name, icon: icon, positon: .trailing) + } + + static var none = SortFilterMenuItemFullConfigurationButton(positon: Position.none) +} + +public extension EnvironmentValues { + var sortFilterMenuItemFullConfigurationButton: SortFilterMenuItemFullConfigurationButton { + get { + self[SortFilterMenuItemFullConfigurationButtonKey.self] + } + set { + self[SortFilterMenuItemFullConfigurationButtonKey.self] = newValue + } + } +} + +public extension View { + func leadingFullConfigurationMenuItem(name: String) -> some View { + self.environment(\.sortFilterMenuItemFullConfigurationButton, .leading(name: name)) + } + + func leadingFullConfigurationMenuItem(icon: String) -> some View { + self.environment(\.sortFilterMenuItemFullConfigurationButton, .leading(icon: icon)) + } + + func leadingFullConfigurationMenuItem(name: String, icon: String) -> some View { + self.environment(\.sortFilterMenuItemFullConfigurationButton, .leading(name: name, icon: icon)) + } + + func trailingFullConfigurationMenuItem(name: String) -> some View { + self.environment(\.sortFilterMenuItemFullConfigurationButton, .trailing(name: name)) + } + + func trailingFullConfigurationMenuItem(icon: String) -> some View { + self.environment(\.sortFilterMenuItemFullConfigurationButton, .trailing(icon: icon)) + } + + func trailingFullConfigurationMenuItem(name: String, icon: String) -> some View { + self.environment(\.sortFilterMenuItemFullConfigurationButton, .trailing(name: name, icon: icon)) + } +} diff --git a/Sources/FioriSwiftUICore/Views/SwitchPicker+View.swift b/Sources/FioriSwiftUICore/Views/SwitchPicker+View.swift new file mode 100644 index 000000000..851a694f5 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/SwitchPicker+View.swift @@ -0,0 +1,158 @@ +import FioriThemeManager +import SwiftUI + +extension SwitchPicker: View { + public var body: some View { + AnyView( + Toggle(_name ?? "", isOn: .convert(from: _value, ifNilUse: false)) + .toggleStyle(fioriToggleStyle) + ).typeErased + } +} + +private extension Binding { + static func convert(from value: Binding, ifNilUse defaultValue: Bool) -> Binding { + Binding( + get: { + value.wrappedValue ?? defaultValue + }, + set: { + value.wrappedValue = $0 + } + ) + } +} + +public struct FioriToggleStyle: ToggleStyle { + @ScaledMetric var scale: CGFloat = 1 + + let labelColor: Color + + let onColor: Color + let offColor: Color + + let onThumbColor: Color + let offThumbColor: Color + + let onBorderColor: Color + let offBorderColor: Color + + public init( + labelColor: Color = Color.preferredColor(.primaryLabel), + onColor: Color = Color.preferredColor(.tintColor), + offColor: Color = Color.preferredColor(.secondaryFill), + onThumbColor: Color = Color.preferredColor(.primaryBackground), + offThumbColor: Color = Color.preferredColor(.primaryBackground), + onBorderColor: Color = Color.preferredColor(.separator), + offBorderColor: Color = Color.preferredColor(.separator) + ) { + self.labelColor = labelColor + + self.onColor = onColor + self.offColor = offColor + + self.onThumbColor = onThumbColor + self.offThumbColor = offThumbColor + + self.onBorderColor = onBorderColor + self.offBorderColor = offBorderColor + } + + public func makeBody(configuration: Self.Configuration) -> some View { + HStack { + configuration.label + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(labelColor) + + Spacer() + ZStack { + RoundedRectangle(cornerRadius: 16 * scale, style: .circular) + .stroke(configuration.isOn ? onBorderColor : offBorderColor, lineWidth: 0.5 * scale) + .frame(width: 51 * scale, height: 30 * scale) + + RoundedRectangle(cornerRadius: 16 * scale, style: .circular) + .fill(configuration.isOn ? onColor : offColor) + .frame(width: 51 * scale, height: 30 * scale) + .overlay( + Circle() + .fill(configuration.isOn ? onThumbColor : offThumbColor) + .shadow(radius: 1 * scale, x: 0, y: 1 * scale) + .padding(1.5 * scale) + .offset(x: configuration.isOn ? 10 * scale : -10 * scale)) + .animation(Animation.easeInOut(duration: 0.2), value: configuration.isOn) + .frame(minHeight: 44) + .onTapGesture { configuration.isOn.toggle() } + } + } + .padding(.horizontal) + } +} + +// public struct DefaultToggleStyle: ToggleStyle { +// public func makeBody(configuration: Configuration) -> some View { +// VStack { +// Toggle(configuration) +// .labelsHidden() +// .foregroundColor(Color.tintColor) +// configuration.label +// .font(.system(size: 22, weight: .semibold)).lineLimit(2) +// .padding() +// .overlay( +// RoundedRectangle(cornerRadius: 10) +// .stroke(configuration.isOn ? Color.green: Color.gray, lineWidth: 1) +// ) +// } +// } +// } + +public struct FioriToggleStyleKey: EnvironmentKey { + public static var defaultValue: any ToggleStyle = FioriToggleStyle() +} + +public extension EnvironmentValues { + var fioriToggleStyle: any ToggleStyle { + get { + self[FioriToggleStyleKey.self] + } + set { + self[FioriToggleStyleKey.self] = newValue + } + } +} + +//public extension View { +// func fioriToggleStyle(_ style: FioriToggleStyle) -> some View { +// self.toggleStyle(style) +// } +//} +/* + // FIXME: - Implement SwitchPicker specific LibraryContentProvider + + @available(iOS 14.0, macOS 11.0, *) + struct SwitchPickerLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SwitchPicker(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } + } + */ + +private struct TestSwitchPicker: View { + @State var v1: Bool? = true + @State var v2: Bool? = false + @State var v3: Bool? = nil + + var body: some View { + VStack { + SwitchPicker(value: $v1, hint: nil) + SwitchPicker(value: $v2, hint: nil) + SwitchPicker(value: $v3, hint: nil) + } + } +} + +#Preview { + TestSwitchPicker() + .frame(width: 375) +} diff --git a/Sources/FioriSwiftUICore/_generated/Components/Component+Protocols.generated.swift b/Sources/FioriSwiftUICore/_generated/Components/Component+Protocols.generated.swift index 9ffbfc4de..ba02aa975 100644 --- a/Sources/FioriSwiftUICore/_generated/Components/Component+Protocols.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/Components/Component+Protocols.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.1.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import SwiftUI @@ -158,6 +158,14 @@ public protocol FootnoteIconsComponent { var footnoteIcons: [TextOrIcon]? { get } } +public protocol LeftIconComponent { + var leftIcon: Image? { get } +} + +public protocol RightIconComponent { + var rightIcon: Image? { get } +} + public protocol ActionComponent { var actionText: String? { get } @@ -194,10 +202,51 @@ public protocol KpiProgressComponent : KpiComponent { var fraction: Double? { get } } +public protocol OptionListPickerComponent : AnyObject { + // sourcery: bindingProperty + // sourcery: no_view + var value: [Int] { get set } + // sourcery: no_view + var valueOptions: [String] { get } + // sourcery: default.value=nil + // sourcery: no_view + var hint: String? { get } +} + public protocol ProgressIndicatorComponent { var progressIndicatorText: String? { get } } +public protocol SliderPickerComponent : AnyObject { + // sourcery: bindingProperty + // sourcery: no_view + var value: Int? { get set } + // sourcery: default.value=nil + // sourcery: no_view + var formatter: String? { get } + // sourcery: default.value=0 + // sourcery: no_view + var minimumValue: Int { get } + // sourcery: default.value=100 + // sourcery: no_view + var maximumValue: Int { get } + // sourcery: default.value=nil + // sourcery: no_view + var hint: String? { get } +} + +public protocol SwitchPickerComponent : AnyObject { + // sourcery: bindingProperty + // sourcery: no_view + var value: Bool? { get set } + // sourcery: default.value=nil + // sourcery: no_view + var name: String? { get } + // sourcery: default.value=nil + // sourcery: no_view + var hint: String? { get } +} + public protocol TextInputComponent : AnyObject { // sourcery: bindingPropertyOptional=.constant("") var textInputValue: String { get set } diff --git a/Sources/FioriSwiftUICore/_generated/Components/ComponentProtocols+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/Components/ComponentProtocols+Extension.generated.swift index 97eae186c..013e39d75 100644 --- a/Sources/FioriSwiftUICore/_generated/Components/ComponentProtocols+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/Components/ComponentProtocols+Extension.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.1.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import SwiftUI @@ -142,12 +142,24 @@ public extension KpiProgressComponent { } } +public extension LeftIconComponent { + var leftIcon: Image? { + return nil + } +} + public extension LowerBoundTitleComponent { var lowerBoundTitle: String? { return nil } } +public extension OptionListPickerComponent { + var hint: String? { + return nil + } +} + public extension PlaceholderComponent { var placeholder: String? { return nil @@ -160,6 +172,12 @@ public extension ProgressIndicatorComponent { } } +public extension RightIconComponent { + var rightIcon: Image? { + return nil + } +} + public extension SecondActionTitleComponent { var secondActionTitle: String? { return nil @@ -184,6 +202,28 @@ public extension SecondaryValuesAxisTitleComponent { } } +public extension SliderPickerComponent { + var formatter: String? { + return nil + } + + var minimumValue: Int { + return 0 + } + + var maximumValue: Int { + return 100 + } + + var hint: String? { + return nil + } + + var value: Int? { + return nil + } +} + public extension StatusComponent { var status: TextOrIcon? { return nil @@ -202,6 +242,20 @@ public extension SubtitleComponent { } } +public extension SwitchPickerComponent { + var name: String? { + return nil + } + + var hint: String? { + return nil + } + + var value: Bool? { + return nil + } +} + public extension TagsComponent { var tags: [String]? { return nil diff --git a/Sources/FioriSwiftUICore/_generated/Components/EnvironmentKey+Styles.generated.swift b/Sources/FioriSwiftUICore/_generated/Components/EnvironmentKey+Styles.generated.swift index 43459f573..98937a7c0 100644 --- a/Sources/FioriSwiftUICore/_generated/Components/EnvironmentKey+Styles.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/Components/EnvironmentKey+Styles.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.1.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import SwiftUI @@ -146,19 +146,23 @@ struct FootnoteIconsModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } -struct ActionTextModifierKey: EnvironmentKey { +struct LeftIconModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } -struct ActionItemsModifierKey: EnvironmentKey { +struct RightIconModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } -struct ProgressIndicatorTextModifierKey: EnvironmentKey { +struct ActionTextModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } -struct NodeModifierKey: EnvironmentKey { +struct ActionItemsModifierKey: EnvironmentKey { + public static let defaultValue = AnyViewModifier { $0 } +} + +struct ProgressIndicatorTextModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } @@ -206,6 +210,22 @@ struct SaveActionModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } +struct NodeModifierKey: EnvironmentKey { + public static let defaultValue = AnyViewModifier { $0 } +} + +struct ItemsModifierKey: EnvironmentKey { + public static let defaultValue = AnyViewModifier { $0 } +} + +struct ResetActionModifierKey: EnvironmentKey { + public static let defaultValue = AnyViewModifier { $0 } +} + +struct ApplyActionModifierKey: EnvironmentKey { + public static let defaultValue = AnyViewModifier { $0 } +} + struct NextActionModifierKey: EnvironmentKey { public static let defaultValue = AnyViewModifier { $0 } } diff --git a/Sources/FioriSwiftUICore/_generated/Components/EnvironmentValue+Styles.generated.swift b/Sources/FioriSwiftUICore/_generated/Components/EnvironmentValue+Styles.generated.swift index 016d1dda8..98e973869 100644 --- a/Sources/FioriSwiftUICore/_generated/Components/EnvironmentValue+Styles.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/Components/EnvironmentValue+Styles.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.1.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import SwiftUI @@ -184,6 +184,16 @@ extension EnvironmentValues { set { self[FootnoteIconsModifierKey.self] = newValue } } + public var leftIconModifier: AnyViewModifier { + get { return self[LeftIconModifierKey.self] } + set { self[LeftIconModifierKey.self] = newValue } + } + + public var rightIconModifier: AnyViewModifier { + get { return self[RightIconModifierKey.self] } + set { self[RightIconModifierKey.self] = newValue } + } + public var actionTextModifier: AnyViewModifier { get { return self[ActionTextModifierKey.self] } set { self[ActionTextModifierKey.self] = newValue } @@ -199,11 +209,6 @@ extension EnvironmentValues { set { self[ProgressIndicatorTextModifierKey.self] = newValue } } - public var nodeModifier: AnyViewModifier { - get { return self[NodeModifierKey.self] } - set { self[NodeModifierKey.self] = newValue } - } - public var textInputValueModifier: AnyViewModifier { get { return self[TextInputValueModifierKey.self] } set { self[TextInputValueModifierKey.self] = newValue } @@ -259,6 +264,26 @@ extension EnvironmentValues { set { self[SaveActionModifierKey.self] = newValue } } + public var nodeModifier: AnyViewModifier { + get { return self[NodeModifierKey.self] } + set { self[NodeModifierKey.self] = newValue } + } + + public var itemsModifier: AnyViewModifier { + get { return self[ItemsModifierKey.self] } + set { self[ItemsModifierKey.self] = newValue } + } + + public var resetActionModifier: AnyViewModifier { + get { return self[ResetActionModifierKey.self] } + set { self[ResetActionModifierKey.self] = newValue } + } + + public var applyActionModifier: AnyViewModifier { + get { return self[ApplyActionModifierKey.self] } + set { self[ApplyActionModifierKey.self] = newValue } + } + public var nextActionModifier: AnyViewModifier { get { return self[NextActionModifierKey.self] } set { self[NextActionModifierKey.self] = newValue } @@ -463,6 +488,16 @@ public extension View { self.environment(\.footnoteIconsModifier, AnyViewModifier(transform)) } + @ViewBuilder + func leftIconModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { + self.environment(\.leftIconModifier, AnyViewModifier(transform)) + } + + @ViewBuilder + func rightIconModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { + self.environment(\.rightIconModifier, AnyViewModifier(transform)) + } + @ViewBuilder func actionTextModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { self.environment(\.actionTextModifier, AnyViewModifier(transform)) @@ -478,11 +513,6 @@ public extension View { self.environment(\.progressIndicatorTextModifier, AnyViewModifier(transform)) } - @ViewBuilder - func nodeModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { - self.environment(\.nodeModifier, AnyViewModifier(transform)) - } - @ViewBuilder func textInputValueModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { self.environment(\.textInputValueModifier, AnyViewModifier(transform)) @@ -538,6 +568,26 @@ public extension View { self.environment(\.saveActionModifier, AnyViewModifier(transform)) } + @ViewBuilder + func nodeModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { + self.environment(\.nodeModifier, AnyViewModifier(transform)) + } + + @ViewBuilder + func itemsModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { + self.environment(\.itemsModifier, AnyViewModifier(transform)) + } + + @ViewBuilder + func resetActionModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { + self.environment(\.resetActionModifier, AnyViewModifier(transform)) + } + + @ViewBuilder + func applyActionModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { + self.environment(\.applyActionModifier, AnyViewModifier(transform)) + } + @ViewBuilder func nextActionModifier(_ transform: @escaping (AnyViewModifier.Content) -> V) -> some View { self.environment(\.nextActionModifier, AnyViewModifier(transform)) diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionChip+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionChip+API.generated.swift new file mode 100644 index 000000000..b7e8c3e77 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionChip+API.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct OptionChip { + @Environment(\.leftIconModifier) private var leftIconModifier + @Environment(\.titleModifier) private var titleModifier + @Environment(\.optionChipStyle) var optionChipStyle + + let _leftIcon: LeftIcon + let _title: Title + let _isSelected: Bool + + + private var isModelInit: Bool = false + private var isLeftIconNil: Bool = false + + public init( + @ViewBuilder leftIcon: () -> LeftIcon, + @ViewBuilder title: () -> Title, + isSelected: Bool + ) { + self._leftIcon = leftIcon() + self._title = title() + self._isSelected = isSelected + } + + @ViewBuilder var leftIcon: some View { + if isModelInit { + _leftIcon.modifier(leftIconModifier.concat(Fiori.OptionChip.leftIcon).concat(Fiori.OptionChip.leftIconCumulative)) + } else { + _leftIcon.modifier(leftIconModifier.concat(Fiori.OptionChip.leftIcon)) + } + } + @ViewBuilder var title: some View { + if isModelInit { + _title.modifier(titleModifier.concat(Fiori.OptionChip.title).concat(Fiori.OptionChip.titleCumulative)) + } else { + _title.modifier(titleModifier.concat(Fiori.OptionChip.title)) + } + } + + var isLeftIconEmptyView: Bool { + ((isModelInit && isLeftIconNil) || LeftIcon.self == EmptyView.self) ? true : false + } +} + +extension OptionChip where LeftIcon == _ConditionalContent, + Title == Text { + + public init(model: OptionChipModel) { + self.init(leftIcon: model.leftIcon, title: model.title, isSelected: model.isSelected) + } + + public init(leftIcon: Image? = nil, title: String, isSelected: Bool) { + self._leftIcon = leftIcon != nil ? ViewBuilder.buildEither(first: leftIcon!) : ViewBuilder.buildEither(second: EmptyView()) + self._title = Text(title) + self._isSelected = isSelected + + isModelInit = true + isLeftIconNil = leftIcon == nil ? true : false + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPicker+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPicker+API.generated.swift new file mode 100644 index 000000000..ef062bf3f --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPicker+API.generated.swift @@ -0,0 +1,23 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct OptionListPicker { + @Environment(\.sortFilterMenuItemStyle) var sortFilterMenuItemStyle + + var _value: Binding<[Int]> + var _valueOptions: [String] + var _hint: String? = nil + var _onTap: ((_ index: Int) -> Void)? = nil + + public init(model: OptionListPickerModel) { + self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, onTap: model.onTap) + } + + public init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, onTap: ((_ index: Int) -> Void)? = nil) { + self._value = value + self._valueOptions = valueOptions + self._hint = hint + self._onTap = onTap + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SliderPicker+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SliderPicker+API.generated.swift new file mode 100644 index 000000000..ad4308fe7 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SliderPicker+API.generated.swift @@ -0,0 +1,25 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct SliderPicker { + @Environment(\.sortFilterMenuItemStyle) var sortFilterMenuItemStyle + + var _value: Binding + var _formatter: String? = nil + var _minimumValue: Int + var _maximumValue: Int + var _hint: String? = nil + + public init(model: SliderPickerModel) { + self.init(value: Binding(get: { model.value }, set: { model.value = $0 }), formatter: model.formatter, minimumValue: model.minimumValue, maximumValue: model.maximumValue, hint: model.hint) + } + + public init(value: Binding, formatter: String? = nil, minimumValue: Int = 0, maximumValue: Int = 100, hint: String? = nil) { + self._value = value + self._formatter = formatter + self._minimumValue = minimumValue + self._maximumValue = maximumValue + self._hint = hint + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterFullCFG+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterFullCFG+API.generated.swift new file mode 100644 index 000000000..52b58f7b1 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterFullCFG+API.generated.swift @@ -0,0 +1,116 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct SortFilterFullCFG { + @Environment(\.titleModifier) private var titleModifier + @Environment(\.itemsModifier) private var itemsModifier + @Environment(\.cancelActionModifier) private var cancelActionModifier + @Environment(\.resetActionModifier) private var resetActionModifier + @Environment(\.applyActionModifier) private var applyActionModifier + @Environment(\.dismiss) var dismiss + + let _title: Title + var _items: Items + let _cancelAction: CancelActionView + let _resetAction: ResetActionView + let _applyAction: ApplyActionView + let _onUpdate: (() -> Void)? + @State var context: SortFilterContext = SortFilterContext() + + private var isModelInit: Bool = false + private var isCancelActionNil: Bool = false + private var isResetActionNil: Bool = false + private var isApplyActionNil: Bool = false + private var isOnUpdateNil: Bool = false + + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder cancelAction: () -> CancelActionView, + @ViewBuilder resetAction: () -> ResetActionView, + @ViewBuilder applyAction: () -> ApplyActionView, + onUpdate: (() -> Void)? = nil + ) { + self._title = title() + self._items = items() + self._cancelAction = cancelAction() + self._resetAction = resetAction() + self._applyAction = applyAction() + self._onUpdate = onUpdate + } + + @ViewBuilder var title: some View { + if isModelInit { + _title.modifier(titleModifier.concat(Fiori.SortFilterFullCFG.title).concat(Fiori.SortFilterFullCFG.titleCumulative)) + } else { + _title.modifier(titleModifier.concat(Fiori.SortFilterFullCFG.title)) + } + } + @ViewBuilder var items: some View { + if isModelInit { + _items.modifier(itemsModifier.concat(Fiori.SortFilterFullCFG.items).concat(Fiori.SortFilterFullCFG.itemsCumulative)) + } else { + _items.modifier(itemsModifier.concat(Fiori.SortFilterFullCFG.items)) + } + } + @ViewBuilder var cancelAction: some View { + if isModelInit { + _cancelAction.modifier(cancelActionModifier.concat(Fiori.SortFilterFullCFG.cancelAction).concat(Fiori.SortFilterFullCFG.cancelActionCumulative)) + } else { + _cancelAction.modifier(cancelActionModifier.concat(Fiori.SortFilterFullCFG.cancelAction)) + } + } + @ViewBuilder var resetAction: some View { + if isModelInit { + _resetAction.modifier(resetActionModifier.concat(Fiori.SortFilterFullCFG.resetAction).concat(Fiori.SortFilterFullCFG.resetActionCumulative)) + } else { + _resetAction.modifier(resetActionModifier.concat(Fiori.SortFilterFullCFG.resetAction)) + } + } + @ViewBuilder var applyAction: some View { + if isModelInit { + _applyAction.modifier(applyActionModifier.concat(Fiori.SortFilterFullCFG.applyAction).concat(Fiori.SortFilterFullCFG.applyActionCumulative)) + } else { + _applyAction.modifier(applyActionModifier.concat(Fiori.SortFilterFullCFG.applyAction)) + } + } + + var isCancelActionEmptyView: Bool { + ((isModelInit && isCancelActionNil) || CancelActionView.self == EmptyView.self) ? true : false + } + + var isResetActionEmptyView: Bool { + ((isModelInit && isResetActionNil) || ResetActionView.self == EmptyView.self) ? true : false + } + + var isApplyActionEmptyView: Bool { + ((isModelInit && isApplyActionNil) || ApplyActionView.self == EmptyView.self) ? true : false + } +} + +extension SortFilterFullCFG where Title == Text, + Items == _SortFilterCFGItemContainer, + CancelActionView == _ConditionalContent, + ResetActionView == _ConditionalContent, + ApplyActionView == _ConditionalContent { + + public init(model: SortFilterFullCFGModel) { + self.init(title: model.title, items: Binding<[[SortFilterItem]]>(get: { model.items }, set: { model.items = $0 }), cancelAction: model.cancelAction != nil ? Action(model: model.cancelAction!) : nil, resetAction: model.resetAction != nil ? Action(model: model.resetAction!) : nil, applyAction: model.applyAction != nil ? Action(model: model.applyAction!) : nil, onUpdate: model.onUpdate) + } + + public init(title: String, items: Binding<[[SortFilterItem]]>, cancelAction: Action? = Action(model: _CancelActionDefault()), resetAction: Action? = Action(model: _ResetActionDefault()), applyAction: Action? = Action(model: _ApplyActionDefault()), onUpdate: (() -> Void)? = nil) { + self._title = Text(title) + self._items = _SortFilterCFGItemContainer(items: items) + self._cancelAction = cancelAction != nil ? ViewBuilder.buildEither(first: cancelAction!) : ViewBuilder.buildEither(second: EmptyView()) + self._resetAction = resetAction != nil ? ViewBuilder.buildEither(first: resetAction!) : ViewBuilder.buildEither(second: EmptyView()) + self._applyAction = applyAction != nil ? ViewBuilder.buildEither(first: applyAction!) : ViewBuilder.buildEither(second: EmptyView()) + self._onUpdate = onUpdate + + isModelInit = true + isCancelActionNil = cancelAction == nil ? true : false + isResetActionNil = resetAction == nil ? true : false + isApplyActionNil = applyAction == nil ? true : false + isOnUpdateNil = onUpdate == nil ? true : false + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenu+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenu+API.generated.swift new file mode 100644 index 000000000..97352cf88 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenu+API.generated.swift @@ -0,0 +1,47 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct SortFilterMenu { + @Environment(\.itemsModifier) private var itemsModifier + + var _items: Items + var _onUpdate: (() -> Void)? + + + private var isModelInit: Bool = false + private var isOnUpdateNil: Bool = false + + public init( + @ViewBuilder items: () -> Items, + onUpdate: (() -> Void)? = nil + ) { + self._items = items() + self._onUpdate = onUpdate + } + + @ViewBuilder var items: some View { + if isModelInit { + _items.modifier(itemsModifier.concat(Fiori.SortFilterMenu.items).concat(Fiori.SortFilterMenu.itemsCumulative)) + } else { + _items.modifier(itemsModifier.concat(Fiori.SortFilterMenu.items)) + } + } + + +} + +extension SortFilterMenu where Items == _SortFilterMenuItemContainer { + + public init(model: SortFilterMenuModel) { + self.init(items: Binding<[[SortFilterItem]]>(get: { model.items }, set: { model.items = $0 }), onUpdate: model.onUpdate) + } + + public init(items: Binding<[[SortFilterItem]]>, onUpdate: (() -> Void)? = nil) { + self._items = _SortFilterMenuItemContainer(items: items) + self._onUpdate = onUpdate + + isModelInit = true + isOnUpdateNil = onUpdate == nil ? true : false + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenuItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenuItem+API.generated.swift new file mode 100644 index 000000000..5469109c4 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SortFilterMenuItem+API.generated.swift @@ -0,0 +1,82 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct SortFilterMenuItem { + @Environment(\.leftIconModifier) private var leftIconModifier + @Environment(\.titleModifier) private var titleModifier + @Environment(\.rightIconModifier) private var rightIconModifier + @Environment(\.sortFilterMenuItemStyle) var sortFilterMenuItemStyle + + let _leftIcon: LeftIcon + let _title: Title + let _rightIcon: RightIcon + let _isSelected: Bool + @State var context: SortFilterContext = SortFilterContext() + + private var isModelInit: Bool = false + private var isLeftIconNil: Bool = false + private var isRightIconNil: Bool = false + + public init( + @ViewBuilder leftIcon: () -> LeftIcon, + @ViewBuilder title: () -> Title, + @ViewBuilder rightIcon: () -> RightIcon, + isSelected: Bool + ) { + self._leftIcon = leftIcon() + self._title = title() + self._rightIcon = rightIcon() + self._isSelected = isSelected + } + + @ViewBuilder var leftIcon: some View { + if isModelInit { + _leftIcon.modifier(leftIconModifier.concat(Fiori.SortFilterMenuItem.leftIcon).concat(Fiori.SortFilterMenuItem.leftIconCumulative)) + } else { + _leftIcon.modifier(leftIconModifier.concat(Fiori.SortFilterMenuItem.leftIcon)) + } + } + @ViewBuilder var title: some View { + if isModelInit { + _title.modifier(titleModifier.concat(Fiori.SortFilterMenuItem.title).concat(Fiori.SortFilterMenuItem.titleCumulative)) + } else { + _title.modifier(titleModifier.concat(Fiori.SortFilterMenuItem.title)) + } + } + @ViewBuilder var rightIcon: some View { + if isModelInit { + _rightIcon.modifier(rightIconModifier.concat(Fiori.SortFilterMenuItem.rightIcon).concat(Fiori.SortFilterMenuItem.rightIconCumulative)) + } else { + _rightIcon.modifier(rightIconModifier.concat(Fiori.SortFilterMenuItem.rightIcon)) + } + } + + var isLeftIconEmptyView: Bool { + ((isModelInit && isLeftIconNil) || LeftIcon.self == EmptyView.self) ? true : false + } + + var isRightIconEmptyView: Bool { + ((isModelInit && isRightIconNil) || RightIcon.self == EmptyView.self) ? true : false + } +} + +extension SortFilterMenuItem where LeftIcon == _ConditionalContent, + Title == Text, + RightIcon == _ConditionalContent { + + public init(model: SortFilterMenuItemModel) { + self.init(leftIcon: model.leftIcon, title: model.title, rightIcon: model.rightIcon, isSelected: model.isSelected) + } + + public init(leftIcon: Image? = nil, title: String, rightIcon: Image? = nil, isSelected: Bool) { + self._leftIcon = leftIcon != nil ? ViewBuilder.buildEither(first: leftIcon!) : ViewBuilder.buildEither(second: EmptyView()) + self._title = Text(title) + self._rightIcon = rightIcon != nil ? ViewBuilder.buildEither(first: rightIcon!) : ViewBuilder.buildEither(second: EmptyView()) + self._isSelected = isSelected + + isModelInit = true + isLeftIconNil = leftIcon == nil ? true : false + isRightIconNil = rightIcon == nil ? true : false + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SwitchPicker+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SwitchPicker+API.generated.swift new file mode 100644 index 000000000..916d3cd29 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SwitchPicker+API.generated.swift @@ -0,0 +1,22 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct SwitchPicker { + @Environment(\.fioriToggleStyle) var fioriToggleStyle + @Environment(\.sortFilterMenuItemStyle) var sortFilterMenuItemStyle + + var _value: Binding + var _name: String? = nil + var _hint: String? = nil + + public init(model: SwitchPickerModel) { + self.init(value: Binding(get: { model.value }, set: { model.value = $0 }), name: model.name, hint: model.hint) + } + + public init(value: Binding, name: String? = nil, hint: String? = nil) { + self._value = value + self._name = name + self._hint = hint + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionChip+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionChip+View.generated.swift new file mode 100644 index 000000000..237897746 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionChip+View.generated.swift @@ -0,0 +1,62 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/OptionChip+View.swift` +//TODO: Implement default Fiori style definitions as `ViewModifier` +//TODO: Implement OptionChip `View` body +//TODO: Implement LibraryContentProvider + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +extension Fiori { + enum OptionChip { + typealias LeftIcon = EmptyModifier + typealias LeftIconCumulative = EmptyModifier + typealias Title = EmptyModifier + typealias TitleCumulative = EmptyModifier + + // TODO: - substitute type-specific ViewModifier for EmptyModifier + /* + // replace `typealias Subtitle = EmptyModifier` with: + + struct Subtitle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .foregroundColor(.preferredColor(.primary3)) + } + } + */ + static let leftIcon = LeftIcon() + static let title = Title() + static let leftIconCumulative = LeftIconCumulative() + static let titleCumulative = TitleCumulative() + } +} + +// FIXME: - Implement OptionChip View body + +extension OptionChip: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement OptionChip specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct OptionChipLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(OptionChip(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionListPicker+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionListPicker+View.generated.swift new file mode 100644 index 000000000..37e1d46ef --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionListPicker+View.generated.swift @@ -0,0 +1,34 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/OptionListPicker+View.swift` +//TODO: Implement OptionListPicker `View` body + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +// FIXME: - Implement OptionListPicker View body + +extension OptionListPicker: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement OptionListPicker specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct OptionListPickerLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(OptionListPicker(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SliderPicker+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SliderPicker+View.generated.swift new file mode 100644 index 000000000..2e6706c9b --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SliderPicker+View.generated.swift @@ -0,0 +1,34 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SliderPicker+View.swift` +//TODO: Implement SliderPicker `View` body + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +// FIXME: - Implement SliderPicker View body + +extension SliderPicker: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement SliderPicker specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct SliderPickerLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SliderPicker(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterFullCFG+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterFullCFG+View.generated.swift new file mode 100644 index 000000000..20e9cfd89 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterFullCFG+View.generated.swift @@ -0,0 +1,74 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SortFilterFullCFG+View.swift` +//TODO: Implement default Fiori style definitions as `ViewModifier` +//TODO: Implement SortFilterFullCFG `View` body +//TODO: Implement LibraryContentProvider + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +extension Fiori { + enum SortFilterFullCFG { + typealias Title = EmptyModifier + typealias TitleCumulative = EmptyModifier + typealias Items = EmptyModifier + typealias ItemsCumulative = EmptyModifier + typealias CancelAction = EmptyModifier + typealias CancelActionCumulative = EmptyModifier + typealias ResetAction = EmptyModifier + typealias ResetActionCumulative = EmptyModifier + typealias ApplyAction = EmptyModifier + typealias ApplyActionCumulative = EmptyModifier + + // TODO: - substitute type-specific ViewModifier for EmptyModifier + /* + // replace `typealias Subtitle = EmptyModifier` with: + + struct Subtitle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .foregroundColor(.preferredColor(.primary3)) + } + } + */ + static let title = Title() + static let items = Items() + static let cancelAction = CancelAction() + static let resetAction = ResetAction() + static let applyAction = ApplyAction() + static let titleCumulative = TitleCumulative() + static let itemsCumulative = ItemsCumulative() + static let cancelActionCumulative = CancelActionCumulative() + static let resetActionCumulative = ResetActionCumulative() + static let applyActionCumulative = ApplyActionCumulative() + } +} + +// FIXME: - Implement SortFilterFullCFG View body + +extension SortFilterFullCFG: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement SortFilterFullCFG specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct SortFilterFullCFGLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterFullCFG(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenu+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenu+View.generated.swift new file mode 100644 index 000000000..d1ae49304 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenu+View.generated.swift @@ -0,0 +1,58 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SortFilterMenu+View.swift` +//TODO: Implement default Fiori style definitions as `ViewModifier` +//TODO: Implement SortFilterMenu `View` body +//TODO: Implement LibraryContentProvider + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +extension Fiori { + enum SortFilterMenu { + typealias Items = EmptyModifier + typealias ItemsCumulative = EmptyModifier + + // TODO: - substitute type-specific ViewModifier for EmptyModifier + /* + // replace `typealias Subtitle = EmptyModifier` with: + + struct Subtitle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .foregroundColor(.preferredColor(.primary3)) + } + } + */ + static let items = Items() + static let itemsCumulative = ItemsCumulative() + } +} + +// FIXME: - Implement SortFilterMenu View body + +extension SortFilterMenu: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement SortFilterMenu specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct SortFilterMenuLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterMenu(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenuItem+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenuItem+View.generated.swift new file mode 100644 index 000000000..7e85ea3f1 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SortFilterMenuItem+View.generated.swift @@ -0,0 +1,66 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SortFilterMenuItem+View.swift` +//TODO: Implement default Fiori style definitions as `ViewModifier` +//TODO: Implement SortFilterMenuItem `View` body +//TODO: Implement LibraryContentProvider + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +extension Fiori { + enum SortFilterMenuItem { + typealias LeftIcon = EmptyModifier + typealias LeftIconCumulative = EmptyModifier + typealias Title = EmptyModifier + typealias TitleCumulative = EmptyModifier + typealias RightIcon = EmptyModifier + typealias RightIconCumulative = EmptyModifier + + // TODO: - substitute type-specific ViewModifier for EmptyModifier + /* + // replace `typealias Subtitle = EmptyModifier` with: + + struct Subtitle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body) + .foregroundColor(.preferredColor(.primary3)) + } + } + */ + static let leftIcon = LeftIcon() + static let title = Title() + static let rightIcon = RightIcon() + static let leftIconCumulative = LeftIconCumulative() + static let titleCumulative = TitleCumulative() + static let rightIconCumulative = RightIconCumulative() + } +} + +// FIXME: - Implement SortFilterMenuItem View body + +extension SortFilterMenuItem: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement SortFilterMenuItem specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct SortFilterMenuItemLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SortFilterMenuItem(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SwitchPicker+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SwitchPicker+View.generated.swift new file mode 100644 index 000000000..4ac15b331 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SwitchPicker+View.generated.swift @@ -0,0 +1,34 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SwitchPicker+View.swift` +//TODO: Implement SwitchPicker `View` body + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +// FIXME: - Implement SwitchPicker View body + +extension SwitchPicker: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement SwitchPicker specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct SwitchPickerLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(SwitchPicker(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionChip+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionChip+Init.generated.swift new file mode 100644 index 000000000..b50093bb2 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionChip+Init.generated.swift @@ -0,0 +1,16 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +extension OptionChip where LeftIcon == EmptyView { + public init( + @ViewBuilder title: () -> Title, + isSelected: Bool + ) { + self.init( + leftIcon: { EmptyView() }, + title: title, + isSelected: isSelected + ) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionListPicker+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionListPicker+Init.generated.swift new file mode 100644 index 000000000..922a0296d --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/OptionListPicker+Init.generated.swift @@ -0,0 +1,3 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SliderPicker+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SliderPicker+Init.generated.swift new file mode 100644 index 000000000..922a0296d --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SliderPicker+Init.generated.swift @@ -0,0 +1,3 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterFullCFG+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterFullCFG+Init.generated.swift new file mode 100644 index 000000000..bad77d3f6 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterFullCFG+Init.generated.swift @@ -0,0 +1,131 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +extension SortFilterFullCFG where CancelActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder resetAction: () -> ResetActionView, + @ViewBuilder applyAction: () -> ApplyActionView, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: { Action(model: _CancelActionDefault()) }, + resetAction: resetAction, + applyAction: applyAction, + onUpdate: onUpdate + ) + } +} + +extension SortFilterFullCFG where ResetActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder cancelAction: () -> CancelActionView, + @ViewBuilder applyAction: () -> ApplyActionView, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: cancelAction, + resetAction: { Action(model: _ResetActionDefault()) }, + applyAction: applyAction, + onUpdate: onUpdate + ) + } +} + +extension SortFilterFullCFG where ApplyActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder cancelAction: () -> CancelActionView, + @ViewBuilder resetAction: () -> ResetActionView, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: cancelAction, + resetAction: resetAction, + applyAction: { Action(model: _ApplyActionDefault()) }, + onUpdate: onUpdate + ) + } +} + +extension SortFilterFullCFG where CancelActionView == Action, ResetActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder applyAction: () -> ApplyActionView, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: { Action(model: _CancelActionDefault()) }, + resetAction: { Action(model: _ResetActionDefault()) }, + applyAction: applyAction, + onUpdate: onUpdate + ) + } +} + +extension SortFilterFullCFG where CancelActionView == Action, ApplyActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder resetAction: () -> ResetActionView, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: { Action(model: _CancelActionDefault()) }, + resetAction: resetAction, + applyAction: { Action(model: _ApplyActionDefault()) }, + onUpdate: onUpdate + ) + } +} + +extension SortFilterFullCFG where ResetActionView == Action, ApplyActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + @ViewBuilder cancelAction: () -> CancelActionView, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: cancelAction, + resetAction: { Action(model: _ResetActionDefault()) }, + applyAction: { Action(model: _ApplyActionDefault()) }, + onUpdate: onUpdate + ) + } +} + +extension SortFilterFullCFG where CancelActionView == Action, ResetActionView == Action, ApplyActionView == Action { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder items: () -> Items, + onUpdate: (() -> Void)? = nil + ) { + self.init( + title: title, + items: items, + cancelAction: { Action(model: _CancelActionDefault()) }, + resetAction: { Action(model: _ResetActionDefault()) }, + applyAction: { Action(model: _ApplyActionDefault()) }, + onUpdate: onUpdate + ) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenu+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenu+Init.generated.swift new file mode 100644 index 000000000..922a0296d --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenu+Init.generated.swift @@ -0,0 +1,3 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenuItem+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenuItem+Init.generated.swift new file mode 100644 index 000000000..1f3fcd0e5 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SortFilterMenuItem+Init.generated.swift @@ -0,0 +1,47 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +extension SortFilterMenuItem where LeftIcon == EmptyView { + public init( + @ViewBuilder title: () -> Title, + @ViewBuilder rightIcon: () -> RightIcon, + isSelected: Bool + ) { + self.init( + leftIcon: { EmptyView() }, + title: title, + rightIcon: rightIcon, + isSelected: isSelected + ) + } +} + +extension SortFilterMenuItem where RightIcon == EmptyView { + public init( + @ViewBuilder leftIcon: () -> LeftIcon, + @ViewBuilder title: () -> Title, + isSelected: Bool + ) { + self.init( + leftIcon: leftIcon, + title: title, + rightIcon: { EmptyView() }, + isSelected: isSelected + ) + } +} + +extension SortFilterMenuItem where LeftIcon == EmptyView, RightIcon == EmptyView { + public init( + @ViewBuilder title: () -> Title, + isSelected: Bool + ) { + self.init( + leftIcon: { EmptyView() }, + title: title, + rightIcon: { EmptyView() }, + isSelected: isSelected + ) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SwitchPicker+Init.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SwitchPicker+Init.generated.swift new file mode 100644 index 000000000..922a0296d --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Init+Extensions/SwitchPicker+Init.generated.swift @@ -0,0 +1,3 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerModel+Extensions.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerModel+Extensions.generated.swift new file mode 100644 index 000000000..bd22eec19 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerModel+Extensions.generated.swift @@ -0,0 +1,9 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public extension OptionListPickerModel { + var onTap: ((_ index: Int) -> Void)? { + return nil + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterFullCFGModel+Extensions.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterFullCFGModel+Extensions.generated.swift new file mode 100644 index 000000000..2a7904961 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterFullCFGModel+Extensions.generated.swift @@ -0,0 +1,21 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public extension SortFilterFullCFGModel { + var cancelAction: ActionModel? { + return _CancelActionDefault() + } + + var resetAction: ActionModel? { + return _ResetActionDefault() + } + + var applyAction: ActionModel? { + return _ApplyActionDefault() + } + + var onUpdate: (() -> Void)? { + return nil + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterMenuModel+Extensions.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterMenuModel+Extensions.generated.swift new file mode 100644 index 000000000..e0d70748f --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SortFilterMenuModel+Extensions.generated.swift @@ -0,0 +1,9 @@ +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public extension SortFilterMenuModel { + var onUpdate: (() -> Void)? { + return nil + } +} diff --git a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings index 6516463b6..ddb06b936 100644 --- a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings +++ b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings @@ -129,3 +129,6 @@ /* XBUT: voice over label for row selection status in DataTable */ "not selected" = "not selected"; + +/* XBUT: calendar time component label */ +"Time" = "Time";