diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 8078f4a3e..c3b4a1cde 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -33,6 +33,20 @@ 8732C2C72C3524B6002110E9 /* TimelineItemsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */; }; 8732C2C92C3524C9002110E9 /* SimpleTimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */; }; 8732C2CB2C3524D9002110E9 /* CustomTimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2CA2C3524D9002110E9 /* CustomTimelineExample.swift */; }; + 6D6E86252C50D42000EDB6F4 /* FioriButtonInListExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86242C50D42000EDB6F4 /* FioriButtonInListExample.swift */; }; + 6D6E86292C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86282C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift */; }; + 6D6E86672C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86662C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift */; }; + 6D6E866B2C5238A000EDB6F4 /* MultiLoadingButtonStatusChangeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E866A2C5238A000EDB6F4 /* MultiLoadingButtonStatusChangeExample.swift */; }; + 6D6E866D2C53969A00EDB6F4 /* CardTwoButtonsChangeToOneExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E866C2C53969A00EDB6F4 /* CardTwoButtonsChangeToOneExample.swift */; }; + 6D6E866F2C539CDE00EDB6F4 /* InPlaceLoadingFlexibleButtonExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E866E2C539CDE00EDB6F4 /* InPlaceLoadingFlexibleButtonExample.swift */; }; + 6D6E86712C53A0D500EDB6F4 /* CardViewWithTwoButtonsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86702C53A0D500EDB6F4 /* CardViewWithTwoButtonsExample.swift */; }; + 6DEC31F42C463ED50084DD20 /* FioriButtonTestsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC31F32C463ED50084DD20 /* FioriButtonTestsExample.swift */; }; + 6DEC31F82C47B7850084DD20 /* FioriButtonStyleToggleExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC31F72C47B7850084DD20 /* FioriButtonStyleToggleExample.swift */; }; + 6DEC31FA2C48B35D0084DD20 /* FioriButtonCustomButtonExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC31F92C48B35D0084DD20 /* FioriButtonCustomButtonExample.swift */; }; + 6DEC31FE2C48FAA50084DD20 /* InPlaceLoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC31FD2C48FAA50084DD20 /* InPlaceLoadingContentView.swift */; }; + 6DEC32002C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC31FF2C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift */; }; + 6DEC32022C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC32012C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift */; }; + 6DEC32042C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC32032C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift */; }; 878219C42BEE128E002FDFBC /* StepperViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878219C32BEE128E002FDFBC /* StepperViewExample.swift */; }; 8A55795724C1286E0098003A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55795624C1286E0098003A /* AppDelegate.swift */; }; 8A55795924C1286E0098003A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55795824C1286E0098003A /* SceneDelegate.swift */; }; @@ -220,6 +234,20 @@ 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemsExample.swift; sourceTree = ""; }; 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTimelineExample.swift; sourceTree = ""; }; 8732C2CA2C3524D9002110E9 /* CustomTimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimelineExample.swift; sourceTree = ""; }; + 6D6E86242C50D42000EDB6F4 /* FioriButtonInListExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonInListExample.swift; sourceTree = ""; }; + 6D6E86282C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonInListMultipleLineExample.swift; sourceTree = ""; }; + 6D6E86662C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonInCollectionExample.swift; sourceTree = ""; }; + 6D6E866A2C5238A000EDB6F4 /* MultiLoadingButtonStatusChangeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLoadingButtonStatusChangeExample.swift; sourceTree = ""; }; + 6D6E866C2C53969A00EDB6F4 /* CardTwoButtonsChangeToOneExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardTwoButtonsChangeToOneExample.swift; sourceTree = ""; }; + 6D6E866E2C539CDE00EDB6F4 /* InPlaceLoadingFlexibleButtonExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPlaceLoadingFlexibleButtonExample.swift; sourceTree = ""; }; + 6D6E86702C53A0D500EDB6F4 /* CardViewWithTwoButtonsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewWithTwoButtonsExample.swift; sourceTree = ""; }; + 6DEC31F32C463ED50084DD20 /* FioriButtonTestsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonTestsExample.swift; sourceTree = ""; }; + 6DEC31F72C47B7850084DD20 /* FioriButtonStyleToggleExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonStyleToggleExample.swift; sourceTree = ""; }; + 6DEC31F92C48B35D0084DD20 /* FioriButtonCustomButtonExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonCustomButtonExample.swift; sourceTree = ""; }; + 6DEC31FD2C48FAA50084DD20 /* InPlaceLoadingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPlaceLoadingContentView.swift; sourceTree = ""; }; + 6DEC31FF2C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonSingleStatusExample.swift; sourceTree = ""; }; + 6DEC32012C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFullWidthSingleButtonExample.swift; sourceTree = ""; }; + 6DEC32032C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFixedWidthButtonsExample.swift; sourceTree = ""; }; 878219C32BEE128E002FDFBC /* StepperViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepperViewExample.swift; sourceTree = ""; }; 8A1E99AD24D59C8000ED8A39 /* cloud-sdk-ios-fiori */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "cloud-sdk-ios-fiori"; path = ../..; sourceTree = ""; }; 8A55795324C1286E0098003A /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -390,6 +418,20 @@ children = ( 1F90888B261A59820015A84D /* FioriButtonExample.swift */, 1F26DCF9261A5CD9006C43B1 /* FioriButtonContentView.swift */, + 6DEC31F32C463ED50084DD20 /* FioriButtonTestsExample.swift */, + 6DEC31F72C47B7850084DD20 /* FioriButtonStyleToggleExample.swift */, + 6D6E86242C50D42000EDB6F4 /* FioriButtonInListExample.swift */, + 6D6E86282C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift */, + 6D6E86662C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift */, + 6DEC31F92C48B35D0084DD20 /* FioriButtonCustomButtonExample.swift */, + 6DEC31FD2C48FAA50084DD20 /* InPlaceLoadingContentView.swift */, + 6DEC31FF2C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift */, + 6D6E866A2C5238A000EDB6F4 /* MultiLoadingButtonStatusChangeExample.swift */, + 6DEC32012C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift */, + 6DEC32032C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift */, + 6D6E866C2C53969A00EDB6F4 /* CardTwoButtonsChangeToOneExample.swift */, + 6D6E866E2C539CDE00EDB6F4 /* InPlaceLoadingFlexibleButtonExample.swift */, + 6D6E86702C53A0D500EDB6F4 /* CardViewWithTwoButtonsExample.swift */, ); path = FioriButton; sourceTree = ""; @@ -972,10 +1014,12 @@ B8101D52268BB84B00D32560 /* ContactItemTapStateExamples.swift in Sources */, 8A55795724C1286E0098003A /* AppDelegate.swift in Sources */, C106AD4A2B33970500FE8B35 /* SearchPromptFontAndColor.swift in Sources */, + 6DEC32022C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift in Sources */, B1BA1F972B2167DC00E6C052 /* ToolbarExample.swift in Sources */, B18D2E9F2988B07B000A1821 /* KPIHeaderExample.swift in Sources */, 9DEC27992C3C5C620070B571 /* RatingControlExample.swift in Sources */, 8AB6C01428DF6583002F32BE /* LazyView.swift in Sources */, + 6D6E86672C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift in Sources */, 691DE21925F2A30B00094D4A /* KPIViewExample.swift in Sources */, AB988B102631270300483D87 /* DataTableExample.swift in Sources */, 99B6EF8C2672224D00515E8E /* UserConsentSample.swift in Sources */, @@ -991,6 +1035,8 @@ B80DA9BE260C1CC200C0B2E9 /* ListDataProtocol.swift in Sources */, B1DD86532B0758F000D7EDFD /* NavigationBarPopover.swift in Sources */, 3B62AB7E2C0EE257003262EB /* EditableSideBarExample.swift in Sources */, + 6DEC32002C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift in Sources */, + 6DEC31FA2C48B35D0084DD20 /* FioriButtonCustomButtonExample.swift in Sources */, B1DD86552B0759DD00D7EDFD /* NavigationBarCustomItem.swift in Sources */, B84D24EE2652F343007F2373 /* ObjectHeaderViewScenarios.swift in Sources */, B1A98FF72C12EA1600FC9998 /* BannerMessageCustomInitExample.swift in Sources */, @@ -1005,6 +1051,7 @@ 8A557A2424C12F380098003A /* ChartDetailView.swift in Sources */, 8A5579D024C1293C0098003A /* SettingsLine.swift in Sources */, B88CB6122B716C0300013B37 /* MobileCardExample.swift in Sources */, + 6DEC31FE2C48FAA50084DD20 /* InPlaceLoadingContentView.swift in Sources */, 1FC30412270540FB004BEE00 /* 72-Fonts.swift in Sources */, C1A0FDB32AD893FA0001738E /* SortFilterView+Extensions.swift in Sources */, B84D24ED2652F343007F2373 /* HeaderChartExample.swift in Sources */, @@ -1012,6 +1059,7 @@ 1F1A1FFA2C0BDA54007109D8 /* MenuSelectionExample.swift in Sources */, B846F94626815CC90085044B /* ContactItemExample.swift in Sources */, 8A5579CC24C1293C0098003A /* SettingsColorForCategory.swift in Sources */, + 6D6E866B2C5238A000EDB6F4 /* MultiLoadingButtonStatusChangeExample.swift in Sources */, C18868D12B32535100F865F7 /* SearchFontAndColor.swift in Sources */, 9D0B26092B9BA5C0004278A5 /* KeyValueFormViewExample.swift in Sources */, 8732C2C52C350957002110E9 /* TimelineExample.swift in Sources */, @@ -1019,6 +1067,7 @@ 8A557A1A24C12C820098003A /* ChartsContentView.swift in Sources */, 8A5579CE24C1293C0098003A /* SettingColor.swift in Sources */, 1F55FEF32AC941FF00D7A1BE /* View+Extensions.swift in Sources */, + 6DEC31F42C463ED50084DD20 /* FioriButtonTestsExample.swift in Sources */, 8A6DE30B28DD27F9003222E3 /* Colors.swift in Sources */, 8AD9DFB225D49967007448EC /* StylingModifierExample.swift in Sources */, 9D0B260A2B9BA5C0004278A5 /* NoteFormViewExample.swift in Sources */, @@ -1030,12 +1079,14 @@ B1A98FF52C12E9A000FC9998 /* BannerMessageModifierExample.swift in Sources */, B1BA1F922B19AAEE00E6C052 /* TabViewDetailView.swift in Sources */, B1DD864F2B07441C00D7EDFD /* UIFont+Fiori.swift in Sources */, + 6D6E86712C53A0D500EDB6F4 /* CardViewWithTwoButtonsExample.swift in Sources */, B8D4376F25F980340024EE7D /* ObjectCell_Spec_Jan2018.swift in Sources */, 8A5579CF24C1293C0098003A /* SettingsAxis.swift in Sources */, B80DA9BC260BED9400C0B2E9 /* SingleActionCollectionView.swift in Sources */, 8732C2CB2C3524D9002110E9 /* CustomTimelineExample.swift in Sources */, 8A5579D924C1293C0098003A /* SettingsSelection.swift in Sources */, B1A98FF22C11592B00FC9998 /* BannerMessageExample.swift in Sources */, + 6D6E866D2C53969A00EDB6F4 /* CardTwoButtonsChangeToOneExample.swift in Sources */, B80DA9C62612A54E00C0B2E9 /* ActivationScreenSample.swift in Sources */, B8D437732609479E0024EE7D /* SingleActionFollowButton.swift in Sources */, C18868D32B32580800F865F7 /* ColorEntity.swift in Sources */, @@ -1045,6 +1096,7 @@ B84D24F12652F343007F2373 /* ObjectHeaderSpec.swift in Sources */, B846F94A26815DF30085044B /* ContactItemCompactExamples.swift in Sources */, C106AD442B33710800FE8B35 /* SearchWithScope.swift in Sources */, + 6DEC32042C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift in Sources */, B1C7DC8129FBB13F00DC5EEB /* SPIModelExample.swift in Sources */, C106AD462B338D1300FE8B35 /* SearchWithToken.swift in Sources */, 99193C852B719B8800F33BAF /* InformationViewExample.swift in Sources */, @@ -1068,6 +1120,7 @@ B80DA9C72612A54E00C0B2E9 /* WelcomeScreenSample.swift in Sources */, 8A5579D724C1293C0098003A /* SettingsCategoryAxis.swift in Sources */, 9D0086692BA8F6820004BE15 /* TitleFormViewExample.swift in Sources */, + 6DEC31F82C47B7850084DD20 /* FioriButtonStyleToggleExample.swift in Sources */, B141D6BB29261F9E008A8BD6 /* SearchableListViewExample.swift in Sources */, C106AD482B33940600FE8B35 /* SearchWithBookmark.swift in Sources */, 975CB76B256C5A7400DB7A15 /* SignatureCaptureViewExample.swift in Sources */, @@ -1076,17 +1129,20 @@ 8732C2C92C3524C9002110E9 /* SimpleTimelineExample.swift in Sources */, B190065A2C201BBE000C8B10 /* ProfileHeaderExample.swift in Sources */, B86F02A82679835F0049DDA7 /* ObjectItemInitExamples.swift in Sources */, + 6D6E86292C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift in Sources */, 692F338B26556A6A009B98DA /* SideBarExample.swift in Sources */, 8A5579D324C1293C0098003A /* SettingsPoint.swift in Sources */, B80DA9BA260BBF8600C0B2E9 /* SingleActionProfiles.swift in Sources */, B18D593C2B0C52C700ABB1AD /* TabViewExample.swift in Sources */, 8A5579D424C1293C0098003A /* SettingsBaseline.swift in Sources */, 1F90888C261A59820015A84D /* FioriButtonExample.swift in Sources */, + 6D6E86252C50D42000EDB6F4 /* FioriButtonInListExample.swift in Sources */, B8D4377125F983730024EE7D /* ObjectCell_Rules_Alignment.swift in Sources */, 8A5579D524C1293C0098003A /* SettingsSeries.swift in Sources */, 9DEC27B52C3F3DB30070B571 /* KeyValueItemExample.swift in Sources */, 8A557A2224C12C9B0098003A /* CoreContentView.swift in Sources */, 8A5579D224C1293C0098003A /* Color+Extensions.swift in Sources */, + 6D6E866F2C539CDE00EDB6F4 /* InPlaceLoadingFlexibleButtonExample.swift in Sources */, B1BCB6E12C2EB362008AC070 /* ProfileHeaderStaticExample.swift in Sources */, C1C764882A818BEC00BCB0F7 /* SortFilterExample.swift in Sources */, B1F6FC302B22BDDA005190F9 /* ToolbarView.swift in Sources */, diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardFixedWidthButtonsExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardFixedWidthButtonsExample.swift new file mode 100644 index 000000000..4555151a5 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardFixedWidthButtonsExample.swift @@ -0,0 +1,196 @@ +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +struct CardFixedWidthButtonsExample: View { + @State private var buttonTitle = "Check in" + + @Environment(\.dismiss) private var dismiss + + @State private var _dataSource: [CardFullWidthSingleButtonItem] = [ + CardFullWidthSingleButtonItem(title: "1", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "2", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "3", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "4", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "5", loadingState: .unspecified, id: UUID()) + ] + + private var profileHeader: some View { + ProfileHeader(detailImage: { + Image("rw").resizable() + }, title: { + Text("Harry Ford") + }, subtitle: { + Text("The boy wizard, the boy wizard") + }, description: { + Text("This is a description.") + }) { + HStack { + Spacer() + Button { + print("tap message") + } label: { + FioriIcon.callout.discussion + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap email") + } label: { + FioriIcon.actions.email + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap call") + } label: { + FioriIcon.actions.call + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap video") + } label: { + FioriIcon.actions.video + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap hint") + } label: { + FioriIcon.actions.hint + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + } + } + } + + var body: some View { + List { + Section { + Divider() + .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0)) + + HStack { + Text("My Schedule") + .font(.fiori(forTextStyle: .subheadline)) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + Spacer() + Button { + print("see all") + } label: { + Text("See all (\(self._dataSource.count))") + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(Color.preferredColor(.tintColor)) + } + } + .padding(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)) + + ScrollView(.horizontal) { + HStack(spacing: 8, content: { + ForEach(0 ..< self._dataSource.count, id: \.self) { index in + let item = self._dataSource[index] + HStack { + Card { + Text("Schedule\(item.title)") + } subtitle: { + Text("Subtitle") + } detailImage: { + Image("ProfilePic") + .resizable() + .frame(width: 45, height: 45) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } headerAction: { + FioriIcon.shopping.cart + } row1: { + Text("Body text could be really long description that requires wrapping, with suggested 2 lines from Fiori Design Guideline perspective to make the UI concise. SDK default setting of numberOfLines for body is 6. Application Developer can override it with : cell.body.numOfLines = preferredNumberOfLines.") + .lineLimit(2) + } action: { + FioriButton(isSelectionPersistent: false, action: { _ in + self.updateDataSource(id: item.id) + }, label: { _ in + self.primaryActionLabel(item.loadingState) + }, image: { _ in + EmptyView() + }, imagePosition: .leading, imageTitleSpacing: 8.0) + .fioriButtonStyle(FioriPrimaryButtonStyle(118, loadingState: item.loadingState)) + .disabled(item.loadingState != .unspecified) + } secondaryAction: { + FioriButton(isSelectionPersistent: false, title: "Decline", action: { _ in + print("tap Decline") + }) + .fioriButtonStyle(FioriSecondaryButtonStyle(colorStyle: .negative, maxWidth: 118)) + } + .frame(width: 300, height: 192) + .background(Color.white) + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .preferredColor(.cardShadow), radius: 16) // + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + } + }) + .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) + } + Spacer() + } header: { + self.profileHeader + .frame(maxWidth: .infinity) + .padding() + .background(Color.preferredColor(.secondaryGroupedBackground)) + } + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden) + } + .listStyle(.grouped) + .navigationTitle("Object Card - Full Width Single Button") + .navigationBarTitleDisplayMode(.inline) + } + + @ViewBuilder + func primaryActionLabel(_ loadingState: FioriButtonLoadingState) -> any View { + loadingState != .unspecified ? AnyView(EmptyView()) : AnyView(Text("Check in")) + } + + func updateDataSource(id: UUID) { + for i in 0 ..< self._dataSource.count { + let item = self._dataSource[i] + if item.id == id { + var timeInterval = 0.0 + if item.loadingState == .unspecified { + item.loadingState = .processing + self._dataSource[i] = item + timeInterval = 2.0 + } else if item.loadingState == .processing { + item.loadingState = .success + self._dataSource[i] = item + timeInterval = 1.0 + } else { + self._dataSource.remove(at: i) + if self._dataSource.isEmpty { + self.dismiss() + } + return + } + + _ = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false, block: { _ in + self.updateDataSource(id: id) + }) + break + } + } + } +} + +#Preview { + CardFixedWidthButtonsExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardFullWidthSingleButtonExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardFullWidthSingleButtonExample.swift new file mode 100644 index 000000000..f5ca849be --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardFullWidthSingleButtonExample.swift @@ -0,0 +1,199 @@ +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +class CardFullWidthSingleButtonItem: Identifiable, ObservableObject { + @Published var title: String + @Published var loadingState: FioriButtonLoadingState + @Published var id: UUID + + init(title: String, loadingState: FioriButtonLoadingState, id: UUID) { + self.title = title + self.loadingState = loadingState + self.id = id + } +} + +struct CardFullWidthSingleButtonExample: View { + @State private var buttonTitle = "Check in" + @Environment(\.dismiss) private var dismiss + + @State private var _dataSource: [CardFullWidthSingleButtonItem] = [ + CardFullWidthSingleButtonItem(title: "1", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "2", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "3", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "4", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "5", loadingState: .unspecified, id: UUID()) + ] + + var profileHeader: some View { + ProfileHeader(detailImage: { + Image("rw").resizable() + }, title: { + Text("Harry Ford") + }, subtitle: { + Text("The boy wizard, the boy wizard") + }, description: { + Text("This is a description.") + }) { + HStack { + Spacer() + Button { + print("tap message") + } label: { + FioriIcon.callout.discussion + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap email") + } label: { + FioriIcon.actions.email + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap call") + } label: { + FioriIcon.actions.call + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap video") + } label: { + FioriIcon.actions.video + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap hint") + } label: { + FioriIcon.actions.hint + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + } + } + } + + var body: some View { + Section { + Divider() + HStack { + Text("My Schedule") + .font(.fiori(forTextStyle: .subheadline)) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + Spacer() + Button { + print("see all") + } label: { + Text("See all (\(self._dataSource.count))") + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(Color.preferredColor(.tintColor)) + } + } + .padding(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)) + + ScrollView(.horizontal) { + HStack(spacing: 8, content: { + ForEach(0 ..< self._dataSource.count, id: \.self) { index in + let item = self._dataSource[index] + HStack { + Card { + Text("Schedule\(item.title)") + } subtitle: { + Text("Subtitle") + } detailImage: { + Image("ProfilePic") + .resizable() + .frame(width: 45, height: 45) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } headerAction: { + FioriIcon.shopping.cart + } row1: { + Text("Body text could be really long description that requires wrapping, with suggested 2 lines from Fiori Design Guideline perspective to make the UI concise. SDK default setting of numberOfLines for body is 6. Application Developer can override it with : cell.body.numOfLines = preferredNumberOfLines.") + .lineLimit(2) + } action: { + FioriButton(action: { _ in + self.updateDataSource(id: item.id) + }, label: { _ in + Text(self.titleStr(item.loadingState)) + }) + .fioriButtonStyle(FioriPrimaryButtonStyle(.infinity, loadingState: item.loadingState)) + .disabled(item.loadingState != .unspecified) + } + .frame(width: 300, height: 192) + .background(Color.white) + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .preferredColor(.cardShadow), radius: 16) // + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + } + }) + .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) + } + Spacer() + } header: { + self.profileHeader + .frame(maxWidth: .infinity) + .padding() + .background(Color.preferredColor(.secondaryGroupedBackground)) + } + .navigationTitle("Object Card - Full Width Single Button") + .navigationBarTitleDisplayMode(.inline) + } + + func updateDataSource(id: UUID) { + for i in 0 ..< self._dataSource.count { + let item = self._dataSource[i] + if item.id == id { + var timeInterval = 0.0 + if item.loadingState == .unspecified { + item.loadingState = .processing + self._dataSource[i] = item + timeInterval = 2.0 + } else if item.loadingState == .processing { + item.loadingState = .success + self._dataSource[i] = item + timeInterval = 1.0 + } else { + self._dataSource.remove(at: i) + if self._dataSource.isEmpty { + self.dismiss() + } + return + } + + _ = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false, block: { _ in + self.updateDataSource(id: id) + }) + break + } + } + } + + func titleStr(_ loadingState: FioriButtonLoadingState) -> AttributedString { + switch loadingState { + case .unspecified: + "Check in" + case .processing: + "Checking in" + case .success: + "Checked in" + } + } +} + +#Preview { + CardFullWidthSingleButtonExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardTwoButtonsChangeToOneExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardTwoButtonsChangeToOneExample.swift new file mode 100644 index 000000000..63783d96f --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardTwoButtonsChangeToOneExample.swift @@ -0,0 +1,202 @@ +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +struct CardTwoButtonsChangeToOneExample: View { + @State private var buttonTitle = "Check in" + @Environment(\.dismiss) private var dismiss + + @State private var _dataSource: [CardFullWidthSingleButtonItem] = [ + CardFullWidthSingleButtonItem(title: "1", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "2", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "3", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "4", loadingState: .unspecified, id: UUID()), + CardFullWidthSingleButtonItem(title: "5", loadingState: .unspecified, id: UUID()) + ] + + var profileHeader: some View { + ProfileHeader(detailImage: { + Image("rw").resizable() + }, title: { + Text("Harry Ford") + }, subtitle: { + Text("The boy wizard, the boy wizard") + }, description: { + Text("This is a description.") + }) { + HStack { + Spacer() + Button { + print("tap message") + } label: { + FioriIcon.callout.discussion + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap email") + } label: { + FioriIcon.actions.email + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap call") + } label: { + FioriIcon.actions.call + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap video") + } label: { + FioriIcon.actions.video + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + + Button { + print("tap hint") + } label: { + FioriIcon.actions.hint + .imageScale(.large) + .fontWeight(.light) + } + Spacer() + } + } + } + + var body: some View { + List { + Section { + Divider() + .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0)) + + HStack { + Text("My Schedule") + .font(.fiori(forTextStyle: .subheadline)) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + Spacer() + Button { + print("see all") + } label: { + Text("See all (\(self._dataSource.count))") + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(Color.preferredColor(.tintColor)) + } + } + .padding(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)) + + ScrollView(.horizontal) { + HStack(spacing: 8, content: { + ForEach(0 ..< self._dataSource.count, id: \.self) { index in + let item = self._dataSource[index] + HStack { + Card { + Text("Schedule\(item.title)") + } subtitle: { + Text("Subtitle") + } detailImage: { + Image("ProfilePic") + .resizable() + .frame(width: 45, height: 45) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } headerAction: { + FioriIcon.shopping.cart + } row1: { + Text("Body text could be really long description that requires wrapping, with suggested 2 lines from Fiori Design Guideline perspective to make the UI concise. SDK default setting of numberOfLines for body is 6. Application Developer can override it with : cell.body.numOfLines = preferredNumberOfLines.") + .lineLimit(2) + } action: { + let maxWidth: CGFloat = item.loadingState == .unspecified ? 118 : .infinity + + FioriButton(isSelectionPersistent: false, title: self.titleStr(item.loadingState)) { _ in + self.updateDataSource(id: item.id) + } + .fioriButtonStyle(FioriPrimaryButtonStyle(maxWidth, loadingState: item.loadingState)) + .disabled(item.loadingState != .unspecified) + + } secondaryAction: { + if item.loadingState == .unspecified { + FioriButton(title: "Decline", action: { _ in + print("tap Decline") + }) + .fioriButtonStyle(FioriSecondaryButtonStyle(colorStyle: .negative, maxWidth: 118)) + } + } + .frame(width: 300, height: 192) + .background(Color.white) + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .preferredColor(.cardShadow), radius: 16) // + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + } + }) + .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) + } + Spacer() + } header: { + self.profileHeader + .frame(maxWidth: .infinity) + .padding() + .background(Color.preferredColor(.secondaryGroupedBackground)) + } + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden) + } + .listStyle(.grouped) + .navigationTitle("Object Card - Full Width Single Button") + .navigationBarTitleDisplayMode(.inline) + } + + func updateDataSource(id: UUID) { + for i in 0 ..< self._dataSource.count { + let item = self._dataSource[i] + if item.id == id { + var timeInterval = 0.0 + if item.loadingState == .unspecified { + item.loadingState = .processing + self._dataSource[i] = item + timeInterval = 2.0 + } else if item.loadingState == .processing { + item.loadingState = .success + self._dataSource[i] = item + timeInterval = 1.0 + } else { + self._dataSource.remove(at: i) + if self._dataSource.isEmpty { + self.dismiss() + } + return + } + + _ = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false, block: { _ in + self.updateDataSource(id: id) + }) + break + } + } + } + + func titleStr(_ loadingState: FioriButtonLoadingState) -> AttributedString { + switch loadingState { + case .unspecified: + "Check in" + case .processing: + "Checking in" + case .success: + "Checked in" + } + } +} + +#Preview { + CardTwoButtonsChangeToOneExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardViewWithTwoButtonsExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardViewWithTwoButtonsExample.swift new file mode 100644 index 000000000..ea2ff9067 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/CardViewWithTwoButtonsExample.swift @@ -0,0 +1,119 @@ +import FioriSwiftUICore +import SwiftUI + +struct CardViewWithTwoButtonsExample: View { + @State private var _loadingState: FioriButtonLoadingState = .unspecified + @Environment(\.dismiss) private var dismiss + + var body: some View { + ScrollView { + HStack { + Card { + Image("card_image") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 145) + } description: { + Text("Title") + } title: { + Text("Title that goes to three lines before truncating just like that") + } subtitle: { + Text("Subtitle that goes to two lines before truncating just like that") + } detailImage: { + Image(systemName: "person.crop.circle") + .frame(width: 90, height: 90) + .background(Color.gray.opacity(0.3)) + .cornerRadius(45) + } headerAction: { + Button { + print("tapped") + } label: { + Text("Long button") + } + } counter: { + Text("1 of 3") + } row1: { + FlowLayout(spacing: 8, lineSpacing: 2, lineLimit: 3) { + LabelItem(icon: Image(systemName: "exclamationmark.triangle.fill"), title: "Negative") + .titleStyle { config in + config.title.foregroundStyle(Color.preferredColor(.negativeLabel)) + } + LabelItem(title: "Critical") + .titleStyle { config in + config.title.foregroundStyle(Color.preferredColor(.criticalLabel)) + } + LabelItem(icon: Image(systemName: "checkmark.circle"), title: "Positive") + .titleStyle { config in + config.title.foregroundStyle(Color.preferredColor(.positiveLabel)) + } + Image(systemName: "star") + LabelItem(title: "Long long long label") + Image(systemName: "star.fill") + LabelItem(title: "Multiple lines row1") + } + } row2: { + HStack(spacing: 2) { + Image(systemName: "star.fill") + Image(systemName: "star.fill") + Image(systemName: "star") + Image(systemName: "star") + Image(systemName: "star") + } + } row3: {} cardBody: {} action: { + FioriButton(title: { _ in + self.titleStr(self._loadingState) + }, action: { _ in + self.updateDataSource() + }) + .fioriButtonStyle(FioriPrimaryButtonStyle(.infinity, loadingState: self._loadingState)) + .disabled(self._loadingState != .unspecified) + + } secondaryAction: { + FioriButton(title: "Decline", action: { _ in + print("tap Decline") + }) + .fioriButtonStyle(FioriSecondaryButtonStyle(colorStyle: .negative, maxWidth: 118)) + } + .background(Color.white) + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .preferredColor(.cardShadow), radius: 16) // + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + } + .padding(10) + .navigationTitle("Card View - With Two Buttons") + } + + func updateDataSource() { + var timeInterval = 0.0 + if self._loadingState == .unspecified { + self._loadingState = .processing + timeInterval = 2.0 + } else if self._loadingState == .processing { + self._loadingState = .success + timeInterval = 1.0 + } else { + self.dismiss() + return + } + + _ = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false, block: { _ in + self.updateDataSource() + }) + } + + func titleStr(_ loadingState: FioriButtonLoadingState) -> AttributedString { + switch loadingState { + case .unspecified: + "Check in" + case .processing: + "Checking in" + case .success: + "Checked in" + } + } +} + +#Preview { + CardViewWithTwoButtonsExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonContentView.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonContentView.swift index 5b81368a1..94bd3ded5 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonContentView.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonContentView.swift @@ -4,8 +4,15 @@ import SwiftUI struct FioriButtonContentView: View { var body: some View { List { - NavigationLink("FioriButton", destination: FioriButtonExample()) - NavigationLink("StatefulButtonStyle", destination: StatefulButtonStyleExample()) + NavigationLink("FioriButton", destination: LazyView(FioriButtonExample())) + NavigationLink("StatefulButtonStyle", destination: LazyView(StatefulButtonStyleExample())) + NavigationLink("Button Tests", destination: LazyView(FioriButtonTestsExample())) + NavigationLink("Button Style Toggle", destination: LazyView(FioriButtonStyleToggleExample())) + NavigationLink("Button In List", destination: LazyView(FioriButtonInListExample())) + NavigationLink("Button In List - Multiple Lines", destination: LazyView(FioriButtonInListMultipleLineExample())) + NavigationLink("Custom Button", destination: LazyView(FioriButtonCustomButtonExample())) + NavigationLink("In-Place Loading Button", destination: LazyView(InPlaceLoadingContentView())) } + .navigationTitle("FioriButton") } } diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonCustomButtonExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonCustomButtonExample.swift new file mode 100644 index 000000000..69f48758f --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonCustomButtonExample.swift @@ -0,0 +1,182 @@ +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +private enum FioriButtonImagePosition { + case top + case leading + case bottom + case trailing +} + +private enum FioriButtonTitleLength { + case short + case long + case extra +} + +private enum FioriButtonLineLimit { + case none + case one + case two + case three +} + +struct FioriButtonCustomButtonExample: View { + @State private var _truncationMode: Text.TruncationMode = .head + @State private var _loadingState: FioriButtonLoadingState = .unspecified + @State private var _imagePosition: FioriButtonImagePosition = .leading + @State private var _titleAlignment: TextAlignment = .center + @State private var _titleLength: FioriButtonTitleLength = .short + @State private var _lineLimit: FioriButtonLineLimit = .none + + private let spacing = 10.0 + + var body: some View { + ScrollView { + VStack(alignment: .center, spacing: self.spacing) { + FioriButton { _ in + switch self._imagePosition { + case .top: + VStack(spacing: self.spacing, content: { + self.imageView() + self.titleView() + }) + case .leading: + HStack(spacing: self.spacing, content: { + self.imageView() + self.titleView() + }) + case .bottom: + VStack(spacing: self.spacing, content: { + self.titleView() + self.imageView() + }) + case .trailing: + HStack(spacing: self.spacing, content: { + self.titleView() + self.imageView() + }) + } + } + .fioriButtonStyle(FioriPrimaryButtonStyle().eraseToAnyFioriButtonStyle()) + + HStack { + Text("Loading State:") + Spacer() + } + Picker("Loading State:", selection: self.$_loadingState) { + Text("unspecified").tag(FioriButtonLoadingState.unspecified) + Text("processing").tag(FioriButtonLoadingState.processing) + Text("success").tag(FioriButtonLoadingState.success) + } + .pickerStyle(.segmented) + + HStack { + Text("Image Position:") + Spacer() + } + Picker("Image Position:", selection: self.$_imagePosition) { + Text("top").tag(FioriButtonImagePosition.top) + Text("leading").tag(FioriButtonImagePosition.leading) + Text("bottom").tag(FioriButtonImagePosition.bottom) + Text("trailing").tag(FioriButtonImagePosition.trailing) + } + .pickerStyle(.segmented) + + HStack { + Text("Title Alignment:") + Spacer() + } + Picker("Title Alignment", selection: self.$_titleAlignment) { + Text("leading").tag(TextAlignment.leading) + Text("center").tag(TextAlignment.center) + Text("trailing").tag(TextAlignment.trailing) + } + .pickerStyle(.segmented) + + HStack { + Text("Truncation Mode:") + Spacer() + } + Picker("Truncation Mode", selection: self.$_truncationMode) { + Text("head").tag(Text.TruncationMode.head) + Text("middle").tag(Text.TruncationMode.middle) + Text("tail").tag(Text.TruncationMode.tail) + } + .pickerStyle(.segmented) + + HStack { + Text("Title Length:") + Spacer() + } + Picker("Title Length", selection: self.$_titleLength) { + Text("short").tag(FioriButtonTitleLength.short) + Text("long").tag(FioriButtonTitleLength.long) + Text("extra").tag(FioriButtonTitleLength.extra) + } + .pickerStyle(.segmented) + + HStack { + Text("Line Limit:") + Spacer() + } + Picker("Line Limit", selection: self.$_lineLimit) { + Text("none").tag(FioriButtonLineLimit.none) + Text("one").tag(FioriButtonLineLimit.one) + Text("two").tag(FioriButtonLineLimit.two) + Text("three").tag(FioriButtonLineLimit.three) + } + .pickerStyle(.segmented) + } + .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) + } + .navigationTitle("Custom Button") + .font(.headline.bold()) + } + + func imageView() -> some View { + switch self._loadingState { + case .unspecified: + AnyView(FioriIcon.actions.actionSettingsFill.font(.fiori(forTextStyle: .subheadline))) + case .processing: + AnyView(ProgressView(value: 0) + .progressViewStyle(.circular)) + case .success: + AnyView(FioriIcon.status.sysEnter.font(.fiori(forTextStyle: .subheadline))) + } + } + + func titleView() -> some View { + var titleStr = "" + switch self._titleLength { + case .short: + titleStr = "Short Title" + case .long: + titleStr = "The title is too too too too too too too too too too too too too too too too long" + case .extra: + titleStr = "The title is too too too too too too too too too too too too too too too too extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra long" + } + return Text(titleStr) + .truncationMode(self._truncationMode) + .multilineTextAlignment(self._titleAlignment) + .lineLimit(self.lineLimit()) + } + + func lineLimit() -> Int? { + switch self._lineLimit { + case .none: + return nil + case .one: + return 1 + case .two: + return 2 + case .three: + return 3 + } + } +} + +#Preview { + FioriButtonCustomButtonExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInCollectionExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInCollectionExample.swift new file mode 100644 index 000000000..c45dc4763 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInCollectionExample.swift @@ -0,0 +1,12 @@ +import SwiftUI + +struct FioriButtonInCollectionExample: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + Label("hello", image: "") + } +} + +#Preview { + FioriButtonInCollectionExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInListExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInListExample.swift new file mode 100644 index 000000000..ea7c4df3c --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInListExample.swift @@ -0,0 +1,42 @@ +import FioriSwiftUICore +import SwiftUI + +struct FioriButtonInListExample: View { + var body: some View { + List { + Section { + ForEach(0 ..< 6, id: \.self) { index in + FioriButton { _ in + HStack(spacing: 8.0, content: { + if index % 2 == 1 { // align content right + Spacer() + } + Image(fioriName: "fiori.paper.plane").fontWeight(.bold).font(.fiori(forTextStyle: .body)) + Text("Button Label test \(index)") + if index % 5 == 2 { // align content left + Spacer() + } + }) + } + .disabled(index % 2 == 1) + .fioriButtonStyle(FioriPrimaryButtonStyle(.infinity)) + } + } header: { + Text("Button Title Alignment") + .textCase(.none) + .font(.subheadline) + .fontWeight(.semibold) + } + .alignmentGuide(.listRowSeparatorLeading, computeValue: { _ in + 0 + }) + } + .listStyle(.plain) + .navigationTitle("Button In List") + .navigationBarTitleDisplayMode(.large) + } +} + +#Preview { + FioriButtonInListExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInListMultipleLineExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInListMultipleLineExample.swift new file mode 100644 index 000000000..1d31b6972 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonInListMultipleLineExample.swift @@ -0,0 +1,50 @@ +import FioriSwiftUICore +import SwiftUI + +struct FioriButtonInListMultipleLineExample: View { + var body: some View { + List { + Section { + ForEach(0 ..< 6, id: \.self) { index in + FioriButton { _ in + HStack(spacing: 8.0, content: { + if index % 2 == 1 { // align content right + Spacer() + } + Image(fioriName: "fiori.paper.plane").fontWeight(.bold).font(.fiori(forTextStyle: .body)) + Text(self.buttonTitle(for: index)) + if index % 5 == 2 { // align content left + Spacer() + } + }) + } + .disabled(index % 2 == 1) + .fioriButtonStyle(FioriPrimaryButtonStyle(.infinity)) + } + } header: { + Text("Button Title Alignment") + .textCase(.none) + .font(.subheadline) + .fontWeight(.semibold) + } + .alignmentGuide(.listRowSeparatorLeading, computeValue: { _ in + 0 + }) + } + .listStyle(.plain) + .navigationTitle("Button In List - Multiple Lines") + .navigationBarTitleDisplayMode(.large) + } + + func buttonTitle(for index: Int) -> String { + var text = "Button Label test" + for _ in 0 ..< index { + text += "\n" + "Button Label test" + } + return text + } +} + +#Preview { + FioriButtonInListMultipleLineExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonStyleToggleExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonStyleToggleExample.swift new file mode 100644 index 000000000..1c534f678 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonStyleToggleExample.swift @@ -0,0 +1,92 @@ +import FioriSwiftUICore +import SwiftUI + +enum ToggleFioriButtonStyle: Int { + case primary + case secondary + case tertiary +} + +struct FioriButtonStyleToggleExample: View { + @State private var _buttonStyle = ToggleFioriButtonStyle.primary + @State private var _colorStyle = FioriButtonColorStyle.normal + @State private var _isSelectionPersistent = false + + init(_buttonStyle: ToggleFioriButtonStyle = ToggleFioriButtonStyle.primary, _colorStyle: FioriButtonColorStyle = FioriButtonColorStyle.normal, _isSelectionPersistent: Bool = false) { + self._buttonStyle = _buttonStyle + self._colorStyle = _colorStyle + self._isSelectionPersistent = _isSelectionPersistent + } + + var body: some View { + ScrollView { + VStack(alignment: .center, spacing: 20, content: { + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { state in + HStack(spacing: 8.0, content: { + Image(fioriName: "fiori.paper.plane").fontWeight(.bold).font(.fiori(forTextStyle: .subheadline)) + Text(self.titleForState(state: state)) + }) + }) + .fioriButtonStyle(self.fioriButtonStyle().eraseToAnyFioriButtonStyle()) + + Picker("Button Style", selection: self.$_buttonStyle) { + Text("primary").tag(ToggleFioriButtonStyle.primary) + Text("secondary").tag(ToggleFioriButtonStyle.secondary) + Text("tertiary").tag(ToggleFioriButtonStyle.tertiary) + } + .pickerStyle(.segmented) + .controlSize(.large) + .frame(height: 51) + + Picker("Color Style", selection: self.$_colorStyle) { + Text("normal").tag(FioriButtonColorStyle.normal) + Text("tint").tag(FioriButtonColorStyle.tint) + Text("negative").tag(FioriButtonColorStyle.negative) + } + .pickerStyle(.segmented) // realized by UISegmentedControl + .controlSize(.large) + .frame(height: 51) + + HStack(alignment: .center, spacing: 20, content: { + Toggle("", isOn: self.$_isSelectionPersistent).labelsHidden() + Text("isSelectionPersistent") + Spacer() + }) + }) + .padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) + } + .navigationTitle("Button Style Toggle") + } + + func fioriButtonStyle() -> any FioriButtonStyle { + switch self._buttonStyle { + case .primary: + return FioriPrimaryButtonStyle() + case .secondary: + return FioriSecondaryButtonStyle(colorStyle: self._colorStyle) + case .tertiary: + return FioriTertiaryButtonStyle(colorStyle: self._colorStyle) + } + } + + func titleForState(state: UIControl.State) -> String { + var title = "" + switch state { + case .disabled: + title = "Disabled" + case .highlighted: + title = "Highlighted" + case [.selected, .highlighted]: + title = "Selected Highlighted" + case .selected: + title = "Selected" + default: + title = "Normal" + } + return title + } +} + +#Preview { + FioriButtonStyleToggleExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonTestsExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonTestsExample.swift new file mode 100644 index 000000000..59301855b --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/FioriButtonTestsExample.swift @@ -0,0 +1,111 @@ +import FioriSwiftUICore +import Foundation +import SwiftUI + +struct FioriButtonTestsExample: View { + @State private var _showSettings = false + @State private var _isEnabled = true + @State private var _isSelectionPersistent = false + @State private var _colorStyle = FioriButtonColorStyle.normal + + var types: [any FioriButtonStyle] { + [FioriPrimaryButtonStyle(), FioriSecondaryButtonStyle(colorStyle: self._colorStyle), FioriTertiaryButtonStyle(colorStyle: self._colorStyle)] + } + + let ButtonImage: some View = Image(fioriName: "fiori.action.settings.fill").fontWeight(.bold).font(.fiori(forTextStyle: .subheadline)) + + var imageTitlePadding = 10.0 + + var body: some View { + ScrollView { + VStack(alignment: .center, spacing: 10.0, content: { + ForEach(0 ..< self.types.count, id: \.self) { index in + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { _ in + self.ButtonImage + }) + .fioriButtonStyle(self.types[index].eraseToAnyFioriButtonStyle()) + .disabled(!self._isEnabled) + + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { _ in + Text("Button Long Long Long Long Title Long Long Long Long Title \(self.styleName(style: self.types[index]))") + .multilineTextAlignment(.center) + }) + .fioriButtonStyle(self.types[index].eraseToAnyFioriButtonStyle()) + .disabled(!self._isEnabled) + + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { _ in + HStack(spacing: self.imageTitlePadding, content: { + self.ButtonImage + Text("Button \(self.styleName(style: self.types[index]))") + .font(.fiori(forTextStyle: .footnote, isItalic: false)) + }) + }) + .fioriButtonStyle(self.types[index].eraseToAnyFioriButtonStyle()) + .disabled(!self._isEnabled) + + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { _ in + VStack(spacing: self.imageTitlePadding, content: { + self.ButtonImage + Text("Button \(self.styleName(style: self.types[index]))") + }) + }) + .fioriButtonStyle(self.types[index].eraseToAnyFioriButtonStyle()) + .disabled(!self._isEnabled) + + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { _ in + HStack(spacing: self.imageTitlePadding, content: { + Text("Button \(self.styleName(style: self.types[index]))") + self.ButtonImage + }) + }) + .fioriButtonStyle(self.types[index].eraseToAnyFioriButtonStyle()) + .disabled(!self._isEnabled) + + FioriButton(isSelectionPersistent: self._isSelectionPersistent, label: { _ in + VStack(spacing: self.imageTitlePadding, content: { + Text("Button \(self.styleName(style: self.types[index]))") + self.ButtonImage + }) + }) + .fioriButtonStyle(self.types[index].eraseToAnyFioriButtonStyle()) + .disabled(!self._isEnabled) + } + }) + .frame(maxWidth: .infinity) + } + .listStyle(.grouped) + .navigationTitle("Button Tests") + .settingsSheet(isPresented: self.$_showSettings) { + Toggle(isOn: self.$_isEnabled) { + Text("isEnabled") + } + Toggle(isOn: self.$_isSelectionPersistent) { + Text("isSelectionPersistent") + } + Picker(selection: self.$_colorStyle) { + Text("normal").tag(FioriButtonColorStyle.normal) + Text("tint").tag(FioriButtonColorStyle.tint) + Text("negative").tag(FioriButtonColorStyle.negative) + } label: { + Text("color style") + } + } + } + + func styleName(style: any FioriButtonStyle) -> String { + switch style { + case is FioriPrimaryButtonStyle: + return "Primary" + case is FioriSecondaryButtonStyle: + return "Secondary" + case is FioriTertiaryButtonStyle: + return "Tertiary" + default: + return "Plain" + } + } +} + +#Preview { + FioriButtonTestsExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/InPlaceLoadingContentView.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/InPlaceLoadingContentView.swift new file mode 100644 index 000000000..8202a09bd --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/InPlaceLoadingContentView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct InPlaceLoadingContentView: View { + var body: some View { + List { + NavigationLink("Loading Button Single Status", destination: LazyView(LoadingButtonSingleStatusExample())) + NavigationLink("Multi Loading Button Status Change", destination: LazyView(MultiLoadingButtonStatusChangeExample())) + NavigationLink("Card - Full Width Single Button", destination: LazyView(CardFullWidthSingleButtonExample())) + NavigationLink("Card - With Two Buttons", destination: LazyView(CardFixedWidthButtonsExample())) + NavigationLink("Card - Two Buttons Change To One", destination: LazyView(CardTwoButtonsChangeToOneExample())) + NavigationLink("Card View - With Two Buttons", destination: LazyView(CardViewWithTwoButtonsExample())) + NavigationLink("Flexible Button", destination: LazyView(InPlaceLoadingFlexibleButtonExample())) + } + .navigationTitle("In-Place Loading Button") + } +} + +#Preview { + InPlaceLoadingContentView() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/InPlaceLoadingFlexibleButtonExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/InPlaceLoadingFlexibleButtonExample.swift new file mode 100644 index 000000000..6f6b6ac2c --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/InPlaceLoadingFlexibleButtonExample.swift @@ -0,0 +1,55 @@ +import FioriSwiftUICore +import SwiftUI + +struct InPlaceLoadingFlexibleButtonExample: View { + @State private var _loadingState: FioriButtonLoadingState = .unspecified + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack { + Spacer() + + FioriButton(title: self.titleStr(self._loadingState), action: { _ in + self.updateDataSource() + }) + .fioriButtonStyle(FioriPrimaryButtonStyle(nil, loadingState: self._loadingState)) + .disabled(self._loadingState != .unspecified) + + Spacer() + } + .navigationTitle("Flexible Button") + } + + func updateDataSource() { + var timeInterval = 0.0 + if self._loadingState == .unspecified { + self._loadingState = .processing + timeInterval = 2.0 + } else if self._loadingState == .processing { + self._loadingState = .success + timeInterval = 1.0 + } else { + self.dismiss() + return + } + + _ = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false, block: { _ in + self.updateDataSource() + }) + } + + func titleStr(_ loadingState: FioriButtonLoadingState) -> AttributedString { + switch loadingState { + case .unspecified: + "Next" + case .processing: + "Loading" + case .success: + "Connected" + } + } +} + +#Preview { + InPlaceLoadingFlexibleButtonExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/LoadingButtonSingleStatusExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/LoadingButtonSingleStatusExample.swift new file mode 100644 index 000000000..39eff4e6f --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/LoadingButtonSingleStatusExample.swift @@ -0,0 +1,113 @@ +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +struct FioriButtonProcessingStyle: FioriButtonStyle { + @State var minWidth: CGFloat = 44.0 + @State var maxWidth: CGFloat? + @State var minHeight: CGFloat = 44 + let loadingState: FioriButtonLoadingState + + func makeBody(configuration: FioriButtonStyle.Configuration) -> some View { + let foregroundColor = Color.white + let backgroundColor = Color.preferredColor(.tintColorTapState) + + return self.containerView(self.loadingState, configuration: configuration) + .font(.fiori(forTextStyle: .body, weight: .semibold)) + .foregroundColor(foregroundColor) + .tint(foregroundColor) + .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .frame(minWidth: self.minWidth, maxWidth: self.maxWidth, minHeight: self.minHeight) + .background(RoundedRectangle(cornerRadius: 8).fill(backgroundColor)) + .contentShape(Rectangle()) + } + + @ViewBuilder + func containerView(_ loadingState: FioriButtonLoadingState, configuration: FioriButtonStyle.Configuration) -> some View { + let showImageView = showImageView(loadingState, image: configuration.image) + let label = configuration.label + switch configuration.imagePosition { + case .top: + VStack(spacing: configuration.imageTitleSpacing, content: { + showImageView + label + }) + case .leading: + HStack(spacing: configuration.imageTitleSpacing, content: { + showImageView + label + }) + case .bottom: + VStack(spacing: configuration.imageTitleSpacing, content: { + label + showImageView + }) + case .trailing: + HStack(spacing: configuration.imageTitleSpacing, content: { + label + showImageView + }) + } + } + + @ViewBuilder + private func showImageView(_ loadingState: FioriButtonLoadingState, image: FioriButtonStyleConfiguration.Image) -> some View { + switch loadingState { + case .unspecified: + image + case .processing: + ProgressView(value: 0).progressViewStyle(.circular) + case .success: + FioriIcon.status.sysEnter + } + } +} + +struct LoadingButtonSingleStatusExample: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 40) { + HStack { + FioriButton() + .fioriButtonStyle(FioriButtonProcessingStyle(minHeight: 38, loadingState: .processing).eraseToAnyFioriButtonStyle()) + .disabled(true) + + Spacer() + } + + FioriButton(label: { _ in + Text("Loading...") + }) + .fioriButtonStyle(FioriButtonProcessingStyle(minWidth: 142, minHeight: 38, loadingState: .processing).eraseToAnyFioriButtonStyle()) + .disabled(true) + + FioriButton() + .fioriButtonStyle(FioriButtonProcessingStyle(minWidth: 142, minHeight: 38, loadingState: .processing).eraseToAnyFioriButtonStyle()) + .disabled(true) + + FioriButton(label: { _ in + Text("Loading...") + }) + .fioriButtonStyle(FioriButtonProcessingStyle(maxWidth: .infinity, loadingState: .processing).eraseToAnyFioriButtonStyle()) + .disabled(true) + + FioriButton(label: { _ in + Text("Loading...") + }, imagePosition: .trailing).fioriButtonStyle(FioriButtonProcessingStyle(maxWidth: .infinity, loadingState: .processing).eraseToAnyFioriButtonStyle()) + .disabled(true) + + FioriButton() + .fioriButtonStyle(FioriButtonProcessingStyle(maxWidth: .infinity, loadingState: .processing).eraseToAnyFioriButtonStyle()) + .disabled(true) + } + .frame(maxWidth: .infinity) + .padding(EdgeInsets(top: 30, leading: 30, bottom: 30, trailing: 30)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .navigationTitle("Loading Button Single Status") + } +} + +#Preview { + LoadingButtonSingleStatusExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/MultiLoadingButtonStatusChangeExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/MultiLoadingButtonStatusChangeExample.swift new file mode 100644 index 000000000..0f5772f7b --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/FioriButton/MultiLoadingButtonStatusChangeExample.swift @@ -0,0 +1,104 @@ +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +private enum FioriButtonMultiLoadingButtonsStatus: Int { + case greyInitial + case greyTap + case greyLoading + case greySuccess + case tintInitial + case tintTap + case tintLoading + case tintSuccess + case negativeInitial + case negativeTap + case negativeLoading + case negativeSuccess +} + +struct MultiLoadingButtonStatusChangeExample: View { + @State private var _loadingState: FioriButtonLoadingState = .unspecified + private let sectionTitles = [ + "Primary Button (Tint):", + "Secondary Button (Grey):", + "Secondary Button (Tint):", + "Secondary Button (Negative):", + "Tertiary Button (Grey):", + "Tertiary Button (Tint):", + "Tertiary Button (Negative):" + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20, content: { + ForEach(0 ..< self.sectionTitles.count, id: \.self) { index in + HStack { + Text(self.sectionTitles[index]) + Spacer() + } + + FioriButton(isSelectionPersistent: false, action: nil, label: { _ in + Text(self.customTitle) + }, image: { _ in + index == 2 ? AnyView(EmptyView()) : AnyView(FioriIcon.actions.add.font(.fiori(forTextStyle: .subheadline))) + }) + .fioriButtonStyle(self.fioriButtonStyle(at: index).eraseToAnyFioriButtonStyle()) + .disabled(self._loadingState != .unspecified) + } + }) + .padding(30.0) + } + .navigationTitle("Multi Loading Buttons Status Change") + .onAppear { + self.changeLoadingStatus() + } + } + + func changeLoadingStatus() { + _ = Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { _ in + switch self._loadingState { + case .unspecified: + self._loadingState = .processing + case .processing: + self._loadingState = .success + case .success: + self._loadingState = .unspecified + } + }) + } + + var customTitle: AttributedString { + switch self._loadingState { + case .unspecified: + "Label" + case .processing: + "Loading..." + case .success: + "Success" + } + } + + func fioriButtonStyle(at index: Int) -> any FioriButtonStyle { + switch index { + case 0: + return FioriPrimaryButtonStyle(142, loadingState: self._loadingState) + case 1: + return FioriSecondaryButtonStyle(colorStyle: .normal, maxWidth: 142, loadingState: self._loadingState) + case 2: + return FioriSecondaryButtonStyle(colorStyle: .tint, maxWidth: 142, loadingState: self._loadingState) + case 3: + return FioriSecondaryButtonStyle(colorStyle: .negative, maxWidth: 142, loadingState: self._loadingState) + case 4: + return FioriTertiaryButtonStyle(colorStyle: .normal, maxWidth: 142, loadingState: self._loadingState) + case 5: + return FioriTertiaryButtonStyle(colorStyle: .tint, maxWidth: 142, loadingState: self._loadingState) + default: + return FioriTertiaryButtonStyle(colorStyle: .negative, maxWidth: 142, loadingState: self._loadingState) + } + } +} + +#Preview { + MultiLoadingButtonStatusChangeExample() +} diff --git a/Sources/FioriSwiftUICore/FioriButton/FioriButton.swift b/Sources/FioriSwiftUICore/FioriButton/FioriButton.swift index abb8f0ee5..009b25cd4 100644 --- a/Sources/FioriSwiftUICore/FioriButton/FioriButton.swift +++ b/Sources/FioriSwiftUICore/FioriButton/FioriButton.swift @@ -59,6 +59,9 @@ public struct FioriButton: View { let action: ((UIControl.State) -> Void)? let label: (UIControl.State) -> any View let isSelectionPersistent: Bool + let image: (UIControl.State) -> any View + let imagePosition: FioriButtonImagePosition + let imageTitleSpacing: CGFloat private let touchAreaInset: CGFloat = 50 @Environment(\.isEnabled) private var isEnabled @@ -82,9 +85,32 @@ public struct FioriButton: View { action: ((UIControl.State) -> Void)? = nil, @ViewBuilder label: @escaping (UIControl.State) -> any View) { + self.init(isSelectionPersistent: isSelectionPersistent, action: action, label: label, image: { _ in + EmptyView() + }, imagePosition: .leading, imageTitleSpacing: 8.0) + } + + /// Create a fiori button. + /// - Parameters: + /// - isSelectionPersistent: A boolean value determines whether the selection should be persistent or not. + /// - action: Action triggered when tap on button. + /// - label: A closure that returns a label for each state. For a button with non-persistent selection, `.normal`, `.disabled`, `.highlighted` are supported. For a button with persistent selection, use `.selected` instead of `.highlighted`. + /// - image: Image of the button. + /// - imagePosition: Place the image along the top, leading, bottom, or trailing edge of the button. + /// - imageTitleSpacing: Spacing between image and title. + public init(isSelectionPersistent: Bool = false, + action: ((UIControl.State) -> Void)? = nil, + @ViewBuilder label: @escaping (UIControl.State) -> any View = { _ in EmptyView() }, + @ViewBuilder image: @escaping (UIControl.State) -> any View = { _ in EmptyView() }, + imagePosition: FioriButtonImagePosition = .leading, + imageTitleSpacing: CGFloat = 8.0) + { + self.isSelectionPersistent = isSelectionPersistent self.action = action self.label = label - self.isSelectionPersistent = isSelectionPersistent + self.image = image + self.imagePosition = imagePosition + self.imageTitleSpacing = imageTitleSpacing } /// Create a fiori button. @@ -96,10 +122,12 @@ public struct FioriButton: View { title: @escaping (UIControl.State) -> AttributedString, action: ((UIControl.State) -> Void)? = nil) { - self.init(isSelectionPersistent: isSelectionPersistent, action: action, label: { + self.init(isSelectionPersistent: isSelectionPersistent, action: action) { let text = title($0) - return Text(text) - }) + Text(text) + } image: { _ in + EmptyView() + } } /// Create a fiori button. @@ -111,15 +139,22 @@ public struct FioriButton: View { title: AttributedString, action: ((UIControl.State) -> Void)? = nil) { - self.init(isSelectionPersistent: isSelectionPersistent, action: action, label: { _ in Text(title) }) + self.init(isSelectionPersistent: isSelectionPersistent, action: action) { _ in + Text(title) + } image: { _ in + EmptyView() + } } /// The content of the button. public var body: some View { - let config = FioriButtonStyleConfiguration(state: state) { state in + let config = FioriButtonStyleConfiguration(state: state, _label: { state in let v = self.label(state) return FioriButtonStyleConfiguration.Label(v) - } + }, _image: { state in + let v = self.image(state) + return FioriButtonStyleConfiguration.Image(v) + }, imagePosition: self.imagePosition, imageTitleSpacing: self.imageTitleSpacing) return Group { if self.isSelectionPersistent { @@ -133,7 +168,7 @@ public struct FioriButton: View { } label: { EmptyView() } - .buttonStyle(_ButtonStyleImpl(fioriButtonStyle: self.fioriButtonStyle, label: self.label, isEnabled: self.isEnabled)) + .buttonStyle(_ButtonStyleImpl(fioriButtonStyle: self.fioriButtonStyle, label: self.label, image: self.image, imagePosition: self.imagePosition, imageTitleSpacing: self.imageTitleSpacing, isEnabled: self.isEnabled)) } } } @@ -183,21 +218,33 @@ public extension FioriButton { self.label = { Text(title($0)) } + self.image = { _ in + EmptyView() + } + self.imagePosition = .leading + self.imageTitleSpacing = 8.0 } } private struct _ButtonStyleImpl: ButtonStyle { let fioriButtonStyle: AnyFioriButtonStyle let label: (UIControl.State) -> any View + let image: (UIControl.State) -> any View + let imagePosition: FioriButtonImagePosition + let imageTitleSpacing: CGFloat let isEnabled: Bool func makeBody(configuration: Configuration) -> some View { let state: UIControl.State = self.isEnabled ? (configuration.isPressed ? .highlighted : .normal) : .disabled - let config = FioriButtonStyleConfiguration(state: state) { state in + + let config = FioriButtonStyleConfiguration(state: state, _label: { state in let v = self.label(state) return FioriButtonStyleConfiguration.Label(v) - } - + }, _image: { state in + let v = self.image(state) + return FioriButtonStyleConfiguration.Image(v) + }, imagePosition: self.imagePosition, imageTitleSpacing: self.imageTitleSpacing) + return ZStack { self.fioriButtonStyle.makeBody(configuration: config) @@ -205,3 +252,15 @@ private struct _ButtonStyleImpl: ButtonStyle { } } } + +/// Place the image along the top, leading, bottom, or trailing edge of the button. +public enum FioriButtonImagePosition { + /// place the image along the top edge of the button. + case top + /// place the image along the leading edge of the button. + case leading + /// place the image along the bottom edge of the button. + case bottom + /// place the image along the trailing edge of the button. + case trailing +} diff --git a/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyle.swift b/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyle.swift index c6bf7770b..cb9403640 100644 --- a/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyle.swift +++ b/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyle.swift @@ -1,3 +1,4 @@ +import FioriThemeManager import Foundation import SwiftUI @@ -42,15 +43,39 @@ public struct FioriButtonStyleConfiguration { } } + /// A type-erased Image of a button. + public struct Image: View { + let view: AnyView + init(_ view: some View) { + self.view = AnyView(view) + } + + /// The content of the Image. + public var body: some View { + self.view + } + } + /// The current state of the button. public let state: UIControl.State let _label: (UIControl.State) -> Label + let _image: (UIControl.State) -> Image + + /// Place the image along the top, leading, bottom, or trailing edge of the button. + public let imagePosition: FioriButtonImagePosition + /// Spacing between image and title. + public let imageTitleSpacing: CGFloat /// The label for the current state. public var label: Label { label(for: self.state) } + + /// The Image for the current state. + public var image: Image { + image(for: self.state) + } /// Returns the label for the specific state. /// - Parameter state: A valid state for a button. For a Fiori button with non-persistent selection, `.normal`, `.disabled`, and `.highlighted` are supported. For a button with persistent selection, use `.selected` instead of `.highlighted`. @@ -58,6 +83,52 @@ public struct FioriButtonStyleConfiguration { public func label(for state: UIControl.State) -> Label { self._label(state) } + + /// Returns the Image for the specific state. + /// - Parameter state: A valid state for a button. For a Fiori button with non-persistent selection, `.normal`, `.disabled`, and `.highlighted` are supported. For a button with persistent selection, use `.selected` instead of `.highlighted`. + /// - Returns: The Image for the specific state. + public func image(for state: UIControl.State) -> Image { + self._image(state) + } + + @ViewBuilder + func containerView(_ loadingState: FioriButtonLoadingState) -> some View { + let showImageView = showImageView(loadingState, image: image) + switch self.imagePosition { + case .top: + VStack(spacing: self.imageTitleSpacing, content: { + showImageView + self.label + }) + case .leading: + HStack(spacing: self.imageTitleSpacing, content: { + showImageView + self.label + }) + case .bottom: + VStack(spacing: self.imageTitleSpacing, content: { + self.label + showImageView + }) + case .trailing: + HStack(spacing: self.imageTitleSpacing, content: { + self.label + showImageView + }) + } + } + + @ViewBuilder + private func showImageView(_ loadingState: FioriButtonLoadingState, image: Image) -> some View { + switch loadingState { + case .unspecified: + image + case .processing: + ProgressView(value: 0).progressViewStyle(.circular) + case .success: + FioriIcon.status.sysEnter.font(.fiori(forTextStyle: .subheadline)) + } + } } /// A Fiori button style for the plain button. @@ -76,14 +147,18 @@ public struct FioriPlainButtonStyle: FioriButtonStyle { /// A Fiori button style for the primary button. public struct FioriPrimaryButtonStyle: FioriButtonStyle { private let maxWidth: CGFloat? + private let loadingState: FioriButtonLoadingState /// Create a `FioriPrimaryButtonStyle` instance. - public init(_ maxWidth: CGFloat? = nil) { self.maxWidth = maxWidth } + public init(_ maxWidth: CGFloat? = nil, loadingState: FioriButtonLoadingState = .unspecified) { + self.maxWidth = maxWidth + self.loadingState = loadingState + } public func makeBody(configuration: Configuration) -> some View { - let config = FioriButtonStyleProvider.getPrimaryButtonStyle(state: configuration.state).withMaxWidth(self.maxWidth) + let config = FioriButtonStyleProvider.getPrimaryButtonStyle(state: configuration.state, loadingState: self.loadingState).withMaxWidth(self.maxWidth) - return configuration.label + return configuration.containerView(self.loadingState) .fioriButtonConfiguration(config) } } @@ -92,18 +167,20 @@ public struct FioriPrimaryButtonStyle: FioriButtonStyle { public struct FioriSecondaryButtonStyle: FioriButtonStyle { private let colorStyle: FioriButtonColorStyle private let maxWidth: CGFloat? + private let loadingState: FioriButtonLoadingState /// Create a `FioriSecondaryButtonStyle` instance. /// - Parameter colorStyle: The color style used for this button style. - public init(colorStyle: FioriButtonColorStyle = .tint, maxWidth: CGFloat? = nil) { + public init(colorStyle: FioriButtonColorStyle = .tint, maxWidth: CGFloat? = nil, loadingState: FioriButtonLoadingState = .unspecified) { self.colorStyle = colorStyle self.maxWidth = maxWidth + self.loadingState = loadingState } public func makeBody(configuration: Configuration) -> some View { - let config = FioriButtonStyleProvider.getSecondaryButtonStyle(colorStyle: self.colorStyle, for: configuration.state).withMaxWidth(self.maxWidth) + let config = FioriButtonStyleProvider.getSecondaryButtonStyle(colorStyle: self.colorStyle, for: configuration.state, loadingState: self.loadingState).withMaxWidth(self.maxWidth) - return configuration.label + return configuration.containerView(self.loadingState) .fioriButtonConfiguration(config) } } @@ -112,18 +189,20 @@ public struct FioriSecondaryButtonStyle: FioriButtonStyle { public struct FioriTertiaryButtonStyle: FioriButtonStyle { private let colorStyle: FioriButtonColorStyle private let maxWidth: CGFloat? + private let loadingState: FioriButtonLoadingState /// Create a `FioriTertiaryButtonStyle` instance. /// - Parameter colorStyle: The color style used for this button style. - public init(colorStyle: FioriButtonColorStyle = .tint, maxWidth: CGFloat? = nil) { + public init(colorStyle: FioriButtonColorStyle = .tint, maxWidth: CGFloat? = nil, loadingState: FioriButtonLoadingState = .unspecified) { self.colorStyle = colorStyle self.maxWidth = maxWidth + self.loadingState = loadingState } public func makeBody(configuration: Configuration) -> some View { - let config = FioriButtonStyleProvider.getTertiaryButtonStyle(colorStyle: self.colorStyle, for: configuration.state).withMaxWidth(self.maxWidth) + let config = FioriButtonStyleProvider.getTertiaryButtonStyle(colorStyle: self.colorStyle, for: configuration.state, loadingState: self.loadingState).withMaxWidth(self.maxWidth) - return configuration.label + return configuration.containerView(self.loadingState) .fioriButtonConfiguration(config) } } diff --git a/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyleProvider.swift b/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyleProvider.swift index 5aa0350a8..625add4ac 100644 --- a/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyleProvider.swift +++ b/Sources/FioriSwiftUICore/FioriButton/FioriButtonStyleProvider.swift @@ -7,118 +7,161 @@ enum FioriButtonStyleProvider { let foregroundColor: Color switch state { case .normal: - foregroundColor = Color.preferredColor(.tintColor) + foregroundColor = .preferredColor(.tintColor) case .highlighted, .selected: - foregroundColor = Color.preferredColor(.tintColorTapState) + foregroundColor = .preferredColor(.tintColorTapState) default: - foregroundColor = Color.preferredColor(.separator) + foregroundColor = .preferredColor(.separator) } return FioriButtonConfiguration(foregroundColor: foregroundColor, backgroundColor: backgroundColor, font: .fiori(forTextStyle: .callout), padding: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } - static func getPrimaryButtonStyle(state: UIControl.State) -> FioriButtonConfiguration { + static func getPrimaryButtonStyle(state: UIControl.State, loadingState: FioriButtonLoadingState = .unspecified) -> FioriButtonConfiguration { let backgroundColor: Color let foregroundColor: Color - switch state { - case .normal: - foregroundColor = Color.preferredColor(.base2) - backgroundColor = Color.preferredColor(.tintColor) - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.base2) - backgroundColor = Color.preferredColor(.tintColorTapState) + + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.baseWhite) + backgroundColor = .preferredColor(.tintColorTapState) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = Color.preferredColor(.tertiaryFill) + switch state { + case .normal: + foregroundColor = .preferredColor(.base2) + backgroundColor = .preferredColor(.tintColor) + case .highlighted, .selected: + foregroundColor = .preferredColor(.base2) + backgroundColor = .preferredColor(.tintColorTapState) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .preferredColor(.tertiaryFill) + } } return FioriButtonConfiguration(foregroundColor: foregroundColor, backgroundColor: backgroundColor) } - static func getSecondaryButtonStyle(colorStyle: FioriButtonColorStyle, for state: UIControl.State) -> FioriButtonConfiguration { + static func getSecondaryButtonStyle(colorStyle: FioriButtonColorStyle, for state: UIControl.State, loadingState: FioriButtonLoadingState = .unspecified) -> FioriButtonConfiguration { let backgroundColor: Color let foregroundColor: Color switch colorStyle { case .tint: - switch state { - case .normal: - foregroundColor = Color.preferredColor(.tintColor2) - backgroundColor = Color.preferredColor(.informationBackground) - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.tintColorTapState) - backgroundColor = Color.preferredColor(.informationBackground) + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.tintColorTapState) + backgroundColor = .preferredColor(.secondaryFill) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = Color.preferredColor(.tertiaryFill) + switch state { + case .normal: + foregroundColor = .preferredColor(.tintColor2) + backgroundColor = .preferredColor(.informationBackground) + case .highlighted, .selected: + foregroundColor = .preferredColor(.tintColorTapState) + backgroundColor = .preferredColor(.informationBackground) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .preferredColor(.tertiaryFill) + } } case .normal: - switch state { - case .normal: - foregroundColor = Color.preferredColor(.secondaryLabel) - backgroundColor = Color.preferredColor(.secondaryFill) - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.secondaryLabel) - backgroundColor = Color.preferredColor(.secondaryFill) + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.secondaryLabel) + backgroundColor = .preferredColor(.secondaryFill) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = Color.preferredColor(.tertiaryFill) + switch state { + case .normal: + foregroundColor = .preferredColor(.secondaryLabel) + backgroundColor = .preferredColor(.secondaryFill) + case .highlighted, .selected: + foregroundColor = .preferredColor(.secondaryLabel) + backgroundColor = .preferredColor(.secondaryFill) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .preferredColor(.tertiaryFill) + } } case .negative: - switch state { - case .normal: - foregroundColor = Color.preferredColor(.negativeLabel) - backgroundColor = Color.preferredColor(.negativeBackground) - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.negativeLabelTapState) - backgroundColor = Color.preferredColor(.negativeBackgroundTapState) + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.negativeLabelTapState) + backgroundColor = .preferredColor(.secondaryFill) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = Color.preferredColor(.tertiaryFill) + switch state { + case .normal: + foregroundColor = .preferredColor(.negativeLabel) + backgroundColor = .preferredColor(.negativeBackground) + case .highlighted, .selected: + foregroundColor = .preferredColor(.negativeLabelTapState) + backgroundColor = .preferredColor(.negativeBackgroundTapState) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .preferredColor(.tertiaryFill) + } } } return FioriButtonConfiguration(foregroundColor: foregroundColor, backgroundColor: backgroundColor) } - static func getTertiaryButtonStyle(colorStyle: FioriButtonColorStyle, for state: UIControl.State) -> FioriButtonConfiguration { + static func getTertiaryButtonStyle(colorStyle: FioriButtonColorStyle, for state: UIControl.State, loadingState: FioriButtonLoadingState = .unspecified) -> FioriButtonConfiguration { let backgroundColor: Color let foregroundColor: Color switch colorStyle { case .tint: - switch state { - case .normal: - foregroundColor = Color.preferredColor(.tintColor2) - backgroundColor = .clear - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.tintColorTapState) - backgroundColor = Color.preferredColor(.secondaryFill) + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.tintColorTapState) + backgroundColor = .preferredColor(.secondaryFill) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = .clear + switch state { + case .normal: + foregroundColor = .preferredColor(.tintColor2) + backgroundColor = .clear + case .highlighted, .selected: + foregroundColor = .preferredColor(.tintColorTapState) + backgroundColor = .preferredColor(.secondaryFill) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .clear + } } case .normal: - switch state { - case .normal: - foregroundColor = Color.preferredColor(.primaryLabel) - backgroundColor = .clear - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.secondaryLabel) - backgroundColor = Color.preferredColor(.secondaryFill) + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.primaryLabel) + backgroundColor = .preferredColor(.secondaryFill) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = .clear + switch state { + case .normal: + foregroundColor = .preferredColor(.primaryLabel) + backgroundColor = .clear + case .highlighted, .selected: + foregroundColor = .preferredColor(.secondaryLabel) + backgroundColor = .preferredColor(.secondaryFill) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .clear + } } case .negative: - switch state { - case .normal: - foregroundColor = Color.preferredColor(.negativeLabel) - backgroundColor = .clear - case .highlighted, .selected: - foregroundColor = Color.preferredColor(.negativeLabel) - backgroundColor = Color.preferredColor(.secondaryFill) + switch loadingState { + case .processing, .success: + foregroundColor = .preferredColor(.negativeLabelTapState) + backgroundColor = .preferredColor(.secondaryFill) default: - foregroundColor = Color.preferredColor(.separator) - backgroundColor = Color.preferredColor(.tertiaryFill) + switch state { + case .normal: + foregroundColor = .preferredColor(.negativeLabel) + backgroundColor = .clear + case .highlighted, .selected: + foregroundColor = .preferredColor(.negativeLabel) + backgroundColor = .preferredColor(.secondaryFill) + default: + foregroundColor = .preferredColor(.separator) + backgroundColor = .preferredColor(.tertiaryFill) + } } } @@ -132,17 +175,19 @@ struct FioriButtonConfiguration { let font: Font let padding: EdgeInsets let maxWidth: CGFloat? + let loadingState: FioriButtonLoadingState - init(foregroundColor: Color, backgroundColor: Color, font: Font = .fiori(forTextStyle: .body, weight: .semibold), padding: EdgeInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16), maxWidth: CGFloat? = nil) { + init(foregroundColor: Color, backgroundColor: Color, font: Font = .fiori(forTextStyle: .body, weight: .semibold), padding: EdgeInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16), maxWidth: CGFloat? = nil, loadingState: FioriButtonLoadingState = .unspecified) { self.foregroundColor = foregroundColor self.backgroundColor = backgroundColor self.font = font self.padding = padding self.maxWidth = maxWidth + self.loadingState = loadingState } func withMaxWidth(_ maxWidth: CGFloat?) -> FioriButtonConfiguration { - FioriButtonConfiguration(foregroundColor: self.foregroundColor, backgroundColor: self.backgroundColor, font: self.font, padding: self.padding, maxWidth: maxWidth) + FioriButtonConfiguration(foregroundColor: self.foregroundColor, backgroundColor: self.backgroundColor, font: self.font, padding: self.padding, maxWidth: maxWidth, loadingState: self.loadingState) } } @@ -151,9 +196,20 @@ extension View { self .font(config.font) .foregroundColor(config.foregroundColor) + .tint(config.foregroundColor) .padding(config.padding) .frame(minWidth: 44, maxWidth: config.maxWidth, minHeight: 44) .background(RoundedRectangle(cornerRadius: 8).fill(config.backgroundColor)) .contentShape(Rectangle()) } } + +/// loading state of `FioriButton` +public enum FioriButtonLoadingState { + /// FioriButton loading state is not specified by developer + case unspecified + /// FioriButton with activity indicator + case processing + /// FioriButton with success icon + case success +}