From b8e76be3a82221e03adc79ed3f40cf7101ad4cb7 Mon Sep 17 00:00:00 2001 From: Robin Lemaire Date: Thu, 27 Jun 2024 11:11:31 +0200 Subject: [PATCH 1/2] [IRP-1015] SPM migration --- {spark/Demo => .Demo/App}/AppDelegate.swift | 2 + {spark/Demo => .Demo/App}/SceneDelegate.swift | 2 - .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/spark-design.png | Bin .../BottomSheet.imageset/BottomSheet.png | Bin .../BottomSheet.imageset/Contents.json | 0 .../Assets.xcassets}/Contents.json | 0 .../alert-circle.imageset/Contents.json | 0 .../alert-circle.imageset/alert-outline.svg | 0 .../alert.imageset/Contents.json | 0 .../alert.imageset/Style=outlinewarning.svg | 0 .../arrow.imageset/Contents.json | 0 .../Assets.xcassets/arrow.imageset/icon.svg | 0 .../check.imageset}/Contents.json | 0 .../Assets.xcassets/check.imageset/check.svg | 0 .../checkbox-selected.imageset/Contents.json | 0 .../checkbox-selected.imageset/check-fill.svg | 0 .../close.imageset}/Contents.json | 0 .../Assets.xcassets/close.imageset/close.svg | 0 .../info.imageset/Contents.json | 0 .../info.imageset/Style=outlineinfo.svg | 0 .../Classes/Enum/ComponentVersion.swift | 0 .../Classes/Enum/SpaceContainer.swift | 0 .../Classes/Enum/UIComponent.swift | 0 .../Classes/Extension/CaseIterable-Name.swift | 0 .../Extension/DeviceShakeViewModifier.swift | 0 .../Classes/Extension/EnviromentValues.swift | 0 .../Classes/Extension/NumberFormatter.swift | 0 .../Classes/Extension/UIApplication.swift | 0 .../Classes/Extension/UIDevice+Shake.swift | 0 .../Classes/Extension/UITextField.swift | 0 .../Extension/UIViewController+Present.swift | 0 .../Classes/Extension/UIWindow+Shake.swift | 0 .../Classes/Extension/View+Shake.swift | 0 .../Classes/Helper/AnyIsTrue.swift | 0 .../Helper/NSAttributedStringBuilder.swift | 0 .../Iconography}/DemoIconography.swift | 2 +- .../Classes/Tabbar/ConsoleView.swift | 13 +- .../Tabbar/SparkTabbarController.swift | 13 +- .../Theme/PurpleContent/PurpleBorder.swift | 0 .../Theme/PurpleContent/PurpleColors.swift | 0 .../Theme/PurpleContent/PurpleElevation.swift | 0 .../Theme/PurpleContent/PurpleLayout.swift | 0 .../PurpleContent/PurpleTypography.swift | 0 .../Classes/Theme/PurpleTheme.swift | 0 .../Classes/Theme/SparkThemePublisher.swift | 1 - .../View/ComponentVersionViewController.swift | 0 .../Components/Badge/BadgeFormat-Names.swift | 0 .../Badge/BadgePreviewFormatter.swift | 2 +- .../Badge/SwiftUI/BadgeComponentView.swift | 2 +- .../Badge/UIKit/BadgeComponentUIView.swift | 2 +- .../UIKit/BadgeComponentUIViewModel.swift | 2 +- .../UIKit/BadgeComponentViewController.swift | 6 +- .../SwiftUI/BottomSheetPresentedView.swift | 6 +- .../SwiftUI/BottomSheetPresentingView.swift | 136 +- .../UIKit/BottomSheetDemoScrollView.swift | 0 .../UIKit/BottomSheetDemoUIController.swift | 0 .../UIKit/BottomSheetDemoView.swift | 0 .../UIKit/BottomSheetPresentingUIView.swift | 0 ...ottomSheetPresentingUIViewController.swift | 0 .../Components/Button/ButtonContent.swift | 0 .../Button/SwiftUI/ButtonComponentView.swift | 4 +- .../Button/UIKit/ButtonComponentUIView.swift | 4 +- .../UIKit/ButtonComponentUIViewModel.swift | 1 - .../UIKit/ButtonComponentViewController.swift | 6 +- .../Button/UIKit/ButtonControlType.swift | 0 .../Checkbox/ComponentsCheckboxListView.swift | 0 .../Checkbox/SwiftUI/CheckboxGroupView.swift | 2 +- .../Checkbox/SwiftUI/CheckboxView.swift | 2 +- .../Checkbox/CheckboxComponentUIView.swift | 2 +- .../CheckboxComponentUIViewController.swift | 6 +- .../CheckboxComponentUIViewModel.swift | 2 +- .../CheckboxGroupComponentUIView.swift | 2 +- ...eckboxGroupComponentUIViewController.swift | 6 +- .../CheckboxGroupComponentUIViewModel.swift | 2 +- .../Chip/SwiftUI/ChipComponentView.swift | 2 +- .../ChipComponentViewRepresentable.swift | 1 - .../Chip/UIKit/ChipComponentUIView.swift | 2 +- .../UIKit/ChipComponentUIViewController.swift | 6 +- .../Chip/UIKit/ChipComponentUIViewModel.swift | 2 +- .../View/Components/ComponentsView.swift | 2 +- .../Components/ComponentsViewController.swift | 0 .../SwiftUI/FormFieldComponentView.swift | 2 +- .../UIKit/FormFieldComponentUIView.swift | 2 +- .../FormFieldComponentUIViewController.swift | 6 +- .../UIKit/FormFieldComponentUIViewModel.swift | 2 +- .../Icon/SwiftUI/IconComponentView.swift | 1 - .../Icon/UIKit/IconComponentUIView.swift | 2 +- .../UIKit/IconComponentUIViewController.swift | 6 +- .../Icon/UIKit/IconComponentUIViewModel.swift | 2 +- .../IconButton/IconButtonContent.swift | 0 .../SwiftUI/IconButtonComponentView.swift | 3 +- .../UIKit/IconButtonComponentUIView.swift | 3 +- .../IconButtonComponentUIViewModel.swift | 1 - .../IconButtonComponentViewController.swift | 6 +- .../Components/Main/ComponentUIView.swift | 2 + .../Main/ComponentUIViewModel.swift | 0 .../Main/Configuration/SwiftUI/Checkbox.swift | 0 .../Configuration/SwiftUI/Component.swift | 1 + .../Configuration/SwiftUI/EnumSelector.swift | 0 .../Configuration/SwiftUI/RangeSelector.swift | 0 .../Configuration/SwiftUI/ThemeSelector.swift | 0 .../UIKit/ComponentsConfigurationView.swift | 1 + .../ComponentsConfigurationViewModel.swift | 0 .../ComponentsConfigurationItemUIType.swift | 0 .../ComponentsConfigurationItemView.swift | 17 +- ...ComponentsConfigurationItemViewModel.swift | 0 .../UIKit/Item/NumberSelector.swift | 1 + .../Constants/ProgressBarConstants.swift | 0 ...rogressBarIndeterminateComponentView.swift | 2 +- ...gressBarIndeterminateComponentUIView.swift | 2 +- ...determinateComponentUIViewController.swift | 7 +- ...BarIndeterminateComponentUIViewModel.swift | 2 +- .../SwiftUI/ProgressBarComponentView.swift | 2 +- .../UIKit/ProgressBarComponentUIView.swift | 2 +- ...ProgressBarComponentUIViewController.swift | 6 +- .../ProgressBarComponentUIViewModel.swift | 2 +- .../SwiftUI/ProgressTrackerComponent.swift | 0 .../ProgressTrackerComponentUIView.swift | 3 +- ...ressTrackerComponentUIViewController.swift | 7 +- .../ProgressTrackerComponentUIViewModel.swift | 2 +- .../RadioButtonGroupState-Extension.swift | 0 .../SwiftUI/RadioButtonComponent.swift | 2 +- .../UIKit/RadioButtonComponentUIView.swift | 3 +- ...RadioButtonComponentUIViewController.swift | 6 +- .../RadioButtonComponentUIViewModel.swift | 0 .../Rating/SwiftUI/RatingComponent.swift | 0 .../Rating/SwiftUI/RatingInputComponent.swift | 0 .../UIKit/RatingDisplayComponentUIView.swift | 3 +- .../RatingDisplayComponentUIViewModel.swift | 2 +- ...RatingDisplayComponentViewController.swift | 6 +- .../UIKit/RatingInputComponentUIView.swift | 3 +- .../RatingInputComponentUIViewModel.swift | 2 +- .../RatingInputComponentViewController.swift | 6 +- .../Rating/UIKit/StarComponentUIView.swift | 3 +- .../UIKit/StarComponentUIViewModel.swift | 0 .../UIKit/StarComponentViewController.swift | 3 +- .../Slider/SwiftUI/SliderComponentView.swift | 0 .../UIKit/SliderComponentUIControl.swift | 2 +- .../SliderComponentUIViewController.swift | 6 +- .../UIKit/SliderComponentUIViewModel.swift | 2 +- .../Spinner/SwiftUI/SpinnerComponent.swift | 2 +- .../UIKit/SpinnerComponentUIView.swift | 2 +- .../SpinnerComponentUIViewController.swift | 6 +- .../UIKit/SpinnerComponentUIViewModel.swift | 2 +- .../Switch/Enum/SwitchTextContent.swift | 0 .../Switch/SwiftUI/SwitchComponentView.swift | 1 - .../SwiftUI/SwitchComponentViewModel.swift | 2 +- .../Switch/UIKit/SwitchComponentUIView.swift | 2 +- .../SwitchComponentUIViewController.swift | 6 +- .../UIKit/SwitchComponentUIViewModel.swift | 1 - .../Components/Tab/SwiftUI/TabComponent.swift | 2 +- .../Tab/UIKit/TabComponentUIView.swift | 2 + .../UIKit/TabComponentUIViewController.swift | 6 +- .../Tab/UIKit/TabComponentUIViewModel.swift | 0 .../Tag/SwiftUI/TagComponentView.swift | 2 +- .../Tag/SwiftUI/TagComponentViewModel.swift | 1 + .../View/Components/Tag/TagContent.swift | 0 .../Tag/UIKit/TagComponentUIView.swift | 2 +- .../UIKit/TagComponentUIViewController.swift | 6 +- .../Tag/UIKit/TagComponentUIViewModel.swift | 2 +- .../TextFieldAddonsComponentView.swift | 0 .../Addons/TextFieldAddonContent.swift | 0 .../TextFieldAddonsComponentUIView.swift | 1 + ...FieldAddonsComponentUIViewController.swift | 5 +- .../TextFieldAddonsComponentUIViewModel.swift | 0 .../SwiftUI/TextFieldComponentView.swift | 0 .../TextField/TextFieldContentSide.swift | 0 .../TextField/TextFieldSideViewContent.swift | 0 .../UIKit/TextFieldComponentUIView.swift | 1 + .../TextFieldComponentUIViewController.swift | 5 +- .../UIKit/TextFieldComponentUIViewModel.swift | 0 .../Constants/TextLinkConstants.swift | 0 .../SwiftUI/TextLinkComponentView.swift | 2 +- .../Components/TextLink/TextLinkContent.swift | 0 .../UIKit/TextLinkComponentUIView.swift | 2 +- .../TextLinkComponentUIViewController.swift | 6 +- .../UIKit/TextLinkComponentUIViewModel.swift | 2 +- .../TextLink/UIKit/TextLinkControlType.swift | 0 .../ListView/Cells/BadgeCell/BadgeCell.swift | 0 .../Cells/BadgeCell/BadgeConfiguration.swift | 0 .../Cells/ButtonCell/ButtonCell.swift | 0 .../ButtonCell/ButtonConfiguration.swift | 0 .../Cells/CheckboxCell/CheckboxCell.swift | 0 .../CheckboxCell/CheckboxConfiguration.swift | 0 .../CheckboxGroupCell/CheckboxGroupCell.swift | 0 .../CheckboxGroupConfiguration.swift | 0 .../ListView/Cells/ChipCell/ChipCell.swift | 0 .../Cells/ChipCell/ChipConfiguration.swift | 0 .../ListView/Cells/IconCell/IconCell.swift | 0 .../Cells/IconCell/IconConfiguration.swift | 0 .../ProgressBarIndeterminateCell.swift | 0 ...rogressBarIndeterminateConfiguration.swift | 0 .../ProgressBarSingleCell.swift | 0 .../ProgressBarSingleConfiguration.swift | 0 .../RadioButtonCell/RadioButtonCell.swift | 0 .../RadioButtonConfiguration.swift | 0 .../RadioButtonGroupCell.swift | 0 .../RadioButtonGroupConfiguration.swift | 0 .../RatingDisplayCell/RatingDisplayCell.swift | 0 .../RatingDisplayConfiguration.swift | 0 .../RatingInputCell/RatingInputCell.swift | 0 .../RatingInputConfiguration.swift | 0 .../Cells/SpinnerCell/SpinnerCell.swift | 0 .../SpinnerCell/SpinnerConfiguration.swift | 0 .../ListView/Cells/StarCell/StarCell.swift | 0 .../Cells/StarCell/StarConfiguration.swift | 0 .../SwitchButtonCell/SwitchButtonCell.swift | 0 .../SwitchButtonConfiguration.swift | 0 .../View/ListView/Cells/TabCell/TabCell.swift | 0 .../Cells/TabCell/TabConfiguration.swift | 0 .../View/ListView/Cells/TagCell/TagCell.swift | 0 .../Cells/TagCell/TagConfiguration.swift | 0 .../RadioCheckboxUIViewController.swift | 3 +- .../Controllers/RadioCheckboxView.swift | 2 +- .../ListComponentsViewController.swift | 0 .../View/ListView/ListView+Protocols.swift | 0 .../View/ListView/ListViewController.swift | 0 .../View/ListView/ListViewDatasource.swift | 0 .../View/ListView/ListViewDelegate.swift | 0 .../Classes/View/SettingsViewController.swift | 0 .../Classes/View/SparkActionSheet.swift | 0 .../View/Theme/Border/BorderItemView.swift | 2 +- .../Theme/Border/BorderItemViewModel.swift | 0 .../Theme/Border/BorderSectionViewModel.swift | 0 .../View/Theme/Border/BorderView.swift | 2 +- .../View/Theme/Border/BorderViewModel.swift | 2 +- .../View/Theme/Color/ColorItemView.swift | 0 .../View/Theme/Color/ColorItemViewModel.swift | 0 .../Classes/View/Theme/Color/ColorView.swift | 2 +- .../View/Theme/Color/ColorViewModel.swift | 0 .../Sections/Enum/ColorSectionType.swift | 2 +- .../Sections/View/ColorSectionView.swift | 3 +- .../ColorSectionAccentViewModel.swift | 0 .../ViewModel/ColorSectionBaseViewModel.swift | 0 .../ColorSectionBasicViewModel.swift | 0 .../ColorSectionFeedbackViewModel.swift | 0 .../ViewModel/ColorSectionMainViewModel.swift | 0 .../ColorSectionStatesViewModel.swift | 0 .../ColorSectionSupportViewModel.swift | 0 .../ViewModel/ColorSectionViewModelable.swift | 0 .../Classes/View/Theme/Dims/DimItemView.swift | 2 +- .../View/Theme/Dims/DimItemViewModel.swift | 0 .../Classes/View/Theme/Dims/DimsView.swift | 2 +- .../View/Theme/Dims/DimsViewModel.swift | 2 +- .../DropShadow/DropShadowItemViewModel.swift | 0 .../Elevation/DropShadow/DropShadowView.swift | 2 +- .../DropShadow/DropShadowViewModel.swift | 2 +- .../View/Theme/Elevation/ElevationView.swift | 0 .../Theme/Layout/LayoutSpacingItemView.swift | 0 .../Layout/LayoutSpacingItemViewModel.swift | 0 .../View/Theme/Layout/LayoutView.swift | 2 +- .../View/Theme/Layout/LayoutViewModel.swift | 2 +- .../Classes/View/Theme/ThemeCellModel.swift | 0 .../Classes/View/Theme/ThemeView.swift | 2 +- .../Theme/Typography/TypographyItemView.swift | 0 .../Typography/TypographyItemViewModel.swift | 0 .../Theme/Typography/TypographyView.swift | 2 +- .../Typography/TypographyViewModel.swift | 2 +- .../Preview Assets.xcassets}/Contents.json | 0 .gitignore | 14 +- .gitscript/inject_repository_package.swift | 104 ++ postGenCommand.sh => .postGenCommand.sh | 0 .sourcery.yml | 41 - .swiftlint.yml | 113 +- Gemfile | 3 - Package.swift | 230 +++ README.md | 36 +- Sources/Core/Core.swift | 23 + Sources/Testing/TestingCore.swift | 4 + Tests/CoreTests.swift | 12 + .../Global/Publisher+SubscribeExtension.swift | 24 - .../Common/Combine/Global/UIScheduler.swift | 76 - .../Combine/Publisher/EventPublisher.swift | 86 - .../Publisher/EventPublisherTests.swift | 91 - .../Publisher/Publisher-SubscribeTests.swift | 27 - .../Combine/Publisher/ValueBinding.swift | 25 - .../PropertyState/ControlPropertyState.swift | 23 - .../ControlPropertyStateTests.swift | 27 - .../ControlPropertyStates.swift | 81 - .../ControlPropertyStatesTests.swift | 394 ---- .../Common/Control/State/ControlState.swift | 21 - .../Common/Control/Status/ControlStatus.swift | 40 - .../Control/Status/ControlStatusTests.swift | 59 - .../Control/SwiftUI/ControlStateImage.swift | 43 - .../SwiftUI/ControlStateImageTests.swift | 81 - .../Control/SwiftUI/ControlStateText.swift | 85 - .../SwiftUI/ControlStateTextTests.swift | 168 -- .../UIView/UIControlStateImageView.swift | 83 - .../Control/UIView/UIControlStateLabel.swift | 210 --- core/Sources/Common/DataType/Array-Safe.swift | 19 - .../Common/DataType/Sequence-Compacted.swift | 15 - core/Sources/Common/DataType/Updateable.swift | 35 - .../Enum/DisplayedTextType.swift | 28 - .../Enum/DisplayedTextTypeTests.swift | 48 - .../Model/DisplayedText+ExtensionTests.swift | 22 - .../DisplayedText/Model/DisplayedText.swift | 37 - .../Model/DisplayedTextTests.swift | 119 -- .../GetDidDisplayedTextChangeUseCase.swift | 77 - ...etDidDisplayedTextChangeUseCaseTests.swift | 287 --- .../GetDisplayedTextTypeUseCase.swift | 54 - .../GetDisplayedTextTypeUseCaseTests.swift | 127 -- .../ViewModel/DisplayedTextViewModel.swift | 101 - .../DisplayedTextViewModelTests.swift | 226 --- core/Sources/Common/Enum/Either/Either.swift | 47 - .../Common/Enum/Either/EitherTests.swift | 34 - .../Either/Type/AttributedStringEither.swift | 12 - .../Common/Enum/Either/Type/ImageEither.swift | 12 - .../Common/Enum/Either/Type/ViewEither.swift | 12 - core/Sources/Common/Enum/FrameworkType.swift | 12 - core/Sources/Common/Enum/TextStyle.swift | 36 - .../CGFloat+ScaledMetricExtension.swift | 56 - .../Extension/CGPoint-Distance.swift | 17 - .../Extension/CGPointDistanceTests.swift | 25 - .../Foundation/Extension/CGRect-Center.swift | 26 - .../Extension/CGRect-Location.swift | 20 - .../Extension/CGRectCenterTests.swift | 24 - .../Extension/CGRectLocationTests.swift | 46 - .../Extension/Optional+Extension.swift | 16 - .../Extension/Optional+ExtensionTests.swift | 83 - .../Foundation/Extension/UIView-Closest.swift | 32 - .../Extension/UIViewClosestTests.swift | 24 - .../EdgeInsets/EdgeInsets+Extension.swift | 29 - .../EdgeInsets+ExtensionTests.swift | 38 - .../Extension/Shape/Shape+Extension.swift | 19 - .../View/View+ProportionalWidth.swift | 23 - .../IfModifier/IfModifier.swift | 31 - .../AccessibilityViewModifier.swift | 37 - .../Modifier/Border/BorderViewModifier.swift | 39 - .../Border/View+BorderExtension.swift | 26 - .../SwiftUI/View/IsEnabledModifier.swift | 36 - .../Common/SwiftUI/View/NoButtonStyle.swift | 16 - .../SwiftUI/View/PressedButtonStyle.swift | 34 - .../AccessibilityLabelManager.swift | 59 - .../AccessibilityLabelManagerTests.swift | 105 -- .../Animation/UIView+ExecuteExtension.swift | 59 - .../CGSize/CGSize+TraitCollection.swift | 19 - ...LayoutConstraint+MultiplierExtension.swift | 30 - .../UIControl/UIControl+Extensions.swift | 29 - .../UIEdgeInsets/UIEdgeInsets+Extension.swift | 29 - .../UIEdgeInsets+ExtensionTests.swift | 37 - .../UITraitCollection-SizeAppearance.swift | 15 - .../UIView/UIStackView-RemoveAll.swift | 31 - .../UIView+AccessibilityExtension.swift | 21 - .../UIView/UIView+LayerExtension.swift | 29 - .../Extension/UIView/UIView-Attributes.swift | 15 - .../NSLayoutConstraint+Extension.swift | 63 - .../UIView/UIView+Layout.swift | 88 - .../BadgeAccessibilityIdentifier.swift | 16 - .../Badge/Constants/BadgeConstants.swift | 17 - .../Properties/Private/BadgeColors.swift | 20 - .../BadgeSizeDependentAttributes.swift | 24 - .../Badge/Properties/Public/BadgeBorder.swift | 29 - .../Badge/Properties/Public/BadgeFormat.swift | 59 - .../Properties/Public/BadgeIntentType.swift | 23 - .../Properties/Public/BadgePosition.swift | 19 - .../Badge/Properties/Public/BadgeSize.swift | 19 - .../BadgeGetSizeAttributesUseCase.swift | 57 - .../BadgeGetSizeAttributesUseCaseTests.swift | 47 - .../BadgeGetIntentColorsUseCase.swift | 83 - .../BadgeGetIntentColorsUseCaseTests.swift | 148 -- .../Badge/View/SwiftUI/BadgeView.swift | 122 -- .../Badge/View/UIKit/BadgeUIView.swift | 427 ----- .../Badge/ViewModel/BadgeViewModel.swift | 142 -- .../Badge/ViewModel/BadgeViewModelTests.swift | 215 --- .../BottomSheet/SwiftUI/View-Height.swift | 46 - ...ntationController-customHeightDetent.swift | 57 - .../ButtonAccessibilityIdentifier.swift | 28 - .../Button/Constants/ButtonConstants.swift | 18 - .../Button/Enum/Internal/ButtonType.swift | 12 - .../Public/Alignment/ButtonAlignment.swift | 25 - .../Alignment/ButtonAlignmentTests.swift | 35 - .../Button/Enum/Public/ButtonIntent.swift | 24 - .../Button/Enum/Public/ButtonShape.swift | 21 - .../Button/Enum/Public/ButtonSize.swift | 21 - .../Button/Enum/Public/ButtonVariant.swift | 27 - .../Border/ButtonBorder+ExtensionTests.swift | 25 - .../Internal/Border/ButtonBorder.swift | 17 - .../Colors/ButtonColors+ExtensionTests.swift | 30 - .../Internal/Colors/ButtonColors.swift | 40 - .../Internal/Colors/ButtonColorsTests.swift | 53 - .../ButtonCurrentColors+ExtensionTests.swift | 28 - .../CurrentColors/ButtonCurrentColors.swift | 39 - .../ButtonCurrentColorsTests.swift | 52 - .../Sizes/ButtonSizes+ExtensionTests.swift | 25 - .../Internal/Sizes/ButtonSizes.swift | 17 - .../ButtonSpacings+ExtensionTests.swift | 25 - .../Internal/Spacings/ButtonSpacings.swift | 17 - .../State/ButtonState+ExtensionTests.swift | 25 - .../Internal/State/ButtonState.swift | 17 - .../GetBorder/ButtonGetBorderUseCase.swift | 45 - .../ButtonGetBorderUseCaseTests.swift | 127 -- .../GetColors/ButtonGetColorsUseCase.swift | 72 - .../ButtonGetColorsUseCaseTests.swift | 200 -- .../ButtonGetCurrentColorsUseCase.swift | 39 - .../ButtonGetCurrentColorsUseCaseTests.swift | 92 - .../GetSizes/ButtonGetSizesUseCase.swift | 55 - .../GetSizes/ButtonGetSizesUseCaseTests.swift | 108 -- .../ButtonGetSpacingsUseCase.swift | 27 - .../ButtonGetSpacingsUseCaseTests.swift | 36 - .../GetState/ButtonGetStateUseCase.swift | 33 - .../GetState/ButtonGetStateUseCaseTests.swift | 63 - .../ButtonGetVariantContrastUseCase.swift | 107 -- ...ButtonGetVariantContrastUseCaseTests.swift | 196 -- .../ButtonGetVariantFilledUseCase.swift | 106 -- .../ButtonGetVariantFilledUseCaseTests.swift | 196 -- .../ButtonGetVariantGhostUseCase.swift | 108 -- .../ButtonGetVariantGhostUseCaseTests.swift | 196 -- .../ButtonGetVariantOutlinedUseCase.swift | 107 -- ...ButtonGetVariantOutlinedUseCaseTests.swift | 196 -- .../ButtonGetVariantTintedUseCase.swift | 106 -- .../ButtonGetVariantTintedUseCaseTests.swift | 196 -- .../ButtonGetVariantUseCaseTests.swift | 47 - .../ButtonGetVariantUseCaseable.swift | 15 - .../ButtonConfigurationSnapshotTests.swift | 110 -- .../Common/ButtonScenarioSnapshotTests.swift | 262 --- ...IconButtonConfigurationSnapshotTests.swift | 69 - .../IconButtonScenarioSnapshotTests.swift | 242 --- .../Internal/ButtonContainerView.swift | 106 -- .../SwiftUI/Internal/ButtonImageView.swift | 42 - .../SwiftUI/Public/Button/ButtonView.swift | 191 -- .../Button/ButtonViewSnapshotTests.swift | 79 - .../SwiftUI/Public/Icon/IconButtonView.swift | 91 - .../Icon/IconButtonViewSnapshotTests.swift | 78 - .../View/UIKit/Button/ButtonUIView.swift | 382 ---- .../Button/ButtonUIViewSnapshotTests.swift | 68 - .../View/UIKit/Icon/IconButtonUIView.swift | 82 - .../Icon/IconButtonUIViewSnapshotTests.swift | 52 - .../View/UIKit/Main/ButtonMainUIView.swift | 418 ----- .../ViewModel/Button/ButtonSUIViewModel.swift | 44 - .../Button/ButtonSUIViewModelTests.swift | 51 - .../ViewModel/Button/ButtonViewModel.swift | 83 - .../Button/ButtonViewModelTests.swift | 271 --- .../Icon/IconButtonSUIViewModel.swift | 41 - .../Icon/IconButtonSUIViewModelTests.swift | 41 - .../ViewModel/Icon/IconButtonViewModel.swift | 31 - .../Main/ButtonMainSUIViewModel.swift | 73 - .../Main/ButtonMainSUIViewModelTests.swift | 270 --- .../ViewModel/Main/ButtonMainViewModel.swift | 191 -- .../Main/ButtonMainViewModelTests.swift | 907 --------- .../CheckboxAccessibilityIdentifier.swift | 29 - .../Checkbox/Enum/CheckboxAlignment.swift | 18 - .../Checkbox/Enum/CheckboxGroupLayout.swift | 19 - .../Checkbox/Enum/CheckboxIntent.swift | 22 - .../Enum/CheckboxSelectionState.swift | 22 - .../Checkbox/Enum/SelectButtonState.swift | 16 - .../Checkbox/Model/CheckboxColors.swift | 17 - .../Model/CheckboxGroupItemProtocol.swift | 30 - .../Model/CheckboxGroupViewModel.swift | 64 - .../Model/CheckboxGroupViewModelTests.swift | 95 - .../Checkbox/Model/CheckboxViewModel.swift | 104 -- .../Model/CheckboxViewModelTests.swift | 148 -- .../CheckboxConfigurationSnapshotTests.swift | 45 - ...ckboxGroupConfigurationSnapshotTests.swift | 37 - .../CheckboxGroupScenarioSnapshotTests.swift | 219 --- .../CheckboxScenarioSnapshotTests.swift | 167 -- .../UseCase/CheckboxGetSpacingUseCase.swift | 22 - .../CheckboxGetSpacingUseCaseTests.swift | 34 - .../Colors/CheckboxColorsUseCase.swift | 92 - .../Colors/CheckboxColorsUseCaseTests.swift | 118 -- .../View/CheckboxGroupItemDefault.swift | 80 - .../View/SwiftUI/CheckboxGroupView.swift | 219 --- .../CheckboxGroupViewSnapshotTests.swift | 75 - .../Checkbox/View/SwiftUI/CheckboxView.swift | 248 --- .../SwiftUI/CheckboxViewSnapshotTests.swift | 74 - .../View/UIKit/CheckboxControlUIView.swift | 199 -- .../View/UIKit/CheckboxGroupUIView.swift | 366 ---- .../CheckboxGroupUIViewActionTests.swift | 72 - .../UIKit/CheckboxGroupUIViewDelegate.swift | 20 - .../CheckboxGroupUIViewSnapshotTests.swift | 62 - .../Checkbox/View/UIKit/CheckboxUIView.swift | 428 ----- .../View/UIKit/CheckboxUIViewDelegate.swift | 19 - .../UIKit/CheckboxUIViewSnapshotTests.swift | 75 - .../ChipAccessibilityIdentifier.swift | 22 - .../Components/Chip/Enum/ChipAlignment.swift | 20 - .../Components/Chip/Enum/ChipConstants.swift | 19 - .../Components/Chip/Enum/ChipIntent.swift | 23 - .../Components/Chip/Enum/ChipVariant.swift | 16 - .../Components/Chip/Model/ChipContent.swift | 16 - .../Chip/Model/ChipIntentColors.swift | 39 - .../Components/Chip/Model/ChipState.swift | 22 - .../Chip/Model/ChipStateColors.swift | 37 - .../Chip/Model/ChipStateColorsTests.swift | 47 - .../Chip/Model/ChipStateTests.swift | 31 - .../Chip/UseCase/ChipGetColorsUseCase.swift | 96 - .../UseCase/ChipGetColorsUseCaseTests.swift | 166 -- .../ChipGetOutlinedIntentColorsUseCase.swift | 122 -- ...pGetOutlinedIntentColorsUseCaseTests.swift | 358 ---- .../ChipGetTintedIntentColorsUseCase.swift | 115 -- ...hipGetTintedIntentColorsUseCaseTests.swift | 392 ---- .../Components/Chip/View/ChipViewModel.swift | 167 -- .../Chip/View/ChipViewModelTests.swift | 265 --- .../ChipConfigurationSnapshotTests.swift | 42 - .../ChipScenarioSnapshotTests.swift | 222 --- .../CommonTests/ChipStateSnapshotTests.swift | 20 - .../Chip/View/SwiftUI/ChipView.swift | 296 --- .../View/SwiftUI/ChipViewSnapshotTests.swift | 47 - .../Chip/View/UIKit/ChipUIView.swift | 572 ------ .../View/UIKit/ChipUIViewSnapshotTests.swift | 47 - .../Chip/View/UIKit/ChipUIViewTests.swift | 55 - .../FormFieldAccessibilityIdentifier.swift | 18 - .../Enum/FormFieldFeedbackState.swift | 15 - .../FormField/Model/FormFieldColors.swift | 15 - .../FormField/Model/FormFieldViewModel.swift | 101 - .../Model/FormFieldViewModelTests.swift | 130 -- .../FormfieldConfigurationSnapshotTests.swift | 36 - .../FormfieldScenarioSnapshotTests.swift | 252 --- .../UseCase/FormFieldColorsUseCase.swift | 34 - .../UseCase/FormFieldColorsUseCaseTests.swift | 56 - .../UseCase/FormfieldTitleUseCase.swift | 43 - .../UseCase/FormfieldTitleUseCaseTests.swift | 51 - .../View/SwiftUI/FormFieldView.swift | 95 - .../SwiftUI/FormFieldViewSnapshotTests.swift | 70 - .../View/UIKit/FormFieldUIView.swift | 308 --- .../UIKit/FormFieldUIViewSnapshotTests.swift | 109 -- .../IconAccessibilityIdentifier.swift | 16 - .../Components/Icon/Enum/IconIntent.swift | 21 - .../Components/Icon/Enum/IconSize.swift | 48 - .../Icon/UseCase/IconGetColorUseCase.swift | 40 - .../UseCase/IconGetColorUseCaseTests.swift | 78 - .../Icon/View/Model/IconViewModel.swift | 64 - .../Icon/View/Model/IconViewModelTests.swift | 239 --- .../Icon/View/SwiftUI/IconView.swift | 48 - .../View/SwiftUI/IconViewSnapshotTests.swift | 45 - .../Icon/View/UIKit/IconUIView.swift | 175 -- .../View/UIKit/IconUIViewSnapshotTests.swift | 39 - .../ProgressBarAccessibilityIdentifier.swift | 22 - .../Constants/ProgressBarConstants.swift | 21 - ...rogressBarIndeterminateAnimationType.swift | 27 - ...ssBarIndeterminateAnimationTypeTests.swift | 50 - .../ProgressBarIndeterminateStatus.swift | 12 - .../Enum/Intent/ProgressBarDoubleIntent.swift | 17 - .../Enum/Intent/ProgressBarIntent.swift | 20 - .../ProgressBar/Enum/ProgressBarShape.swift | 15 - ...ogressBarAnimatedData+ExtensionTests.swift | 25 - .../ProgressBarAnimatedData.swift | 17 - ...ogressBarDoubleColors+ExtensionTests.swift | 26 - .../Double/ProgressBarDoubleColors.swift | 33 - .../Double/ProgressBarDoubleColorsTests.swift | 51 - .../Colors/ProgressBarMainColors.swift | 12 - .../ProgressBarColors+ExtensionTests.swift | 24 - .../Colors/Single/ProgressBarColors.swift | 30 - .../Single/ProgressBarColorsTests.swift | 47 - .../ProgressBarGetAnimatedDataUseCase.swift | 52 - ...ogressBarGetAnimatedDataUseCaseTests.swift | 85 - .../ProgressBarDoubleGetColorsUseCase.swift | 40 - ...ogressBarDoubleGetColorsUseCaseTests.swift | 104 -- .../ProgressBarMainGetColorsUseCaseable.swift | 18 - .../Single/ProgressBarGetColorsUseCase.swift | 45 - .../ProgressBarGetColorsUseCaseTests.swift | 119 -- .../ProgressBarGetCornerRadiusUseCase.swift | 40 - ...ogressBarGetCornerRadiusUseCaseTests.swift | 60 - ...rogressBarConfigurationSnapshotTests.swift | 36 - .../ProgressBarScenarioSnapshotTests.swift | 132 -- .../Internal/ProgressBarContentView.swift | 55 - .../Public/ProgressBarDoubleView.swift | 98 - .../ProgressBarDoubleViewSnapshotTests.swift | 47 - .../Public/ProgressBarIndeterminateView.swift | 112 -- .../View/SwiftUI/Public/ProgressBarView.swift | 76 - .../Public/ProgressBarViewSnapshotTests.swift | 46 - .../View/UIKit/ProgressBarDoubleUIView.swift | 185 -- ...ProgressBarDoubleUIViewSnapshotTests.swift | 47 - .../ProgressBarIndeterminateUIView.swift | 236 --- .../View/UIKit/ProgressBarUIView.swift | 141 -- .../ProgressBarUIViewSnapshotTests.swift | 46 - .../View/UIKit/ProgressMainBarUIView.swift | 202 -- .../Double/ProgressBarDoubleViewModel.swift | 32 - .../ProgressBarIndeterminateViewModel.swift | 104 -- ...ogressBarIndeterminateViewModelTests.swift | 341 ---- .../Main/ProgressBarMainViewModel.swift | 113 -- .../Main/ProgressBarMainViewModelTests.swift | 428 ----- .../Single/ProgressBarViewModel.swift | 32 - .../Value/ProgressBarValueViewModel.swift | 26 - .../ProgressBarValueViewModelTests.swift | 68 - ...ogressTrackerAccessibilityIdentifier.swift | 28 - ...sTrackerAccessibilityIdentifierTests.swift | 21 - .../Enum/ProgressTrackerIntent.swift | 22 - .../ProgressTrackerInteractionState.swift | 17 - .../Enum/ProgressTrackerOrientation.swift | 15 - .../Enum/ProgressTrackerSize.swift | 16 - .../Enum/ProgressTrackerVariant.swift | 15 - .../Model/ProgressTrackerColors.swift | 22 - .../Model/ProgressTrackerConstants.swift | 16 - .../Model/ProgressTrackerContent.swift | 189 -- .../Model/ProgressTrackerContentTests.swift | 241 --- .../ProgressTrackerSizePreferences.swift | 24 - .../Model/ProgressTrackerSpacing.swift | 15 - .../Model/ProgressTrackerState.swift | 28 - .../Model/ProgressTrackerTintedColors.swift | 15 - .../ProgressTrackerGetColorsUseCase.swift | 46 - ...ProgressTrackerGetColorsUseCaseTests.swift | 77 - ...gressTrackerGetOutlinedColorsUseCase.swift | 185 -- ...TrackerGetOutlinedColorsUseCaseTests.swift | 234 --- .../ProgressTrackerGetSpacingsUseCase.swift | 33 - ...ogressTrackerGetSpacingsUseCaseTests.swift | 39 - ...rogressTrackerGetTintedColorsUseCase.swift | 159 -- ...ssTrackerGetTintedColorsUseCaseTests.swift | 229 --- .../ProgressTrackerGetTrackColorUseCase.swift | 34 - ...ressTrackerGetTrackColorUseCaseTests.swift | 48 - ...ssTrackerGetVariantColorsUseCaseable.swift | 17 - ...essTrackerConfigurationSnapshotTests.swift | 71 - ...ProgressTrackerScenarioSnapshotTests.swift | 289 --- .../ProgressTrackerIndicatorViewModel.swift | 96 - ...ogressTrackerIndicatorViewModelTests.swift | 246 --- .../View/ProgressTrackerTrackViewModel.swift | 62 - .../ProgressTrackerTrackViewModelTests.swift | 157 -- .../View/ProgressTrackerViewModel.swift | 156 -- .../View/ProgressTrackerViewModelTests.swift | 261 --- ...ackerAccessibilityTraitsViewModifier.swift | 45 - ...TrackerContinuousGestureHandlerTests.swift | 153 -- ...ssTrackerDiscreteGestureHandlerTests.swift | 126 -- .../ProgressTrackerGestureHandler.swift | 199 -- .../ProgressTrackerHorizontalView.swift | 163 -- ...rackerIndependentGestureHandlerTests.swift | 147 -- .../ProgressTrackerIndicatorView.swift | 96 - .../SwiftUI/ProgressTrackerTrackView.swift | 55 - .../SwiftUI/ProgressTrackerVerticalView.swift | 180 -- .../View/SwiftUI/ProgressTrackerView.swift | 269 --- ...rogressTrackerAccessibilityUIControl.swift | 20 - ...TrackerContinuousUITouchHandlerTests.swift | 199 -- ...ssTrackerDiscreteUITouchHandlerTests.swift | 157 -- ...rackerIndependentUITouchHandlerTests.swift | 157 -- .../ProgressTrackerIndicatorUIControl.swift | 290 --- .../UIKit/ProgressTrackerTrackUIView.swift | 136 -- .../View/UIKit/ProgressTrackerUIControl.swift | 917 --------- ...rogressTrackerUIControlSnapshotTests.swift | 103 -- .../UIKit/ProgressTrackerUITouchHandler.swift | 220 --- ...ssTrackerUITouchHandlerCreationTests.swift | 51 - .../ProgressTrackerUITouchHandlerTests.swift | 22 - .../RadioButtonAccessibilityIdentifier.swift | 24 - .../Enum/RadioButtonGroupLayout.swift | 19 - .../RadioButton/Enum/RadioButtonIntent.swift | 22 - .../Enum/RadioButtonLabelPosition.swift | 42 - .../RadioButton/Enum/RadioButtonState.swift | 36 - .../Internal/RadioButtonAttributes.swift | 23 - .../Internal/RadioButtonColors.swift | 32 - .../Internal/RadioButtonConstants.swift | 15 - .../Internal/RadioButtonGroupContent.swift | 14 - .../Internal/RadioButtonStateAttribute.swift | 14 - .../Properties/Public/RadioButtonItem.swift | 27 - .../Properties/Public/RadioButtonUIItem.swift | 35 - .../Public/RadioButtonUIItemTests.swift | 38 - .../RadioButtonGetAttributesUseCase.swift | 50 - ...RadioButtonGetAttributesUseCaseTests.swift | 97 - .../RadioButtonGetColorsUseCase.swift | 127 -- .../RadioButtonGetColorsUseCaseTests.swift | 263 --- .../RadioButtonGetGroupColorUseCase.swift | 46 - ...RadioButtonGetGroupColorUseCaseTests.swift | 106 -- .../View/RadioButtonGroupViewModel.swift | 81 - .../View/RadioButtonGroupViewModelTests.swift | 87 - .../View/RadioButtonViewModel.swift | 157 -- .../View/RadioButtonViewModelTests.swift | 191 -- .../View/SwiftUI/RadioButtonGroupView.swift | 181 -- .../View/SwiftUI/RadioButtonView.swift | 211 --- .../View/UIKit/RadioButtonToggleUIView.swift | 108 -- .../View/UIKit/RadioButtonUIGroupView.swift | 584 ------ .../RadioButtonUIGroupViewDelegate.swift | 14 - .../View/UIKit/RadioButtonUIView.swift | 512 ----- ...RatingDisplayAccessibilityIdentifier.swift | 18 - .../RatingInputAccessibilityIdentifier.swift | 18 - .../Rating/Cache/CGLayerCache.swift | 29 - .../Rating/Enum/RatingDisplaySize.swift | 16 - .../Components/Rating/Enum/RatingIntent.swift | 13 - .../Rating/Enum/RatingStarsCount.swift | 14 - .../Components/Rating/Enum/StarDefaults.swift | 18 - .../Components/Rating/Enum/StarFillMode.swift | 50 - .../Rating/Enum/StarFillModeUnitTests.swift | 46 - .../Rating/Graphics/ShapeLayer.swift | 91 - .../Components/Rating/Graphics/Star.swift | 144 -- .../Rating/Model/RatingColors.swift | 21 - .../Rating/Model/RatingSizeAttributes.swift | 15 - .../Components/Rating/Model/RatingState.swift | 18 - .../Rating/Model/StarConfiguration.swift | 40 - ...ingDisplayConfigurationSnapshotTests.swift | 39 - .../RatingDisplayScenarioSnapshotTests.swift | 109 -- ...atingInputConfigurationSnapshotTests.swift | 43 - .../RatingInputScenarioSnapshotTests.swift | 104 -- .../UseCases/RatingGetColorsUseCase.swift | 58 - .../RatingGetColorsUseCaseUnitTests.swift | 76 - .../RatingSizeAttributesUseCase.swift | 48 - .../RatingSizeAttributesUseCaseTests.swift | 57 - .../Rating/View/RatingDisplayViewModel.swift | 127 -- .../View/RatingDisplayViewModelTests.swift | 146 -- .../View/SwiftUI/RatingDisplayView.swift | 105 -- .../RatingDisplayViewSnapshotTests.swift | 44 - .../Rating/View/SwiftUI/RatingInputView.swift | 159 -- .../RatingInputViewSnapshotTests.swift | 56 - .../Rating/View/SwiftUI/StarShape.swift | 45 - .../Rating/View/SwiftUI/StarView.swift | 127 -- .../View/UIKit/RatingDisplayUIView.swift | 277 --- .../RatingDisplayUIViewSnapshotTests.swift | 46 - .../Rating/View/UIKit/RatingInputUIView.swift | 219 --- .../UIKit/RatingInputUIViewDelegate.swift | 17 - .../RatingInputUIViewSnapshotTests.swift | 50 - .../Rating/View/UIKit/StarUIView.swift | 240 --- .../Rating/View/UIKit/StarUIViewTests.swift | 37 - .../SliderAccessibilityIdentifier.swift | 18 - .../Slider/Constant/SliderConstants.swift | 15 - .../Slider/Handle/View/SliderHandle.swift | 49 - .../Handle/View/SliderHandleUIControl.swift | 113 -- .../ViewModel/SliderHandleViewModel.swift | 22 - .../Private/SliderColors+ExtensionTests.swift | 21 - .../Properties/Private/SliderColors.swift | 32 - .../Private/SliderColorsTests.swift | 44 - .../Private/SliderRadii+ExtensionTests.swift | 19 - .../Properties/Private/SliderRadii.swift | 14 - .../Properties/Public/SliderIntent.swift | 23 - .../Properties/Public/SliderShape.swift | 16 - .../SliderCreateStepsUseCase.swift | 36 - ...iderCreateValuesFromStepUseCaseTests.swift | 74 - ...SliderGetClosestValueInBoundsUseCase.swift | 22 - ...rGetClosestValueUseCasableMock+Tests.swift | 48 - .../SliderGetClosestValueUseCaseTests.swift | 83 - ...eCasableGeneratedMock+ExtensionTests.swift | 20 - .../GetColors/SliderGetColorsUseCase.swift | 92 - .../SliderGetColorsUseCaseTests.swift | 192 -- ...eCasableGeneratedMock+ExtensionTests.swift | 20 - .../SliderGetCornerRadiiUseCase.swift | 29 - .../SliderGetCornerRadiiUseCaseTests.swift | 45 - ...epValuesInBoundsUseCasableMock+Tests.swift | 48 - .../SliderGetStepValuesInBoundsUseCase.swift | 19 - ...derGetStepValuesInBoundsUseCaseTests.swift | 41 - .../View/SliderScenario+SnapshotTests.swift | 69 - .../Slider/View/SwiftUI/Slider.swift | 148 -- .../Slider/View/UIKit/SliderUIControl.swift | 288 --- .../UIKit/SliderUIControlSnapshotTests.swift | 69 - .../ViewModel/Base/SliderViewModel.swift | 148 -- .../ViewModel/Base/SliderViewModelTests.swift | 641 ------- .../Base/SliderViewModelWithMocksTests.swift | 57 - .../Single/SingleSliderViewModel.swift | 40 - .../Single/SingleSliderViewModelTests.swift | 291 --- .../SpinnerAccessibilityIdentifier.swift | 18 - .../Spinner/Enum/SpinnerIntent.swift | 22 - .../Components/Spinner/Enum/SpinnerSize.swift | 15 - .../Components/Spinner/SpinnerViewModel.swift | 85 - .../Spinner/SpinnerViewModelTests.swift | 93 - .../Spinner/SwiftUI/SpinnerView.swift | 79 - .../Spinner/UIKit/SpinnerUIView.swift | 158 -- .../GetSpinnerIntentColorUseCase.swift | 44 - .../GetSpinnerIntentColorUseCaseTests.swift | 61 - .../SwitchAccessibilityIdentifier.swift | 22 - .../Switch/Constants/SwitchConstants.swift | 27 - .../Switch/Either/SwitchImagesEither.swift | 12 - .../Switch/Enum/SwitchAlignment.swift | 15 - .../Components/Switch/Enum/SwitchIntent.swift | 20 - .../Colors/SwitchColors+ExtensionTests.swift | 28 - .../Model/Internal/Colors/SwitchColors.swift | 36 - .../Internal/Colors/SwitchColorsTests.swift | 67 - .../SwitchStatusColors+ExtensionTests.swift | 24 - .../Internal/Colors/SwitchStatusColors.swift | 30 - .../Colors/SwitchStatusColorsTests.swift | 42 - .../SwitchImagesState+ExtensionTests.swift | 33 - .../ImagesState/SwitchImagesState.swift | 19 - .../SwitchPosition+ExtensionTests.swift | 25 - .../Internal/Position/SwitchPosition.swift | 17 - .../SwitchToggleState+ExtensionTests.swift | 25 - .../ToggleState/SwitchToggleState.swift | 17 - .../Model/Public/Images/SwitchImages.swift | 24 - .../Model/Public/Images/SwitchUIImages.swift | 24 - .../GetColor/SwitchGetColorUseCase.swift | 52 - .../GetColor/SwitchGetColorUseCaseTests.swift | 107 -- .../GetColors/SwitchGetColorsUseCase.swift | 56 - .../SwitchGetColorsUseCaseTests.swift | 101 - .../SwitchGetImagesStateUseCase.swift | 40 - .../SwitchGetImagesStateUseCaseTests.swift | 153 -- .../SwitchGetPositionUseCase.swift | 49 - .../SwitchGetPositionUseCaseTests.swift | 86 - .../SwitchGetToggleColorUseCase.swift | 27 - .../SwitchGetToggleColorUseCaseTests.swift | 60 - .../SwitchGetToggleStateUseCase.swift | 32 - .../SwitchGetToggleStateUseCaseTests.swift | 63 - .../View/Common/SwitchSutSnapshotTests.swift | 140 -- .../SubviewType/SwitchSubviewType.swift | 35 - .../SubviewType/SwitchSubviewTypeTests.swift | 71 - .../Switch/View/SwiftUI/SwitchView.swift | 289 --- .../SwiftUI/SwitchViewSnapshotTests.swift | 78 - .../Switch/View/UIKit/SwitchUIView.swift | 903 --------- .../UIKit/SwitchUIViewSnapshotTests.swift | 103 -- .../Switch/ViewModel/SwitchViewModel.swift | 273 --- .../SwitchViewModelDependencies.swift | 60 - .../ViewModel/SwitchViewModelTests.swift | 1645 ----------------- .../TabAccessibilityIdentifier.swift | 14 - .../Components/Tab/Enum/TabIntent.swift | 16 - .../Sources/Components/Tab/Enum/TabSize.swift | 16 - .../Tab/Properties/TabItemColors.swift | 41 - .../Tab/Properties/TabItemContent.swift | 50 - .../Tab/Properties/TabItemHeights.swift | 19 - .../Tab/Properties/TabItemSpacings.swift | 26 - .../Components/Tab/Properties/TabState.swift | 26 - .../Tab/Properties/TabStateAttributes.swift | 29 - .../Tab/Properties/TabUIItemContent.swift | 46 - .../Tab/Properties/TabsAttributes.swift | 27 - .../Tab/UseCases/TabGetFontUseCase.swift | 36 - .../Tab/UseCases/TabGetFontUseCaseTests.swift | 44 - .../UseCases/TabGetIntentColorUseCase.swift | 38 - .../TabGetIntentColorUseCaseTests.swift | 37 - .../TabGetStateAttributesUseCase.swift | 152 -- .../TabGetStateAttributesUseCaseTests.swift | 197 -- .../UseCases/TabsGetAttributesUseCase.swift | 46 - .../TabsGetAttributesUseCaseTests.swift | 86 - .../View/SwiftUI/TabApportionsSizeView.swift | 123 -- .../Tab/View/SwiftUI/TabBackgroundLine.swift | 25 - .../Tab/View/SwiftUI/TabEqualSizeView.swift | 131 -- .../Tab/View/SwiftUI/TabItemView.swift | 138 -- .../SwiftUI/TabItemViewSnapshotTests.swift | 179 -- .../Tab/View/SwiftUI/TabSingleItem.swift | 50 - .../Components/Tab/View/SwiftUI/TabView.swift | 126 -- .../View/SwiftUI/TabViewSnapshotTests.swift | 165 -- .../Tab/View/UIKit/TabItemUIView.swift | 507 ----- .../UIKit/TabItemUIViewSnapshotTests.swift | 157 -- .../Tab/View/UIKit/TabItemUIViewTests.swift | 219 --- .../Components/Tab/View/UIKit/TabUIView.swift | 640 ------- .../Tab/View/UIKit/TabUIViewDelegate.swift | 20 - .../View/UIKit/TabUIViewSnapshotTests.swift | 148 -- .../Tab/View/UIKit/TabUIViewTests.swift | 258 --- .../Tab/ViewModel/TabContainerViewModel.swift | 13 - .../Tab/ViewModel/TabItemViewModel.swift | 149 -- .../Tab/ViewModel/TabItemViewModelTests.swift | 317 ---- .../Tab/ViewModel/TabViewModel.swift | 87 - .../Tab/ViewModel/TabViewModelTests.swift | 211 --- .../TagAccessibilityIdentifier.swift | 18 - .../Tag/Constants/TagConstants.swift | 13 - .../Components/Tag/Enum/TagIntent.swift | 20 - .../Components/Tag/Enum/TagVariant.swift | 17 - .../Colors/TagColors+ExtensionTests.swift | 26 - .../Tag/Model/Colors/TagColors.swift | 33 - .../Tag/Model/Colors/TagColorsTests.swift | 47 - .../TagContentColors+ExtensionTests.swift | 28 - .../ContentColors/TagContentColors.swift | 36 - .../ContentColors/TagContentColorsTests.swift | 54 - .../GetColors/TagGetColorsUseCase.swift | 61 - .../GetColors/TagGetColorsUseCaseTests.swift | 210 --- .../TagGetContentColorsUseCase.swift | 96 - .../TagGetIntentColorsUseCaseTests.swift | 214 --- .../TagConfigurationSnapshotTests.swift | 119 -- .../Common/TagScenarioSnapshotTests.swift | 174 -- .../Components/Tag/View/SwiftUI/TagView.swift | 157 -- .../View/SwiftUI/TagViewSnapshotTests.swift | 75 - .../Components/Tag/View/UIKit/TagUIView.swift | 505 ----- .../View/UIKit/TagUIViewSnapshotTests.swift | 88 - .../Tag/ViewModel/TagViewModel.swift | 121 -- .../Tag/ViewModel/TagViewModelTests.swift | 277 --- ...xtFieldAddonsAccessibilityIdentifier.swift | 16 - .../Addons/View/SwiftUI/TextFieldAddon.swift | 35 - .../Addons/View/SwiftUI/TextFieldAddons.swift | 149 -- .../View/UIKit/TextFieldAddonsUIView.swift | 263 --- .../ViewModel/TextFieldAddonsViewModel.swift | 92 - .../TextFieldAddonsViewModelTests.swift | 244 --- .../TextFieldViewModelForAddons.swift | 79 - .../TextFieldViewModelForAddonsTests.swift | 125 -- .../TextField/Enum/TextFieldBorderStyle.swift | 35 - .../TextField/Enum/TextFieldIntent.swift | 16 - ...TextFieldBorderLayout+ExtensionTests.swift | 16 - .../Model/TextFieldBorderLayout.swift | 14 - .../TextFieldColors+ExtensionTests.swift | 26 - .../TextField/Model/TextFieldColors.swift | 23 - .../TextFieldSpacings+ExtensionTests.swift | 16 - .../TextField/Model/TextFieldSpacings.swift | 15 - ...eCasableGeneratedMock+ExtensionTests.swift | 19 - .../TextFieldGetBorderLayoutUseCase.swift | 35 - ...TextFieldGetBorderLayoutUseCaseTests.swift | 87 - ...eCasableGeneratedMock+ExtensionTests.swift | 18 - .../GetColors/TextFieldGetColorsUseCase.swift | 55 - .../TextFieldGetColorsUseCaseTests.swift | 175 -- .../TextFieldGetSpacingsUseCase.swift | 34 - ...sUseCaseGeneratedMock+ExtensionTests.swift | 18 - .../TextFieldGetSpacingsUseCaseTests.swift | 63 - .../TextFieldAccessibilityIdentifier.swift | 16 - .../View/SwiftUI/TextFieldView.swift | 111 -- .../View/SwiftUI/TextFieldViewInternal.swift | 87 - .../View/SwiftUI/TextFieldViewType.swift | 13 - .../TextFieldScenario+SnapshotTests.swift | 183 -- .../View/UIKit/TextFieldUIView.swift | 289 --- .../UIKit/TextFieldUIViewSnapshotTests.swift | 128 -- .../ViewModel/TextFieldViewModel.swift | 170 -- .../ViewModel/TextFieldViewModelTests.swift | 717 ------- .../TextLinkAccessibilityIdentifier.swift | 24 - .../Public/Alignment/TextLinkAlignment.swift | 25 - .../Alignment/TextLinkAlignmentTests.swift | 35 - .../TextLink/Enum/Public/TextLinkIntent.swift | 21 - .../Enum/Public/TextLinkTypography.swift | 39 - .../Enum/Public/TextLinkVariant.swift | 19 - .../ImageSize/TextLinkImageSize.swift | 17 - ...TextLinkImageSizeMock+ExtensionTests.swift | 25 - .../Typographies/TextLinkTypographies.swift | 24 - ...LinkTypographiesMock+ExtensionsTests.swift | 30 - .../TextLinkGetAttributedStringUseCase.swift | 151 -- ...tLinkGetAttributedStringUseCaseTests.swift | 244 --- .../GetColor/TextLinkGetColorUseCase.swift | 50 - .../TextLinkGetColorUseCaseTests.swift | 127 -- .../TextLinkGetImageSizeUseCase.swift | 29 - .../TextLinkGetImageSizeUseCaseTests.swift | 39 - .../TextLinkGetTypographiesUseCase.swift | 89 - .../TextLinkGetTypographiesUseCaseTests.swift | 114 -- .../TextLinkGetUnderlineUseCase.swift | 39 - .../TextLinkGetUnderlineUseCaseTests.swift | 74 - .../TextLinkConfigurationSnapshotTests.swift | 110 -- .../TextLinkScenarioSnapshotTests.swift | 171 -- .../TextLink/View/SwiftUI/TextLinkView.swift | 169 -- .../SwiftUI/TextLinkViewSnapshotTests.swift | 53 - .../TextLink/View/UIKit/TextLinkUIView.swift | 428 ----- .../UIKit/TextLinkUIViewSnapshotTests.swift | 52 - .../ViewModel/TextLinkViewModel.swift | 208 --- .../ViewModel/TextLinkViewModelTests.swift | 801 -------- .../Font/FontTextStyle+Extension.swift | 41 - .../Font/FontTextStyle+ExtensionTests.swift | 43 - .../PropertyWrapper/ScaledUIMetric.swift | 82 - .../PropertyWrapper/ScaledUIMetricTests.swift | 154 -- .../SparkAttributedString.swift | 13 - .../UIFont/UIFontTextStyle+Extension.swift | 41 - .../UIFontTextStyle+ExtensionTests.swift | 43 - .../Theming/Content/Border/Border.swift | 45 - .../Content/Border/BorderDefault.swift | 66 - .../BorderGeneratedMock+ExtensionTests.swift | 39 - .../Colors/ColorToken+ExtensionTests.swift | 15 - ...lorTokenGeneratedMock+ExtensionTests.swift | 78 - .../Content/Colors/ColorTokenTests.swift | 22 - .../Theming/Content/Colors/Colors.swift | 90 - .../Content/Colors/ColorsDefault.swift | 68 - .../ColorsGeneratedMock+ExtensionTests.swift | 28 - .../Colors/Content/Accent/ColorsAccent.swift | 17 - .../Content/Accent/ColorsAccentDefault.swift | 35 - ...rsAccentGeneratedMock+ExtensionTests.swift | 29 - .../Colors/Content/Base/ColorsBase.swift | 35 - .../Content/Base/ColorsBaseDefault.swift | 53 - ...lorsBaseGeneratedMock+ExtensionTests.swift | 36 - .../Colors/Content/Basic/ColorsBasic.swift | 15 - .../Content/Basic/ColorsBasicDefault.swift | 29 - ...orsBasicGeneratedMock+ExtensionTests.swift | 26 - .../Content/Feedback/ColorsFeedback.swift | 46 - .../Feedback/ColorsFeedbackDefault.swift | 77 - ...FeedbackGeneratedMock+ExtensionTests.swift | 45 - .../Colors/Content/Main/ColorsMain.swift | 17 - .../Content/Main/ColorsMainDefault.swift | 35 - ...lorsMainGeneratedMock+ExtensionTests.swift | 29 - .../Colors/Content/States/ColorsStates.swift | 50 - .../Content/States/ColorsStatesDefault.swift | 86 - ...rsStatesGeneratedMock+ExtensionTests.swift | 49 - .../Content/Support/ColorsSupport.swift | 17 - .../Support/ColorsSupportDefault.swift | 35 - ...sSupportGeneratedMock+ExtensionTests.swift | 29 - core/Sources/Theming/Content/Dims/Dims.swift | 25 - .../Theming/Content/Dims/DimsDefault.swift | 34 - .../DimsGeneratedMock+ExtensionTests.swift | 25 - .../Theming/Content/Elevation/Elevation.swift | 11 - .../Content/Elevation/ElevationDefault.swift | 20 - .../Shadow/Drop/ElevationDropShadows.swift | 25 - .../Drop/ElevationDropShadowsDefault.swift | 29 - .../Elevation/Shadow/ElevationShadow.swift | 17 - .../Shadow/ElevationShadowDefault.swift | 32 - .../Shadow/UIView+ElevationShadow.swift | 26 - .../Shadow/View+ElevationShadow.swift | 18 - .../Theming/Content/Layout/Layout.swift | 31 - .../Content/Layout/LayoutDefault.swift | 52 - .../LayoutGeneratedMock+ExtensionTests.swift | 39 - .../Content/Typography/Typography.swift | 44 - .../Typography/TypographyDefault.swift | 108 -- ...pographyGeneratedMock+ExtensionTests.swift | 68 - core/Sources/Theming/Theme/Theme.swift | 20 - core/Sources/Theming/Theme/ThemeDefault.swift | 35 - .../ThemeGeneratedMock+ExtensionTests.swift | 24 - core/Sources/UseCaseDemo.swift | 13 - core/Sources/UseCaseDemoTests.swift | 19 - .../Classes/Publisher/PublisherMock.swift | 61 - .../Publisher/XCTest+PublisherMock.swift | 159 -- core/Unit-tests/ColorSnapshotTests.swift | 75 - ...dStringEither+ExtensionSnapshotTests.swift | 55 - .../ImageEither+ExtensionSnapshotTests.swift | 26 - .../Extensions/Bool+ExtensionsTests.swift | 14 - .../Extensions/Color+ExtensionTests.swift | 21 - .../UITraitCollection+ExtensionTests.swift | 20 - .../Resources/IconographyTests.swift | 51 - .../arrow.imageset/icon.svg | 3 - .../switchOff.imageset/close.svg | 3 - .../switchOn.imageset/check.svg | 3 - .../ColorTokenGeneratedMock+Extensions.swift | 22 - core/Unit-tests/SparkCoreSnapshotTests.swift | 37 - .../SparkCoreSnapshotTestsUtils.swift | 34 - .../ComponentSnapshotTestConstants.swift | 27 - .../Enum/ComponentSnapshotTestMode.swift | 37 - .../ComponentSnapshotTestHelpers.swift | 72 - .../SwiftUIComponentSnapshotTestCase.swift | 63 - .../UIKitComponentSnapshotTestCase.swift | 64 - .../ComponentSnapshotTestCase.swift | 193 -- .../TestCase/SnapshotTestCase.swift | 23 - .../TestCase/SnapshotTestCaseTracker.swift | 101 - core/Unit-tests/TestCase/TestCase.swift | 20 - fastlane/Fastfile | 47 - fastlane/scripts/xcodeproj/generate.sh | 21 - project-ci.yml | 35 - project.yml | 66 +- scripts/swiftgen.sh | 6 - scripts/swiftlint.sh | 5 - .../arrow.imageset/Contents.json | 16 - .../check.imageset/Contents.json | 16 - .../checkbox-selected.imageset/Contents.json | 16 - .../checkbox-selected.imageset/check-fill.svg | 3 - .../close.imageset/Contents.json | 16 - .../Colors.xcassets/Accent/Contents.json | 6 - .../accent-container.colorset/Contents.json | 38 - .../accent-variant.colorset/Contents.json | 38 - .../Accent/accent.colorset/Contents.json | 38 - .../Contents.json | 38 - .../on-accent-variant.colorset/Contents.json | 38 - .../Accent/on-accent.colorset/Contents.json | 38 - .../Colors.xcassets/Base/Contents.json | 6 - .../background-variant.colorset/Contents.json | 38 - .../Base/background.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Base/on-background.colorset/Contents.json | 38 - .../Base/on-overlay.colorset/Contents.json | 38 - .../on-surface-inverse.colorset/Contents.json | 38 - .../Base/on-surface.colorset/Contents.json | 38 - .../Base/outline-high.colorset/Contents.json | 38 - .../Base/outline.colorset/Contents.json | 38 - .../Base/overlay.colorset/Contents.json | 38 - .../surface-inverse.colorset/Contents.json | 38 - .../Base/surface.colorset/Contents.json | 38 - .../Colors.xcassets/Basic/Contents.json | 6 - .../basic-container.colorset/Contents.json | 38 - .../Basic/basic.colorset/Contents.json | 38 - .../on-basic-container.colorset/Contents.json | 38 - .../Basic/on-basic.colorset/Contents.json | 38 - .../Resources/Colors.xcassets/Contents.json | 6 - .../Colors.xcassets/Feedback/Contents.json | 6 - .../alert-container.colorset/Contents.json | 38 - .../Feedback/alert.colorset/Contents.json | 38 - .../error-container.colorset/Contents.json | 38 - .../Feedback/error.colorset/Contents.json | 38 - .../info-container.colorset/Contents.json | 38 - .../Feedback/info.colorset/Contents.json | 38 - .../neutral-container.colorset/Contents.json | 38 - .../Feedback/neutral.colorset/Contents.json | 38 - .../on-alert-container.colorset/Contents.json | 38 - .../Feedback/on-alert.colorset/Contents.json | 38 - .../on-error-container.colorset/Contents.json | 38 - .../Feedback/on-error.colorset/Contents.json | 38 - .../on-info-container.colorset/Contents.json | 38 - .../Feedback/on-info.colorset/Contents.json | 38 - .../Contents.json | 38 - .../on-neutral.colorset/Contents.json | 38 - .../Contents.json | 38 - .../on-success.colorset/Contents.json | 38 - .../success-container.colorset/Contents.json | 38 - .../Feedback/success.colorset/Contents.json | 38 - .../Colors.xcassets/Main/Contents.json | 6 - .../main-container.colorset/Contents.json | 38 - .../Main/main-variant.colorset/Contents.json | 38 - .../Main/main.colorset/Contents.json | 38 - .../on-main-container.colorset/Contents.json | 38 - .../on-main-variant.colorset/Contents.json | 38 - .../Main/on-main.colorset/Contents.json | 38 - .../Colors.xcassets/States/Contents.json | 6 - .../Contents.json | 38 - .../accent-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../alert-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../basic-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../error-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../info-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../main-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../neutral-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../success-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../support-pressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../surface-pressed.colorset/Contents.json | 38 - .../Colors.xcassets/Support/Contents.json | 6 - .../Contents.json | 38 - .../on-support-variant.colorset/Contents.json | 38 - .../Support/on-support.colorset/Contents.json | 38 - .../support-container.colorset/Contents.json | 38 - .../support-variant.colorset/Contents.json | 38 - .../Support/support.colorset/Contents.json | 38 - .../Resources/Font/NunitoSans-Bold.ttf | Bin 141236 -> 0 bytes .../Resources/Font/NunitoSans-Regular.ttf | Bin 139168 -> 0 bytes .../Accent/Contents.json | 6 - .../Contents.json | 38 - .../Contents.json | 38 - .../purple-accent.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../purple-on-accent.colorset/Contents.json | 38 - .../PurpleColors.xcassets/Base/Contents.json | 6 - .../purple-background.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../purple-onOverlay.colorset/Contents.json | 38 - .../purple-onSurface.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-outline.colorset/Contents.json | 38 - .../purple-outlineHigh.colorset/Contents.json | 38 - .../purple-overlay.colorset/Contents.json | 38 - .../purple-surface.colorset/Contents.json | 38 - .../Contents.json | 38 - .../PurpleColors.xcassets/Basic/Contents.json | 6 - .../Contents.json | 38 - .../Basic/purple-basic.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-on-basic.colorset/Contents.json | 38 - .../PurpleColors.xcassets/Contents.json | 6 - .../Feedback/Contents.json | 6 - .../purple-alert.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-error.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-info.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-neutral.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-onAlert.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-onError.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-onInfo.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-onNeutral.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-onSuccess.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-success.colorset/Contents.json | 38 - .../Contents.json | 38 - .../PurpleColors.xcassets/Main/Contents.json | 6 - .../Main/purple-main.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-mainVariant.colorset/Contents.json | 38 - .../Main/purple-onMain.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../States/Contents.json | 6 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../purple-infoPressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../purple-mainPressed.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Support/Contents.json | 6 - .../purple-onSupport.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../purple-support.colorset/Contents.json | 38 - .../Contents.json | 38 - .../Contents.json | 38 - .../Configuration/Bundle+Extension.swift | 46 - .../Configuration/SparkConfiguration.swift | 29 - .../Sources/Theming/Content/SparkBorder.swift | 21 - .../Sources/Theming/Content/SparkColors.swift | 108 -- .../Theming/Content/SparkColorsTests.swift | 52 - .../Theming/Content/SparkElevation.swift | 49 - .../Sources/Theming/Content/SparkLayout.swift | 21 - .../Theming/Content/SparkTypography.swift | 93 - .../Content/SparkTypographyTests.swift | 45 - spark/Sources/Theming/SparkTheme.swift | 31 - spark/Sources/UseCaseDemo.swift | 13 - spark/Sources/UseCaseDemoTests.swift | 19 - spark/Unit-tests/SparkTests.swift | 16 - .../SparkCoreAutoMockTest.stencil | 144 -- .../SparkCoreAutoMockable.stencil | 239 --- .../SparkCoreAutoPublisherTest.stencil | 86 - .../SparkCoreAutoViewModelStub.stencil | 89 - swiftgen.yml | 11 - wiki_assets/badge_anatomy.png | Bin 45509 -> 0 bytes wiki_assets/button_anatomy.png | Bin 55945 -> 0 bytes wiki_assets/checkbox_anatomy.png | Bin 42564 -> 0 bytes wiki_assets/chip_anatomy.png | Bin 31611 -> 0 bytes wiki_assets/iconbutton_anatomy.png | Bin 45619 -> 0 bytes wiki_assets/progress_tracker_anantomy.png | Bin 51376 -> 0 bytes wiki_assets/progressbar_anatomy.png | Bin 25375 -> 0 bytes .../project-installation/current_branch.png | Bin 14162 -> 0 bytes .../project-installation/dependencies.png | Bin 86924 -> 0 bytes wiki_assets/project-installation/target.png | Bin 50908 -> 0 bytes wiki_assets/radiobutton_anatomy.png | Bin 45970 -> 0 bytes wiki_assets/slider_anatomy.png | Bin 14193 -> 0 bytes wiki_assets/spinner_anatomy.png | Bin 32895 -> 0 bytes wiki_assets/switch_anatomy.png | Bin 38691 -> 0 bytes wiki_assets/tab_anatomy.png | Bin 65693 -> 0 bytes wiki_assets/tag_anatomy.png | Bin 29056 -> 0 bytes wiki_assets/textlink_anatomy.png | Bin 3305 -> 0 bytes xcodegen/spark-core-snapshot-tests.yml | 40 - xcodegen/spark-core-unit-tests.yml | 38 - xcodegen/spark-core.yml | 25 - xcodegen/spark-shared.yml | 14 - xcodegen/spark.yml | 99 - 1210 files changed, 714 insertions(+), 71763 deletions(-) rename {spark/Demo => .Demo/App}/AppDelegate.swift (94%) rename {spark/Demo => .Demo/App}/SceneDelegate.swift (96%) rename {spark/Demo => .Demo}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/AppIcon.appiconset/spark-design.png (100%) rename {spark/Demo => .Demo}/Assets.xcassets/BottomSheet.imageset/BottomSheet.png (100%) rename {spark/Demo => .Demo}/Assets.xcassets/BottomSheet.imageset/Contents.json (100%) rename {core/Unit-tests/Resources/IconographyTests.xcassets => .Demo/Assets.xcassets}/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/alert-circle.imageset/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/alert-circle.imageset/alert-outline.svg (100%) rename {spark/Demo => .Demo}/Assets.xcassets/alert.imageset/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/alert.imageset/Style=outlinewarning.svg (100%) rename {core/Unit-tests/Resources/IconographyTests.xcassets => .Demo/Assets.xcassets}/arrow.imageset/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/arrow.imageset/icon.svg (100%) rename {core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset => .Demo/Assets.xcassets/check.imageset}/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/check.imageset/check.svg (100%) rename {core/Unit-tests/Resources/IconographyTests.xcassets => .Demo/Assets.xcassets}/checkbox-selected.imageset/Contents.json (100%) rename {core/Unit-tests/Resources/IconographyTests.xcassets => .Demo/Assets.xcassets}/checkbox-selected.imageset/check-fill.svg (100%) rename {core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset => .Demo/Assets.xcassets/close.imageset}/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/close.imageset/close.svg (100%) rename {spark/Demo => .Demo}/Assets.xcassets/info.imageset/Contents.json (100%) rename {spark/Demo => .Demo}/Assets.xcassets/info.imageset/Style=outlineinfo.svg (100%) rename {spark/Demo => .Demo}/Classes/Enum/ComponentVersion.swift (100%) rename {spark/Demo => .Demo}/Classes/Enum/SpaceContainer.swift (100%) rename {spark/Demo => .Demo}/Classes/Enum/UIComponent.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/CaseIterable-Name.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/DeviceShakeViewModifier.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/EnviromentValues.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/NumberFormatter.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/UIApplication.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/UIDevice+Shake.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/UITextField.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/UIViewController+Present.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/UIWindow+Shake.swift (100%) rename {spark/Demo => .Demo}/Classes/Extension/View+Shake.swift (100%) rename {spark/Demo => .Demo}/Classes/Helper/AnyIsTrue.swift (100%) rename {spark/Demo => .Demo}/Classes/Helper/NSAttributedStringBuilder.swift (100%) rename {spark/Demo/Classes => .Demo/Classes/Iconography}/DemoIconography.swift (96%) rename {spark/Demo => .Demo}/Classes/Tabbar/ConsoleView.swift (94%) rename {spark/Demo => .Demo}/Classes/Tabbar/SparkTabbarController.swift (90%) rename {spark/Demo => .Demo}/Classes/Theme/PurpleContent/PurpleBorder.swift (100%) rename {spark/Demo => .Demo}/Classes/Theme/PurpleContent/PurpleColors.swift (100%) rename {spark/Demo => .Demo}/Classes/Theme/PurpleContent/PurpleElevation.swift (100%) rename {spark/Demo => .Demo}/Classes/Theme/PurpleContent/PurpleLayout.swift (100%) rename {spark/Demo => .Demo}/Classes/Theme/PurpleContent/PurpleTypography.swift (100%) rename {spark/Demo => .Demo}/Classes/Theme/PurpleTheme.swift (100%) rename {spark/Demo => .Demo}/Classes/Theme/SparkThemePublisher.swift (96%) rename {spark/Demo => .Demo}/Classes/View/ComponentVersionViewController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Badge/BadgeFormat-Names.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Badge/BadgePreviewFormatter.swift (91%) rename {spark/Demo => .Demo}/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift (91%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift (82%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoScrollView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoUIController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIViewController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Button/ButtonContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Button/UIKit/ButtonControlType.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/ComponentsCheckboxListView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/ComponentsView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/ComponentsViewController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/IconButton/IconButtonContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/ComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/ComponentUIViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/SwiftUI/Checkbox.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/SwiftUI/EnumSelector.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/SwiftUI/RangeSelector.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/SwiftUI/ThemeSelector.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemUIType.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Constants/ProgressBarConstants.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressTracker/SwiftUI/ProgressTrackerComponent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/RadioButton/RadioButtonGroupState-Extension.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/SwiftUI/RatingComponent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/SwiftUI/RatingInputComponent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/StarComponentUIViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Slider/SwiftUI/SliderComponentView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Switch/Enum/SwitchTextContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift (95%) rename {spark/Demo => .Demo}/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Tab/SwiftUI/TabComponent.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Tab/UIKit/TabComponentUIViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Tag/TagContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/Addons/SwiftUI/TextFieldAddonsComponentView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/Addons/TextFieldAddonContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/SwiftUI/TextFieldComponentView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/TextFieldContentSide.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/TextFieldSideViewContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/Constants/TextLinkConstants.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/TextLinkContent.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift (99%) rename {spark/Demo => .Demo}/Classes/View/Components/TextLink/UIKit/TextLinkControlType.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/BadgeCell/BadgeCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/BadgeCell/BadgeConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ButtonCell/ButtonCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ButtonCell/ButtonConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/CheckboxCell/CheckboxCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/CheckboxCell/CheckboxConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ChipCell/ChipCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ChipCell/ChipConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/IconCell/IconCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/IconCell/IconConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RatingInputCell/RatingInputCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/RatingInputCell/RatingInputConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/SpinnerCell/SpinnerCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/SpinnerCell/SpinnerConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/StarCell/StarCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/StarCell/StarConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/TabCell/TabCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/TabCell/TabConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/TagCell/TagCell.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Cells/TagCell/TagConfiguration.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift (99%) rename {spark/Demo => .Demo}/Classes/View/ListView/Controllers/RadioCheckboxView.swift (98%) rename {spark/Demo => .Demo}/Classes/View/ListView/ListComponentsViewController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/ListView+Protocols.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/ListViewController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/ListViewDatasource.swift (100%) rename {spark/Demo => .Demo}/Classes/View/ListView/ListViewDelegate.swift (100%) rename {spark/Demo => .Demo}/Classes/View/SettingsViewController.swift (100%) rename {spark/Demo => .Demo}/Classes/View/SparkActionSheet.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Border/BorderItemView.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Theme/Border/BorderItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Border/BorderSectionViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Border/BorderView.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Theme/Border/BorderViewModel.swift (98%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/ColorItemView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/ColorItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/ColorView.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/ColorViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift (95%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionAccentViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBaseViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBasicViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionFeedbackViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionMainViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionStatesViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionSupportViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionViewModelable.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Dims/DimItemView.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Theme/Dims/DimItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Dims/DimsView.swift (95%) rename {spark/Demo => .Demo}/Classes/View/Theme/Dims/DimsViewModel.swift (94%) rename {spark/Demo => .Demo}/Classes/View/Theme/Elevation/DropShadow/DropShadowItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift (95%) rename {spark/Demo => .Demo}/Classes/View/Theme/Elevation/ElevationView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Layout/LayoutSpacingItemView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Layout/LayoutSpacingItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Layout/LayoutView.swift (95%) rename {spark/Demo => .Demo}/Classes/View/Theme/Layout/LayoutViewModel.swift (95%) rename {spark/Demo => .Demo}/Classes/View/Theme/ThemeCellModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/ThemeView.swift (97%) rename {spark/Demo => .Demo}/Classes/View/Theme/Typography/TypographyItemView.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Typography/TypographyItemViewModel.swift (100%) rename {spark/Demo => .Demo}/Classes/View/Theme/Typography/TypographyView.swift (96%) rename {spark/Demo => .Demo}/Classes/View/Theme/Typography/TypographyViewModel.swift (98%) rename {spark/Demo/Assets.xcassets => .Demo/Preview Content/Preview Assets.xcassets}/Contents.json (100%) create mode 100644 .gitscript/inject_repository_package.swift rename postGenCommand.sh => .postGenCommand.sh (100%) delete mode 100644 .sourcery.yml delete mode 100644 Gemfile create mode 100644 Package.swift create mode 100644 Sources/Core/Core.swift create mode 100644 Sources/Testing/TestingCore.swift create mode 100644 Tests/CoreTests.swift delete mode 100644 core/Sources/Common/Combine/Global/Publisher+SubscribeExtension.swift delete mode 100644 core/Sources/Common/Combine/Global/UIScheduler.swift delete mode 100644 core/Sources/Common/Combine/Publisher/EventPublisher.swift delete mode 100644 core/Sources/Common/Combine/Publisher/EventPublisherTests.swift delete mode 100644 core/Sources/Common/Combine/Publisher/Publisher-SubscribeTests.swift delete mode 100644 core/Sources/Common/Combine/Publisher/ValueBinding.swift delete mode 100644 core/Sources/Common/Control/PropertyState/ControlPropertyState.swift delete mode 100644 core/Sources/Common/Control/PropertyState/ControlPropertyStateTests.swift delete mode 100644 core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift delete mode 100644 core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift delete mode 100644 core/Sources/Common/Control/State/ControlState.swift delete mode 100644 core/Sources/Common/Control/Status/ControlStatus.swift delete mode 100644 core/Sources/Common/Control/Status/ControlStatusTests.swift delete mode 100644 core/Sources/Common/Control/SwiftUI/ControlStateImage.swift delete mode 100644 core/Sources/Common/Control/SwiftUI/ControlStateImageTests.swift delete mode 100644 core/Sources/Common/Control/SwiftUI/ControlStateText.swift delete mode 100644 core/Sources/Common/Control/SwiftUI/ControlStateTextTests.swift delete mode 100644 core/Sources/Common/Control/UIView/UIControlStateImageView.swift delete mode 100644 core/Sources/Common/Control/UIView/UIControlStateLabel.swift delete mode 100644 core/Sources/Common/DataType/Array-Safe.swift delete mode 100644 core/Sources/Common/DataType/Sequence-Compacted.swift delete mode 100644 core/Sources/Common/DataType/Updateable.swift delete mode 100644 core/Sources/Common/DisplayedText/Enum/DisplayedTextType.swift delete mode 100644 core/Sources/Common/DisplayedText/Enum/DisplayedTextTypeTests.swift delete mode 100644 core/Sources/Common/DisplayedText/Model/DisplayedText+ExtensionTests.swift delete mode 100644 core/Sources/Common/DisplayedText/Model/DisplayedText.swift delete mode 100644 core/Sources/Common/DisplayedText/Model/DisplayedTextTests.swift delete mode 100644 core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCase.swift delete mode 100644 core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCaseTests.swift delete mode 100644 core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCase.swift delete mode 100644 core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCaseTests.swift delete mode 100644 core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModel.swift delete mode 100644 core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModelTests.swift delete mode 100644 core/Sources/Common/Enum/Either/Either.swift delete mode 100644 core/Sources/Common/Enum/Either/EitherTests.swift delete mode 100644 core/Sources/Common/Enum/Either/Type/AttributedStringEither.swift delete mode 100644 core/Sources/Common/Enum/Either/Type/ImageEither.swift delete mode 100644 core/Sources/Common/Enum/Either/Type/ViewEither.swift delete mode 100644 core/Sources/Common/Enum/FrameworkType.swift delete mode 100644 core/Sources/Common/Enum/TextStyle.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGPoint-Distance.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGPointDistanceTests.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGRect-Center.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGRect-Location.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGRectCenterTests.swift delete mode 100644 core/Sources/Common/Foundation/Extension/CGRectLocationTests.swift delete mode 100644 core/Sources/Common/Foundation/Extension/Optional+Extension.swift delete mode 100644 core/Sources/Common/Foundation/Extension/Optional+ExtensionTests.swift delete mode 100644 core/Sources/Common/Foundation/Extension/UIView-Closest.swift delete mode 100644 core/Sources/Common/Foundation/Extension/UIViewClosestTests.swift delete mode 100644 core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+Extension.swift delete mode 100644 core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+ExtensionTests.swift delete mode 100644 core/Sources/Common/SwiftUI/Extension/Shape/Shape+Extension.swift delete mode 100644 core/Sources/Common/SwiftUI/Extension/View/View+ProportionalWidth.swift delete mode 100644 core/Sources/Common/SwiftUI/GlobalExtension/IfModifier/IfModifier.swift delete mode 100644 core/Sources/Common/SwiftUI/Modifier/Accessibility/AccessibilityViewModifier.swift delete mode 100644 core/Sources/Common/SwiftUI/Modifier/Border/BorderViewModifier.swift delete mode 100644 core/Sources/Common/SwiftUI/Modifier/Border/View+BorderExtension.swift delete mode 100644 core/Sources/Common/SwiftUI/View/IsEnabledModifier.swift delete mode 100644 core/Sources/Common/SwiftUI/View/NoButtonStyle.swift delete mode 100644 core/Sources/Common/SwiftUI/View/PressedButtonStyle.swift delete mode 100644 core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManager.swift delete mode 100644 core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManagerTests.swift delete mode 100644 core/Sources/Common/UIKit/Extension/Animation/UIView+ExecuteExtension.swift delete mode 100644 core/Sources/Common/UIKit/Extension/CGSize/CGSize+TraitCollection.swift delete mode 100644 core/Sources/Common/UIKit/Extension/NSLayoutConstraint/NSLayoutConstraint+MultiplierExtension.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIControl/UIControl+Extensions.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+Extension.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+ExtensionTests.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UITraitCollection/UITraitCollection-SizeAppearance.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIView/UIStackView-RemoveAll.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIView/UIView+AccessibilityExtension.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIView/UIView+LayerExtension.swift delete mode 100644 core/Sources/Common/UIKit/Extension/UIView/UIView-Attributes.swift delete mode 100644 core/Sources/Common/UIKit/GlobalExtension/NSLayoutConstraint/NSLayoutConstraint+Extension.swift delete mode 100644 core/Sources/Common/UIKit/GlobalExtension/UIView/UIView+Layout.swift delete mode 100644 core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Badge/Constants/BadgeConstants.swift delete mode 100644 core/Sources/Components/Badge/Properties/Private/BadgeColors.swift delete mode 100644 core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift delete mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift delete mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift delete mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift delete mode 100644 core/Sources/Components/Badge/Properties/Public/BadgePosition.swift delete mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeSize.swift delete mode 100644 core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift delete mode 100644 core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift delete mode 100644 core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift delete mode 100644 core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift delete mode 100644 core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift delete mode 100644 core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift delete mode 100644 core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift delete mode 100644 core/Sources/Components/BottomSheet/SwiftUI/View-Height.swift delete mode 100644 core/Sources/Components/BottomSheet/UIKit/UISheetPresentationController-customHeightDetent.swift delete mode 100644 core/Sources/Components/Button/AccessibilityIdentifier/ButtonAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Button/Constants/ButtonConstants.swift delete mode 100644 core/Sources/Components/Button/Enum/Internal/ButtonType.swift delete mode 100644 core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignment.swift delete mode 100644 core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignmentTests.swift delete mode 100644 core/Sources/Components/Button/Enum/Public/ButtonIntent.swift delete mode 100644 core/Sources/Components/Button/Enum/Public/ButtonShape.swift delete mode 100644 core/Sources/Components/Button/Enum/Public/ButtonSize.swift delete mode 100644 core/Sources/Components/Button/Enum/Public/ButtonVariant.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder+ExtensionTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Colors/ButtonColorsTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColorsTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes+ExtensionTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings+ExtensionTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/State/ButtonState+ExtensionTests.swift delete mode 100644 core/Sources/Components/Button/Properties/Internal/State/ButtonState.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCase.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseTests.swift delete mode 100644 core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseable.swift delete mode 100644 core/Sources/Components/Button/View/Common/ButtonConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/Common/ButtonScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/Common/IconButtonConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/Common/IconButtonScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/SwiftUI/Internal/ButtonContainerView.swift delete mode 100644 core/Sources/Components/Button/View/SwiftUI/Internal/ButtonImageView.swift delete mode 100644 core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonView.swift delete mode 100644 core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonView.swift delete mode 100644 core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/UIKit/Button/ButtonUIView.swift delete mode 100644 core/Sources/Components/Button/View/UIKit/Button/ButtonUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIView.swift delete mode 100644 core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Button/View/UIKit/Main/ButtonMainUIView.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModel.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModelTests.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Button/ButtonViewModel.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Button/ButtonViewModelTests.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModel.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModelTests.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Icon/IconButtonViewModel.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModel.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModelTests.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModel.swift delete mode 100644 core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModelTests.swift delete mode 100644 core/Sources/Components/Checkbox/AccessibilityIdentifier/CheckboxAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Checkbox/Enum/CheckboxAlignment.swift delete mode 100644 core/Sources/Components/Checkbox/Enum/CheckboxGroupLayout.swift delete mode 100644 core/Sources/Components/Checkbox/Enum/CheckboxIntent.swift delete mode 100644 core/Sources/Components/Checkbox/Enum/CheckboxSelectionState.swift delete mode 100644 core/Sources/Components/Checkbox/Enum/SelectButtonState.swift delete mode 100644 core/Sources/Components/Checkbox/Model/CheckboxColors.swift delete mode 100644 core/Sources/Components/Checkbox/Model/CheckboxGroupItemProtocol.swift delete mode 100644 core/Sources/Components/Checkbox/Model/CheckboxGroupViewModel.swift delete mode 100644 core/Sources/Components/Checkbox/Model/CheckboxGroupViewModelTests.swift delete mode 100644 core/Sources/Components/Checkbox/Model/CheckboxViewModel.swift delete mode 100644 core/Sources/Components/Checkbox/Model/CheckboxViewModelTests.swift delete mode 100644 core/Sources/Components/Checkbox/TestHelper/CheckboxConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/TestHelper/CheckboxGroupConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/TestHelper/CheckboxGroupScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/TestHelper/CheckboxScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCase.swift delete mode 100644 core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCaseTests.swift delete mode 100644 core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCase.swift delete mode 100644 core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Checkbox/View/CheckboxGroupItemDefault.swift delete mode 100644 core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupView.swift delete mode 100644 core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/View/SwiftUI/CheckboxView.swift delete mode 100644 core/Sources/Components/Checkbox/View/SwiftUI/CheckboxViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxControlUIView.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIView.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewActionTests.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewDelegate.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxUIView.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewDelegate.swift delete mode 100644 core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Chip/AccessiilityIdentifier/ChipAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Chip/Enum/ChipAlignment.swift delete mode 100644 core/Sources/Components/Chip/Enum/ChipConstants.swift delete mode 100644 core/Sources/Components/Chip/Enum/ChipIntent.swift delete mode 100644 core/Sources/Components/Chip/Enum/ChipVariant.swift delete mode 100644 core/Sources/Components/Chip/Model/ChipContent.swift delete mode 100644 core/Sources/Components/Chip/Model/ChipIntentColors.swift delete mode 100644 core/Sources/Components/Chip/Model/ChipState.swift delete mode 100644 core/Sources/Components/Chip/Model/ChipStateColors.swift delete mode 100644 core/Sources/Components/Chip/Model/ChipStateColorsTests.swift delete mode 100644 core/Sources/Components/Chip/Model/ChipStateTests.swift delete mode 100644 core/Sources/Components/Chip/UseCase/ChipGetColorsUseCase.swift delete mode 100644 core/Sources/Components/Chip/UseCase/ChipGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCase.swift delete mode 100644 core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCase.swift delete mode 100644 core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Chip/View/ChipViewModel.swift delete mode 100644 core/Sources/Components/Chip/View/ChipViewModelTests.swift delete mode 100644 core/Sources/Components/Chip/View/CommonTests/ChipConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Chip/View/CommonTests/ChipScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Chip/View/CommonTests/ChipStateSnapshotTests.swift delete mode 100644 core/Sources/Components/Chip/View/SwiftUI/ChipView.swift delete mode 100644 core/Sources/Components/Chip/View/SwiftUI/ChipViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Chip/View/UIKit/ChipUIView.swift delete mode 100644 core/Sources/Components/Chip/View/UIKit/ChipUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Chip/View/UIKit/ChipUIViewTests.swift delete mode 100644 core/Sources/Components/FormField/AccessibilityIdentifier/FormFieldAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/FormField/Enum/FormFieldFeedbackState.swift delete mode 100644 core/Sources/Components/FormField/Model/FormFieldColors.swift delete mode 100644 core/Sources/Components/FormField/Model/FormFieldViewModel.swift delete mode 100644 core/Sources/Components/FormField/Model/FormFieldViewModelTests.swift delete mode 100644 core/Sources/Components/FormField/TestHelper/FormfieldConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/FormField/TestHelper/FormfieldScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/FormField/UseCase/FormFieldColorsUseCase.swift delete mode 100644 core/Sources/Components/FormField/UseCase/FormFieldColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/FormField/UseCase/FormfieldTitleUseCase.swift delete mode 100644 core/Sources/Components/FormField/UseCase/FormfieldTitleUseCaseTests.swift delete mode 100644 core/Sources/Components/FormField/View/SwiftUI/FormFieldView.swift delete mode 100644 core/Sources/Components/FormField/View/SwiftUI/FormFieldViewSnapshotTests.swift delete mode 100644 core/Sources/Components/FormField/View/UIKit/FormFieldUIView.swift delete mode 100644 core/Sources/Components/FormField/View/UIKit/FormFieldUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Icon/AccessibilityIdentifier/IconAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Icon/Enum/IconIntent.swift delete mode 100644 core/Sources/Components/Icon/Enum/IconSize.swift delete mode 100644 core/Sources/Components/Icon/UseCase/IconGetColorUseCase.swift delete mode 100644 core/Sources/Components/Icon/UseCase/IconGetColorUseCaseTests.swift delete mode 100644 core/Sources/Components/Icon/View/Model/IconViewModel.swift delete mode 100644 core/Sources/Components/Icon/View/Model/IconViewModelTests.swift delete mode 100644 core/Sources/Components/Icon/View/SwiftUI/IconView.swift delete mode 100644 core/Sources/Components/Icon/View/SwiftUI/IconViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Icon/View/UIKit/IconUIView.swift delete mode 100644 core/Sources/Components/Icon/View/UIKit/IconUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/AccessibilityIdentifier/ProgressBarAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/ProgressBar/Constants/ProgressBarConstants.swift delete mode 100644 core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationType.swift delete mode 100644 core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationTypeTests.swift delete mode 100644 core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateStatus.swift delete mode 100644 core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarDoubleIntent.swift delete mode 100644 core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarIntent.swift delete mode 100644 core/Sources/Components/ProgressBar/Enum/ProgressBarShape.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData+ExtensionTests.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColorsTests.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/ProgressBarMainColors.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors.swift delete mode 100644 core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColorsTests.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCase.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCase.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetColors/Protocol/ProgressBarMainGetColorsUseCaseable.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCase.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCase.swift delete mode 100644 core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/Common/ProgressBarConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/Common/ProgressBarScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/SwiftUI/Internal/ProgressBarContentView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleViewSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarIndeterminateView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarViewSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/UIKit/ProgressBarIndeterminateUIView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIView.swift delete mode 100644 core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressBar/View/UIKit/ProgressMainBarUIView.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Double/ProgressBarDoubleViewModel.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModel.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModelTests.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModel.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModelTests.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Single/ProgressBarViewModel.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModel.swift delete mode 100644 core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModelTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifierTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/Enum/ProgressTrackerIntent.swift delete mode 100644 core/Sources/Components/ProgressTracker/Enum/ProgressTrackerInteractionState.swift delete mode 100644 core/Sources/Components/ProgressTracker/Enum/ProgressTrackerOrientation.swift delete mode 100644 core/Sources/Components/ProgressTracker/Enum/ProgressTrackerSize.swift delete mode 100644 core/Sources/Components/ProgressTracker/Enum/ProgressTrackerVariant.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerColors.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerConstants.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerContent.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerContentTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerSizePreferences.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerSpacing.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerState.swift delete mode 100644 core/Sources/Components/ProgressTracker/Model/ProgressTrackerTintedColors.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCase.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCase.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCase.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCase.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCase.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCaseTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetVariantColorsUseCaseable.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModel.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModelTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModel.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModelTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/ProgressTrackerViewModel.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/ProgressTrackerViewModelTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerAccessibilityTraitsViewModifier.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerContinuousGestureHandlerTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerDiscreteGestureHandlerTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerGestureHandler.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerHorizontalView.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndependentGestureHandlerTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndicatorView.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerTrackView.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerVerticalView.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerView.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerAccessibilityUIControl.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerContinuousUITouchHandlerTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerDiscreteUITouchHandlerTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndependentUITouchHandlerTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndicatorUIControl.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerTrackUIView.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControl.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControlSnapshotTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandler.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerCreationTests.swift delete mode 100644 core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerTests.swift delete mode 100644 core/Sources/Components/RadioButton/AccessibilityIdentifier/RadioButtonAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/RadioButton/Enum/RadioButtonGroupLayout.swift delete mode 100644 core/Sources/Components/RadioButton/Enum/RadioButtonIntent.swift delete mode 100644 core/Sources/Components/RadioButton/Enum/RadioButtonLabelPosition.swift delete mode 100644 core/Sources/Components/RadioButton/Enum/RadioButtonState.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Internal/RadioButtonAttributes.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Internal/RadioButtonColors.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Internal/RadioButtonConstants.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Internal/RadioButtonGroupContent.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Internal/RadioButtonStateAttribute.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Public/RadioButtonItem.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItem.swift delete mode 100644 core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItemTests.swift delete mode 100644 core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCase.swift delete mode 100644 core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCaseTests.swift delete mode 100644 core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCase.swift delete mode 100644 core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCase.swift delete mode 100644 core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCaseTests.swift delete mode 100644 core/Sources/Components/RadioButton/View/RadioButtonGroupViewModel.swift delete mode 100644 core/Sources/Components/RadioButton/View/RadioButtonGroupViewModelTests.swift delete mode 100644 core/Sources/Components/RadioButton/View/RadioButtonViewModel.swift delete mode 100644 core/Sources/Components/RadioButton/View/RadioButtonViewModelTests.swift delete mode 100644 core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonGroupView.swift delete mode 100644 core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonView.swift delete mode 100644 core/Sources/Components/RadioButton/View/UIKit/RadioButtonToggleUIView.swift delete mode 100644 core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupView.swift delete mode 100644 core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupViewDelegate.swift delete mode 100644 core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIView.swift delete mode 100644 core/Sources/Components/Rating/AccessibilityIdentifier/RatingDisplayAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Rating/AccessibilityIdentifier/RatingInputAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Rating/Cache/CGLayerCache.swift delete mode 100644 core/Sources/Components/Rating/Enum/RatingDisplaySize.swift delete mode 100644 core/Sources/Components/Rating/Enum/RatingIntent.swift delete mode 100644 core/Sources/Components/Rating/Enum/RatingStarsCount.swift delete mode 100644 core/Sources/Components/Rating/Enum/StarDefaults.swift delete mode 100644 core/Sources/Components/Rating/Enum/StarFillMode.swift delete mode 100644 core/Sources/Components/Rating/Enum/StarFillModeUnitTests.swift delete mode 100644 core/Sources/Components/Rating/Graphics/ShapeLayer.swift delete mode 100644 core/Sources/Components/Rating/Graphics/Star.swift delete mode 100644 core/Sources/Components/Rating/Model/RatingColors.swift delete mode 100644 core/Sources/Components/Rating/Model/RatingSizeAttributes.swift delete mode 100644 core/Sources/Components/Rating/Model/RatingState.swift delete mode 100644 core/Sources/Components/Rating/Model/StarConfiguration.swift delete mode 100644 core/Sources/Components/Rating/TestHelpers/RatingDisplayConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/TestHelpers/RatingDisplayScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/TestHelpers/RatingInputConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/TestHelpers/RatingInputScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/UseCases/RatingGetColorsUseCase.swift delete mode 100644 core/Sources/Components/Rating/UseCases/RatingGetColorsUseCaseUnitTests.swift delete mode 100644 core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCase.swift delete mode 100644 core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCaseTests.swift delete mode 100644 core/Sources/Components/Rating/View/RatingDisplayViewModel.swift delete mode 100644 core/Sources/Components/Rating/View/RatingDisplayViewModelTests.swift delete mode 100644 core/Sources/Components/Rating/View/SwiftUI/RatingDisplayView.swift delete mode 100644 core/Sources/Components/Rating/View/SwiftUI/RatingDisplayViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/View/SwiftUI/RatingInputView.swift delete mode 100644 core/Sources/Components/Rating/View/SwiftUI/RatingInputViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/View/SwiftUI/StarShape.swift delete mode 100644 core/Sources/Components/Rating/View/SwiftUI/StarView.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/RatingDisplayUIView.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/RatingDisplayUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/RatingInputUIView.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/RatingInputUIViewDelegate.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/RatingInputUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/StarUIView.swift delete mode 100644 core/Sources/Components/Rating/View/UIKit/StarUIViewTests.swift delete mode 100644 core/Sources/Components/Slider/AccessibilityIdentifiier/SliderAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Slider/Constant/SliderConstants.swift delete mode 100644 core/Sources/Components/Slider/Handle/View/SliderHandle.swift delete mode 100644 core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift delete mode 100644 core/Sources/Components/Slider/Handle/ViewModel/SliderHandleViewModel.swift delete mode 100644 core/Sources/Components/Slider/Properties/Private/SliderColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Slider/Properties/Private/SliderColors.swift delete mode 100644 core/Sources/Components/Slider/Properties/Private/SliderColorsTests.swift delete mode 100644 core/Sources/Components/Slider/Properties/Private/SliderRadii+ExtensionTests.swift delete mode 100644 core/Sources/Components/Slider/Properties/Private/SliderRadii.swift delete mode 100644 core/Sources/Components/Slider/Properties/Public/SliderIntent.swift delete mode 100644 core/Sources/Components/Slider/Properties/Public/SliderShape.swift delete mode 100644 core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateStepsUseCase.swift delete mode 100644 core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateValuesFromStepUseCaseTests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueInBoundsUseCase.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCasableMock+Tests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCaseTests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCasableGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCase.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCasableGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCase.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCaseTests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCasableMock+Tests.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCase.swift delete mode 100644 core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCaseTests.swift delete mode 100644 core/Sources/Components/Slider/View/SliderScenario+SnapshotTests.swift delete mode 100644 core/Sources/Components/Slider/View/SwiftUI/Slider.swift delete mode 100644 core/Sources/Components/Slider/View/UIKit/SliderUIControl.swift delete mode 100644 core/Sources/Components/Slider/View/UIKit/SliderUIControlSnapshotTests.swift delete mode 100644 core/Sources/Components/Slider/ViewModel/Base/SliderViewModel.swift delete mode 100644 core/Sources/Components/Slider/ViewModel/Base/SliderViewModelTests.swift delete mode 100644 core/Sources/Components/Slider/ViewModel/Base/SliderViewModelWithMocksTests.swift delete mode 100644 core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModel.swift delete mode 100644 core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModelTests.swift delete mode 100644 core/Sources/Components/Spinner/AccessibilityIdentifiier/SpinnerAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Spinner/Enum/SpinnerIntent.swift delete mode 100644 core/Sources/Components/Spinner/Enum/SpinnerSize.swift delete mode 100644 core/Sources/Components/Spinner/SpinnerViewModel.swift delete mode 100644 core/Sources/Components/Spinner/SpinnerViewModelTests.swift delete mode 100644 core/Sources/Components/Spinner/SwiftUI/SpinnerView.swift delete mode 100644 core/Sources/Components/Spinner/UIKit/SpinnerUIView.swift delete mode 100644 core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCase.swift delete mode 100644 core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/AccessibilityIdentifier/SwitchAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Switch/Constants/SwitchConstants.swift delete mode 100644 core/Sources/Components/Switch/Either/SwitchImagesEither.swift delete mode 100644 core/Sources/Components/Switch/Enum/SwitchAlignment.swift delete mode 100644 core/Sources/Components/Switch/Enum/SwitchIntent.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Colors/SwitchColorsTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColorsTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState+ExtensionTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition+ExtensionTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState+ExtensionTests.swift delete mode 100644 core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState.swift delete mode 100644 core/Sources/Components/Switch/Model/Public/Images/SwitchImages.swift delete mode 100644 core/Sources/Components/Switch/Model/Public/Images/SwitchUIImages.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCase.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCase.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCase.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCase.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCase.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCase.swift delete mode 100644 core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCaseTests.swift delete mode 100644 core/Sources/Components/Switch/View/Common/SwitchSutSnapshotTests.swift delete mode 100644 core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift delete mode 100644 core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift delete mode 100644 core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift delete mode 100644 core/Sources/Components/Switch/View/SwiftUI/SwitchViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Switch/View/UIKit/SwitchUIView.swift delete mode 100644 core/Sources/Components/Switch/View/UIKit/SwitchUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Switch/ViewModel/SwitchViewModel.swift delete mode 100644 core/Sources/Components/Switch/ViewModel/SwitchViewModelDependencies.swift delete mode 100644 core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift delete mode 100644 core/Sources/Components/Tab/AccessibilityIdentifier/TabAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Tab/Enum/TabIntent.swift delete mode 100644 core/Sources/Components/Tab/Enum/TabSize.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabItemColors.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabItemContent.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabItemHeights.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabItemSpacings.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabState.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabStateAttributes.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabUIItemContent.swift delete mode 100644 core/Sources/Components/Tab/Properties/TabsAttributes.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabGetFontUseCase.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabGetFontUseCaseTests.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCase.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCaseTests.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCase.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCaseTests.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCase.swift delete mode 100644 core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCaseTests.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabApportionsSizeView.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabBackgroundLine.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabEqualSizeView.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabItemView.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabItemViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabSingleItem.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabView.swift delete mode 100644 core/Sources/Components/Tab/View/SwiftUI/TabViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabItemUIView.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabItemUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabItemUIViewTests.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabUIView.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabUIViewDelegate.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Tab/View/UIKit/TabUIViewTests.swift delete mode 100644 core/Sources/Components/Tab/ViewModel/TabContainerViewModel.swift delete mode 100644 core/Sources/Components/Tab/ViewModel/TabItemViewModel.swift delete mode 100644 core/Sources/Components/Tab/ViewModel/TabItemViewModelTests.swift delete mode 100644 core/Sources/Components/Tab/ViewModel/TabViewModel.swift delete mode 100644 core/Sources/Components/Tab/ViewModel/TabViewModelTests.swift delete mode 100644 core/Sources/Components/Tag/AccessibilityIdentifier/TagAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/Tag/Constants/TagConstants.swift delete mode 100644 core/Sources/Components/Tag/Enum/TagIntent.swift delete mode 100644 core/Sources/Components/Tag/Enum/TagVariant.swift delete mode 100644 core/Sources/Components/Tag/Model/Colors/TagColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Tag/Model/Colors/TagColors.swift delete mode 100644 core/Sources/Components/Tag/Model/Colors/TagColorsTests.swift delete mode 100644 core/Sources/Components/Tag/Model/ContentColors/TagContentColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/Tag/Model/ContentColors/TagContentColors.swift delete mode 100644 core/Sources/Components/Tag/Model/ContentColors/TagContentColorsTests.swift delete mode 100644 core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCase.swift delete mode 100644 core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Tag/UseCase/GetContentColors/TagGetContentColorsUseCase.swift delete mode 100644 core/Sources/Components/Tag/UseCase/GetContentColors/TagGetIntentColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/Tag/View/Common/TagConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/Tag/View/Common/TagScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/Tag/View/SwiftUI/TagView.swift delete mode 100644 core/Sources/Components/Tag/View/SwiftUI/TagViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Tag/View/UIKit/TagUIView.swift delete mode 100644 core/Sources/Components/Tag/View/UIKit/TagUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/Tag/ViewModel/TagViewModel.swift delete mode 100644 core/Sources/Components/Tag/ViewModel/TagViewModelTests.swift delete mode 100644 core/Sources/Components/TextField/Addons/View/AccessibilityIdentifiier/TextFieldAddonsAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddon.swift delete mode 100644 core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddons.swift delete mode 100644 core/Sources/Components/TextField/Addons/View/UIKit/TextFieldAddonsUIView.swift delete mode 100644 core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModel.swift delete mode 100644 core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModelTests.swift delete mode 100644 core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddons.swift delete mode 100644 core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddonsTests.swift delete mode 100644 core/Sources/Components/TextField/Enum/TextFieldBorderStyle.swift delete mode 100644 core/Sources/Components/TextField/Enum/TextFieldIntent.swift delete mode 100644 core/Sources/Components/TextField/Model/TextFieldBorderLayout+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextField/Model/TextFieldBorderLayout.swift delete mode 100644 core/Sources/Components/TextField/Model/TextFieldColors+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextField/Model/TextFieldColors.swift delete mode 100644 core/Sources/Components/TextField/Model/TextFieldSpacings+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextField/Model/TextFieldSpacings.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCasableGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCase.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCaseTests.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCasableGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCase.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCaseTests.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCase.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseTests.swift delete mode 100644 core/Sources/Components/TextField/View/AccessibilityIdentifiier/TextFieldAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/TextField/View/SwiftUI/TextFieldView.swift delete mode 100644 core/Sources/Components/TextField/View/SwiftUI/TextFieldViewInternal.swift delete mode 100644 core/Sources/Components/TextField/View/SwiftUI/TextFieldViewType.swift delete mode 100644 core/Sources/Components/TextField/View/TextFieldScenario+SnapshotTests.swift delete mode 100644 core/Sources/Components/TextField/View/UIKit/TextFieldUIView.swift delete mode 100644 core/Sources/Components/TextField/View/UIKit/TextFieldUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/TextField/ViewModel/TextFieldViewModel.swift delete mode 100644 core/Sources/Components/TextField/ViewModel/TextFieldViewModelTests.swift delete mode 100644 core/Sources/Components/TextLink/AccessibilityIdentifier/TextLinkAccessibilityIdentifier.swift delete mode 100644 core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignment.swift delete mode 100644 core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignmentTests.swift delete mode 100644 core/Sources/Components/TextLink/Enum/Public/TextLinkIntent.swift delete mode 100644 core/Sources/Components/TextLink/Enum/Public/TextLinkTypography.swift delete mode 100644 core/Sources/Components/TextLink/Enum/Public/TextLinkVariant.swift delete mode 100644 core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSize.swift delete mode 100644 core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSizeMock+ExtensionTests.swift delete mode 100644 core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographies.swift delete mode 100644 core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographiesMock+ExtensionsTests.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCase.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCaseTests.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCase.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCaseTests.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCase.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCaseTests.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCase.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCaseTests.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCase.swift delete mode 100644 core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCaseTests.swift delete mode 100644 core/Sources/Components/TextLink/View/Common/TextLinkConfigurationSnapshotTests.swift delete mode 100644 core/Sources/Components/TextLink/View/Common/TextLinkScenarioSnapshotTests.swift delete mode 100644 core/Sources/Components/TextLink/View/SwiftUI/TextLinkView.swift delete mode 100644 core/Sources/Components/TextLink/View/SwiftUI/TextLinkViewSnapshotTests.swift delete mode 100644 core/Sources/Components/TextLink/View/UIKit/TextLinkUIView.swift delete mode 100644 core/Sources/Components/TextLink/View/UIKit/TextLinkUIViewSnapshotTests.swift delete mode 100644 core/Sources/Components/TextLink/ViewModel/TextLinkViewModel.swift delete mode 100644 core/Sources/Components/TextLink/ViewModel/TextLinkViewModelTests.swift delete mode 100644 core/Sources/Extension/Font/FontTextStyle+Extension.swift delete mode 100644 core/Sources/Extension/Font/FontTextStyle+ExtensionTests.swift delete mode 100644 core/Sources/Extension/PropertyWrapper/ScaledUIMetric.swift delete mode 100644 core/Sources/Extension/PropertyWrapper/ScaledUIMetricTests.swift delete mode 100644 core/Sources/Extension/SparkAttributedString/SparkAttributedString.swift delete mode 100644 core/Sources/Extension/UIFont/UIFontTextStyle+Extension.swift delete mode 100644 core/Sources/Extension/UIFont/UIFontTextStyle+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Border/Border.swift delete mode 100644 core/Sources/Theming/Content/Border/BorderDefault.swift delete mode 100644 core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/ColorToken+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/ColorTokenGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/ColorTokenTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Colors.swift delete mode 100644 core/Sources/Theming/Content/Colors/ColorsDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/ColorsGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccent.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Base/ColorsBase.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasic.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedback.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Main/ColorsMain.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Main/ColorsMainDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Main/ColorsMainGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/States/ColorsStates.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/States/ColorsStatesDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/States/ColorsStatesGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Support/ColorsSupport.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportDefault.swift delete mode 100644 core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Dims/Dims.swift delete mode 100644 core/Sources/Theming/Content/Dims/DimsDefault.swift delete mode 100644 core/Sources/Theming/Content/Dims/DimsGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Elevation.swift delete mode 100644 core/Sources/Theming/Content/Elevation/ElevationDefault.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadows.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadowsDefault.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Shadow/ElevationShadow.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Shadow/ElevationShadowDefault.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Shadow/UIView+ElevationShadow.swift delete mode 100644 core/Sources/Theming/Content/Elevation/Shadow/View+ElevationShadow.swift delete mode 100644 core/Sources/Theming/Content/Layout/Layout.swift delete mode 100644 core/Sources/Theming/Content/Layout/LayoutDefault.swift delete mode 100644 core/Sources/Theming/Content/Layout/LayoutGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Content/Typography/Typography.swift delete mode 100644 core/Sources/Theming/Content/Typography/TypographyDefault.swift delete mode 100644 core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/Theming/Theme/Theme.swift delete mode 100644 core/Sources/Theming/Theme/ThemeDefault.swift delete mode 100644 core/Sources/Theming/Theme/ThemeGeneratedMock+ExtensionTests.swift delete mode 100644 core/Sources/UseCaseDemo.swift delete mode 100644 core/Sources/UseCaseDemoTests.swift delete mode 100644 core/Unit-tests/Classes/Publisher/PublisherMock.swift delete mode 100644 core/Unit-tests/Classes/Publisher/XCTest+PublisherMock.swift delete mode 100644 core/Unit-tests/ColorSnapshotTests.swift delete mode 100644 core/Unit-tests/Either/AttributedStringEither+ExtensionSnapshotTests.swift delete mode 100644 core/Unit-tests/Either/ImageEither+ExtensionSnapshotTests.swift delete mode 100644 core/Unit-tests/Extensions/Bool+ExtensionsTests.swift delete mode 100644 core/Unit-tests/Extensions/Color+ExtensionTests.swift delete mode 100644 core/Unit-tests/Extensions/UITraitCollection+ExtensionTests.swift delete mode 100644 core/Unit-tests/Resources/IconographyTests.swift delete mode 100644 core/Unit-tests/Resources/IconographyTests.xcassets/arrow.imageset/icon.svg delete mode 100644 core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset/close.svg delete mode 100644 core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset/check.svg delete mode 100644 core/Unit-tests/Sourcery/ColorTokenGeneratedMock+Extensions.swift delete mode 100644 core/Unit-tests/SparkCoreSnapshotTests.swift delete mode 100644 core/Unit-tests/SparkCoreSnapshotTestsUtils.swift delete mode 100644 core/Unit-tests/TestCase/Component/Constants/ComponentSnapshotTestConstants.swift delete mode 100644 core/Unit-tests/TestCase/Component/Enum/ComponentSnapshotTestMode.swift delete mode 100644 core/Unit-tests/TestCase/Component/Helpers/ComponentSnapshotTestHelpers.swift delete mode 100644 core/Unit-tests/TestCase/Component/TestCase/SwiftUIComponentSnapshotTestCase.swift delete mode 100644 core/Unit-tests/TestCase/Component/TestCase/UIKitComponentSnapshotTestCase.swift delete mode 100644 core/Unit-tests/TestCase/Deprecated/ComponentSnapshotTestCase.swift delete mode 100644 core/Unit-tests/TestCase/SnapshotTestCase.swift delete mode 100644 core/Unit-tests/TestCase/SnapshotTestCaseTracker.swift delete mode 100644 core/Unit-tests/TestCase/TestCase.swift delete mode 100644 fastlane/Fastfile delete mode 100755 fastlane/scripts/xcodeproj/generate.sh delete mode 100644 project-ci.yml delete mode 100755 scripts/swiftgen.sh delete mode 100755 scripts/swiftlint.sh delete mode 100644 spark/Demo/Assets.xcassets/arrow.imageset/Contents.json delete mode 100644 spark/Demo/Assets.xcassets/check.imageset/Contents.json delete mode 100644 spark/Demo/Assets.xcassets/checkbox-selected.imageset/Contents.json delete mode 100644 spark/Demo/Assets.xcassets/checkbox-selected.imageset/check-fill.svg delete mode 100644 spark/Demo/Assets.xcassets/close.imageset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/accent-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/accent-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/accent.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/on-accent-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/on-accent-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Accent/on-accent.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/background-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/background.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/on-background-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/on-background.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/on-overlay.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/on-surface-inverse.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/on-surface.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/outline-high.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/outline.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/overlay.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/surface-inverse.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Base/surface.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Basic/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Basic/basic-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Basic/basic.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Basic/on-basic-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Basic/on-basic.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/alert-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/alert.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/error-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/error.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/info-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/info.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/neutral-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/neutral.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-alert-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-alert.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-error-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-error.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-info-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-info.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-success-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/on-success.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/success-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Feedback/success.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/main-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/main-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/main.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/on-main-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/on-main-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Main/on-main.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/accent-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/accent-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/accent-variant-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/alert-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/alert-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/basic-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/basic-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/error-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/error-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/info-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/info-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/main-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/main-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/main-variant-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/neutral-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/neutral-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/success-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/success-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/support-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/support-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/support-variant-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/surface-inverse-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/States/surface-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/on-support-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/on-support-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/on-support.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/support-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/support-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Colors.xcassets/Support/support.colorset/Contents.json delete mode 100644 spark/Sources/Resources/Font/NunitoSans-Bold.ttf delete mode 100644 spark/Sources/Resources/Font/NunitoSans-Regular.ttf delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/purple-accent-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/purple-accent-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/purple-accent.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/purple-on-accent-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/purple-on-accent-variant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Accent/purple-on-accent.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-background.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-backgroundVariant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-onBackground.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-onBackgroundVariant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-onOverlay.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-onSurface.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-onSurfaceInverse.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-outline.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-outlineHigh.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-overlay.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-surface.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Base/purple-surfaceInverse.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Basic/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Basic/purple-basic-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Basic/purple-basic.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Basic/purple-on-basic-container.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Basic/purple-on-basic.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-alert.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-alertContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-error.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-errorContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-info.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-infoContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-neutral.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-neutralContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onAlert.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onAlertContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onError.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onErrorContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onInfo.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onInfoContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onNeutral.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onNeutralContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onSuccess.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-onSuccessContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-success.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Feedback/purple-successContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/purple-main.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/purple-mainContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/purple-mainVariant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/purple-onMain.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/purple-onMainContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Main/purple-onMainVariant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-accent-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-accent-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-accent-variant-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-alertContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-alertPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-basic-container-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-basic-pressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-errorContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-errorPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-infoContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-infoPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-mainContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-mainPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-mainVariantPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-neutralContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-neutralPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-successContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-successPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-supportContainerPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-supportPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-supportVariantPressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-surfaceInversePressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/States/purple-surfacePressed.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/purple-onSupport.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/purple-onSupportContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/purple-onSupportVariant.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/purple-support.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/purple-supportContainer.colorset/Contents.json delete mode 100644 spark/Sources/Resources/PurpleColors.xcassets/Support/purple-supportVariant.colorset/Contents.json delete mode 100644 spark/Sources/Theming/Configuration/Bundle+Extension.swift delete mode 100644 spark/Sources/Theming/Configuration/SparkConfiguration.swift delete mode 100644 spark/Sources/Theming/Content/SparkBorder.swift delete mode 100644 spark/Sources/Theming/Content/SparkColors.swift delete mode 100644 spark/Sources/Theming/Content/SparkColorsTests.swift delete mode 100644 spark/Sources/Theming/Content/SparkElevation.swift delete mode 100644 spark/Sources/Theming/Content/SparkLayout.swift delete mode 100644 spark/Sources/Theming/Content/SparkTypography.swift delete mode 100644 spark/Sources/Theming/Content/SparkTypographyTests.swift delete mode 100644 spark/Sources/Theming/SparkTheme.swift delete mode 100644 spark/Sources/UseCaseDemo.swift delete mode 100644 spark/Sources/UseCaseDemoTests.swift delete mode 100644 spark/Unit-tests/SparkTests.swift delete mode 100644 stencil/sourcery-template/SparkCoreAutoMockTest.stencil delete mode 100644 stencil/sourcery-template/SparkCoreAutoMockable.stencil delete mode 100644 stencil/sourcery-template/SparkCoreAutoPublisherTest.stencil delete mode 100644 stencil/sourcery-template/SparkCoreAutoViewModelStub.stencil delete mode 100644 swiftgen.yml delete mode 100644 wiki_assets/badge_anatomy.png delete mode 100644 wiki_assets/button_anatomy.png delete mode 100644 wiki_assets/checkbox_anatomy.png delete mode 100644 wiki_assets/chip_anatomy.png delete mode 100644 wiki_assets/iconbutton_anatomy.png delete mode 100644 wiki_assets/progress_tracker_anantomy.png delete mode 100644 wiki_assets/progressbar_anatomy.png delete mode 100644 wiki_assets/project-installation/current_branch.png delete mode 100644 wiki_assets/project-installation/dependencies.png delete mode 100644 wiki_assets/project-installation/target.png delete mode 100644 wiki_assets/radiobutton_anatomy.png delete mode 100644 wiki_assets/slider_anatomy.png delete mode 100644 wiki_assets/spinner_anatomy.png delete mode 100644 wiki_assets/switch_anatomy.png delete mode 100644 wiki_assets/tab_anatomy.png delete mode 100644 wiki_assets/tag_anatomy.png delete mode 100644 wiki_assets/textlink_anatomy.png delete mode 100644 xcodegen/spark-core-snapshot-tests.yml delete mode 100644 xcodegen/spark-core-unit-tests.yml delete mode 100644 xcodegen/spark-core.yml delete mode 100644 xcodegen/spark-shared.yml delete mode 100644 xcodegen/spark.yml diff --git a/spark/Demo/AppDelegate.swift b/.Demo/App/AppDelegate.swift similarity index 94% rename from spark/Demo/AppDelegate.swift rename to .Demo/App/AppDelegate.swift index 449498e72..d1275c8c9 100644 --- a/spark/Demo/AppDelegate.swift +++ b/.Demo/App/AppDelegate.swift @@ -7,6 +7,8 @@ // import UIKit +@_exported import SparkCore +@_exported import SparkCoreTesting /// Appdelegate was added for starting app with a viewcontroller. So It help us to integrate UIkit components to UIViewController class diretly. SwiftUI components are working on UIHostingController class more stablize than UIkit components that was integrated with a UIViewRepresentable class. @main diff --git a/spark/Demo/SceneDelegate.swift b/.Demo/App/SceneDelegate.swift similarity index 96% rename from spark/Demo/SceneDelegate.swift rename to .Demo/App/SceneDelegate.swift index 6c698e88e..7b7732f45 100644 --- a/spark/Demo/SceneDelegate.swift +++ b/.Demo/App/SceneDelegate.swift @@ -8,8 +8,6 @@ import UIKit -// swiftlint:disable all - class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? diff --git a/spark/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/.Demo/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from spark/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json rename to .Demo/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/spark/Demo/Assets.xcassets/AppIcon.appiconset/spark-design.png b/.Demo/Assets.xcassets/AppIcon.appiconset/spark-design.png similarity index 100% rename from spark/Demo/Assets.xcassets/AppIcon.appiconset/spark-design.png rename to .Demo/Assets.xcassets/AppIcon.appiconset/spark-design.png diff --git a/spark/Demo/Assets.xcassets/BottomSheet.imageset/BottomSheet.png b/.Demo/Assets.xcassets/BottomSheet.imageset/BottomSheet.png similarity index 100% rename from spark/Demo/Assets.xcassets/BottomSheet.imageset/BottomSheet.png rename to .Demo/Assets.xcassets/BottomSheet.imageset/BottomSheet.png diff --git a/spark/Demo/Assets.xcassets/BottomSheet.imageset/Contents.json b/.Demo/Assets.xcassets/BottomSheet.imageset/Contents.json similarity index 100% rename from spark/Demo/Assets.xcassets/BottomSheet.imageset/Contents.json rename to .Demo/Assets.xcassets/BottomSheet.imageset/Contents.json diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/Contents.json b/.Demo/Assets.xcassets/Contents.json similarity index 100% rename from core/Unit-tests/Resources/IconographyTests.xcassets/Contents.json rename to .Demo/Assets.xcassets/Contents.json diff --git a/spark/Demo/Assets.xcassets/alert-circle.imageset/Contents.json b/.Demo/Assets.xcassets/alert-circle.imageset/Contents.json similarity index 100% rename from spark/Demo/Assets.xcassets/alert-circle.imageset/Contents.json rename to .Demo/Assets.xcassets/alert-circle.imageset/Contents.json diff --git a/spark/Demo/Assets.xcassets/alert-circle.imageset/alert-outline.svg b/.Demo/Assets.xcassets/alert-circle.imageset/alert-outline.svg similarity index 100% rename from spark/Demo/Assets.xcassets/alert-circle.imageset/alert-outline.svg rename to .Demo/Assets.xcassets/alert-circle.imageset/alert-outline.svg diff --git a/spark/Demo/Assets.xcassets/alert.imageset/Contents.json b/.Demo/Assets.xcassets/alert.imageset/Contents.json similarity index 100% rename from spark/Demo/Assets.xcassets/alert.imageset/Contents.json rename to .Demo/Assets.xcassets/alert.imageset/Contents.json diff --git a/spark/Demo/Assets.xcassets/alert.imageset/Style=outlinewarning.svg b/.Demo/Assets.xcassets/alert.imageset/Style=outlinewarning.svg similarity index 100% rename from spark/Demo/Assets.xcassets/alert.imageset/Style=outlinewarning.svg rename to .Demo/Assets.xcassets/alert.imageset/Style=outlinewarning.svg diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/arrow.imageset/Contents.json b/.Demo/Assets.xcassets/arrow.imageset/Contents.json similarity index 100% rename from core/Unit-tests/Resources/IconographyTests.xcassets/arrow.imageset/Contents.json rename to .Demo/Assets.xcassets/arrow.imageset/Contents.json diff --git a/spark/Demo/Assets.xcassets/arrow.imageset/icon.svg b/.Demo/Assets.xcassets/arrow.imageset/icon.svg similarity index 100% rename from spark/Demo/Assets.xcassets/arrow.imageset/icon.svg rename to .Demo/Assets.xcassets/arrow.imageset/icon.svg diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset/Contents.json b/.Demo/Assets.xcassets/check.imageset/Contents.json similarity index 100% rename from core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset/Contents.json rename to .Demo/Assets.xcassets/check.imageset/Contents.json diff --git a/spark/Demo/Assets.xcassets/check.imageset/check.svg b/.Demo/Assets.xcassets/check.imageset/check.svg similarity index 100% rename from spark/Demo/Assets.xcassets/check.imageset/check.svg rename to .Demo/Assets.xcassets/check.imageset/check.svg diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/checkbox-selected.imageset/Contents.json b/.Demo/Assets.xcassets/checkbox-selected.imageset/Contents.json similarity index 100% rename from core/Unit-tests/Resources/IconographyTests.xcassets/checkbox-selected.imageset/Contents.json rename to .Demo/Assets.xcassets/checkbox-selected.imageset/Contents.json diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/checkbox-selected.imageset/check-fill.svg b/.Demo/Assets.xcassets/checkbox-selected.imageset/check-fill.svg similarity index 100% rename from core/Unit-tests/Resources/IconographyTests.xcassets/checkbox-selected.imageset/check-fill.svg rename to .Demo/Assets.xcassets/checkbox-selected.imageset/check-fill.svg diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset/Contents.json b/.Demo/Assets.xcassets/close.imageset/Contents.json similarity index 100% rename from core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset/Contents.json rename to .Demo/Assets.xcassets/close.imageset/Contents.json diff --git a/spark/Demo/Assets.xcassets/close.imageset/close.svg b/.Demo/Assets.xcassets/close.imageset/close.svg similarity index 100% rename from spark/Demo/Assets.xcassets/close.imageset/close.svg rename to .Demo/Assets.xcassets/close.imageset/close.svg diff --git a/spark/Demo/Assets.xcassets/info.imageset/Contents.json b/.Demo/Assets.xcassets/info.imageset/Contents.json similarity index 100% rename from spark/Demo/Assets.xcassets/info.imageset/Contents.json rename to .Demo/Assets.xcassets/info.imageset/Contents.json diff --git a/spark/Demo/Assets.xcassets/info.imageset/Style=outlineinfo.svg b/.Demo/Assets.xcassets/info.imageset/Style=outlineinfo.svg similarity index 100% rename from spark/Demo/Assets.xcassets/info.imageset/Style=outlineinfo.svg rename to .Demo/Assets.xcassets/info.imageset/Style=outlineinfo.svg diff --git a/spark/Demo/Classes/Enum/ComponentVersion.swift b/.Demo/Classes/Enum/ComponentVersion.swift similarity index 100% rename from spark/Demo/Classes/Enum/ComponentVersion.swift rename to .Demo/Classes/Enum/ComponentVersion.swift diff --git a/spark/Demo/Classes/Enum/SpaceContainer.swift b/.Demo/Classes/Enum/SpaceContainer.swift similarity index 100% rename from spark/Demo/Classes/Enum/SpaceContainer.swift rename to .Demo/Classes/Enum/SpaceContainer.swift diff --git a/spark/Demo/Classes/Enum/UIComponent.swift b/.Demo/Classes/Enum/UIComponent.swift similarity index 100% rename from spark/Demo/Classes/Enum/UIComponent.swift rename to .Demo/Classes/Enum/UIComponent.swift diff --git a/spark/Demo/Classes/Extension/CaseIterable-Name.swift b/.Demo/Classes/Extension/CaseIterable-Name.swift similarity index 100% rename from spark/Demo/Classes/Extension/CaseIterable-Name.swift rename to .Demo/Classes/Extension/CaseIterable-Name.swift diff --git a/spark/Demo/Classes/Extension/DeviceShakeViewModifier.swift b/.Demo/Classes/Extension/DeviceShakeViewModifier.swift similarity index 100% rename from spark/Demo/Classes/Extension/DeviceShakeViewModifier.swift rename to .Demo/Classes/Extension/DeviceShakeViewModifier.swift diff --git a/spark/Demo/Classes/Extension/EnviromentValues.swift b/.Demo/Classes/Extension/EnviromentValues.swift similarity index 100% rename from spark/Demo/Classes/Extension/EnviromentValues.swift rename to .Demo/Classes/Extension/EnviromentValues.swift diff --git a/spark/Demo/Classes/Extension/NumberFormatter.swift b/.Demo/Classes/Extension/NumberFormatter.swift similarity index 100% rename from spark/Demo/Classes/Extension/NumberFormatter.swift rename to .Demo/Classes/Extension/NumberFormatter.swift diff --git a/spark/Demo/Classes/Extension/UIApplication.swift b/.Demo/Classes/Extension/UIApplication.swift similarity index 100% rename from spark/Demo/Classes/Extension/UIApplication.swift rename to .Demo/Classes/Extension/UIApplication.swift diff --git a/spark/Demo/Classes/Extension/UIDevice+Shake.swift b/.Demo/Classes/Extension/UIDevice+Shake.swift similarity index 100% rename from spark/Demo/Classes/Extension/UIDevice+Shake.swift rename to .Demo/Classes/Extension/UIDevice+Shake.swift diff --git a/spark/Demo/Classes/Extension/UITextField.swift b/.Demo/Classes/Extension/UITextField.swift similarity index 100% rename from spark/Demo/Classes/Extension/UITextField.swift rename to .Demo/Classes/Extension/UITextField.swift diff --git a/spark/Demo/Classes/Extension/UIViewController+Present.swift b/.Demo/Classes/Extension/UIViewController+Present.swift similarity index 100% rename from spark/Demo/Classes/Extension/UIViewController+Present.swift rename to .Demo/Classes/Extension/UIViewController+Present.swift diff --git a/spark/Demo/Classes/Extension/UIWindow+Shake.swift b/.Demo/Classes/Extension/UIWindow+Shake.swift similarity index 100% rename from spark/Demo/Classes/Extension/UIWindow+Shake.swift rename to .Demo/Classes/Extension/UIWindow+Shake.swift diff --git a/spark/Demo/Classes/Extension/View+Shake.swift b/.Demo/Classes/Extension/View+Shake.swift similarity index 100% rename from spark/Demo/Classes/Extension/View+Shake.swift rename to .Demo/Classes/Extension/View+Shake.swift diff --git a/spark/Demo/Classes/Helper/AnyIsTrue.swift b/.Demo/Classes/Helper/AnyIsTrue.swift similarity index 100% rename from spark/Demo/Classes/Helper/AnyIsTrue.swift rename to .Demo/Classes/Helper/AnyIsTrue.swift diff --git a/spark/Demo/Classes/Helper/NSAttributedStringBuilder.swift b/.Demo/Classes/Helper/NSAttributedStringBuilder.swift similarity index 100% rename from spark/Demo/Classes/Helper/NSAttributedStringBuilder.swift rename to .Demo/Classes/Helper/NSAttributedStringBuilder.swift diff --git a/spark/Demo/Classes/DemoIconography.swift b/.Demo/Classes/Iconography/DemoIconography.swift similarity index 96% rename from spark/Demo/Classes/DemoIconography.swift rename to .Demo/Classes/Iconography/DemoIconography.swift index ba5ab8a8c..45d050b94 100644 --- a/spark/Demo/Classes/DemoIconography.swift +++ b/.Demo/Classes/Iconography/DemoIconography.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SwiftUI import SparkCore diff --git a/spark/Demo/Classes/Tabbar/ConsoleView.swift b/.Demo/Classes/Tabbar/ConsoleView.swift similarity index 94% rename from spark/Demo/Classes/Tabbar/ConsoleView.swift rename to .Demo/Classes/Tabbar/ConsoleView.swift index a83d7bac4..a53220a8e 100644 --- a/spark/Demo/Classes/Tabbar/ConsoleView.swift +++ b/.Demo/Classes/Tabbar/ConsoleView.swift @@ -9,6 +9,7 @@ import Combine import Foundation import UIKit +@_spi(SI_SPI) import SparkCommon final class Console { static var publisher: some Publisher { @@ -20,7 +21,7 @@ final class Console { } } -final class ConsoleView: UIView { +public final class ConsoleView: UIView { enum Constants { static let fullWidth: CGFloat = 200 @@ -29,7 +30,7 @@ final class ConsoleView: UIView { private var dataSource = [String]() - static let shared = ConsoleView() + public static let shared = ConsoleView() private var width: CGFloat { return self.consoleButton.isSelected ? Constants.fullWidth : Constants.collapsedWidth @@ -87,7 +88,7 @@ final class ConsoleView: UIView { fatalError("init(coder:) has not been implemented") } - func show() { + public func show() { guard let window = UIApplication.shared.windows.last else { return } @@ -178,18 +179,18 @@ final class ConsoleView: UIView { } extension ConsoleView: UITableViewDelegate { - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return 12.0 } } extension ConsoleView: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return section == 0 ? self.dataSource.count : 0 } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = self.tableView.dequeueReusableCell(withIdentifier: ConsoleTableViewCell.cellIdentifier) as? ConsoleTableViewCell else { fatalError("Couldn't dequeue table view cell") } diff --git a/spark/Demo/Classes/Tabbar/SparkTabbarController.swift b/.Demo/Classes/Tabbar/SparkTabbarController.swift similarity index 90% rename from spark/Demo/Classes/Tabbar/SparkTabbarController.swift rename to .Demo/Classes/Tabbar/SparkTabbarController.swift index 5448be93a..403800928 100644 --- a/spark/Demo/Classes/Tabbar/SparkTabbarController.swift +++ b/.Demo/Classes/Tabbar/SparkTabbarController.swift @@ -8,11 +8,10 @@ import UIKit import SwiftUI -import Spark import SparkCore import Combine -final class SparkTabbarController: UITabBarController { +public final class SparkTabbarController: UITabBarController { // MARK: - Published Properties @ObservedObject private var themePublisher = SparkThemePublisher.shared @@ -52,7 +51,7 @@ final class SparkTabbarController: UITabBarController { }() // MARK: - ViewDidLoad - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() self.loadSparkConfiguration() @@ -77,9 +76,9 @@ final class SparkTabbarController: UITabBarController { self.themePublisher .$theme .eraseToAnyPublisher() - .sink(receiveValue: { theme in - self.tabBar.tintColor = theme.colors.main.main.uiColor - }) - .store(in: &cancellables) + .sink { theme in + self.tabBar.tintColor = theme.colors.main.main.uiColor + } + .store(in: &cancellables) } } diff --git a/spark/Demo/Classes/Theme/PurpleContent/PurpleBorder.swift b/.Demo/Classes/Theme/PurpleContent/PurpleBorder.swift similarity index 100% rename from spark/Demo/Classes/Theme/PurpleContent/PurpleBorder.swift rename to .Demo/Classes/Theme/PurpleContent/PurpleBorder.swift diff --git a/spark/Demo/Classes/Theme/PurpleContent/PurpleColors.swift b/.Demo/Classes/Theme/PurpleContent/PurpleColors.swift similarity index 100% rename from spark/Demo/Classes/Theme/PurpleContent/PurpleColors.swift rename to .Demo/Classes/Theme/PurpleContent/PurpleColors.swift diff --git a/spark/Demo/Classes/Theme/PurpleContent/PurpleElevation.swift b/.Demo/Classes/Theme/PurpleContent/PurpleElevation.swift similarity index 100% rename from spark/Demo/Classes/Theme/PurpleContent/PurpleElevation.swift rename to .Demo/Classes/Theme/PurpleContent/PurpleElevation.swift diff --git a/spark/Demo/Classes/Theme/PurpleContent/PurpleLayout.swift b/.Demo/Classes/Theme/PurpleContent/PurpleLayout.swift similarity index 100% rename from spark/Demo/Classes/Theme/PurpleContent/PurpleLayout.swift rename to .Demo/Classes/Theme/PurpleContent/PurpleLayout.swift diff --git a/spark/Demo/Classes/Theme/PurpleContent/PurpleTypography.swift b/.Demo/Classes/Theme/PurpleContent/PurpleTypography.swift similarity index 100% rename from spark/Demo/Classes/Theme/PurpleContent/PurpleTypography.swift rename to .Demo/Classes/Theme/PurpleContent/PurpleTypography.swift diff --git a/spark/Demo/Classes/Theme/PurpleTheme.swift b/.Demo/Classes/Theme/PurpleTheme.swift similarity index 100% rename from spark/Demo/Classes/Theme/PurpleTheme.swift rename to .Demo/Classes/Theme/PurpleTheme.swift diff --git a/spark/Demo/Classes/Theme/SparkThemePublisher.swift b/.Demo/Classes/Theme/SparkThemePublisher.swift similarity index 96% rename from spark/Demo/Classes/Theme/SparkThemePublisher.swift rename to .Demo/Classes/Theme/SparkThemePublisher.swift index 0806849f1..3e46d4760 100644 --- a/spark/Demo/Classes/Theme/SparkThemePublisher.swift +++ b/.Demo/Classes/Theme/SparkThemePublisher.swift @@ -8,7 +8,6 @@ import Foundation import SparkCore -import Spark public class SparkThemePublisher: ObservableObject { public static let shared = SparkThemePublisher() diff --git a/spark/Demo/Classes/View/ComponentVersionViewController.swift b/.Demo/Classes/View/ComponentVersionViewController.swift similarity index 100% rename from spark/Demo/Classes/View/ComponentVersionViewController.swift rename to .Demo/Classes/View/ComponentVersionViewController.swift diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeFormat-Names.swift b/.Demo/Classes/View/Components/Badge/BadgeFormat-Names.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Badge/BadgeFormat-Names.swift rename to .Demo/Classes/View/Components/Badge/BadgeFormat-Names.swift diff --git a/spark/Demo/Classes/View/Components/Badge/BadgePreviewFormatter.swift b/.Demo/Classes/View/Components/Badge/BadgePreviewFormatter.swift similarity index 91% rename from spark/Demo/Classes/View/Components/Badge/BadgePreviewFormatter.swift rename to .Demo/Classes/View/Components/Badge/BadgePreviewFormatter.swift index 635860b3c..a94434444 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgePreviewFormatter.swift +++ b/.Demo/Classes/View/Components/Badge/BadgePreviewFormatter.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift b/.Demo/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift rename to .Demo/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift index 742a5df47..f5b574c76 100644 --- a/spark/Demo/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift +++ b/.Demo/Classes/View/Components/Badge/SwiftUI/BadgeComponentView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift b/.Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift rename to .Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift index 55c43e38a..3fd56440f 100644 --- a/spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift +++ b/.Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIView.swift @@ -9,7 +9,7 @@ import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon final class BadgeComponentUIView: UIView { diff --git a/spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift b/.Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift index f1ba41815..0e5ba2120 100644 --- a/spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Badge/UIKit/BadgeComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift b/.Demo/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift rename to .Demo/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift index 794b38700..bd3568f04 100644 --- a/spark/Demo/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift +++ b/.Demo/Classes/View/Components/Badge/UIKit/BadgeComponentViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class BadgeComponentViewController: UIViewController { @@ -51,11 +51,11 @@ final class BadgeComponentViewController: UIViewController { themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift b/.Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift similarity index 91% rename from spark/Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift rename to .Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift index 0214c03e8..2256d2add 100644 --- a/spark/Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift +++ b/.Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentedView.swift @@ -10,9 +10,9 @@ import SwiftUI struct BottomSheetPresentedView: View { var description: String = """ -Sample of a SwiftUI bottom sheet with little text. -🧡💙 -""" + Sample of a SwiftUI bottom sheet with little text. + 🧡💙 + """ var dismiss: () -> Void var body: some View { diff --git a/spark/Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift b/.Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift similarity index 82% rename from spark/Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift rename to .Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift index 9b39d616e..228314df0 100644 --- a/spark/Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift +++ b/.Demo/Classes/View/Components/BottomSheet/SwiftUI/BottomSheetPresentingView.swift @@ -9,70 +9,70 @@ import SwiftUI private let longDescription: String = """ -Sample of a SwiftUI bottom sheet with a scroll view. -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -""" + Sample of a SwiftUI bottom sheet with a scroll view. + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + """ private let mediumDescription: String = """ -Sample of a SwiftUI bottom sheet with a long text. -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -💙 -🧡 -""" + Sample of a SwiftUI bottom sheet with a long text. + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + 💙 + 🧡 + """ struct BottomSheetPresentingView: View { var body: some View { if #available(iOS 16.4, *) { @@ -121,12 +121,12 @@ struct BottomSheetPresentingViewWithHeightDetent: View { self.showingLongSheet.toggle() } .sheet(isPresented: $showingLongSheet) { - ScrollView { - BottomSheetPresentedView(description: longDescription) { - self.showingLongSheet.toggle() - } + ScrollView { + BottomSheetPresentedView(description: longDescription) { + self.showingLongSheet.toggle() } - .scrollIndicators(.visible) + } + .scrollIndicators(.visible) .presentationDetents([.medium, .large]) } .buttonStyle(.borderedProminent) diff --git a/spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoScrollView.swift b/.Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoScrollView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoScrollView.swift rename to .Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoScrollView.swift diff --git a/spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoUIController.swift b/.Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoUIController.swift similarity index 100% rename from spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoUIController.swift rename to .Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoUIController.swift diff --git a/spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoView.swift b/.Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoView.swift rename to .Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetDemoView.swift diff --git a/spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIView.swift b/.Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIView.swift rename to .Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIView.swift diff --git a/spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIViewController.swift b/.Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIViewController.swift similarity index 100% rename from spark/Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIViewController.swift rename to .Demo/Classes/View/Components/BottomSheet/UIKit/BottomSheetPresentingUIViewController.swift diff --git a/spark/Demo/Classes/View/Components/Button/ButtonContent.swift b/.Demo/Classes/View/Components/Button/ButtonContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Button/ButtonContent.swift rename to .Demo/Classes/View/Components/Button/ButtonContent.swift diff --git a/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift b/.Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift rename to .Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift index 395f1b09d..f9c3de23c 100644 --- a/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift +++ b/.Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct ButtonComponentView: View { @@ -194,7 +194,6 @@ private extension ButtonView { case .highlighted: image = Image("close") case .disabled: image = Image("check") case .selected: image = Image("alert") - @unknown default: break } } @@ -221,7 +220,6 @@ private extension ButtonView { case .highlighted: title = "My Highlighted" case .disabled: title = "My Disabled" case .selected: title = "My Selected" - @unknown default: break } if content.containsAttributedText, let title { diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift b/.Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift rename to .Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift index 76d5e64e2..df5e1c41b 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift +++ b/.Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift @@ -9,7 +9,7 @@ import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon final class ButtonComponentUIView: ComponentUIView { @@ -233,7 +233,6 @@ final class ButtonComponentUIView: ComponentUIView { case .highlighted: return UIImage(named: "close") case .disabled: return UIImage(named: "check") case .selected: return UIImage(named: "alert") - @unknown default: return nil } } @@ -243,7 +242,6 @@ final class ButtonComponentUIView: ComponentUIView { case .highlighted: return "My Highlighted" case .disabled: return "My Disabled" case .selected: return "My Selected" - @unknown default: return nil } } diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift b/.Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift index 30988ada9..fe6fa4da4 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift @@ -7,7 +7,6 @@ // import Combine -import Spark import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift b/.Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift rename to .Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift index cf62026b9..b59d57231 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift +++ b/.Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class ButtonComponentViewController: UIViewController { @@ -53,11 +53,11 @@ final class ButtonComponentViewController: UIViewController { themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift b/.Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift rename to .Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift diff --git a/spark/Demo/Classes/View/Components/Checkbox/ComponentsCheckboxListView.swift b/.Demo/Classes/View/Components/Checkbox/ComponentsCheckboxListView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Checkbox/ComponentsCheckboxListView.swift rename to .Demo/Classes/View/Components/Checkbox/ComponentsCheckboxListView.swift diff --git a/spark/Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift b/.Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift rename to .Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift index fb4583ecc..a69e8762c 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift +++ b/.Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxGroupView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift b/.Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift rename to .Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift index cf4fbea24..0e9aae60e 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift +++ b/.Demo/Classes/View/Components/Checkbox/SwiftUI/CheckboxView.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift b/.Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift rename to .Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift index 4e4003724..ad310b6f0 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift +++ b/.Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class CheckboxComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift b/.Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift rename to .Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift index a8a209a6e..6a460b999 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class CheckboxComponentUIViewController: UIViewController { @@ -51,11 +51,11 @@ final class CheckboxComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift b/.Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift index 2a217251f..33e7a9f5c 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Checkbox/UIKit/Checkbox/CheckboxComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift b/.Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift rename to .Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift index 3758598fe..572eaa128 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift +++ b/.Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class CheckboxGroupComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift b/.Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift rename to .Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift index 60fa3bc2f..93591ba30 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class CheckboxGroupComponentUIViewController: UIViewController { @@ -51,11 +51,11 @@ final class CheckboxGroupComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift b/.Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift index d6f0d5d09..b332595ca 100644 --- a/spark/Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Checkbox/UIKit/CheckboxGroup/CheckboxGroupComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift b/.Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift rename to .Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift index 859ee2798..4119352c0 100644 --- a/spark/Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift +++ b/.Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift b/.Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift rename to .Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift index e9df6c3c1..d10cb951c 100644 --- a/spark/Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift +++ b/.Demo/Classes/View/Components/Chip/SwiftUI/ChipComponentViewRepresentable.swift @@ -7,7 +7,6 @@ // import Combine -import Spark import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift b/.Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift rename to .Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift index 6cc738a96..e37cd7272 100644 --- a/spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift +++ b/.Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIView.swift @@ -9,7 +9,7 @@ import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon final class ChipComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift b/.Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift rename to .Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift index 988a5c203..722c96fb0 100644 --- a/spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class ChipComponentViewController: UIViewController { @@ -53,11 +53,11 @@ final class ChipComponentViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift b/.Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift index 4762015b5..77f1740f8 100644 --- a/spark/Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Chip/UIKit/ChipComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/ComponentsView.swift b/.Demo/Classes/View/Components/ComponentsView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/ComponentsView.swift rename to .Demo/Classes/View/Components/ComponentsView.swift index ff4093abc..9dbc67761 100644 --- a/spark/Demo/Classes/View/Components/ComponentsView.swift +++ b/.Demo/Classes/View/Components/ComponentsView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct ComponentsView: View { diff --git a/spark/Demo/Classes/View/Components/ComponentsViewController.swift b/.Demo/Classes/View/Components/ComponentsViewController.swift similarity index 100% rename from spark/Demo/Classes/View/Components/ComponentsViewController.swift rename to .Demo/Classes/View/Components/ComponentsViewController.swift diff --git a/spark/Demo/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift b/.Demo/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift rename to .Demo/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift index 3e07abf1c..5185308f0 100644 --- a/spark/Demo/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift +++ b/.Demo/Classes/View/Components/FormField/SwiftUI/FormFieldComponentView.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift b/.Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift rename to .Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift index e52387472..ff15e7bd8 100644 --- a/spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift +++ b/.Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class FormFieldComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift b/.Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift rename to .Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift index b1ca401a9..b69967b79 100644 --- a/spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class FormFieldComponentUIViewController: UIViewController { @@ -51,11 +51,11 @@ final class FormFieldComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift b/.Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift rename to .Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift index 740b6df37..fdce08a99 100644 --- a/spark/Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/FormField/UIKit/FormFieldComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift b/.Demo/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift rename to .Demo/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift index 7f1e02482..f49feb9ce 100644 --- a/spark/Demo/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift +++ b/.Demo/Classes/View/Components/Icon/SwiftUI/IconComponentView.swift @@ -7,7 +7,6 @@ // import SwiftUI -import Spark import SparkCore struct IconComponentView: View { diff --git a/spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift b/.Demo/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift rename to .Demo/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift index 43e7e4952..1a7233f4e 100644 --- a/spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift +++ b/.Demo/Classes/View/Components/Icon/UIKit/IconComponentUIView.swift @@ -10,8 +10,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class IconComponentUIView: UIView { diff --git a/spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift b/.Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift rename to .Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift index 2b7035560..7c7443a91 100644 --- a/spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewController.swift @@ -9,10 +9,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class IconComponentUIViewController: UIViewController { @@ -53,11 +53,11 @@ final class IconComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift b/.Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift index 97fb8f7e6..048af380e 100644 --- a/spark/Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Icon/UIKit/IconComponentUIViewModel.swift @@ -9,7 +9,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/IconButton/IconButtonContent.swift b/.Demo/Classes/View/Components/IconButton/IconButtonContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/IconButton/IconButtonContent.swift rename to .Demo/Classes/View/Components/IconButton/IconButtonContent.swift diff --git a/spark/Demo/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift b/.Demo/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift rename to .Demo/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift index 7cf066e92..1b3dcd9ff 100644 --- a/spark/Demo/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift +++ b/.Demo/Classes/View/Components/IconButton/SwiftUI/IconButtonComponentView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct IconButtonComponentView: View { @@ -163,7 +163,6 @@ private extension IconButtonView { case .highlighted: image = Image("close") case .disabled: image = Image("check") case .selected: image = Image("alert") - @unknown default: break } } diff --git a/spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift b/.Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift rename to .Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift index c74fb6682..ffabc835d 100644 --- a/spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift +++ b/.Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIView.swift @@ -9,7 +9,7 @@ import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon final class IconButtonComponentUIView: ComponentUIView { @@ -197,7 +197,6 @@ final class IconButtonComponentUIView: ComponentUIView { case .highlighted: return UIImage(named: "close") case .disabled: return UIImage(named: "check") case .selected: return UIImage(named: "alert") - @unknown default: return nil } } diff --git a/spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift b/.Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift rename to .Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift index e1d57f366..14e25436d 100644 --- a/spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentUIViewModel.swift @@ -7,7 +7,6 @@ // import Combine -import Spark import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift b/.Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift rename to .Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift index 6db770083..2e5acca8c 100644 --- a/spark/Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift +++ b/.Demo/Classes/View/Components/IconButton/UIKit/IconButtonComponentViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class IconButtonComponentViewController: UIViewController { @@ -57,11 +57,11 @@ final class IconButtonComponentViewController: UIViewController { private func addPublisher() { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Main/ComponentUIView.swift b/.Demo/Classes/View/Components/Main/ComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Main/ComponentUIView.swift rename to .Demo/Classes/View/Components/Main/ComponentUIView.swift index 39cb9cd41..5e6f1d1f7 100644 --- a/spark/Demo/Classes/View/Components/Main/ComponentUIView.swift +++ b/.Demo/Classes/View/Components/Main/ComponentUIView.swift @@ -8,6 +8,8 @@ import UIKit import Combine +@_spi(SI_SPI) import SparkCommon +import SparkCore class ComponentUIView: UIView { diff --git a/spark/Demo/Classes/View/Components/Main/ComponentUIViewModel.swift b/.Demo/Classes/View/Components/Main/ComponentUIViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/ComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Main/ComponentUIViewModel.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/Checkbox.swift b/.Demo/Classes/View/Components/Main/Configuration/SwiftUI/Checkbox.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/Checkbox.swift rename to .Demo/Classes/View/Components/Main/Configuration/SwiftUI/Checkbox.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift b/.Demo/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift rename to .Demo/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift index 4621a020b..498925ca2 100644 --- a/spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift +++ b/.Demo/Classes/View/Components/Main/Configuration/SwiftUI/Component.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SparkCore struct Component: View { diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/EnumSelector.swift b/.Demo/Classes/View/Components/Main/Configuration/SwiftUI/EnumSelector.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/EnumSelector.swift rename to .Demo/Classes/View/Components/Main/Configuration/SwiftUI/EnumSelector.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/RangeSelector.swift b/.Demo/Classes/View/Components/Main/Configuration/SwiftUI/RangeSelector.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/RangeSelector.swift rename to .Demo/Classes/View/Components/Main/Configuration/SwiftUI/RangeSelector.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/ThemeSelector.swift b/.Demo/Classes/View/Components/Main/Configuration/SwiftUI/ThemeSelector.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/SwiftUI/ThemeSelector.swift rename to .Demo/Classes/View/Components/Main/Configuration/SwiftUI/ThemeSelector.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift b/.Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift rename to .Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift index 1c09c6119..764089a38 100644 --- a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift +++ b/.Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationView.swift @@ -8,6 +8,7 @@ import UIKit import Combine +@_spi(SI_SPI) import SparkCommon final class ComponentsConfigurationUIView: UIView { diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationViewModel.swift b/.Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationViewModel.swift rename to .Demo/Classes/View/Components/Main/Configuration/UIKit/ComponentsConfigurationViewModel.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemUIType.swift b/.Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemUIType.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemUIType.swift rename to .Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemUIType.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift b/.Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift similarity index 96% rename from spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift rename to .Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift index e3a52e0de..75ce7074c 100644 --- a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift +++ b/.Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemView.swift @@ -9,6 +9,7 @@ import Combine import SparkCore import UIKit +@_spi(SI_SPI) import SparkCommon final class ComponentsConfigurationItemUIViewModelView: UIView { @@ -212,35 +213,35 @@ final class ComponentsConfigurationItemUIViewModelView: UIView { // Colors self.viewModel.$color .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] color in + .sink { [weak self] color in self?.button?.setTitleColor(color, for: .normal) self?.toggle?.onTintColor = color - }) + } .store(in: &self.subscriptions) // Button Title self.viewModel.$buttonTitle .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] buttonTitle in + .sink { [weak self] buttonTitle in self?.button?.setTitle(buttonTitle, for: .normal) - }) + } .store(in: &self.subscriptions) // Toggle isOn self.viewModel.$isOn .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] isOn in + .sink { [weak self] isOn in guard let isOn = isOn else { return } self?.toggle?.isOn = isOn - }) + } .store(in: &self.subscriptions) // Label Text self.viewModel.$labelText .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] text in + .sink { [weak self] text in self?.valueLabel.text = text - }) + } .store(in: &self.subscriptions) } } diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemViewModel.swift b/.Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemViewModel.swift rename to .Demo/Classes/View/Components/Main/Configuration/UIKit/Item/ComponentsConfigurationItemViewModel.swift diff --git a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift b/.Demo/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift rename to .Demo/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift index e806377e2..e519cd466 100644 --- a/spark/Demo/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift +++ b/.Demo/Classes/View/Components/Main/Configuration/UIKit/Item/NumberSelector.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +@_spi(SI_SPI) import SparkCommon /// A control to select a number withing a given range final class NumberSelector: UIControl { diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Constants/ProgressBarConstants.swift b/.Demo/Classes/View/Components/ProgressBar/Constants/ProgressBarConstants.swift similarity index 100% rename from spark/Demo/Classes/View/Components/ProgressBar/Constants/ProgressBarConstants.swift rename to .Demo/Classes/View/Components/ProgressBar/Constants/ProgressBarConstants.swift diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift rename to .Demo/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift index 58f4cb621..2d63ed191 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/SwiftUI/ProgressBarIndeterminateComponentView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct ProgressBarIndeterminateComponentView: View { diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift rename to .Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift index ace37b4a6..532e2fedc 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class ProgressBarIndeterminateComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift similarity index 96% rename from spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift rename to .Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift index f1a70d66a..b920f18fd 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewController.swift @@ -7,10 +7,11 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class ProgressBarIndeterminateComponentUIViewController: UIViewController { @@ -60,11 +61,11 @@ final class ProgressBarIndeterminateComponentUIViewController: UIViewController self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.subscriptions) self.viewModel.showThemeSheet.subscribe(in: &self.subscriptions) { intents in diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift rename to .Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift index 9957e0ee0..ca39bd5f3 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Indeterminate/UIKit/ProgressBarIndeterminateComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift b/.Demo/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift rename to .Demo/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift index 978058d0f..2fc8bd72f 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Single/SwiftUI/ProgressBarComponentView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct ProgressBarComponentView: View { diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift b/.Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift rename to .Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift index 9f09a0a24..749cdca02 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class ProgressBarComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift b/.Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift rename to .Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift index 98dfd6e4f..5a231f136 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class ProgressBarComponentUIViewController: UIViewController { @@ -60,11 +60,11 @@ final class ProgressBarComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.subscriptions) self.viewModel.showThemeSheet.subscribe(in: &self.subscriptions) { intents in diff --git a/spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift b/.Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift rename to .Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift index 23358e8d4..c625542af 100644 --- a/spark/Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/ProgressBar/Single/UIKit/ProgressBarComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/ProgressTracker/SwiftUI/ProgressTrackerComponent.swift b/.Demo/Classes/View/Components/ProgressTracker/SwiftUI/ProgressTrackerComponent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/ProgressTracker/SwiftUI/ProgressTrackerComponent.swift rename to .Demo/Classes/View/Components/ProgressTracker/SwiftUI/ProgressTrackerComponent.swift diff --git a/spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift b/.Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift rename to .Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift index 05da70d65..fe9d30303 100644 --- a/spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift +++ b/.Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIView.swift @@ -9,7 +9,8 @@ import UIKit import Combine @testable import SparkCore -import Spark +import SparkCore +@_spi(SI_SPI) import SparkCommon final class ProgressTrackerComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift b/.Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift rename to .Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift index 594980d5a..f09721389 100644 --- a/spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewController.swift @@ -7,10 +7,11 @@ // import Combine -import Spark +import SparkCore import SwiftUI import UIKit @testable import SparkCore +@_spi(SI_SPI) import SparkCommon final class ProgressTrackerComponentUIViewController: UIViewController { @@ -53,11 +54,11 @@ final class ProgressTrackerComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift b/.Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift rename to .Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift index 9a969d3f0..297bc5a49 100644 --- a/spark/Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/ProgressTracker/UIKit/ProgressTrackerComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon @testable import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/RadioButton/RadioButtonGroupState-Extension.swift b/.Demo/Classes/View/Components/RadioButton/RadioButtonGroupState-Extension.swift similarity index 100% rename from spark/Demo/Classes/View/Components/RadioButton/RadioButtonGroupState-Extension.swift rename to .Demo/Classes/View/Components/RadioButton/RadioButtonGroupState-Extension.swift diff --git a/spark/Demo/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift b/.Demo/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift similarity index 98% rename from spark/Demo/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift rename to .Demo/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift index 823d07648..84bbd7244 100644 --- a/spark/Demo/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift +++ b/.Demo/Classes/View/Components/RadioButton/SwiftUI/RadioButtonComponent.swift @@ -8,7 +8,7 @@ import Foundation -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift b/.Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift rename to .Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift index 825930eb7..85d88356f 100644 --- a/spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift +++ b/.Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIView.swift @@ -8,8 +8,9 @@ import Combine import Foundation -import SparkCore import UIKit +import SparkCore +@_spi(SI_SPI) import SparkCommon final class RadioButtonComponentUIView: ComponentUIView { // MARK: - Components diff --git a/spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift b/.Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift rename to .Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift index 48793de8b..81da900df 100644 --- a/spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class RadioButtonComponentUIViewController: UIViewController { @@ -53,11 +53,11 @@ final class RadioButtonComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { themes in diff --git a/spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewModel.swift b/.Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewModel.swift rename to .Demo/Classes/View/Components/RadioButton/UIKit/RadioButtonComponentUIViewModel.swift diff --git a/spark/Demo/Classes/View/Components/Rating/SwiftUI/RatingComponent.swift b/.Demo/Classes/View/Components/Rating/SwiftUI/RatingComponent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Rating/SwiftUI/RatingComponent.swift rename to .Demo/Classes/View/Components/Rating/SwiftUI/RatingComponent.swift diff --git a/spark/Demo/Classes/View/Components/Rating/SwiftUI/RatingInputComponent.swift b/.Demo/Classes/View/Components/Rating/SwiftUI/RatingInputComponent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Rating/SwiftUI/RatingInputComponent.swift rename to .Demo/Classes/View/Components/Rating/SwiftUI/RatingInputComponent.swift diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift b/.Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift rename to .Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift index 9b710315c..917611c41 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIView.swift @@ -7,11 +7,10 @@ // import Foundation - import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon final class RatingDisplayComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift b/.Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift index 076172b2d..ea9485310 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift b/.Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift rename to .Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift index 9b673c0d6..9df135a0d 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/RatingDisplayComponentViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class RatingDisplayComponentViewController: UIViewController { @@ -53,11 +53,11 @@ final class RatingDisplayComponentViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift b/.Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift rename to .Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift index 417ad2ec6..cfd6724a3 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIView.swift @@ -7,11 +7,10 @@ // import Foundation - import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon final class RatingInputComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift b/.Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift index e4549956a..66d52565e 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentUIViewModel.swift @@ -9,7 +9,7 @@ import Foundation import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift b/.Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift rename to .Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift index 320d41000..ba2aa30e4 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/RatingInputComponentViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class RatingInputComponentViewController: UIViewController { @@ -53,11 +53,11 @@ final class RatingInputComponentViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift b/.Demo/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift rename to .Demo/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift index 9c438dd32..1a673fdbe 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/StarComponentUIView.swift @@ -9,7 +9,8 @@ import UIKit import Combine import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon +@_spi(SI_SPI) import SparkCommon final class StarComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentUIViewModel.swift b/.Demo/Classes/View/Components/Rating/UIKit/StarComponentUIViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Rating/UIKit/StarComponentUIViewModel.swift diff --git a/spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift b/.Demo/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift rename to .Demo/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift index 71cfe6d33..9673d3a8f 100644 --- a/spark/Demo/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift +++ b/.Demo/Classes/View/Components/Rating/UIKit/StarComponentViewController.swift @@ -7,10 +7,11 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SwiftUI import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon final class StarComponentViewController: UIViewController { diff --git a/spark/Demo/Classes/View/Components/Slider/SwiftUI/SliderComponentView.swift b/.Demo/Classes/View/Components/Slider/SwiftUI/SliderComponentView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Slider/SwiftUI/SliderComponentView.swift rename to .Demo/Classes/View/Components/Slider/SwiftUI/SliderComponentView.swift diff --git a/spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift b/.Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift rename to .Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift index bb2996da3..6b69e2773 100644 --- a/spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift +++ b/.Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIControl.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon // swiftlint:disable no_debugging_method final class SliderComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift b/.Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift rename to .Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift index 4cb9d7eab..725e75d68 100644 --- a/spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon // swiftlint:disable no_debugging_method final class SliderComponentUIViewController: UIViewController { @@ -56,11 +56,11 @@ final class SliderComponentUIViewController: UIViewController { private func addPublisher() { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift b/.Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift index cde9fb546..bc45361d9 100644 --- a/spark/Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Slider/UIKit/SliderComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift b/.Demo/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift rename to .Demo/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift index c1edd07bc..a859f06e8 100644 --- a/spark/Demo/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift +++ b/.Demo/Classes/View/Components/Spinner/SwiftUI/SpinnerComponent.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift b/.Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift rename to .Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift index 8484d66e1..0ad1f3db1 100644 --- a/spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift +++ b/.Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIView.swift @@ -10,8 +10,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class SpinnerComponentUIView: UIView { diff --git a/spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift b/.Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift rename to .Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift index 0eef598c5..f4f0d9a40 100644 --- a/spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewController.swift @@ -9,10 +9,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class SpinnerComponentUIViewController: UIViewController { @@ -53,11 +53,11 @@ final class SpinnerComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift b/.Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift index c443c8fce..f156351e0 100644 --- a/spark/Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Spinner/UIKit/SpinnerComponentUIViewModel.swift @@ -9,7 +9,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Switch/Enum/SwitchTextContent.swift b/.Demo/Classes/View/Components/Switch/Enum/SwitchTextContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Switch/Enum/SwitchTextContent.swift rename to .Demo/Classes/View/Components/Switch/Enum/SwitchTextContent.swift diff --git a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift b/.Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift rename to .Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift index 48dd97317..b61acb3f4 100644 --- a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift +++ b/.Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift @@ -7,7 +7,6 @@ // import SwiftUI -import Spark import SparkCore struct SwitchComponentView: View { diff --git a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift b/.Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift similarity index 95% rename from spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift rename to .Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift index e44066b5a..32b9c68bd 100644 --- a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift +++ b/.Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon struct SwitchComponentViewModel { diff --git a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift b/.Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift rename to .Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift index fb9ff71cc..f3cb0d9e2 100644 --- a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift +++ b/.Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class SwitchComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift b/.Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift rename to .Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift index e076dfea3..ead8a149c 100644 --- a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class SwitchComponentUIViewController: UIViewController { @@ -53,11 +53,11 @@ final class SwitchComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { theme in diff --git a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift b/.Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift index dbb09f110..306bb20a4 100644 --- a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIViewModel.swift @@ -7,7 +7,6 @@ // import Combine -import Spark import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/Tab/SwiftUI/TabComponent.swift b/.Demo/Classes/View/Components/Tab/SwiftUI/TabComponent.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Tab/SwiftUI/TabComponent.swift rename to .Demo/Classes/View/Components/Tab/SwiftUI/TabComponent.swift index 1708d88ce..a88b7fc47 100644 --- a/spark/Demo/Classes/View/Components/Tab/SwiftUI/TabComponent.swift +++ b/.Demo/Classes/View/Components/Tab/SwiftUI/TabComponent.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift b/.Demo/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift rename to .Demo/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift index 8cb4227e4..918767e1d 100644 --- a/spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift +++ b/.Demo/Classes/View/Components/Tab/UIKit/TabComponentUIView.swift @@ -10,6 +10,8 @@ import Combine import Foundation import SparkCore import UIKit +import SparkCore +@_spi(SI_SPI) import SparkCommon final class TabComponentUIView: ComponentUIView { // MARK: - Components diff --git a/spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift b/.Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift rename to .Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift index 6b120d02b..1a06bddcb 100644 --- a/spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class TabComponentUIViewController: UIViewController { @@ -53,11 +53,11 @@ final class TabComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { themes in diff --git a/spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewModel.swift b/.Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Tab/UIKit/TabComponentUIViewModel.swift diff --git a/spark/Demo/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift b/.Demo/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift similarity index 98% rename from spark/Demo/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift rename to .Demo/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift index 6ac80041c..9b78a6ba4 100644 --- a/spark/Demo/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift +++ b/.Demo/Classes/View/Components/Tag/SwiftUI/TagComponentView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct TagComponentView: View { diff --git a/spark/Demo/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift b/.Demo/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift rename to .Demo/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift index a11e5623b..66b4f1a72 100644 --- a/spark/Demo/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift +++ b/.Demo/Classes/View/Components/Tag/SwiftUI/TagComponentViewModel.swift @@ -8,6 +8,7 @@ import SparkCore import SwiftUI +import SparkCore struct TagComponentViewModel: Hashable { diff --git a/spark/Demo/Classes/View/Components/Tag/TagContent.swift b/.Demo/Classes/View/Components/Tag/TagContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/Tag/TagContent.swift rename to .Demo/Classes/View/Components/Tag/TagContent.swift diff --git a/spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift b/.Demo/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift rename to .Demo/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift index d2bf2d985..efcc308ef 100644 --- a/spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift +++ b/.Demo/Classes/View/Components/Tag/UIKit/TagComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class TagComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift b/.Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift similarity index 97% rename from spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift rename to .Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift index cc71666dd..03ae70c72 100644 --- a/spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class TagComponentUIViewController: UIViewController { @@ -53,11 +53,11 @@ final class TagComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in diff --git a/spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift b/.Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift rename to .Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift index 77478dfb6..e33301613 100644 --- a/spark/Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/Tag/UIKit/TagComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/TextField/Addons/SwiftUI/TextFieldAddonsComponentView.swift b/.Demo/Classes/View/Components/TextField/Addons/SwiftUI/TextFieldAddonsComponentView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/Addons/SwiftUI/TextFieldAddonsComponentView.swift rename to .Demo/Classes/View/Components/TextField/Addons/SwiftUI/TextFieldAddonsComponentView.swift diff --git a/spark/Demo/Classes/View/Components/TextField/Addons/TextFieldAddonContent.swift b/.Demo/Classes/View/Components/TextField/Addons/TextFieldAddonContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/Addons/TextFieldAddonContent.swift rename to .Demo/Classes/View/Components/TextField/Addons/TextFieldAddonContent.swift diff --git a/spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift b/.Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift rename to .Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift index a9fec0f34..a27777071 100644 --- a/spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift +++ b/.Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIView.swift @@ -9,6 +9,7 @@ import Combine import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon // swiftlint:disable no_debugging_method final class TextFieldAddonsComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift b/.Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift rename to .Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift index 21a91e641..31ad69678 100644 --- a/spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import SwiftUI import SparkCore +@_spi(SI_SPI) import SparkCommon final class TextFieldAddonsComponentUIViewController: UIViewController { @@ -49,11 +50,11 @@ final class TextFieldAddonsComponentUIViewController: UIViewController { private func setupSubscriptions() { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { theme in diff --git a/spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewModel.swift b/.Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewModel.swift rename to .Demo/Classes/View/Components/TextField/Addons/UIKit/TextFieldAddonsComponentUIViewModel.swift diff --git a/spark/Demo/Classes/View/Components/TextField/SwiftUI/TextFieldComponentView.swift b/.Demo/Classes/View/Components/TextField/SwiftUI/TextFieldComponentView.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/SwiftUI/TextFieldComponentView.swift rename to .Demo/Classes/View/Components/TextField/SwiftUI/TextFieldComponentView.swift diff --git a/spark/Demo/Classes/View/Components/TextField/TextFieldContentSide.swift b/.Demo/Classes/View/Components/TextField/TextFieldContentSide.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/TextFieldContentSide.swift rename to .Demo/Classes/View/Components/TextField/TextFieldContentSide.swift diff --git a/spark/Demo/Classes/View/Components/TextField/TextFieldSideViewContent.swift b/.Demo/Classes/View/Components/TextField/TextFieldSideViewContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/TextFieldSideViewContent.swift rename to .Demo/Classes/View/Components/TextField/TextFieldSideViewContent.swift diff --git a/spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift rename to .Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift index 3086ed2bb..4c34fbcc1 100644 --- a/spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift +++ b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift @@ -9,6 +9,7 @@ import Combine import UIKit import SparkCore +@_spi(SI_SPI) import SparkCommon // swiftlint:disable no_debugging_method final class TextFieldComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift rename to .Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift index 68defdf4b..c83f0fb1d 100644 --- a/spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import SwiftUI import SparkCore +@_spi(SI_SPI) import SparkCommon final class TextFieldComponentUIViewController: UIViewController { @@ -49,11 +50,11 @@ final class TextFieldComponentUIViewController: UIViewController { private func setupSubscriptions() { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.cancellables) self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { theme in diff --git a/spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewModel.swift b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewModel.swift rename to .Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIViewModel.swift diff --git a/spark/Demo/Classes/View/Components/TextLink/Constants/TextLinkConstants.swift b/.Demo/Classes/View/Components/TextLink/Constants/TextLinkConstants.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextLink/Constants/TextLinkConstants.swift rename to .Demo/Classes/View/Components/TextLink/Constants/TextLinkConstants.swift diff --git a/spark/Demo/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift b/.Demo/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift rename to .Demo/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift index edd980b50..66eb4c745 100644 --- a/spark/Demo/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift +++ b/.Demo/Classes/View/Components/TextLink/SwiftUI/TextLinkComponentView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct TextLinkComponentView: View { diff --git a/spark/Demo/Classes/View/Components/TextLink/TextLinkContent.swift b/.Demo/Classes/View/Components/TextLink/TextLinkContent.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextLink/TextLinkContent.swift rename to .Demo/Classes/View/Components/TextLink/TextLinkContent.swift diff --git a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift similarity index 99% rename from spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift rename to .Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift index 177c59fa0..3f68a9fa8 100644 --- a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift +++ b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIView.swift @@ -8,8 +8,8 @@ import Combine import SparkCore -import Spark import UIKit +@_spi(SI_SPI) import SparkCommon final class TextLinkComponentUIView: ComponentUIView { diff --git a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift similarity index 98% rename from spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift rename to .Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift index 8f3f5f0f3..8e36d81d1 100644 --- a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift +++ b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewController.swift @@ -7,10 +7,10 @@ // import Combine -import Spark import SparkCore import SwiftUI import UIKit +@_spi(SI_SPI) import SparkCommon final class TextLinkComponentUIViewController: UIViewController { @@ -60,11 +60,11 @@ final class TextLinkComponentUIViewController: UIViewController { self.themePublisher .$theme - .sink(receiveValue: { [weak self] theme in + .sink { [weak self] theme in guard let self = self else { return } self.viewModel.theme = theme self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor - }) + } .store(in: &self.subscriptions) self.viewModel.showThemeSheet.subscribe(in: &self.subscriptions) { intents in diff --git a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift similarity index 99% rename from spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift rename to .Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift index c6a8bb3f2..36611aff4 100644 --- a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift +++ b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkComponentUIViewModel.swift @@ -7,7 +7,7 @@ // import Combine -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import UIKit diff --git a/spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkControlType.swift b/.Demo/Classes/View/Components/TextLink/UIKit/TextLinkControlType.swift similarity index 100% rename from spark/Demo/Classes/View/Components/TextLink/UIKit/TextLinkControlType.swift rename to .Demo/Classes/View/Components/TextLink/UIKit/TextLinkControlType.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/BadgeCell/BadgeCell.swift b/.Demo/Classes/View/ListView/Cells/BadgeCell/BadgeCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/BadgeCell/BadgeCell.swift rename to .Demo/Classes/View/ListView/Cells/BadgeCell/BadgeCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/BadgeCell/BadgeConfiguration.swift b/.Demo/Classes/View/ListView/Cells/BadgeCell/BadgeConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/BadgeCell/BadgeConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/BadgeCell/BadgeConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ButtonCell/ButtonCell.swift b/.Demo/Classes/View/ListView/Cells/ButtonCell/ButtonCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ButtonCell/ButtonCell.swift rename to .Demo/Classes/View/ListView/Cells/ButtonCell/ButtonCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ButtonCell/ButtonConfiguration.swift b/.Demo/Classes/View/ListView/Cells/ButtonCell/ButtonConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ButtonCell/ButtonConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/ButtonCell/ButtonConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxCell.swift b/.Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxCell.swift rename to .Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxConfiguration.swift b/.Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/CheckboxCell/CheckboxConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupCell.swift b/.Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupCell.swift rename to .Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupConfiguration.swift b/.Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/CheckboxGroupCell/CheckboxGroupConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ChipCell/ChipCell.swift b/.Demo/Classes/View/ListView/Cells/ChipCell/ChipCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ChipCell/ChipCell.swift rename to .Demo/Classes/View/ListView/Cells/ChipCell/ChipCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ChipCell/ChipConfiguration.swift b/.Demo/Classes/View/ListView/Cells/ChipCell/ChipConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ChipCell/ChipConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/ChipCell/ChipConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/IconCell/IconCell.swift b/.Demo/Classes/View/ListView/Cells/IconCell/IconCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/IconCell/IconCell.swift rename to .Demo/Classes/View/ListView/Cells/IconCell/IconCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/IconCell/IconConfiguration.swift b/.Demo/Classes/View/ListView/Cells/IconCell/IconConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/IconCell/IconConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/IconCell/IconConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateCell.swift b/.Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateCell.swift rename to .Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateConfiguration.swift b/.Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/ProgressBarIndeterminateCell/ProgressBarIndeterminateConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleCell.swift b/.Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleCell.swift rename to .Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleConfiguration.swift b/.Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/ProgressBarSingleCell/ProgressBarSingleConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonCell.swift b/.Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonCell.swift rename to .Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonConfiguration.swift b/.Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/RadioButtonCell/RadioButtonConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupCell.swift b/.Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupCell.swift rename to .Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupConfiguration.swift b/.Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/RadioButtonGroupCell/RadioButtonGroupConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayCell.swift b/.Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayCell.swift rename to .Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayConfiguration.swift b/.Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/RatingDisplayCell/RatingDisplayConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputCell.swift b/.Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputCell.swift rename to .Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputConfiguration.swift b/.Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/RatingInputCell/RatingInputConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerCell.swift b/.Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerCell.swift rename to .Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerConfiguration.swift b/.Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/SpinnerCell/SpinnerConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/StarCell/StarCell.swift b/.Demo/Classes/View/ListView/Cells/StarCell/StarCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/StarCell/StarCell.swift rename to .Demo/Classes/View/ListView/Cells/StarCell/StarCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/StarCell/StarConfiguration.swift b/.Demo/Classes/View/ListView/Cells/StarCell/StarConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/StarCell/StarConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/StarCell/StarConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonCell.swift b/.Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonCell.swift rename to .Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonConfiguration.swift b/.Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/SwitchButtonCell/SwitchButtonConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/TabCell/TabCell.swift b/.Demo/Classes/View/ListView/Cells/TabCell/TabCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/TabCell/TabCell.swift rename to .Demo/Classes/View/ListView/Cells/TabCell/TabCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/TabCell/TabConfiguration.swift b/.Demo/Classes/View/ListView/Cells/TabCell/TabConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/TabCell/TabConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/TabCell/TabConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/TagCell/TagCell.swift b/.Demo/Classes/View/ListView/Cells/TagCell/TagCell.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/TagCell/TagCell.swift rename to .Demo/Classes/View/ListView/Cells/TagCell/TagCell.swift diff --git a/spark/Demo/Classes/View/ListView/Cells/TagCell/TagConfiguration.swift b/.Demo/Classes/View/ListView/Cells/TagCell/TagConfiguration.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/Cells/TagCell/TagConfiguration.swift rename to .Demo/Classes/View/ListView/Cells/TagCell/TagConfiguration.swift diff --git a/spark/Demo/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift b/.Demo/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift similarity index 99% rename from spark/Demo/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift rename to .Demo/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift index f4c4129c1..95a45fbd5 100644 --- a/spark/Demo/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift +++ b/.Demo/Classes/View/ListView/Controllers/RadioCheckboxUIViewController.swift @@ -7,8 +7,9 @@ // import UIKit -import SparkCore import Combine +import SparkCore +@_spi(SI_SPI) import SparkCommon final class RadioCheckboxUIViewController: UIViewController { diff --git a/spark/Demo/Classes/View/ListView/Controllers/RadioCheckboxView.swift b/.Demo/Classes/View/ListView/Controllers/RadioCheckboxView.swift similarity index 98% rename from spark/Demo/Classes/View/ListView/Controllers/RadioCheckboxView.swift rename to .Demo/Classes/View/ListView/Controllers/RadioCheckboxView.swift index fff301f71..e1ca9ba94 100644 --- a/spark/Demo/Classes/View/ListView/Controllers/RadioCheckboxView.swift +++ b/.Demo/Classes/View/ListView/Controllers/RadioCheckboxView.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/ListView/ListComponentsViewController.swift b/.Demo/Classes/View/ListView/ListComponentsViewController.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/ListComponentsViewController.swift rename to .Demo/Classes/View/ListView/ListComponentsViewController.swift diff --git a/spark/Demo/Classes/View/ListView/ListView+Protocols.swift b/.Demo/Classes/View/ListView/ListView+Protocols.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/ListView+Protocols.swift rename to .Demo/Classes/View/ListView/ListView+Protocols.swift diff --git a/spark/Demo/Classes/View/ListView/ListViewController.swift b/.Demo/Classes/View/ListView/ListViewController.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/ListViewController.swift rename to .Demo/Classes/View/ListView/ListViewController.swift diff --git a/spark/Demo/Classes/View/ListView/ListViewDatasource.swift b/.Demo/Classes/View/ListView/ListViewDatasource.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/ListViewDatasource.swift rename to .Demo/Classes/View/ListView/ListViewDatasource.swift diff --git a/spark/Demo/Classes/View/ListView/ListViewDelegate.swift b/.Demo/Classes/View/ListView/ListViewDelegate.swift similarity index 100% rename from spark/Demo/Classes/View/ListView/ListViewDelegate.swift rename to .Demo/Classes/View/ListView/ListViewDelegate.swift diff --git a/spark/Demo/Classes/View/SettingsViewController.swift b/.Demo/Classes/View/SettingsViewController.swift similarity index 100% rename from spark/Demo/Classes/View/SettingsViewController.swift rename to .Demo/Classes/View/SettingsViewController.swift diff --git a/spark/Demo/Classes/View/SparkActionSheet.swift b/.Demo/Classes/View/SparkActionSheet.swift similarity index 100% rename from spark/Demo/Classes/View/SparkActionSheet.swift rename to .Demo/Classes/View/SparkActionSheet.swift diff --git a/spark/Demo/Classes/View/Theme/Border/BorderItemView.swift b/.Demo/Classes/View/Theme/Border/BorderItemView.swift similarity index 97% rename from spark/Demo/Classes/View/Theme/Border/BorderItemView.swift rename to .Demo/Classes/View/Theme/Border/BorderItemView.swift index 4ea78cd2d..35895a53b 100644 --- a/spark/Demo/Classes/View/Theme/Border/BorderItemView.swift +++ b/.Demo/Classes/View/Theme/Border/BorderItemView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Border/BorderItemViewModel.swift b/.Demo/Classes/View/Theme/Border/BorderItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Border/BorderItemViewModel.swift rename to .Demo/Classes/View/Theme/Border/BorderItemViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Border/BorderSectionViewModel.swift b/.Demo/Classes/View/Theme/Border/BorderSectionViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Border/BorderSectionViewModel.swift rename to .Demo/Classes/View/Theme/Border/BorderSectionViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Border/BorderView.swift b/.Demo/Classes/View/Theme/Border/BorderView.swift similarity index 96% rename from spark/Demo/Classes/View/Theme/Border/BorderView.swift rename to .Demo/Classes/View/Theme/Border/BorderView.swift index c9ad3b22f..5c24945ff 100644 --- a/spark/Demo/Classes/View/Theme/Border/BorderView.swift +++ b/.Demo/Classes/View/Theme/Border/BorderView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Border/BorderViewModel.swift b/.Demo/Classes/View/Theme/Border/BorderViewModel.swift similarity index 98% rename from spark/Demo/Classes/View/Theme/Border/BorderViewModel.swift rename to .Demo/Classes/View/Theme/Border/BorderViewModel.swift index 74181a4ec..20fffc772 100644 --- a/spark/Demo/Classes/View/Theme/Border/BorderViewModel.swift +++ b/.Demo/Classes/View/Theme/Border/BorderViewModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct BorderViewModel { diff --git a/spark/Demo/Classes/View/Theme/Color/ColorItemView.swift b/.Demo/Classes/View/Theme/Color/ColorItemView.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/ColorItemView.swift rename to .Demo/Classes/View/Theme/Color/ColorItemView.swift diff --git a/spark/Demo/Classes/View/Theme/Color/ColorItemViewModel.swift b/.Demo/Classes/View/Theme/Color/ColorItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/ColorItemViewModel.swift rename to .Demo/Classes/View/Theme/Color/ColorItemViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/ColorView.swift b/.Demo/Classes/View/Theme/Color/ColorView.swift similarity index 96% rename from spark/Demo/Classes/View/Theme/Color/ColorView.swift rename to .Demo/Classes/View/Theme/Color/ColorView.swift index 290e937c5..224e613fd 100644 --- a/spark/Demo/Classes/View/Theme/Color/ColorView.swift +++ b/.Demo/Classes/View/Theme/Color/ColorView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Color/ColorViewModel.swift b/.Demo/Classes/View/Theme/Color/ColorViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/ColorViewModel.swift rename to .Demo/Classes/View/Theme/Color/ColorViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift b/.Demo/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift similarity index 96% rename from spark/Demo/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift rename to .Demo/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift index 46c6cd59c..b8fcd2dd8 100644 --- a/spark/Demo/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift +++ b/.Demo/Classes/View/Theme/Color/Sections/Enum/ColorSectionType.swift @@ -5,7 +5,7 @@ // Created by robin.lemaire on 13/03/2023. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore enum ColorSectionType: CaseIterable { diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift b/.Demo/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift similarity index 95% rename from spark/Demo/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift rename to .Demo/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift index b0c4ae439..22e4ba85f 100644 --- a/spark/Demo/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift +++ b/.Demo/Classes/View/Theme/Color/Sections/View/ColorSectionView.swift @@ -5,8 +5,9 @@ // Created by robin.lemaire on 10/03/2023. // -import Spark +@_spi(SI_SPI) import SparkCommon import SwiftUI +import SparkCore struct ColorSectionView: View { diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionAccentViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionAccentViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionAccentViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionAccentViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBaseViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBaseViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBaseViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBaseViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBasicViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBasicViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBasicViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionBasicViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionFeedbackViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionFeedbackViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionFeedbackViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionFeedbackViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionMainViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionMainViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionMainViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionMainViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionStatesViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionStatesViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionStatesViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionStatesViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionSupportViewModel.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionSupportViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionSupportViewModel.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionSupportViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionViewModelable.swift b/.Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionViewModelable.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionViewModelable.swift rename to .Demo/Classes/View/Theme/Color/Sections/ViewModel/ColorSectionViewModelable.swift diff --git a/spark/Demo/Classes/View/Theme/Dims/DimItemView.swift b/.Demo/Classes/View/Theme/Dims/DimItemView.swift similarity index 96% rename from spark/Demo/Classes/View/Theme/Dims/DimItemView.swift rename to .Demo/Classes/View/Theme/Dims/DimItemView.swift index ef34c0a58..379e24ac5 100644 --- a/spark/Demo/Classes/View/Theme/Dims/DimItemView.swift +++ b/.Demo/Classes/View/Theme/Dims/DimItemView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import Spark +@_spi(SI_SPI) import SparkCommon struct DimItemView: View { diff --git a/spark/Demo/Classes/View/Theme/Dims/DimItemViewModel.swift b/.Demo/Classes/View/Theme/Dims/DimItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Dims/DimItemViewModel.swift rename to .Demo/Classes/View/Theme/Dims/DimItemViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Dims/DimsView.swift b/.Demo/Classes/View/Theme/Dims/DimsView.swift similarity index 95% rename from spark/Demo/Classes/View/Theme/Dims/DimsView.swift rename to .Demo/Classes/View/Theme/Dims/DimsView.swift index 9b456ff4d..6ae7393e7 100644 --- a/spark/Demo/Classes/View/Theme/Dims/DimsView.swift +++ b/.Demo/Classes/View/Theme/Dims/DimsView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Dims/DimsViewModel.swift b/.Demo/Classes/View/Theme/Dims/DimsViewModel.swift similarity index 94% rename from spark/Demo/Classes/View/Theme/Dims/DimsViewModel.swift rename to .Demo/Classes/View/Theme/Dims/DimsViewModel.swift index 11e544c23..e96706faf 100644 --- a/spark/Demo/Classes/View/Theme/Dims/DimsViewModel.swift +++ b/.Demo/Classes/View/Theme/Dims/DimsViewModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowItemViewModel.swift b/.Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowItemViewModel.swift rename to .Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowItemViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift b/.Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift similarity index 97% rename from spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift rename to .Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift index f390f36e0..e76bfe3a4 100644 --- a/spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift +++ b/.Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift b/.Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift similarity index 95% rename from spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift rename to .Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift index 10db5c76d..0b984fd4f 100644 --- a/spark/Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift +++ b/.Demo/Classes/View/Theme/Elevation/DropShadow/DropShadowViewModel.swift @@ -7,7 +7,7 @@ // import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon struct DropShadowViewModel { diff --git a/spark/Demo/Classes/View/Theme/Elevation/ElevationView.swift b/.Demo/Classes/View/Theme/Elevation/ElevationView.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Elevation/ElevationView.swift rename to .Demo/Classes/View/Theme/Elevation/ElevationView.swift diff --git a/spark/Demo/Classes/View/Theme/Layout/LayoutSpacingItemView.swift b/.Demo/Classes/View/Theme/Layout/LayoutSpacingItemView.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Layout/LayoutSpacingItemView.swift rename to .Demo/Classes/View/Theme/Layout/LayoutSpacingItemView.swift diff --git a/spark/Demo/Classes/View/Theme/Layout/LayoutSpacingItemViewModel.swift b/.Demo/Classes/View/Theme/Layout/LayoutSpacingItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Layout/LayoutSpacingItemViewModel.swift rename to .Demo/Classes/View/Theme/Layout/LayoutSpacingItemViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Layout/LayoutView.swift b/.Demo/Classes/View/Theme/Layout/LayoutView.swift similarity index 95% rename from spark/Demo/Classes/View/Theme/Layout/LayoutView.swift rename to .Demo/Classes/View/Theme/Layout/LayoutView.swift index 1230d7304..b6156914b 100644 --- a/spark/Demo/Classes/View/Theme/Layout/LayoutView.swift +++ b/.Demo/Classes/View/Theme/Layout/LayoutView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Layout/LayoutViewModel.swift b/.Demo/Classes/View/Theme/Layout/LayoutViewModel.swift similarity index 95% rename from spark/Demo/Classes/View/Theme/Layout/LayoutViewModel.swift rename to .Demo/Classes/View/Theme/Layout/LayoutViewModel.swift index 926a12a7a..ad807e643 100644 --- a/spark/Demo/Classes/View/Theme/Layout/LayoutViewModel.swift +++ b/.Demo/Classes/View/Theme/Layout/LayoutViewModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/ThemeCellModel.swift b/.Demo/Classes/View/Theme/ThemeCellModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/ThemeCellModel.swift rename to .Demo/Classes/View/Theme/ThemeCellModel.swift diff --git a/spark/Demo/Classes/View/Theme/ThemeView.swift b/.Demo/Classes/View/Theme/ThemeView.swift similarity index 97% rename from spark/Demo/Classes/View/Theme/ThemeView.swift rename to .Demo/Classes/View/Theme/ThemeView.swift index 3bc7353f4..ef81a3070 100644 --- a/spark/Demo/Classes/View/Theme/ThemeView.swift +++ b/.Demo/Classes/View/Theme/ThemeView.swift @@ -8,7 +8,7 @@ import SwiftUI import SparkCore -import Spark +@_spi(SI_SPI) import SparkCommon struct ThemeView: View { diff --git a/spark/Demo/Classes/View/Theme/Typography/TypographyItemView.swift b/.Demo/Classes/View/Theme/Typography/TypographyItemView.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Typography/TypographyItemView.swift rename to .Demo/Classes/View/Theme/Typography/TypographyItemView.swift diff --git a/spark/Demo/Classes/View/Theme/Typography/TypographyItemViewModel.swift b/.Demo/Classes/View/Theme/Typography/TypographyItemViewModel.swift similarity index 100% rename from spark/Demo/Classes/View/Theme/Typography/TypographyItemViewModel.swift rename to .Demo/Classes/View/Theme/Typography/TypographyItemViewModel.swift diff --git a/spark/Demo/Classes/View/Theme/Typography/TypographyView.swift b/.Demo/Classes/View/Theme/Typography/TypographyView.swift similarity index 96% rename from spark/Demo/Classes/View/Theme/Typography/TypographyView.swift rename to .Demo/Classes/View/Theme/Typography/TypographyView.swift index 0fb85803d..023c33187 100644 --- a/spark/Demo/Classes/View/Theme/Typography/TypographyView.swift +++ b/.Demo/Classes/View/Theme/Typography/TypographyView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore import SwiftUI diff --git a/spark/Demo/Classes/View/Theme/Typography/TypographyViewModel.swift b/.Demo/Classes/View/Theme/Typography/TypographyViewModel.swift similarity index 98% rename from spark/Demo/Classes/View/Theme/Typography/TypographyViewModel.swift rename to .Demo/Classes/View/Theme/Typography/TypographyViewModel.swift index dab3221dc..a8b7a3dab 100644 --- a/spark/Demo/Classes/View/Theme/Typography/TypographyViewModel.swift +++ b/.Demo/Classes/View/Theme/Typography/TypographyViewModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // -import Spark +@_spi(SI_SPI) import SparkCommon import SparkCore struct TypographyViewModel { diff --git a/spark/Demo/Assets.xcassets/Contents.json b/.Demo/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from spark/Demo/Assets.xcassets/Contents.json rename to .Demo/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/.gitignore b/.gitignore index ef00d3a6a..5d97d8c71 100644 --- a/.gitignore +++ b/.gitignore @@ -93,7 +93,7 @@ iOSInjectionProject/ .DS_Store #xcodeproj -Spark.xcodeproj +*.xcodeproj #fastlane fastlane/README.md @@ -101,9 +101,6 @@ fastlane/README.md #output folder out -#snapshots -spark-ios-snapshots - #plists *.plist !IDETemplateMacros.plist @@ -111,5 +108,14 @@ spark-ios-snapshots #sourcery *Generated/ +# SPM +.swiftpm/ +Package.resolved + +# Other vendor/ *.lock + + +# SwiftLint Remote Config Cache +.swiftlint/RemoteConfigCache \ No newline at end of file diff --git a/.gitscript/inject_repository_package.swift b/.gitscript/inject_repository_package.swift new file mode 100644 index 000000000..565784529 --- /dev/null +++ b/.gitscript/inject_repository_package.swift @@ -0,0 +1,104 @@ +#!/usr/bin/swift + +/** + This script is launched when you add a /buildDemo comment of the spark-ios-component-XYZ repository pull requests . + + The goal of the script is to replace the external component package (spark-ios-component-XYZ repository) to the local (from ../ folder). + */ + +import Foundation + +if CommandLine.argc < 2 { + fatalError("No arguments are passed...") +} else { + let package = CommandLine.arguments.last! + print(package) + + let url = URL(filePath: "../Package.swift") + let dependencyContent = getDependencyContent(url, package: package) + changeToLocalPackage(url, package: package, dependency: dependencyContent) +} + +func getDependencyContent(_ url: URL, package: String) -> String { + do { + let text = try String( + contentsOf: url, + encoding: .utf8 + ) + + var dependency: String! + if let dependencies = text.sliceMultipleTimes(from: "dependencies", to: "]").first { + dependencies.sliceMultipleTimes(from: ".package(", to: ")").forEach { + + print("Packages \($0)") + + if let name = $0.sliceMultipleTimes(from: "path: \"../", to: "\"").first, + package == name { + dependency = $0 + } + } + } else { + fatalError("No dependencies found...") + } + + return dependency + } catch { + fatalError("Files can't be read...") + } +} + +func changeToLocalPackage(_ url: URL, package: String, dependency: String) { + do { + var text = try String( + contentsOf: url, + encoding: .utf8 + ) + + let newContent = dependency + .replacingOccurrences( + of: "// path", + with: "path" + ) + .replacingOccurrences( + of: "url", + with: "// url" + ) + .replacingOccurrences( + of: "/*version*/ ", + with: "// /*version*/ " + ) + .replacingOccurrences( + of: "../\(package)", + with: "../" + ) + + text = text.replacingOccurrences( + of: dependency, + with: newContent + ) + + try text.write( + to: url, + atomically: false, + encoding: .utf8 + ) + + print("Package.swift is updated") + + } catch { + fatalError("Files can't be read...") + } +} + +// MARK: - Extension + +extension String { + + func sliceMultipleTimes(from: String, to: String) -> [String] { + self.components(separatedBy: from).dropFirst().compactMap { sub in + (sub.range(of: to)?.lowerBound).flatMap { endRange in + String(sub[sub.startIndex ..< endRange]) + } + } + } +} diff --git a/postGenCommand.sh b/.postGenCommand.sh similarity index 100% rename from postGenCommand.sh rename to .postGenCommand.sh diff --git a/.sourcery.yml b/.sourcery.yml deleted file mode 100644 index 5b2a03dbf..000000000 --- a/.sourcery.yml +++ /dev/null @@ -1,41 +0,0 @@ -configurations: - - sources: - include: - - core/Sources - templates: - - stencil/sourcery-template/SparkCoreAutoMockable.stencil - output: core/Unit-tests/Sourcery/Generated/AutoMockable.generated.swift - args: - autoMockableImports: [Combine] - autoMockableTestableImports: [SparkCore] - - sources: - include: - - core/Sources - templates: - - stencil/sourcery-template/SparkCoreAutoMockTest.stencil - output: core/Unit-tests/Sourcery/Generated/AutoMockTest.generated.swift - args: - autoMockableImports: [Combine] - autoMockableTestableImports: [SparkCore] - - sources: - include: - - core/Sources - templates: - - stencil/sourcery-template/SparkCoreAutoPublisherTest.stencil - output: core/Unit-tests/Sourcery/Generated/AutoPublisherTest.generated.swift - args: - autoMockableImports: [Combine] - autoMockableTestableImports: [SparkCore] - - sources: - include: - - core/Sources - templates: - - stencil/sourcery-template/SparkCoreAutoViewModelStub.stencil - output: core/Unit-tests/Sourcery/Generated/AutoViewModelStub.generated.swift - args: - autoMockableImports: [Combine] - autoMockableTestableImports: [SparkCore] - - - - \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 0910e0682..299a24a71 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,112 +1 @@ -excluded: - - "**/*.generated.swift" - -# Enabled rules for CI linting -# List of all the rules: https://realm.github.io/SwiftLint/rule-directory.html -only_rules: - - colon - - comment_spacing - - custom_rules - - mark - - trailing_closure - - trailing_whitespace - - vertical_parameter_alignment_on_call - - weak_delegate - - comma - - private_subject - - unneeded_parentheses_in_closure_argument - - multiline_arguments - - multiline_parameters - - lower_acl_than_parent - - syntactic_sugar - - unneeded_break_in_switch - - multiline_function_chains - - operator_usage_whitespace - - redundant_optional_initialization - - statement_position - - explicit_self - - empty_count - - empty_string - - force_cast - - force_try - - force_unwrapping - - empty_collection_literal - - vertical_whitespace - - direct_return - # - file_name - - file_name_no_space - # - file_header - # - missing_docs - - toggle_bool - - yoda_condition - -# Rules triggering ERROR - -force_unwrapping: - severity: error - -comment_spacing: - severity: error - -trailing_whitespace: - severity: error - -mark: - severity: error - -trailing_closure: - only_single_muted_parameter: true - severity: error - -weak_delegate: - severity: error - -colon: - severity: error - -vertical_parameter_alignment_on_call: - severity: error - -private_subject: - severity: error - -unneeded_parentheses_in_closure_argument: - severity: error - -multiline_arguments: - severity: error - -lower_acl_than_parent: - severity: error - -syntactic_sugar: - severity: error - -comma: - severity: error - -unneeded_break_in_switch: - severity: error - -multiline_function_chains: - severity: error - -operator_usage_whitespace: - severity: error - -redundant_optional_initialization: - severity: error - -statement_position: - severity: error - -explicit_self: - severity: error - -custom_rules: - no_debugging_method: - included: ".*\\.swift" - name: "Debugging method" - regex: "(dump\\()|(print\\()|(debugPrint\\()|(NSLog\\()" - message: "Debugging method is not allowed." - severity: error +parent_config: https://raw.githubusercontent.com/adevinta/spark-ios-common/main/.swiftlint.yml \ No newline at end of file diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 7a118b49b..000000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..8a50c9b07 --- /dev/null +++ b/Package.swift @@ -0,0 +1,230 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// swiftlint:disable all +let package = Package( + name: "SparkCore", + platforms: [ + .iOS(.v15) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "SparkCore", + targets: ["SparkCore"] + ), + .library( + name: "SparkCoreTesting", + targets: ["SparkCoreTesting"] + ) + ], + dependencies: [ + .package( + url: "https://github.com/adevinta/spark-ios-common.git", + // path: "../spark-ios-common" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-theming.git", + // path: "../spark-ios-theming" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-badge.git", + // path: "../spark-ios-component-badge" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-bottom-sheet.git", + // path: "../spark-ios-component-bottom-sheet" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-button.git", + // path: "../spark-ios-component-button" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-checkbox.git", + // path: "../spark-ios-component-checkbox" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-chip.git", + // path: "../spark-ios-component-chip" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-form-field.git", + // path: "../spark-ios-component-form-field" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-icon.git", + // path: "../spark-ios-component-icon" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-progress-bar.git", + // path: "../spark-ios-component-progress-bar" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-progress-tracker.git", + // path: "../spark-ios-component-progress-tracker" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-radio-button.git", + // path: "../spark-ios-component-radio-button" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-rating.git", + // path: "../spark-ios-component-rating" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-slider.git", + // path: "../spark-ios-component-slider" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-spinner.git", + // path: "../spark-ios-component-spinner" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-switch.git", + // path: "../spark-ios-component-switch" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-tab.git", + // path: "../spark-ios-component-tab" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-tag.git", + // path: "../spark-ios-component-tag" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-text-field.git", + // path: "../spark-ios-component-text-field" + /*version*/ "0.0.1"..."999.999.999" + ), + .package( + url: "https://github.com/adevinta/spark-ios-component-text-link.git", + // path: "../spark-ios-component-text-link" + /*version*/ "0.0.1"..."999.999.999" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "SparkCore", + dependencies: [ + .product( + name: "SparkCommon", + package: "spark-ios-common" + ), + .product( + name: "SparkTheming", + package: "spark-ios-theming" + ), + .product( + name: "SparkBadge", + package: "spark-ios-component-badge" + ), + .product( + name: "SparkBottomSheet", + package: "spark-ios-component-bottom-sheet" + ), + .product( + name: "SparkButton", + package: "spark-ios-component-button" + ), + .product( + name: "SparkCheckbox", + package: "spark-ios-component-checkbox" + ), + .product( + name: "SparkChip", + package: "spark-ios-component-chip" + ), + .product( + name: "SparkFormField", + package: "spark-ios-component-form-field" + ), + .product( + name: "SparkIcon", + package: "spark-ios-component-icon" + ), + .product( + name: "SparkProgressBar", + package: "spark-ios-component-progress-bar" + ), + .product( + name: "SparkProgressTracker", + package: "spark-ios-component-progress-tracker" + ), + .product( + name: "SparkRadioButton", + package: "spark-ios-component-radio-button" + ), + .product( + name: "SparkRating", + package: "spark-ios-component-rating" + ), + .product( + name: "SparkSlider", + package: "spark-ios-component-slider" + ), + .product( + name: "SparkSpinner", + package: "spark-ios-component-spinner" + ), + .product( + name: "SparkSwitch", + package: "spark-ios-component-switch" + ), + .product( + name: "SparkTab", + package: "spark-ios-component-tab" + ), + .product( + name: "SparkTag", + package: "spark-ios-component-tag" + ), + .product( + name: "SparkTextField", + package: "spark-ios-component-text-field" + ), + .product( + name: "SparkTextLink", + package: "spark-ios-component-text-link" + ) + ], + path: "Sources/Core" + ), + .target( + name: "SparkCoreTesting", + dependencies: [ + "SparkCore", + .product( + name: "SparkTheme", + package: "spark-ios-theming" + ) + ], + path: "Sources/Testing" + ), + .testTarget( + name: "CoreTests", + dependencies: ["SparkCore"] + ), + ] +) diff --git a/README.md b/README.md index d41e8b5bb..b213ea0b5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,38 @@ # Spark-iOS ## Introduction -Spark is [Adevinta’s](https://www.adevinta.com/) iOS Design System. +Spark is [Adevinta’s](https://www.adevinta.com/) iOS (UIKit and SwiftUI) Design System. Its mission is to provide an easy to use, customizable UI experience for consumers. +## Packages + +Spark for iOS is a multi-repositories solution. + +You can plug and play the entire Spark iOS by importing the *SparkCore* package. + +You can also use only one or more packages. + +**SparkCore** contains the following packages: +- [Common](https://github.com/adevinta/spark-ios-common.git) +- [Theming](https://github.com/adevinta/spark-ios-theming.git) +- [Badge](https://github.com/adevinta/spark-ios-component-badge.git) +- [BottomSheet](https://github.com/adevinta/spark-ios-component-bottom-sheet.git) +- [Button](https://github.com/adevinta/spark-ios-component-button.git) +- [Checkbox](https://github.com/adevinta/spark-ios-component-checkbox.git) +- [Chip](https://github.com/adevinta/spark-ios-component-chip.git) +- [FormField](https://github.com/adevinta/spark-ios-component-form-field.git) +- [Icon](https://github.com/adevinta/spark-ios-component-icon.git) +- [ProgressBar](https://github.com/adevinta/spark-ios-component-progress-bar.git) +- [ProgressTracker](https://github.com/adevinta/spark-ios-component-progress-tracker.git) +- [RadioButton](https://github.com/adevinta/spark-ios-component-radio-button.git) +- [Rating](https://github.com/adevinta/spark-ios-component-rating.git) +- [Slider](https://github.com/adevinta/spark-ios-component-slider.git) +- [Spinner](https://github.com/adevinta/spark-ios-component-spinner.git) +- [Switch](https://github.com/adevinta/spark-ios-component-switch.git) +- [Tab](https://github.com/adevinta/spark-ios-component-tab.git) +- [Tag](https://github.com/adevinta/spark-ios-component-tag.git) +- [TextField](https://github.com/adevinta/spark-ios-component-text-field.git) +- [TextLink](https://github.com/adevinta/spark-ios-component-text-link.git) + ## More Details In Wiki [Spark Wiki Page](https://github.com/adevinta/spark-ios/wiki) @@ -10,10 +40,10 @@ Also, you can find design specifications and tech information for supported plat ## Getting Started ### Installation -Carthage: `github "adevinta/spark-ios" == 0.6.1` +- SPM (*Swift Package Manager*): `https://github.com/adevinta/spark-ios.git`, named ```SparkCore```. #### Plug & Play -If you want the easy-to-use Spark, a Plug & Play solution containing a single Theme is provided and ready to be used. For that, import Spark.xcframework +If you want the easy-to-use Spark, a Plug & Play solution containing a single Theme is provided and ready to be used. For that, import SparkCore. It's also possible to create your [own theme](https://github.com/adevinta/spark-ios/wiki/Theming#your-own-theming) ## Contributing diff --git a/Sources/Core/Core.swift b/Sources/Core/Core.swift new file mode 100644 index 000000000..e945c75ec --- /dev/null +++ b/Sources/Core/Core.swift @@ -0,0 +1,23 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@_exported import SparkCommon +@_exported import SparkTheming +@_exported import SparkBadge +@_exported import SparkBottomSheet +@_exported import SparkButton +@_exported import SparkCheckbox +@_exported import SparkChip +@_exported import SparkFormField +@_exported import SparkIcon +@_exported import SparkProgressBar +@_exported import SparkProgressTracker +@_exported import SparkRadioButton +@_exported import SparkRating +@_exported import SparkSlider +@_exported import SparkSpinner +@_exported import SparkSwitch +@_exported import SparkTab +@_exported import SparkTag +@_exported import SparkTextField +@_exported import SparkTextLink diff --git a/Sources/Testing/TestingCore.swift b/Sources/Testing/TestingCore.swift new file mode 100644 index 000000000..479956622 --- /dev/null +++ b/Sources/Testing/TestingCore.swift @@ -0,0 +1,4 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@_exported import SparkTheme diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift new file mode 100644 index 000000000..3cad2b63f --- /dev/null +++ b/Tests/CoreTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import Core + +final class CoreTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/core/Sources/Common/Combine/Global/Publisher+SubscribeExtension.swift b/core/Sources/Common/Combine/Global/Publisher+SubscribeExtension.swift deleted file mode 100644 index 3fceb62a1..000000000 --- a/core/Sources/Common/Combine/Global/Publisher+SubscribeExtension.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Publisher+SubscribeExtension.swift -// SparkCore -// -// Created by michael.zimmermann on 05.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation - -extension Publisher where Failure == Never { - func subscribe( - in subscriptions: inout Set, - on scheduler: S = UIScheduler.shared, - action: @escaping (Self.Output) -> Void ) where S: Scheduler { - self - .receive(on: scheduler) - .sink(receiveValue: { value in - action(value) - }) - .store(in: &subscriptions) - } -} diff --git a/core/Sources/Common/Combine/Global/UIScheduler.swift b/core/Sources/Common/Combine/Global/UIScheduler.swift deleted file mode 100644 index 564caee38..000000000 --- a/core/Sources/Common/Combine/Global/UIScheduler.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// UIScheduler.swift -// SparkCore -// -// Created by robin.lemaire on 12/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation - -/// Combine custom scheduler which schedule action in DispatchQueue.main -struct UIScheduler: Scheduler { - - // MARK: - Type Alias - - typealias SchedulerOptions = Never - typealias SchedulerTimeType = DispatchQueue.SchedulerTimeType - - // MARK: - Static Properties - - /// Shared instance - static let shared = Self() - - // MARK: - Public Properties - - /// This scheduler's definition of the current moment in time. - var now: SchedulerTimeType { self.dispatchQueue.now } - /// The minimum tolerance allowed by the scheduler. - var minimumTolerance: SchedulerTimeType.Stride { self.dispatchQueue.minimumTolerance } - - // MARK: - Private Properties - - private let dispatchQueue: DispatchQueue - private let key = DispatchSpecificKey() - private let value: UInt8 = 0 - - // MARK: - Initialization - - private init(dispatchQueue: DispatchQueue = .main) { - self.dispatchQueue = dispatchQueue - self.dispatchQueue.setSpecific(key: self.key, value: self.value) - } - - // MARK: - Schedule - - /// Performs the action at the next possible opportunity. Maybe immediat if we don't need to change thread - func schedule(options _: SchedulerOptions? = nil, _ action: @escaping () -> Void) { - if DispatchQueue.getSpecific(key: self.key) == self.value { - action() - } else { - self.dispatchQueue.schedule(action) - } - } - - /// Performs the action at some time after the specified date. - func schedule( - after date: SchedulerTimeType, - tolerance: SchedulerTimeType.Stride, - options _: SchedulerOptions? = nil, - _ action: @escaping () -> Void - ) { - self.dispatchQueue.schedule(after: date, tolerance: tolerance, options: nil, action) - } - - /// Performs the action at some time after the specified date, at the specified frequency, optionally taking into account tolerance if possible. - func schedule( - after date: SchedulerTimeType, - interval: SchedulerTimeType.Stride, - tolerance: SchedulerTimeType.Stride, - options _: SchedulerOptions? = nil, - _ action: @escaping () -> Void - ) -> Cancellable { - self.dispatchQueue.schedule(after: date, interval: interval, tolerance: tolerance, options: nil, action) - } -} diff --git a/core/Sources/Common/Combine/Publisher/EventPublisher.swift b/core/Sources/Common/Combine/Publisher/EventPublisher.swift deleted file mode 100644 index e6dfdebbc..000000000 --- a/core/Sources/Common/Combine/Publisher/EventPublisher.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// EventPublisher.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 29.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -// MARK: - Methods -extension UIControl { - func publisher(for event: Event) -> EventPublisher { - EventPublisher(control: self, event: event) - } -} - -// MARK: - Publisher -extension UIControl { - public struct EventPublisher: Publisher { - // MARK: - Result - public typealias Output = UIControl - public typealias Failure = Never - - // MARK: - Properties - var control: UIControl - var event: Event - - // MARK: - Initialize - public init(control: UIControl, event: Event) { - self.control = control - self.event = event - } - - // MARK: - Methods - public func receive( - subscriber: S - ) where S.Input == Output, S.Failure == Failure { - let subscription = EventSubscription( - target: subscriber, - control: self.control, - event: self.event - ) - subscriber.receive(subscription: subscription) - } - } -} - -// MARK: - Event subscription -class EventSubscription: Subscription where Target.Input == UIControl { - // MARK: - Properties - var target: Target? - weak var control: UIControl? - var event: UIControl.Event - - // MARK: - Initialize - init( - target: Target? = nil, - control: UIControl, - event: UIControl.Event - ) { - self.target = target - self.control = control - self.event = event - - control.addTarget( - self, - action: #selector(controlEventAction), - for: self.event - ) - } - - // MARK: - Methods - func request(_ demand: Subscribers.Demand) {} - - func cancel() { - self.control?.removeTarget(self, action: #selector(controlEventAction(sender:)), for: self.event) - self.target = nil - self.control = nil - } - - @objc func controlEventAction(sender: UIControl) { - _ = self.target?.receive(sender) - } -} diff --git a/core/Sources/Common/Combine/Publisher/EventPublisherTests.swift b/core/Sources/Common/Combine/Publisher/EventPublisherTests.swift deleted file mode 100644 index 9dc11fd2a..000000000 --- a/core/Sources/Common/Combine/Publisher/EventPublisherTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// EventPublisherTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 31.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import SwiftUI -import XCTest - -final class EventPublisherTests: XCTestCase { - - // MARK: - Properties - private var button: UIButton! - private var cancellables = Set() - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.button = UIButton(type: .system) - } - - // MARK: - Tests - func test_create() throws { - // Given - let sut = self.button.publisher(for: .touchUpOutside) - - // Then - XCTAssertNotNil(sut) - XCTAssertNotNil(sut.control) - XCTAssertEqual(sut.event, .touchUpOutside) - XCTAssertEqual(self.button.allControlEvents, []) - } - - func test_control_event() throws { - // Given - let sut = self.button.publisher(for: .touchDragExit) - - // Then - XCTAssertEqual(self.button.allControlEvents, []) - - // Given - self.register(publisher: sut) - - // Then - XCTAssertEqual(self.button.allControlEvents, .touchDragExit) - } - - func test_multiple_publishers() throws { - // Given - let publisherTouchUpInside = self.button.publisher(for: .touchUpInside) - let publisherTouchCancel = self.button.publisher(for: .touchCancel) - - // Then - XCTAssertEqual(self.button.allControlEvents, []) - - // Given - self.register(publisher: publisherTouchUpInside) - self.register(publisher: publisherTouchCancel) - - // Then - XCTAssertEqual(self.button.allControlEvents, [.touchUpInside, .touchCancel]) - } - - func test_cancel() throws { - // Given - let sut = self.button.publisher(for: .touchDragExit) - self.register(publisher: sut) - - // Then - XCTAssertEqual(self.button.allControlEvents, .touchDragExit) - - // Given - self.cancellables.removeAll() - - // Then - XCTAssertEqual(self.button.allControlEvents, []) - } - - // MARK: - Helper - - private func register(publisher: UIControl.EventPublisher) { - publisher - .sink(receiveValue: { _ in }) - .store(in: &self.cancellables) - } -} diff --git a/core/Sources/Common/Combine/Publisher/Publisher-SubscribeTests.swift b/core/Sources/Common/Combine/Publisher/Publisher-SubscribeTests.swift deleted file mode 100644 index c1d1fcd7d..000000000 --- a/core/Sources/Common/Combine/Publisher/Publisher-SubscribeTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Publisher-SubscribeTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 13.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import XCTest - -final class Publisher_SubscribeTests: XCTestCase { - private var subscriptions = Set() - - func test_subscribe() { - let sut = CurrentValueSubject(10).eraseToAnyPublisher() - - let exp = expectation(description: "Value should be emitted") - sut.subscribe(in: &self.subscriptions) { value in - XCTAssertEqual(value, 10) - exp.fulfill() - } - - wait(for: [exp], timeout: 0.1) - } -} diff --git a/core/Sources/Common/Combine/Publisher/ValueBinding.swift b/core/Sources/Common/Combine/Publisher/ValueBinding.swift deleted file mode 100644 index 6b253ee57..000000000 --- a/core/Sources/Common/Combine/Publisher/ValueBinding.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// PublishingBinding.swift -// SparkCore -// -// Created by Michael Zimmermann on 24.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI - -final class ValueBinding { - var selectedID: ID? - - lazy var binding = Binding( - get: { self.selectedID }, - set: { newValue in - self.selectedID = newValue - } - ) - - init(selectedID: ID?) { - self.selectedID = selectedID - } -} diff --git a/core/Sources/Common/Control/PropertyState/ControlPropertyState.swift b/core/Sources/Common/Control/PropertyState/ControlPropertyState.swift deleted file mode 100644 index 83f2ae437..000000000 --- a/core/Sources/Common/Control/PropertyState/ControlPropertyState.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ControlPropertyState.swift -// SparkCore -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// Contains the dynamic property for a ControlState. -final class ControlPropertyState { - - // MARK: - Properties - - var value: T? - private let state: ControlState - - // MARK: - Initialization - - /// Init the object with a state. The value is nil by default. - init(for state: ControlState) { - self.state = state - } -} diff --git a/core/Sources/Common/Control/PropertyState/ControlPropertyStateTests.swift b/core/Sources/Common/Control/PropertyState/ControlPropertyStateTests.swift deleted file mode 100644 index 731ddea94..000000000 --- a/core/Sources/Common/Control/PropertyState/ControlPropertyStateTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ControlPropertyStateTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ControlPropertyStateTests: XCTestCase { - - // MARK: - Tests - - func test_default_value() { - // GIVEN / WHEN - let state = ControlPropertyState(for: .normal) - - // THEN - XCTAssertNil( - state.value, - "Wrong value. Should be nil" - ) - } -} diff --git a/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift b/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift deleted file mode 100644 index 9bfc8533b..000000000 --- a/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// ControlPropertyStates.swift -// SparkCore -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Manage all the states for a dynamic property. -final class ControlPropertyStates { - - // MARK: - Type Alias - - private typealias PropertyState = ControlPropertyState - - // MARK: - Properties - - private var normalState = PropertyState(for: .normal) - private var highlightedState = PropertyState(for: .highlighted) - private var disabledState = PropertyState(for: .disabled) - private var selectedState = PropertyState(for: .selected) - - // MARK: - Setter - - /// Set the new value for a state. - /// - Parameters: - /// - value: the new value - /// - state: the state for the new value - func setValue(_ value: PropertyType?, for state: ControlState) { - let propertyState: PropertyState - - switch state { - case .normal: - propertyState = self.normalState - case .highlighted: - propertyState = self.highlightedState - case .disabled: - propertyState = self.disabledState - case .selected: - propertyState = self.selectedState - } - - propertyState.value = value - } - - // MARK: - Getter - - /// Get the value for a state. - /// - Parameters: - /// - state: the state of the value - func value(for state: ControlState) -> PropertyType? { - switch state { - case .normal: return self.normalState.value - case .highlighted: return self.highlightedState.value - case .disabled: return self.disabledState.value - case .selected: return self.selectedState.value - } - } - - /// Get the value for the status of the control. - /// - Parameters: - /// - status: the status of the control - func value(for status: ControlStatus) -> PropertyType? { - // isHighlighted has the highest priority, - // then isDisabled, - // then isSelected, - // and if there is no matching case, we always return the normal value. - - if status.isHighlighted, let value = self.highlightedState.value { - return value - } else if !status.isEnabled, let value = self.disabledState.value { - return value - } else if status.isSelected, let value = self.selectedState.value { - return value - } else { - return self.normalState.value - } - } -} diff --git a/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift b/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift deleted file mode 100644 index 22c75ddcb..000000000 --- a/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift +++ /dev/null @@ -1,394 +0,0 @@ -// -// ControlPropertyStatesTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ControlPropertyStatesTests: XCTestCase { - - // MARK: - Value for States - Tests - - func test_value_for_all_states_when_value_is_set() { - // GIVEN - let expectedValue = "Value" - - let states = ControlState.allCases - - for state in states { - let states = ControlPropertyStates() - states.setValue(expectedValue, for: state) - - // WHEN - let value = states.value(for: state) - - // THEN - XCTAssertEqual( - value, - expectedValue, - "Wrong value for the .\(state) state" - ) - } - } - - func test_value_for_all_states_when_value_is_nil() { - // GIVEN - let states = ControlState.allCases - - for state in states { - let states = ControlPropertyStates() - states.setValue(nil, for: state) - - // WHEN - let value = states.value(for: state) - - // THEN - XCTAssertNil( - value, - "The value should be nil for the .\(state) state" - ) - } - } - - // MARK: - Value for Status - Tests - - func test_all_values_when_status_isHighlighted() { - // GIVEN - let normalStateValue = "normal" - let highlightedValue = "highlighted" - - let states = ControlPropertyStates() - - // Set value for normal state (default state) - states.setValue(normalStateValue, for: .normal) - - // ** - // WHEN - // Test with .highlighted value and true isHighlighted status. - - var status = ControlStatus(isHighlighted: true) - states.setValue(highlightedValue, for: .highlighted) - var value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - highlightedValue, - "Wrong value WHEN status isHighlighted AND set .highlighted state value" - ) - // ** - - // ** - // WHEN - // Test without .highlighted value and true isHighlighted status. - - status = ControlStatus(isHighlighted: true) - states.setValue(nil, for: .highlighted) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN status isHighlighted AND nil .highlighted state value" - ) - // ** - - // ** - // WHEN - // Test with .highlighted value and false isHighlighted status. - - status = .init(isHighlighted: false) - - states.setValue(highlightedValue, for: .highlighted) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN status !isHighlighted AND set .highlighted state value" - ) - // ** - } - - func test_all_values_when_status_isDisabled() { - // GIVEN - let normalStateValue = "normal" - let disabledValue = "disabled" - let highlightedValue = "highlighted" - - let states = ControlPropertyStates() - - // Set value for normal state (default state) - states.setValue(normalStateValue, for: .normal) - - // ** - // WHEN - // Test with .disabled value and false isEnabled status. - - var status = ControlStatus(isEnabled: false) - states.setValue(disabledValue, for: .disabled) - var value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - disabledValue, - "Wrong value WHEN status isDisabled AND set .disabled state value" - ) - // ** - - // ** - // WHEN - // Test without .disabled value and false isEnabled status. - - status = ControlStatus(isEnabled: false) - states.setValue(nil, for: .disabled) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN status isDisabled AND nil .disabled state value" - ) - // ** - - // ** - // WHEN - // Test with .disabled value and false isDisabled status. - - status = .init(isEnabled: true) - - states.setValue(disabledValue, for: .disabled) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN status !isDisabled AND set .disabled state value" - ) - // ** - - // ** - // WHEN - // Test with .disabled value and false isEnabled status. - // AND with value for .highlighted and isHighlighted status is true - - status = .init(isHighlighted: true, isEnabled: false) - - states.setValue(disabledValue, for: .disabled) - states.setValue(highlightedValue, for: .highlighted) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - highlightedValue, - "Wrong value WHEN status isDisabled and isHighlighted AND set .disabled and .highlighted state value" - ) - // ** - - // ** - // WHEN - // Test with .disabled value and false isEnabled status. - // AND without value for .highlighted and isHighlighted status is true - - status = .init(isHighlighted: true, isEnabled: false) - - states.setValue(disabledValue, for: .disabled) - states.setValue(nil, for: .highlighted) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - disabledValue, - "Wrong value WHEN status isDisabled and isHighlighted AND set .disabled and nil .highlighted state value" - ) - // ** - } - - func test_all_values_when_status_isSelected() { - // GIVEN - let normalStateValue = "normal" - let selectedValue = "selected" - let disabledValue = "disabled" - let highlightedValue = "highlighted" - - let states = ControlPropertyStates() - - // Set value for normal state (default state) - states.setValue(normalStateValue, for: .normal) - - var status = ControlStatus(isSelected: true) - - // ** - // WHEN - // Test with .selected value and true isSelected status. - - states.setValue(selectedValue, for: .selected) - var value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - selectedValue, - "Wrong value WHEN status isSelected AND set .selected state value" - ) - // ** - - // ** - // WHEN - // Test without .selected value and true isSelected status. - - states.setValue(nil, for: .selected) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN status isSelected AND nil .selected state value" - ) - // ** - - // ** - // WHEN - // Test with .selected value and false isSelected status. - - status = .init(isSelected: false) - - states.setValue(selectedValue, for: .selected) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN status !isSelected AND set .selected state value" - ) - // ** - - // ** - // WHEN - // Test with .selected value and true isSelected status. - // AND with value for .highlighted and isHighlighted status is true - - status = .init(isHighlighted: true, isSelected: true) - - states.setValue(selectedValue, for: .selected) - states.setValue(highlightedValue, for: .highlighted) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - highlightedValue, - "Wrong value WHEN status isSelected and isHighlighted AND set .selected and .highlighted state value" - ) - // ** - - // ** - // WHEN - // Test with .selected value and true isSelected status. - // AND without value for .highlighted and isHighlighted status is true - - status = .init(isHighlighted: true, isSelected: true) - - states.setValue(selectedValue, for: .selected) - states.setValue(nil, for: .highlighted) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - selectedValue, - "Wrong value WHEN status isSelected and isHighlighted AND set .selected and nil .highlighted state value" - ) - // ** - - // ** - // WHEN - // Test with .selected value and true isSelected status. - // AND with value for .disabled and isEnabled status is false. - - status = .init(isEnabled: false, isSelected: true) - - states.setValue(selectedValue, for: .selected) - states.setValue(disabledValue, for: .disabled) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - disabledValue, - "Wrong value WHEN status isSelected and isDisabled AND set .selected and .disabled state value" - ) - // ** - - // ** - // WHEN - // Test with .selected value and true isSelected status. - // AND without value for .disabled and isEnabled status is false. - - status = .init(isEnabled: false, isSelected: true) - - states.setValue(selectedValue, for: .selected) - states.setValue(nil, for: .disabled) - value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - selectedValue, - "Wrong value WHEN status isSelected and isDisabled AND set .selected and nil .disabled state value" - ) - // ** - } - - func test_all_values_when_status_isNormal() { - // GIVEN - let normalStateValue = "normal" - - let states = ControlPropertyStates() - - // ** - // WHEN - // Test with .normal value and all false properties on status. - - var status = ControlStatus() - states.setValue(normalStateValue, for: .normal) - var value = states.value(for: status) - - // THEN - XCTAssertEqual( - value, - normalStateValue, - "Wrong value WHEN all status properties are false AND set .normal state value" - ) - // ** - - // ** - // WHEN - // Test without .normal value and all false properties on status. - - status = ControlStatus() - states.setValue(nil, for: .normal) - value = states.value(for: status) - - // THEN - XCTAssertNil( - value, - "Wrong value WHEN all status properties are false AND nil .normal state value" - ) - // ** - } -} diff --git a/core/Sources/Common/Control/State/ControlState.swift b/core/Sources/Common/Control/State/ControlState.swift deleted file mode 100644 index e376d9089..000000000 --- a/core/Sources/Common/Control/State/ControlState.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ControlState.swift -// SparkCore -// -// Created by robin.lemaire on 23/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Constants describing the state of a Spark control. -public enum ControlState: CaseIterable, Equatable { - /// The normal, or default, state of a control where the control is enabled but neither selected nor highlighted. - case normal - /// The highlighted state of a control. - case highlighted - /// The disabled state of a control. - case disabled - /// The selected state of a control. - case selected -} diff --git a/core/Sources/Common/Control/Status/ControlStatus.swift b/core/Sources/Common/Control/Status/ControlStatus.swift deleted file mode 100644 index 56f806bb5..000000000 --- a/core/Sources/Common/Control/Status/ControlStatus.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ControlStatus.swift -// SparkCore -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The current status of the control: highlighted or not, disabled or not and selected or not. -final class ControlStatus: Equatable { - - // MARK: - Properties - - /// A Boolean value indicating whether the control draws a highlight. - var isHighlighted: Bool - /// A Boolean value indicating whether the control is in the enabled state. - var isEnabled: Bool - /// A Boolean value indicating whether the control is in the selected state. - var isSelected: Bool - - // MARK: - Initialization - - init( - isHighlighted: Bool = false, - isEnabled: Bool = true, - isSelected: Bool = false - ) { - self.isHighlighted = isHighlighted - self.isEnabled = isEnabled - self.isSelected = isSelected - } - - // MARK: - Equatable - - static func == (lhs: ControlStatus, rhs: ControlStatus) -> Bool { - return lhs.isHighlighted == rhs.isHighlighted && - lhs.isEnabled == rhs.isEnabled && - lhs.isSelected == rhs.isSelected - } -} diff --git a/core/Sources/Common/Control/Status/ControlStatusTests.swift b/core/Sources/Common/Control/Status/ControlStatusTests.swift deleted file mode 100644 index 653234502..000000000 --- a/core/Sources/Common/Control/Status/ControlStatusTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// ControlStatusTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ControlStatusTests: XCTestCase { - - // MARK: - Tests - - func test_init_with_default_values() { - // GIVEN / WHEN - let status = ControlStatus() - - // THEN - XCTAssertFalse(status.isHighlighted, "Wrong isHighlighted value") - XCTAssertTrue(status.isEnabled, "Wrong isSelected value") - XCTAssertFalse(status.isSelected, "Wrong isSelected value") - } - - func test_init() { - // GIVEN - let givenIsHighlighted = true - let givenIsEnabled = true - let givenIsSelected = true - - // WHEN - let status = ControlStatus( - isHighlighted: givenIsHighlighted, - isEnabled: givenIsEnabled, - isSelected: givenIsSelected - ) - - // THEN - XCTAssertEqual( - status.isHighlighted, - givenIsHighlighted, - "Wrong isHighlighted" - ) - - XCTAssertEqual( - status.isEnabled, - givenIsEnabled, - "Wrong isEnabled" - ) - - XCTAssertEqual( - status.isSelected, - givenIsSelected, - "Wrong isSelected" - ) - } -} diff --git a/core/Sources/Common/Control/SwiftUI/ControlStateImage.swift b/core/Sources/Common/Control/SwiftUI/ControlStateImage.swift deleted file mode 100644 index 743bfb505..000000000 --- a/core/Sources/Common/Control/SwiftUI/ControlStateImage.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// ControlStateImage.swift -// SparkCore -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -final class ControlStateImage: ObservableObject { - - // MARK: - Public Published Properties - - @Published var image: Image? - - // MARK: - Private Properties - - private let imageStates = ControlPropertyStates() - - // MARK: - Setter - - /// Set the image for a state. - /// - parameter image: the new image - /// - parameter state: the state of the image - /// - parameter status: the status of the parent control - func setImage( - _ image: Image?, - for state: ControlState, - on status: ControlStatus - ) { - self.imageStates.setValue(image, for: state) - self.updateContent(from: status) - } - - // MARK: - Update UI - - /// Update the label (image or attributed) for a parent control state. - /// - parameter status: the status of the parent control - func updateContent(from status: ControlStatus) { - self.image = self.imageStates.value(for: status) - } -} diff --git a/core/Sources/Common/Control/SwiftUI/ControlStateImageTests.swift b/core/Sources/Common/Control/SwiftUI/ControlStateImageTests.swift deleted file mode 100644 index 1a198fcd8..000000000 --- a/core/Sources/Common/Control/SwiftUI/ControlStateImageTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// ControlStateImageTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI - -@testable import SparkCore - -final class ControlStateImageTests: XCTestCase { - - // MARK: - Tests - - func test_default_image() { - // GIVEN / WHEN - let controlStateImage = ControlStateImage() - - // THEN - XCTAssertNil(controlStateImage.image) - } - - func test_setImage() { - // GIVEN - let givenImage = Image("arrow") - - let controlStateImage = ControlStateImage() - - let statesMock = ControlPropertyStates() - statesMock.setValue( - givenImage, - for: .normal - ) - - // WHEN - controlStateImage.setImage( - givenImage, - for: .normal, - on: .init() - ) - - // THEN - XCTAssertEqual( - controlStateImage.image, - statesMock.value(for: .normal) - ) - } - - func test_updateContent() { - // GIVEN - let givenDisabledImage = Image("switchOff") - - let control = ControlStatus(isEnabled: false) - - let controlStateImage = ControlStateImage() - - let statesMock = ControlPropertyStates() - - controlStateImage.setImage( - givenDisabledImage, - for: .disabled, - on: .init() - ) - statesMock.setValue( - givenDisabledImage, - for: .disabled - ) - - // WHEN - controlStateImage.updateContent(from: control) - - // THEN - XCTAssertEqual( - controlStateImage.image, - statesMock.value(for: .disabled) - ) - } -} diff --git a/core/Sources/Common/Control/SwiftUI/ControlStateText.swift b/core/Sources/Common/Control/SwiftUI/ControlStateText.swift deleted file mode 100644 index ae3a396c6..000000000 --- a/core/Sources/Common/Control/SwiftUI/ControlStateText.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ControlStateText.swift -// SparkCore -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -final class ControlStateText: ObservableObject { - - // MARK: - Public Published Properties - - @Published var text: String? - @Published var attributedText: AttributedString? - - // MARK: - Private Properties - - private let textStates = ControlPropertyStates() - private let attributedTextStates = ControlPropertyStates() - private let textTypesStates = ControlPropertyStates() - - // MARK: - Setter - - /// Set the text for a state. - /// - parameter text: new text - /// - parameter state: state of the text - /// - parameter status: the status of the parent control - func setText( - _ text: String?, - for state: ControlState, - on status: ControlStatus - ) { - self.textStates.setValue(text, for: state) - self.textTypesStates.setValue( - text != nil ? .text : DisplayedTextType.none, - for: state - ) - self.updateContent(from: status) - } - - /// Set the attributed text for a state. - /// - parameter text: new attributed text - /// - parameter state: state of the attributed text - /// - parameter status: the status of the parent control - func setAttributedText( - _ attributedText: AttributedString?, - for state: ControlState, - on status: ControlStatus - ) { - self.attributedTextStates.setValue(attributedText, for: state) - self.textTypesStates.setValue( - attributedText != nil ? .attributedText : DisplayedTextType.none, - for: state - ) - self.updateContent(from: status) - } - - // MARK: - Update UI - - /// Update the label (text or attributed) for a parent control state. - /// - parameter status: the status of the parent control - func updateContent(from status: ControlStatus) { - // Get the current textType from status - let textType = self.textTypesStates.value(for: status) - let textTypeContainsText = textType?.containsText ?? false - - // Set the text or the attributedText from textType and states - if let text = self.textStates.value(for: status), - textType == .text || !textTypeContainsText { - self.attributedText = nil - self.text = text - - } else if let attributedText = self.attributedTextStates.value(for: status), - textType == .attributedText || !textTypeContainsText { - self.text = nil - self.attributedText = attributedText - - } else { // No text to displayed - self.text = nil - self.attributedText = nil - } - } -} diff --git a/core/Sources/Common/Control/SwiftUI/ControlStateTextTests.swift b/core/Sources/Common/Control/SwiftUI/ControlStateTextTests.swift deleted file mode 100644 index 3f972044f..000000000 --- a/core/Sources/Common/Control/SwiftUI/ControlStateTextTests.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// ControlStateTextTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI - -@testable import SparkCore - -final class ControlStateTextTests: XCTestCase { - - // MARK: - Tests - - func test_default_values() { - // GIVEN / WHEN - let controlStateText = ControlStateText() - - // THEN - XCTAssertNil( - controlStateText.text, - "Wrong text value" - ) - XCTAssertNil( - controlStateText.attributedText, - "Wrong attributedText value" - ) - } - - func test_setText() { - // GIVEN - let givenText = "My Text" - - let controlStateText = ControlStateText() - - let statesMock = ControlPropertyStates() - statesMock.setValue( - givenText, - for: .normal - ) - - var control = ControlStatus() - - // WHEN - controlStateText.setText( - givenText, - for: .normal, - on: control - ) - - // THEN - XCTAssertEqual( - controlStateText.text, - statesMock.value(for: .normal), - "Wrong text value" - ) - XCTAssertNil( - controlStateText.attributedText, - "Wrong attributedText value" - ) - - // WHEN - - // Check when a text is nil for an another state, - // The text text for the normal state should be returned - - control.isHighlighted = true - - controlStateText.setText( - nil, - for: .highlighted, - on: control - ) - - // THEN - XCTAssertEqual( - controlStateText.text, - statesMock.value(for: .normal), - "Wrong text value when control isPressed" - ) - } - - func test_setAttributedText() { - // GIVEN - let givenAttributedText = AttributedString("My AT Text") - - let controlStateText = ControlStateText() - - let statesMock = ControlPropertyStates() - statesMock.setValue( - givenAttributedText, - for: .normal - ) - - var control = ControlStatus() - - // WHEN - controlStateText.setAttributedText( - givenAttributedText, - for: .normal, - on: control - ) - - // THEN - XCTAssertNil( - controlStateText.text, - "Wrong text value" - ) - XCTAssertEqual( - controlStateText.attributedText, - statesMock.value(for: .normal), - "Wrong attributedText value" - ) - - // WHEN - - // Check when a attributedText is nil for an another state, - // The attributedText value for the normal state should be returned - - control.isHighlighted = true - - controlStateText.setAttributedText( - nil, - for: .highlighted, - on: control - ) - - // THEN - XCTAssertEqual( - controlStateText.attributedText, - statesMock.value(for: .normal), - "Wrong attributedText value when control isPressed" - ) - } - - func test_updateContent() { - // GIVEN - let givenDisabledText = "My Disabled Text" - - let control = ControlStatus(isEnabled: false) - - let controlStateText = ControlStateText() - - let statesMock = ControlPropertyStates() - - controlStateText.setText( - givenDisabledText, - for: .disabled, - on: .init() - ) - statesMock.setValue( - givenDisabledText, - for: .disabled - ) - - // WHEN - controlStateText.updateContent(from: control) - - // THEN - XCTAssertEqual( - controlStateText.text, - statesMock.value(for: .disabled) - ) - } -} diff --git a/core/Sources/Common/Control/UIView/UIControlStateImageView.swift b/core/Sources/Common/Control/UIView/UIControlStateImageView.swift deleted file mode 100644 index 58a8e3ae7..000000000 --- a/core/Sources/Common/Control/UIView/UIControlStateImageView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// UIControlStateImageView.swift -// SparkCore -// -// Created by robin.lemaire on 25/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// The custom UIImageView which set the correct image from the state of the UIControl. -/// Must be used only on UIControl. -final class UIControlStateImageView: UIImageView { - - // MARK: - Properties - - private let imageStates = ControlPropertyStates() - - /// The image must be stored to lock the posibility to set the image directly like this **self.image = UIImage()**. - /// When the storedImage is set (always from **setImage** function), it set the image. - private var storedImage: UIImage? { - didSet { - self.isImage = self.storedImage != nil - self.image = self.storedImage - } - } - - // MARK: - Published - - @Published var isImage: Bool = false - - // MARK: - Override Properties - - /// It's not possible to set the image outside this class. - /// The only possiblity to change the image is to use the **setImage(_: UIImage?, for: ControlState, on: UIControl)** function. - override var image: UIImage? { - get { - return super.image - } - set { - if newValue == self.storedImage { - super.image = newValue - } - } - } - - // MARK: - Setter & Getter - - /// The image for a state. - /// - parameter state: state of the image - func image(for state: ControlState) -> UIImage? { - return self.imageStates.value(for: state) - } - - /// Set the image for a state. - /// - parameter image: new image - /// - parameter state: state of the image - /// - parameter control: the parent control - func setImage( - _ image: UIImage?, - for state: ControlState, - on control: UIControl - ) { - self.imageStates.setValue(image, for: state) - self.updateContent(from: control) - } - - // MARK: - Update UI - - /// Update the image for a parent control state. - /// - parameter control: the parent control - func updateContent(from control: UIControl) { - // Create the status from the control - let status = ControlStatus( - isHighlighted: control.isHighlighted, - isEnabled: control.isEnabled, - isSelected: control.isSelected - ) - - // Set the image from states - self.storedImage = self.imageStates.value(for: status) - } -} diff --git a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift b/core/Sources/Common/Control/UIView/UIControlStateLabel.swift deleted file mode 100644 index 324829cc7..000000000 --- a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// UIControlStateLabel.swift -// SparkCore -// -// Created by robin.lemaire on 23/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// The custom UILabel which set the correct text or attributedText from the state of the UIControl. -/// Must be used only on UIControl. -final class UIControlStateLabel: UILabel { - - // MARK: - Properties - - private let textStates = ControlPropertyStates() - private let attributedTextStates = ControlPropertyStates() - private let textTypesStates = ControlPropertyStates() - - /// The text must be stored to lock the posibility to set the text directly like this **self.text = "Text"**. - /// When the storedText is set (always from **setText** function), it set the text. - private var storedText: String? { - didSet { - self.isText = self.storedText != nil - self.text = self.storedText - - // Reset styles - if let storedTextFont = self.storedTextFont { - self.font = storedTextFont - } - if let storedTextColor = self.storedTextColor { - self.textColor = storedTextColor - } - } - } - - /// The attributedText must be stored to lock the posibility to set the attributedText directly like this **self.attributedText = NSAttributedString()**. - /// When the storedAttributedText is set (always from **setAttributedText** function), it set the attributedText. - private var storedAttributedText: NSAttributedString? { - didSet { - self.isText = self.storedAttributedText != nil - self.attributedText = self.storedAttributedText - } - } - - /// The storedTextFont is use to reset the font when a new text is set - /// because the previous text can be an attributedText - /// and attributedText has its own styles. - private var storedTextFont: UIFont? - - /// The storedTextColor is use to reset the textColor when a new text is set - /// because the previous text can be an attributedText - /// and attributedText has its own styles. - private var storedTextColor: UIColor? - - // MARK: - Published - - @Published var isText: Bool = false - - // MARK: - Override Properties - - /// It's not possible to set the text outside this class. - /// The only possiblity to change the text is to use the **setText(_: String?, for: ControlState, on: UIControl)** function. - override var text: String? { - get { - return super.text - } - set { - // Set the attributedText only if the current come from setText - if newValue == self.storedText { - super.text = newValue - } - } - } - - /// It's not possible to set the attributedText outside this class. - /// The only possiblity to change the attributedText is to use the **setAttributedText(_: NSAttributedString?, for: ControlState, on: UIControl)** function. - override var attributedText: NSAttributedString? { - get { - return super.attributedText - } - set { - // Set the attributedText only if the current come from setAttributedText - if newValue == self.storedAttributedText { - super.attributedText = newValue - } - } - } - - override var font: UIFont! { - get { - return super.font - } - set { - // We need to store this value to put it back when a new text will be set (and not an attributedText) - self.storedTextFont = newValue - - // Set the font only if the display text is not an attributedText. - if !self.isAttributedDisplayed { - super.font = newValue - } - } - } - - override var textColor: UIColor! { - get { - return super.textColor - } - set { - // We need to store this value to put it back when a new text will be set (and not an attributedText) - self.storedTextColor = newValue - - // Set the color only if the display text is not an attributedText - if !self.isAttributedDisplayed { - super.textColor = newValue - } - } - } - - // MARK: - Setter & Getter - - /// The text for a state. - /// - parameter state: state of the text - func text(for state: ControlState) -> String? { - return self.textStates.value(for: state) - } - - /// Set the text for a state. - /// - parameter text: new text - /// - parameter state: state of the text - /// - parameter control: the parent control - func setText( - _ text: String?, - for state: ControlState, - on control: UIControl - ) { - self.textStates.setValue(text, for: state) - self.textTypesStates.setValue( - text != nil ? .text : DisplayedTextType.none, - for: state - ) - self.updateContent(from: control) - } - - /// The attributedText for a state. - /// - parameter state: state of the attributedText - func attributedText(for state: ControlState) -> NSAttributedString? { - return self.attributedTextStates.value(for: state) - } - - /// Set the attributedText of the button for a state. - /// - parameter attributedText: new attributedText of the button - /// - parameter state: state of the attributedText - func setAttributedText( - _ attributedText: NSAttributedString?, - for state: ControlState, - on control: UIControl - ) { - self.attributedTextStates.setValue(attributedText, for: state) - self.textTypesStates.setValue( - attributedText != nil ? .attributedText : DisplayedTextType.none, - for: state - ) - self.updateContent(from: control) - } - - // MARK: - Update UI - - /// Update the label (text or attributed) for a parent control state. - /// - parameter control: the parent control - func updateContent(from control: UIControl) { - // Create the status from the control - let status = ControlStatus( - isHighlighted: control.isHighlighted, - isEnabled: control.isEnabled, - isSelected: control.isSelected - ) - - // Get the current textType from status - let textType = self.textTypesStates.value(for: status) - let textTypeContainsText = textType?.containsText ?? false - - // Set the text or the attributedText from textType and states - if let text = self.textStates.value(for: status), - textType == .text || !textTypeContainsText { - self.storedAttributedText = nil - self.storedText = text - - } else if let attributedText = self.attributedTextStates.value(for: status), - textType == .attributedText || !textTypeContainsText { - self.storedAttributedText = attributedText - - } else { // No text to displayed - self.storedText = nil - } - } - - // MARK: - Helpers - - /// The attributedText is displayed or not. - private var isAttributedDisplayed: Bool { - // There is an attributedText ? - guard let attributedText = self.attributedText, attributedText.length > 0 else { - return false - } - - return self.storedAttributedText == attributedText - } -} diff --git a/core/Sources/Common/DataType/Array-Safe.swift b/core/Sources/Common/DataType/Array-Safe.swift deleted file mode 100644 index d70a7ae48..000000000 --- a/core/Sources/Common/DataType/Array-Safe.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Array-Safe.swift -// SparkCore -// -// Created by michael.zimmermann on 09.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension Array { - subscript(safe index: Int) -> Element? { - guard index >= 0, index < self.endIndex else { - return nil - } - - return self[index] - } -} diff --git a/core/Sources/Common/DataType/Sequence-Compacted.swift b/core/Sources/Common/DataType/Sequence-Compacted.swift deleted file mode 100644 index 41f97b785..000000000 --- a/core/Sources/Common/DataType/Sequence-Compacted.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Sequence-Compacted.swift -// SparkCore -// -// Created by michael.zimmermann on 31.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension Sequence { - func compacted() -> [ElementOfResult] where Element == ElementOfResult? { - return self.compactMap { $0 } - } -} diff --git a/core/Sources/Common/DataType/Updateable.swift b/core/Sources/Common/DataType/Updateable.swift deleted file mode 100644 index 79edb65e1..000000000 --- a/core/Sources/Common/DataType/Updateable.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Updateable.swift -// Spark -// -// Created by michael.zimmermann on 25.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -protocol Updateable { - associatedtype T - func update(_ keyPath: WritableKeyPath, value: Value) -> T - func updateIfNeeded(keyPath: ReferenceWritableKeyPath, newValue: Value) - func updateIfNeeded(keyPath: ReferenceWritableKeyPath, newValue: any ColorToken) -} - -extension Updateable { - func update(_ keyPath: WritableKeyPath, value: Value) -> Self { - - var copy = self - copy[keyPath: keyPath] = value - return copy - } - - func updateIfNeeded(keyPath: ReferenceWritableKeyPath, newValue: Value) { - guard self[keyPath: keyPath] != newValue else { return } - self[keyPath: keyPath] = newValue - } - - func updateIfNeeded(keyPath: ReferenceWritableKeyPath, newValue: any ColorToken) { - guard self[keyPath: keyPath].equals(newValue) == false else { return } - self[keyPath: keyPath] = newValue - } -} diff --git a/core/Sources/Common/DisplayedText/Enum/DisplayedTextType.swift b/core/Sources/Common/DisplayedText/Enum/DisplayedTextType.swift deleted file mode 100644 index 0888bf77a..000000000 --- a/core/Sources/Common/DisplayedText/Enum/DisplayedTextType.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// DisplayedTextType.swift -// SparkCore -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// Enum used for components which have a text/attributed management. -enum DisplayedTextType: CaseIterable { - /// No text/attributed text is displayed on label - case none - /// Text is displayed on label - case text - /// Attributed text is displayed on label - case attributedText - - // MARK: - Properties - - var containsText: Bool { - switch self { - case .none: - return false - case .text, .attributedText: - return true - } - } -} diff --git a/core/Sources/Common/DisplayedText/Enum/DisplayedTextTypeTests.swift b/core/Sources/Common/DisplayedText/Enum/DisplayedTextTypeTests.swift deleted file mode 100644 index 906c84de9..000000000 --- a/core/Sources/Common/DisplayedText/Enum/DisplayedTextTypeTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// DisplayedTextTypeTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 14/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class DisplayedTextTypeTests: XCTestCase { - - // MARK: - Tests - - func test_containsText_when_type_is_none() { - // GIVEN - let type: DisplayedTextType = .none - - // WHEN - let containsText = type.containsText - - // THEN - XCTAssertFalse(containsText) - } - - func test_containsText_when_type_is_text() { - // GIVEN - let type: DisplayedTextType = .text - - // WHEN - let containsText = type.containsText - - // THEN - XCTAssertTrue(containsText) - } - - func test_containsText_when_type_is_attributedText() { - // GIVEN - let type: DisplayedTextType = .attributedText - - // WHEN - let containsText = type.containsText - - // THEN - XCTAssertTrue(containsText) - } -} diff --git a/core/Sources/Common/DisplayedText/Model/DisplayedText+ExtensionTests.swift b/core/Sources/Common/DisplayedText/Model/DisplayedText+ExtensionTests.swift deleted file mode 100644 index 83a09e068..000000000 --- a/core/Sources/Common/DisplayedText/Model/DisplayedText+ExtensionTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// DisplayedText+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 14/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension DisplayedText { - - // MARK: - Properties - - static func mocked( - text: String = "My text" - ) -> Self { - return .init( - text: text - ) - } -} diff --git a/core/Sources/Common/DisplayedText/Model/DisplayedText.swift b/core/Sources/Common/DisplayedText/Model/DisplayedText.swift deleted file mode 100644 index ca4b09e48..000000000 --- a/core/Sources/Common/DisplayedText/Model/DisplayedText.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// DisplayedText.swift -// SparkCore -// -// Created by robin.lemaire on 13/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct DisplayedText: Equatable { - - // MARK: - Properties - - let text: String? - let attributedText: AttributedStringEither? - - // MARK: - Initialization - - init?(text: String?, attributedText: AttributedStringEither?) { - // Both values cannot be nil - guard text != nil || attributedText != nil else { - return nil - } - - self.text = text - self.attributedText = attributedText - } - - init(text: String) { - self.text = text - self.attributedText = nil - } - - init(attributedText: AttributedStringEither) { - self.text = nil - self.attributedText = attributedText - } -} diff --git a/core/Sources/Common/DisplayedText/Model/DisplayedTextTests.swift b/core/Sources/Common/DisplayedText/Model/DisplayedTextTests.swift deleted file mode 100644 index 8bf884590..000000000 --- a/core/Sources/Common/DisplayedText/Model/DisplayedTextTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// DisplayedTextTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 14/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class DisplayedTextTests: XCTestCase { - - // MARK: - Optional Init - - func test_optional_init_with_only_text() { - // GIVEN - let textMock = "My text" - - // WHEN - let displayedText = DisplayedText( - text: textMock, - attributedText: nil - ) - - // THEN - XCTAssertEqual( - displayedText?.text, - textMock, - "Wrong text" - ) - XCTAssertNil( - displayedText?.attributedText, - "Wrong attributedText" - ) - } - - func test_optional_init_with_only_attributedText() { - // GIVEN - let attributedMock: AttributedStringEither = .left(.init(string: "Holà")) - - // WHEN - let displayedText = DisplayedText( - text: nil, - attributedText: attributedMock - ) - - // THEN - XCTAssertNil( - displayedText?.text, - "Wrong text" - ) - XCTAssertEqual( - displayedText?.attributedText, - attributedMock, - "Wrong attributedText" - ) - } - - func test_optional_init_without_text_and_attributedText() { - // GIVEN / WHEN - let displayedText = DisplayedText( - text: nil, - attributedText: nil - ) - - // THEN - XCTAssertNil( - displayedText, - "Wrong displayedText" - ) - } - - // MARK: - Text Init - - func test_init_with_text() { - // GIVEN - let textMock = "My text" - - // WHEN - let displayedText = DisplayedText( - text: textMock - ) - - // THEN - XCTAssertEqual( - displayedText.text, - textMock, - "Wrong text" - ) - XCTAssertNil( - displayedText.attributedText, - "Wrong attributedText" - ) - } - - // MARK: - AttributedText Init - - func test_init_with_attributedText() { - // GIVEN - let attributedMock: AttributedStringEither = .left(.init(string: "Holà")) - - // WHEN - let displayedText = DisplayedText( - attributedText: attributedMock - ) - - // THEN - XCTAssertNil( - displayedText.text, - "Wrong text" - ) - XCTAssertEqual( - displayedText.attributedText, - attributedMock, - "Wrong attributedText" - ) - } -} diff --git a/core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCase.swift b/core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCase.swift deleted file mode 100644 index 19abf0f3f..000000000 --- a/core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCase.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// GetDidDisplayedTextChangeUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol GetDidDisplayedTextChangeUseCaseable { - func execute(currentText: String?, - newText: String?, - displayedTextType: DisplayedTextType) -> Bool - - func execute(currentAttributedText: AttributedStringEither?, - newAttributedText: AttributedStringEither?, - displayedTextType: DisplayedTextType) -> Bool -} - -struct GetDidDisplayedTextChangeUseCase: GetDidDisplayedTextChangeUseCaseable { - - // MARK: - Execute - - /// The displayed text changed ? - func execute( - currentText: String?, - newText: String?, - displayedTextType: DisplayedTextType - ) -> Bool { - return self.execute( - currentValue: currentText, - newValue: newText, - displayedTextType: displayedTextType, - expectedDisplayedTextType: .text // expectedDisplayedTextType - ) - } - - /// The displayed attributed text changed ? - func execute( - currentAttributedText: AttributedStringEither?, - newAttributedText: AttributedStringEither?, - displayedTextType: DisplayedTextType - ) -> Bool { - return self.execute( - currentValue: currentAttributedText, - newValue: newAttributedText, - displayedTextType: displayedTextType, - expectedDisplayedTextType: .attributedText - ) - } - - // MARK: - Private Execute - - private func execute( - currentValue: T?, - newValue: T?, - displayedTextType: DisplayedTextType, - expectedDisplayedTextType: DisplayedTextType - ) -> Bool { - switch (currentValue, newValue) { - // Values are differents - case let (value1, value2) where value1 != value2: - return true - - // Value are same and set - case (_?, _?) where displayedTextType != expectedDisplayedTextType: - return true - - // Values are nil - case (nil, nil) where displayedTextType == expectedDisplayedTextType: - return true - - default: - return false - } - } -} diff --git a/core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCaseTests.swift b/core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCaseTests.swift deleted file mode 100644 index 1c2a9cd1c..000000000 --- a/core/Sources/Common/DisplayedText/UseCase/GetDidDisplayedTextChange/GetDidDisplayedTextChangeUseCaseTests.swift +++ /dev/null @@ -1,287 +0,0 @@ -// -// GetDidDisplayedTextChangeUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class GetDidDisplayedTextChangeUseCaseTests: XCTestCase { - - // MARK: - Tests Execute with text - - func test_execute_when_currentText_is_equal_to_newText_and_displayedTextType_is_none() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: "Hello", - newText: "Hello", - displayedTextType: .none - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentText_is_equal_to_newText_and_displayedTextType_is_text() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: "Hello", - newText: "Hello", - displayedTextType: .text - ) - - // THEN - XCTAssertFalse(isChanged) - } - - func test_execute_when_currentText_is_equal_to_newText_and_displayedTextType_is_attributedText() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: "Hello", - newText: "Hello", - displayedTextType: .attributedText - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentText_is_not_equal_to_newText_and_displayedTextType_is_none() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: "Hello", - newText: "Bye", - displayedTextType: .none - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentText_is_not_equal_to_newText_and_displayedTextType_is_text() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: "Hello", - newText: "Bye", - displayedTextType: .text - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentText_is_not_equal_to_newText_and_displayedTextType_is_attributedText() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: "Hello", - newText: "Bye", - displayedTextType: .attributedText - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentText_and_newText_are_nil_and_displayedTextType_is_none() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: nil, - newText: nil, - displayedTextType: .none - ) - - // THEN - XCTAssertFalse(isChanged) - } - - func test_execute_when_currentText_and_newText_are_nil_and_displayedTextType_is_text() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: nil, - newText: nil, - displayedTextType: .text - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentText_and_newText_are_nil_and_displayedTextType_is_attributedText() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentText: nil, - newText: nil, - displayedTextType: .attributedText - ) - - // THEN - XCTAssertFalse(isChanged) - } - - // MARK: - Tests Execute with attributed text - - func test_execute_when_currentAttributedText_is_equal_to_newAttributedText_and_displayedTextType_is_none() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: .left(NSAttributedString(string: "Hello")), - newAttributedText: .left(NSAttributedString(string: "Hello")), - displayedTextType: .none - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentAttributedText_is_equal_to_newAttributedText_and_displayedTextType_is_text() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: .left(NSAttributedString(string: "Hello")), - newAttributedText: .left(NSAttributedString(string: "Hello")), - displayedTextType: .text - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentAttributedText_is_equal_to_newAttributedText_and_displayedTextType_is_attributedText() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: .left(NSAttributedString(string: "Hello")), - newAttributedText: .left(NSAttributedString(string: "Hello")), - displayedTextType: .attributedText - ) - - // THEN - XCTAssertFalse(isChanged) - } - - func test_execute_when_currentAttributedText_is_not_equal_to_newAttributedText_and_displayedTextType_is_none() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: .left(NSAttributedString(string: "Hello")), - newAttributedText: .left(NSAttributedString(string: "Bye")), - displayedTextType: .none - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentAttributedText_is_not_equal_to_newAttributedText_and_displayedTextType_is_text() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: .left(NSAttributedString(string: "Hello")), - newAttributedText: .left(NSAttributedString(string: "Bye")), - displayedTextType: .text - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentAttributedText_is_not_equal_to_newAttributedText_and_displayedTextType_is_attributedText() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: .left(NSAttributedString(string: "Hello")), - newAttributedText: .left(NSAttributedString(string: "Bye")), - displayedTextType: .attributedText - ) - - // THEN - XCTAssertTrue(isChanged) - } - - func test_execute_when_currentAttributedText_and_newAttributedText_are_nil_and_displayedTextType_is_none() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: nil, - newAttributedText: nil, - displayedTextType: .none - ) - - // THEN - XCTAssertFalse(isChanged) - } - - func test_execute_when_currentAttributedText_and_newAttributedText_are_nil_and_displayedTextType_is_text() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: nil, - newAttributedText: nil, - displayedTextType: .text - ) - - // THEN - XCTAssertFalse(isChanged) - } - - func test_execute_when_currentAttributedText_and_newAttributedText_are_nil_and_displayedTextType_is_attributedText() { - // GIVEN - let useCase = GetDidDisplayedTextChangeUseCase() - - // WHEN - let isChanged = useCase.execute( - currentAttributedText: nil, - newAttributedText: nil, - displayedTextType: .attributedText - ) - - // THEN - XCTAssertTrue(isChanged) - } -} diff --git a/core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCase.swift b/core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCase.swift deleted file mode 100644 index c2b103719..000000000 --- a/core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCase.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// GetDisplayedTextTypeUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol GetDisplayedTextTypeUseCaseable { - func execute(text: String?, - attributedText: AttributedStringEither?) -> DisplayedTextType - - func execute(text: String?) -> DisplayedTextType - func execute(attributedText: AttributedStringEither?) -> DisplayedTextType -} - -struct GetDisplayedTextTypeUseCase: GetDisplayedTextTypeUseCaseable { - - /// Get the displayed text type from a text and attributedText. - func execute( - text: String?, - attributedText: AttributedStringEither? - ) -> DisplayedTextType { - switch (text, attributedText) { - case (nil, nil): - return .none - case (_, nil): - return .text - case (nil, _), (_, _): - return .attributedText - } - } - - /// Get the new displayed text type when text changed - func execute( - text: String? - ) -> DisplayedTextType { - return self.execute( - text: text, - attributedText: nil - ) - } - - /// Get the new displayed text type when attributedText changed - func execute( - attributedText: AttributedStringEither? - ) -> DisplayedTextType { - return self.execute( - text: nil, - attributedText: attributedText - ) - } -} diff --git a/core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCaseTests.swift b/core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCaseTests.swift deleted file mode 100644 index 53ed2dda5..000000000 --- a/core/Sources/Common/DisplayedText/UseCase/GetDisplayedTextType/GetDisplayedTextTypeUseCaseTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// GetDisplayedTextTypeUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class GetDisplayedTextTypeUseCaseTests: XCTestCase { - - // MARK: - Tests Execute with text and attributedText - - func test_execute_with_text_and_attributedText_parameters() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - text: "Hello", - attributedText: .left(.init(string: "Holà")) - ) - - // THEN - XCTAssertEqual(displayedTextType, .attributedText) - } - - func test_execute_with_text_and_nil_attributedText_parameters() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - text: "Hello", - attributedText: nil - ) - - // THEN - XCTAssertEqual(displayedTextType, .text) - } - - func test_execute_with_nil_text_and_attributedText_parameters() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - text: nil, - attributedText: .left(.init(string: "Holà")) - ) - - // THEN - XCTAssertEqual(displayedTextType, .attributedText) - } - - func test_execute_with_nil_text_and_nil_attributedText_parameters() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - text: nil, - attributedText: nil - ) - - // THEN - XCTAssertEqual(displayedTextType, .none) - } - - // MARK: - Tests Execute with only text - - func test_execute_with_text_parameter() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - text: "Hello" - ) - - // THEN - XCTAssertEqual(displayedTextType, .text) - } - - func test_execute_with_nil_text_parameter() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - text: nil - ) - - // THEN - XCTAssertEqual(displayedTextType, .none) - } - - // MARK: - Tests Execute with only attributed text - - func test_execute_with_attributedText_parameter() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - attributedText: .left(.init(string: "Holà")) - ) - - // THEN - XCTAssertEqual(displayedTextType, .attributedText) - } - - func test_execute_with_nil_attributedText_parameter() { - // GIVEN - let useCase = GetDisplayedTextTypeUseCase() - - // WHEN - let displayedTextType = useCase.execute( - attributedText: nil - ) - - // THEN - XCTAssertEqual(displayedTextType, .none) - } -} diff --git a/core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModel.swift b/core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModel.swift deleted file mode 100644 index ccb7a2961..000000000 --- a/core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModel.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// DisplayedTextViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol DisplayedTextViewModel { - var displayedTextType: DisplayedTextType { get } - var displayedText: DisplayedText? { get } - - var containsText: Bool { get } - - /// Update the text only if the value changed. - /// Return true if value changed. - @discardableResult - func textChanged(_ text: String?) -> Bool - /// Update the attributed text only if the value changed. - /// Return true if value changed. - @discardableResult - func attributedTextChanged(_ attributedText: AttributedStringEither?) -> Bool -} - -// View model used by a component view model that contains a text/attributed management. -final class DisplayedTextViewModelDefault: DisplayedTextViewModel { - - // MARK: - Internal Properties - - private(set) var displayedTextType: DisplayedTextType - private(set) var displayedText: DisplayedText? - - var containsText: Bool { - self.displayedTextType.containsText - } - - // MARK: - Private Properties - - private let getDisplayedTextTypeUseCase: GetDisplayedTextTypeUseCaseable - private let getDidDisplayedTextChangeUseCase: GetDidDisplayedTextChangeUseCaseable - - // MARK: - Initialization - - init( - text: String?, - attributedText: AttributedStringEither?, - getDisplayedTextTypeUseCase: GetDisplayedTextTypeUseCaseable = GetDisplayedTextTypeUseCase(), - getDidDisplayedTextChangeUseCase: GetDidDisplayedTextChangeUseCaseable = GetDidDisplayedTextChangeUseCase() - ) { - self.displayedText = .init( - text: text, - attributedText: attributedText - ) - self.displayedTextType = getDisplayedTextTypeUseCase.execute( - text: text, - attributedText: attributedText - ) - - self.getDisplayedTextTypeUseCase = getDisplayedTextTypeUseCase - self.getDidDisplayedTextChangeUseCase = getDidDisplayedTextChangeUseCase - } - - // MARK: - Internal Methods - - func textChanged(_ text: String?) -> Bool { - // Displayed text changed ? - if self.getDidDisplayedTextChangeUseCase.execute( - currentText: self.displayedText?.text, - newText: text, - displayedTextType: self.displayedTextType - ) { - self.displayedText = text.map { .init(text: $0) } - self.displayedTextType = self.getDisplayedTextTypeUseCase.execute( - text: text - ) - - return true - } - - return false - } - - func attributedTextChanged(_ attributedText: AttributedStringEither?) -> Bool { - // Displayed attributed text changed ? - if self.getDidDisplayedTextChangeUseCase.execute( - currentAttributedText: self.displayedText?.attributedText, - newAttributedText: attributedText, - displayedTextType: self.displayedTextType - ) { - self.displayedTextType = self.getDisplayedTextTypeUseCase.execute( - attributedText: attributedText - ) - self.displayedText = attributedText.map { .init(attributedText: $0) } - - return true - } - - return false - } -} diff --git a/core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModelTests.swift b/core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModelTests.swift deleted file mode 100644 index ff72e0d6a..000000000 --- a/core/Sources/Common/DisplayedText/ViewModel/DisplayedTextViewModelTests.swift +++ /dev/null @@ -1,226 +0,0 @@ -// -// DisplayedTextViewModelTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class DisplayedTextViewModelTests: XCTestCase { - - // MARK: - Tests Init - - func test_properties_after_init() { - // GIVEN - let textMock = "Hello" - let attributedTextMock: AttributedStringEither = .left(.init(string: "Holà")) - let displayedTextTypeMock: DisplayedTextType = .text - - let getDisplayedTextTypeUseCaseMock = GetDisplayedTextTypeUseCaseableGeneratedMock() - getDisplayedTextTypeUseCaseMock.executeWithTextAndAttributedTextReturnValue = displayedTextTypeMock - - // WHEN - let viewModel = DisplayedTextViewModelDefault( - text: textMock, - attributedText: attributedTextMock, - getDisplayedTextTypeUseCase: getDisplayedTextTypeUseCaseMock - ) - - // THEN - XCTAssertEqual(viewModel.displayedTextType, - displayedTextTypeMock, - "Wrong displayedTextType value") - XCTAssertEqual(viewModel.displayedText?.text, - textMock, - "Wrong displayedTextType text value") - XCTAssertEqual(viewModel.displayedText?.attributedText, - attributedTextMock, - "Wrong displayedTextType attributedText value") - XCTAssertEqual(viewModel.containsText, - displayedTextTypeMock.containsText, - "Wrong containsText value") - - // ** - // GetDisplayedTextTypeUseCase - XCTAssertEqual(getDisplayedTextTypeUseCaseMock.executeWithTextAndAttributedTextCallsCount, - 1, - "Wrong call number on execute with text and attributedText parameters on getDisplayedTextTypeUseCase") - - let getDisplayedTextTypeUseCaseArgs = getDisplayedTextTypeUseCaseMock.executeWithTextAndAttributedTextReceivedArguments - XCTAssertEqual(getDisplayedTextTypeUseCaseArgs?.text, - textMock, - "Wrong text parameter on execute on getDisplayedTextTypeUseCase") - XCTAssertEqual(getDisplayedTextTypeUseCaseArgs?.attributedText, - attributedTextMock, - "Wrong attributedText parameter on execute on getDisplayedTextTypeUseCase") - // ** - } - - // MARK: - Tests TextChanged - - func test_textChanged_when_getDidDisplayedTextChangeUseCase_return_true() { - self.testTextChanged(givenGetDidDisplayedTextChange: true) - } - - func test_textChanged_when_getDidDisplayedTextChangeUseCase_return_false() { - self.testTextChanged(givenGetDidDisplayedTextChange: false) - } - - func testTextChanged( - givenGetDidDisplayedTextChange: Bool - ) { - // GIVEN - let displayedTextTypeMock: DisplayedTextType = .text - let newText = "Hey" - let textMock = "Hello" - - let getDisplayedTextTypeUseCaseMock = GetDisplayedTextTypeUseCaseableGeneratedMock() - getDisplayedTextTypeUseCaseMock.executeWithTextAndAttributedTextReturnValue = displayedTextTypeMock - getDisplayedTextTypeUseCaseMock.executeWithTextReturnValue = DisplayedTextType.none - - let getDidDisplayedTextChangeUseCaseMock = GetDidDisplayedTextChangeUseCaseableGeneratedMock() - getDidDisplayedTextChangeUseCaseMock.executeWithCurrentTextAndNewTextAndDisplayedTextTypeReturnValue = givenGetDidDisplayedTextChange - - let viewModel = DisplayedTextViewModelDefault( - text: textMock, - attributedText: nil, - getDisplayedTextTypeUseCase: getDisplayedTextTypeUseCaseMock, - getDidDisplayedTextChangeUseCase: getDidDisplayedTextChangeUseCaseMock - ) - - // WHEN - let textChanged = viewModel.textChanged(newText) - - // THEN - XCTAssertEqual(textChanged, - givenGetDidDisplayedTextChange, - "Wrong textChanged value") - - XCTAssertEqual(viewModel.displayedText, - .init(text: givenGetDidDisplayedTextChange ? newText : textMock), - "Wrong displayedText value") - XCTAssertEqual(viewModel.displayedTextType, - givenGetDidDisplayedTextChange ? .none : displayedTextTypeMock, - "Wrong displayedTextType value") - XCTAssertEqual(viewModel.containsText, - givenGetDidDisplayedTextChange ? DisplayedTextType.none.containsText : displayedTextTypeMock.containsText, - "Wrong containsText value") - - // ** - // GetDidDisplayedTextChangeUseCase - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMock.executeWithCurrentTextAndNewTextAndDisplayedTextTypeCallsCount, - 1, - "Wrong call number on execute parameters on getDidDisplayedTextChangeUseCase") - - let getDidDisplayedTextChangeUseCaseMockArgs = getDidDisplayedTextChangeUseCaseMock.executeWithCurrentTextAndNewTextAndDisplayedTextTypeReceivedArguments - - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMockArgs?.currentText, - textMock, - "Wrong currentText parameter on execute on getDidDisplayedTextChangeUseCase") - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMockArgs?.newText, - newText, - "Wrong newText parameter on execute on getDidDisplayedTextChangeUseCase") - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMockArgs?.displayedTextType, - displayedTextTypeMock, - "Wrong displayedTextType parameter on execute on getDidDisplayedTextChangeUseCase") - // ** - - // ** - // GetDisplayedTextTypeUseCase - XCTAssertEqual(getDisplayedTextTypeUseCaseMock.executeWithTextCallsCount, - givenGetDidDisplayedTextChange ? 1 : 0, - "Wrong call number on execute on getDisplayedTextTypeUseCase") - - if givenGetDidDisplayedTextChange { - XCTAssertEqual(getDisplayedTextTypeUseCaseMock.executeWithTextReceivedText, - newText, - "Wrong text parameter on execute on getDisplayedTextTypeUseCase") - } - // ** - } - - // MARK: - Tests AttributedTextChanged - - func test_attributedTextChanged_when_getIsDisplayedAttributedTextChangedUseCase_return_true() { - self.testAttributedTextChanged(givenGetIsDisplayedAttributedTextChanged: true) - } - - func test_attributedTextChanged_when_getIsDisplayedAttributedTextChangedUseCase_return_false() { - self.testAttributedTextChanged(givenGetIsDisplayedAttributedTextChanged: false) - } - - func testAttributedTextChanged( - givenGetIsDisplayedAttributedTextChanged: Bool - ) { - // GIVEN - let displayedTextTypeMock: DisplayedTextType = .attributedText - let newAttributedTextMock: AttributedStringEither = .left(.init(string: "Hey")) - let attributedTextMock: AttributedStringEither = .left(.init(string: "Hello")) - - let getDisplayedTextTypeUseCaseMock = GetDisplayedTextTypeUseCaseableGeneratedMock() - getDisplayedTextTypeUseCaseMock.executeWithTextAndAttributedTextReturnValue = displayedTextTypeMock - getDisplayedTextTypeUseCaseMock.executeWithAttributedTextReturnValue = DisplayedTextType.none - - let getDidDisplayedTextChangeUseCaseMock = GetDidDisplayedTextChangeUseCaseableGeneratedMock() - getDidDisplayedTextChangeUseCaseMock.executeWithCurrentAttributedTextAndNewAttributedTextAndDisplayedTextTypeReturnValue = givenGetIsDisplayedAttributedTextChanged - - let viewModel = DisplayedTextViewModelDefault( - text: nil, - attributedText: attributedTextMock, - getDisplayedTextTypeUseCase: getDisplayedTextTypeUseCaseMock, - getDidDisplayedTextChangeUseCase: getDidDisplayedTextChangeUseCaseMock - ) - - // WHEN - let attributedTextChanged = viewModel.attributedTextChanged(newAttributedTextMock) - - // THEN - XCTAssertEqual(attributedTextChanged, - givenGetIsDisplayedAttributedTextChanged, - "Wrong attributedTextChanged value") - - XCTAssertEqual(viewModel.displayedText, - .init(attributedText: givenGetIsDisplayedAttributedTextChanged ? newAttributedTextMock : attributedTextMock), - "Wrong displayedText value") - XCTAssertEqual(viewModel.displayedTextType, - givenGetIsDisplayedAttributedTextChanged ? .none : displayedTextTypeMock, - "Wrong displayedTextType value") - XCTAssertEqual(viewModel.containsText, - givenGetIsDisplayedAttributedTextChanged ? DisplayedTextType.none.containsText : displayedTextTypeMock.containsText, - "Wrong containsText value") - - // ** - // GetDidDisplayedTextChangeUseCase - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMock.executeWithCurrentAttributedTextAndNewAttributedTextAndDisplayedTextTypeCallsCount, - 1, - "Wrong call number on execute parameters on getDidDisplayedTextChangeUseCase") - - let getDidDisplayedTextChangeUseCaseMockArgs = getDidDisplayedTextChangeUseCaseMock.executeWithCurrentAttributedTextAndNewAttributedTextAndDisplayedTextTypeReceivedArguments - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMockArgs?.currentAttributedText, - attributedTextMock, - "Wrong currentAttributedText parameter on execute on getDidDisplayedTextChangeUseCase") - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMockArgs?.newAttributedText, - newAttributedTextMock, - "Wrong newAttributedText parameter on execute on getDidDisplayedTextChangeUseCase") - XCTAssertEqual(getDidDisplayedTextChangeUseCaseMockArgs?.displayedTextType, - displayedTextTypeMock, - "Wrong displayedTextType parameter on execute on getDidDisplayedTextChangeUseCase") - // ** - - // ** - // GetDisplayedTextTypeUseCase - XCTAssertEqual(getDisplayedTextTypeUseCaseMock.executeWithAttributedTextCallsCount, - givenGetIsDisplayedAttributedTextChanged ? 1 : 0, - "Wrong call number on execute on getDisplayedTextTypeUseCase") - - if givenGetIsDisplayedAttributedTextChanged { - XCTAssertEqual(getDisplayedTextTypeUseCaseMock.executeWithAttributedTextReceivedAttributedText, - newAttributedTextMock, - "Wrong attributedText parameter on execute on getDisplayedTextTypeUseCase") - } - // ** - } -} diff --git a/core/Sources/Common/Enum/Either/Either.swift b/core/Sources/Common/Enum/Either/Either.swift deleted file mode 100644 index 5d6fc438c..000000000 --- a/core/Sources/Common/Enum/Either/Either.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Either.swift -// SparkCore -// -// Created by michael.zimmermann on 19.06.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum Either { - case left(Left) - case right(Right) -} - -// MARK: - Equatable - -extension Either: Equatable where Left: Equatable, Right: Equatable { -} - -// MARK: - Properties - -extension Either { - var rightValue: Right { - switch self { - case let .right(value): return value - case .left: fatalError("No value for right part") - } - } - - var leftValue: Left { - switch self { - case let .left(value): return value - case .right: fatalError("No value for left part") - } - } -} - -extension Either { - static func of(_ left: Left?, or right: Right) -> Either { - if let left = left { - return .left(left) - } else { - return .right(right) - } - } -} diff --git a/core/Sources/Common/Enum/Either/EitherTests.swift b/core/Sources/Common/Enum/Either/EitherTests.swift deleted file mode 100644 index eb9e602b8..000000000 --- a/core/Sources/Common/Enum/Either/EitherTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// EitherTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 19.06.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore -import XCTest - -final class EitherTests: XCTestCase { - - func test_left_value() { - let sut: Either = .left(1) - XCTAssertEqual(sut.leftValue, 1) - } - - func test_right_value() { - let sut: Either = .right("A") - XCTAssertEqual(sut.rightValue, "A") - } - - func test_or_value() { - let sut: Either = .of(1, or: "Hello") - XCTAssertEqual(sut, .left(1)) - } - - func test_or_other_value() { - let sut: Either = .of(nil, or: "Hello") - XCTAssertEqual(sut, .right("Hello")) - } -} diff --git a/core/Sources/Common/Enum/Either/Type/AttributedStringEither.swift b/core/Sources/Common/Enum/Either/Type/AttributedStringEither.swift deleted file mode 100644 index a5d3e0880..000000000 --- a/core/Sources/Common/Enum/Either/Type/AttributedStringEither.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Either+AttributedStringEquatable.swift -// SparkCore -// -// Created by robin.lemaire on 04/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -typealias AttributedStringEither = Either diff --git a/core/Sources/Common/Enum/Either/Type/ImageEither.swift b/core/Sources/Common/Enum/Either/Type/ImageEither.swift deleted file mode 100644 index 339e64986..000000000 --- a/core/Sources/Common/Enum/Either/Type/ImageEither.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Either+ImageEquatable.swift -// SparkCore -// -// Created by robin.lemaire on 04/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -typealias ImageEither = Either \ No newline at end of file diff --git a/core/Sources/Common/Enum/Either/Type/ViewEither.swift b/core/Sources/Common/Enum/Either/Type/ViewEither.swift deleted file mode 100644 index eef86b39e..000000000 --- a/core/Sources/Common/Enum/Either/Type/ViewEither.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ViewEither.swift -// SparkCoreSnapshotTests -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -typealias ViewEither = Either diff --git a/core/Sources/Common/Enum/FrameworkType.swift b/core/Sources/Common/Enum/FrameworkType.swift deleted file mode 100644 index 3475169dc..000000000 --- a/core/Sources/Common/Enum/FrameworkType.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// FrameworkType.swift -// SparkCore -// -// Created by robin.lemaire on 13/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -enum FrameworkType { - case uiKit - case swiftUI -} diff --git a/core/Sources/Common/Enum/TextStyle.swift b/core/Sources/Common/Enum/TextStyle.swift deleted file mode 100644 index e6ba45851..000000000 --- a/core/Sources/Common/Enum/TextStyle.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// TextStyle.swift -// SparkCore -// -// Created by robin.lemaire on 18/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit - -/// The TextStyle used by SwiftUI and UIKit -public enum TextStyle { - /// The font style for large titles. - case largeTitle - /// The font used for first level hierarchical headings. - case title - /// The font used for second level hierarchical headings. - case title2 - /// The font used for third level hierarchical headings. - case title3 - /// The font used for headings. - case headline - /// The font used for subheadings. - case subheadline - /// The font used for body text. - case body - /// The font used for callouts. - case callout - /// The font used in footnotes. - case footnote - /// The font used for standard captions. - case caption - /// The font used for alternate captions. - case caption2 -} diff --git a/core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift b/core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift deleted file mode 100644 index ce40364ac..000000000 --- a/core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// CGFloat+ScaledMetricExtension.swift -// SparkCore -// -// Created by robin.lemaire on 14/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension CGFloat { - - /// Because the @ScaledMetric cannot be updated, - /// we add this the scaled metric multiplier value. - /// This value must be multiplied with you want to make dynamic (width, height, padding, ...) - /// - note: - Please use this value only for @ScaledMetric on SwiftUI - /// - /// **Example** - /// This example shows how to create view this multiplier on SwiftUI View - /// ```swift - /// @ScaledMetric private var spacingMultiplier: CGFloat = ScaledMetric.scaledMetricMultiplier - /// ``` - static var scaledMetricMultiplier: CGFloat { - return 1 - } - - /// Because the @ScaledMetric cannot be updated, - /// we must multiply the value that you want to make dynamic - /// with the scaled value mutiliplier get from CGFloat.scaledMetricMultiplier - /// - Parameter multiplier: the scaled value mutiliplier get from CGFloat.scaledMetricMultiplier and stock on @ScaledMetric var - /// - note: - Please use this value only for @ScaledMetric on SwiftUI - /// - /// **Example** - /// This example shows how to implement the scaled metric value for the width of a view - /// ```swift - /// @ScaledMetric private var spacingMultiplier: CGFloat = ScaledMetric.scaledMetricMultiplier - /// @ObservedObject private var viewModel: MyViewModel - /// - /// var body: any View { - /// Spacer() - /// .frame(width: self.viewModel.spacing.scaledMetric(self.spacingMultiplier)) - /// } - /// ``` - func scaledMetric(with multiplier: CGFloat) -> CGFloat { - self * multiplier - } -} - -public extension Optional where Wrapped == CGFloat { - - /// Same as **scaledMetric(with:)** func with optional value - /// If the CGFloat is nil, the default value is 0 - func scaledMetric(with multiplier: CGFloat) -> CGFloat { - return (self ?? 0).scaledMetric(with: multiplier) - } -} diff --git a/core/Sources/Common/Foundation/Extension/CGPoint-Distance.swift b/core/Sources/Common/Foundation/Extension/CGPoint-Distance.swift deleted file mode 100644 index afcd7053e..000000000 --- a/core/Sources/Common/Foundation/Extension/CGPoint-Distance.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CGPoint-Distance.swift -// SparkCore -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension CGPoint { - - /// Returns the distance between two points - func distance(to other: CGPoint) -> CGFloat { - CGFloat(hypotf(Float(self.x - other.x), Float(self.y - other.y))) - } -} diff --git a/core/Sources/Common/Foundation/Extension/CGPointDistanceTests.swift b/core/Sources/Common/Foundation/Extension/CGPointDistanceTests.swift deleted file mode 100644 index a3c9ba589..000000000 --- a/core/Sources/Common/Foundation/Extension/CGPointDistanceTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CGPointDistanceTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class CGPointDistanceTests: XCTestCase { - - func test_distance_same() throws { - let point1 = CGPoint(x: 10, y: 10) - let point2 = CGPoint(x: 10, y: -100) - - let distance1 = point1.distance(to: point2) - let distance2 = point2.distance(to: point1) - - XCTAssertEqual(distance1, 110.0, "Expected distance does not match") - XCTAssertEqual(distance1, distance2, "Expected both distances to be the same") - } -} diff --git a/core/Sources/Common/Foundation/Extension/CGRect-Center.swift b/core/Sources/Common/Foundation/Extension/CGRect-Center.swift deleted file mode 100644 index 42f5bd647..000000000 --- a/core/Sources/Common/Foundation/Extension/CGRect-Center.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// CGRect.swift -// SparkCore -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension CGRect { - /// Returns the center of the x-coordinate of the rect - var centerX: CGFloat { - return (self.minX + self.maxX) / 2 - } - - /// Returns the center of the y-coordinate of the rect - var centerY: CGFloat { - return (self.minY + self.maxY) / 2 - } - - /// The center point of the rect - var center: CGPoint { - return CGPoint(x: self.centerX, y: self.centerY) - } -} diff --git a/core/Sources/Common/Foundation/Extension/CGRect-Location.swift b/core/Sources/Common/Foundation/Extension/CGRect-Location.swift deleted file mode 100644 index b10253fbe..000000000 --- a/core/Sources/Common/Foundation/Extension/CGRect-Location.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CGRect-location.swift -// SparkCore -// -// Created by Michael Zimmermann on 07.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension CGRect { - func pointIndex(of point: CGPoint, horizontalSlices items: Int) -> Int? { - guard items > 0, self.contains(point) else { - return nil - } - - let itemWidth = self.width / CGFloat(items) - return max(Int(ceil(point.x / itemWidth)) - 1, 0) - } -} diff --git a/core/Sources/Common/Foundation/Extension/CGRectCenterTests.swift b/core/Sources/Common/Foundation/Extension/CGRectCenterTests.swift deleted file mode 100644 index 207c0ed59..000000000 --- a/core/Sources/Common/Foundation/Extension/CGRectCenterTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CGRectCenterTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class CGRectCenterTests: XCTestCase { - - func testExample() throws { - let rect = CGRect(x: 10, y: 10, width: 110, height: 30) - - XCTAssertEqual(rect.centerX, 65, "CenterX doesn't match expected value") - XCTAssertEqual(rect.centerY, 25, "CenterY doesn't match expected value") - - XCTAssertEqual(rect.center, CGPoint(x: 65, y: 25), "Center point is not correct") - } - -} diff --git a/core/Sources/Common/Foundation/Extension/CGRectLocationTests.swift b/core/Sources/Common/Foundation/Extension/CGRectLocationTests.swift deleted file mode 100644 index 735925080..000000000 --- a/core/Sources/Common/Foundation/Extension/CGRectLocationTests.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// CGRectLocationTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 08.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class CGRectLocationTests: XCTestCase { - - func test_last_element() throws { - let sut = CGRect(x: 0, y: 0, width: 100, height: 20) - - XCTAssertEqual( - sut.pointIndex(of: CGPoint(x: 0, y: 0), horizontalSlices: 5), - 0, - "Expected to be index 0") - XCTAssertEqual( - sut.pointIndex(of: CGPoint(x: 80, y: 0), horizontalSlices: 5), - 3, - "Expected to be index 3") - XCTAssertEqual( - sut.pointIndex(of: CGPoint(x: 81, y: 0), horizontalSlices: 5), - 4, - "Expected to be index 4") - XCTAssertEqual( - sut.pointIndex(of: CGPoint(x: 99, y: 0), horizontalSlices: 5), - 4, - "Expected to be index 4") - } - - func test_edge_cases() throws { - let sut = CGRect(x: 0, y: 0, width: 100, height: 20) - - XCTAssertNil( - sut.pointIndex(of: CGPoint(x: 99, y: 0), horizontalSlices: 0), - "Expected not to have an index") - XCTAssertNil( - sut.pointIndex(of: CGPoint(x: 101, y: 10), horizontalSlices: 5), - "Expected point outside of frame to have no index") - } -} diff --git a/core/Sources/Common/Foundation/Extension/Optional+Extension.swift b/core/Sources/Common/Foundation/Extension/Optional+Extension.swift deleted file mode 100644 index 64dde0f8d..000000000 --- a/core/Sources/Common/Foundation/Extension/Optional+Extension.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Optional+Extension.swift -// SparkCore -// -// Created by robin.lemaire on 24/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -extension Optional where Wrapped: Collection { - - var isEmptyOrNil: Bool { - return self?.isEmpty ?? true - } -} diff --git a/core/Sources/Common/Foundation/Extension/Optional+ExtensionTests.swift b/core/Sources/Common/Foundation/Extension/Optional+ExtensionTests.swift deleted file mode 100644 index ff91c335b..000000000 --- a/core/Sources/Common/Foundation/Extension/Optional+ExtensionTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Optional+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 24/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class OptionalExtensionTests: XCTestCase { - - // MARK: - String Tests - - func test_nil_string() { - // GIVEN - let string: String? = nil - - // WHEN - let isEmptyOrNil = string.isEmptyOrNil - - // THEN - XCTAssertTrue(isEmptyOrNil) - } - - func test_empty_string() { - // GIVEN - let string: String? = "" - - // WHEN - let isEmptyOrNil = string.isEmptyOrNil - - // THEN - XCTAssertTrue(isEmptyOrNil) - } - - func test_string() { - // GIVEN - let string: String? = "Hello" - - // WHEN - let isEmptyOrNil = string.isEmptyOrNil - - // THEN - XCTAssertFalse(isEmptyOrNil) - } - - // MARK: - Array Tests - - func test_nil_array() { - // GIVEN - let array: [Int]? = nil - - // WHEN - let isEmptyOrNil = array.isEmptyOrNil - - // THEN - XCTAssertTrue(isEmptyOrNil) - } - - func test_empty_array() { - // GIVEN - let array: [Int]? = [] - - // WHEN - let isEmptyOrNil = array.isEmptyOrNil - - // THEN - XCTAssertTrue(isEmptyOrNil) - } - - func test_array() { - // GIVEN - let array: [Int]? = [10, 22, 3] - - // WHEN - let isEmptyOrNil = array.isEmptyOrNil - - // THEN - XCTAssertFalse(isEmptyOrNil) - } -} diff --git a/core/Sources/Common/Foundation/Extension/UIView-Closest.swift b/core/Sources/Common/Foundation/Extension/UIView-Closest.swift deleted file mode 100644 index d425ad0d8..000000000 --- a/core/Sources/Common/Foundation/Extension/UIView-Closest.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// UIView-Closest.swift -// SparkCore -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -extension Array where Element: UIView { - - /// Returns the index of the array of views which is closest to the point. - func index(closestTo location: CGPoint) -> Int? { - return self.map(\.frame).index(closestTo: location) - } -} - -extension Array where Element == CGRect { - /// Returns the index of the array of rects which is closest to the point. - func index(closestTo location: CGPoint) -> Int? { - let distances = self.map{ rect in - rect.center.distance(to: location) - } - let nearest = distances.enumerated().min { left, right in - return left.element < right.element - } - return nearest?.offset - } - -} diff --git a/core/Sources/Common/Foundation/Extension/UIViewClosestTests.swift b/core/Sources/Common/Foundation/Extension/UIViewClosestTests.swift deleted file mode 100644 index fec87177a..000000000 --- a/core/Sources/Common/Foundation/Extension/UIViewClosestTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// UIViewClosestTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class UIViewClosestTests: XCTestCase { - - func test_closest() throws { - let positions = [0, 100, 200, 300] - let views = positions.map{ CGRect(x: $0, y: 10, width: 50, height: 50) }.map(UIView.init(frame:)) - - for (index, position) in positions.enumerated() { - let closestIndex = views.index(closestTo: CGPoint(x: position + 50, y: 100)) - XCTAssertEqual(closestIndex, index, "Expected \(String(describing: closestIndex)) to be equal to \(index)") - } - } - -} diff --git a/core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+Extension.swift b/core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+Extension.swift deleted file mode 100644 index 4970ab147..000000000 --- a/core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+Extension.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// EdgeInsets+Extension.swift -// SparkCore -// -// Created by robin.lemaire on 04/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -extension EdgeInsets { - - // MARK: - Init - - /// Init EdgeInsets with same value for top, leading, bottom and trailing - /// - Parameters: - /// - all: inset value - init(all value: CGFloat) { - self = .init(top: value, leading: value, bottom: value, trailing: value) - } - - /// Init EdgeInsets with vertical (use to set top and bottom insets) and horizontal value (use to set leading and trailing insets) - /// - Parameters: - /// - vertical: horizontal inset value use to set left and right insets. - /// - horizontal: horizontal inset value use to set left and right insets. - init(vertical: CGFloat = 0, horizontal: CGFloat = 0) { - self = .init(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) - } -} diff --git a/core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+ExtensionTests.swift b/core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+ExtensionTests.swift deleted file mode 100644 index 62a9ec6e1..000000000 --- a/core/Sources/Common/SwiftUI/Extension/EdgeInsets/EdgeInsets+ExtensionTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// EdgeInsets+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 06/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class EdgeInsetsExtensionsTests: XCTestCase { - - // MARK: - Tests - - func test_init_all() { - // GIVEN/WHEN - let edgetInsets = EdgeInsets(all: 99) - - // THEN - XCTAssertEqual(edgetInsets.top, 99, "Wrong top value") - XCTAssertEqual(edgetInsets.leading, 99, "Wrong leading value") - XCTAssertEqual(edgetInsets.bottom, 99, "Wrong bottom value") - XCTAssertEqual(edgetInsets.trailing, 99, "Wrong trailing value") - } - - func test_init_vertical_and_horizontal() { - // GIVEN/WHEN - let edgetInsets = EdgeInsets(vertical: 13, horizontal: 44) - - // THEN - XCTAssertEqual(edgetInsets.top, 13, "Wrong top value") - XCTAssertEqual(edgetInsets.leading, 44, "Wrong leading value") - XCTAssertEqual(edgetInsets.bottom, 13, "Wrong bottom value") - XCTAssertEqual(edgetInsets.trailing, 44, "Wrong trailing value") - } -} diff --git a/core/Sources/Common/SwiftUI/Extension/Shape/Shape+Extension.swift b/core/Sources/Common/SwiftUI/Extension/Shape/Shape+Extension.swift deleted file mode 100644 index 4a999cb7e..000000000 --- a/core/Sources/Common/SwiftUI/Extension/Shape/Shape+Extension.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Shape+Extension.swift -// SparkCore -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -extension Shape { - - /// Add fill color from an color token. - /// - Parameters: - /// - fillColorToken: fill color token of the rectangle. If color token is the, fill is .clear - func fill(_ fillColorToken: (any ColorToken)?) -> some View { - self.fill(fillColorToken?.color ?? .clear) - } -} diff --git a/core/Sources/Common/SwiftUI/Extension/View/View+ProportionalWidth.swift b/core/Sources/Common/SwiftUI/Extension/View/View+ProportionalWidth.swift deleted file mode 100644 index 167d0880a..000000000 --- a/core/Sources/Common/SwiftUI/Extension/View/View+ProportionalWidth.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// View+ProportionalWidth.swift -// SparkCore -// -// Created by robin.lemaire on 27/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -extension View { - - /// Set a proportional width from the parent width - /// - Parameters: - /// - ratio: ratio of the parent view width. - func proportionalWidth( - from ratio: CGFloat - ) -> some View { - GeometryReader { reader in - self.frame(width: ratio * reader.size.width) - } - } -} diff --git a/core/Sources/Common/SwiftUI/GlobalExtension/IfModifier/IfModifier.swift b/core/Sources/Common/SwiftUI/GlobalExtension/IfModifier/IfModifier.swift deleted file mode 100644 index e14a3b212..000000000 --- a/core/Sources/Common/SwiftUI/GlobalExtension/IfModifier/IfModifier.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// IfModifier.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 06.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -extension View { - @available(*, deprecated, message: "Will be removed soon") - func `if`(_ conditional: Bool, content: (Self) -> Content) -> some View { - if conditional { - return AnyView(content(self)) - } else { - return AnyView(self) - } - } - - func `if`( - _ conditional: Bool, - then ifHandler: (Self) -> Content, - else elseHandler: (Self) -> Content2) -> some View { - if conditional { - return AnyView(ifHandler(self)) - } else { - return AnyView(elseHandler(self)) - } - } -} diff --git a/core/Sources/Common/SwiftUI/Modifier/Accessibility/AccessibilityViewModifier.swift b/core/Sources/Common/SwiftUI/Modifier/Accessibility/AccessibilityViewModifier.swift deleted file mode 100644 index f087b7bf8..000000000 --- a/core/Sources/Common/SwiftUI/Modifier/Accessibility/AccessibilityViewModifier.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// AccessibilityViewModifier.swift -// SparkCore -// -// Created by robin.lemaire on 04/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -struct AccessibilityViewModifier: ViewModifier { - - // MARK: - Properties - - private let identifier: String? - private let label: String? - - // MARK: - Initialization - - init(identifier: String?, - label: String?) { - self.identifier = identifier - self.label = label - } - - // MARK: - View - - func body(content: Content) -> some View { - content - .if(identifier != nil) { - $0.accessibilityIdentifier(identifier ?? "") - } - .if(label != nil) { - $0.accessibilityLabel(label ?? "") - } - } -} diff --git a/core/Sources/Common/SwiftUI/Modifier/Border/BorderViewModifier.swift b/core/Sources/Common/SwiftUI/Modifier/Border/BorderViewModifier.swift deleted file mode 100644 index 72ec4c2fe..000000000 --- a/core/Sources/Common/SwiftUI/Modifier/Border/BorderViewModifier.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// BorderViewModifier.swift -// SparkCore -// -// Created by robin.lemaire on 31/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -struct BorderViewModifier: ViewModifier { - - // MARK: - Properties - - private let width: CGFloat - private let radius: CGFloat - private let colorToken: any ColorToken - - // MARK: - Initialization - - init(width: CGFloat, - radius: CGFloat, - colorToken: any ColorToken) { - self.width = width - self.radius = radius - self.colorToken = colorToken - } - - // MARK: - View - - func body(content: Content) -> some View { - content - .cornerRadius(self.radius) - .overlay( - RoundedRectangle(cornerRadius: self.radius) - .stroke(self.colorToken.color, lineWidth: self.width) - ) - } -} diff --git a/core/Sources/Common/SwiftUI/Modifier/Border/View+BorderExtension.swift b/core/Sources/Common/SwiftUI/Modifier/Border/View+BorderExtension.swift deleted file mode 100644 index 93fbaca57..000000000 --- a/core/Sources/Common/SwiftUI/Modifier/Border/View+BorderExtension.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// View+BorderExtension.swift -// SparkCore -// -// Created by robin.lemaire on 31/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public extension View { - - /// Add a border to the current view. - /// - Parameters: - /// - width: The border width. - /// - radius: The border radius. - /// - colorToken: The color token of the border. - /// - Returns: Current View. - func border(width: CGFloat, - radius: CGFloat, - colorToken: any ColorToken) -> some View { - self.modifier(BorderViewModifier(width: width, - radius: radius, - colorToken: colorToken)) - } -} diff --git a/core/Sources/Common/SwiftUI/View/IsEnabledModifier.swift b/core/Sources/Common/SwiftUI/View/IsEnabledModifier.swift deleted file mode 100644 index aa5fee5f2..000000000 --- a/core/Sources/Common/SwiftUI/View/IsEnabledModifier.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// IsEnabledModifier.swift -// SparkCore -// -// Created by Michael Zimmermann on 03.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -struct IsEnabledModifier: ViewModifier { - let isEnabled: Bool - let action: (Bool) -> Void - - func body(content: Content) -> some View { - DispatchQueue.main.async { - self.action(self.isEnabled) - } - - return content.disabled(!self.isEnabled) - } -} - -struct IsEnabledEnvironmentModifier: EnvironmentalModifier { - let action: (Bool) -> Void - - func resolve(in environment: EnvironmentValues) -> IsEnabledModifier { - return IsEnabledModifier(isEnabled: environment.isEnabled, action: action) - } -} - -extension View { - func isEnabledChanged(_ action: @escaping (Bool) -> Void) -> some View { - self.modifier(IsEnabledEnvironmentModifier(action: action)) - } -} diff --git a/core/Sources/Common/SwiftUI/View/NoButtonStyle.swift b/core/Sources/Common/SwiftUI/View/NoButtonStyle.swift deleted file mode 100644 index f3f24ba70..000000000 --- a/core/Sources/Common/SwiftUI/View/NoButtonStyle.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// NoButtonStyle.swift -// SparkCore -// -// Created by Michael Zimmermann on 04.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -struct NoButtonStyle: ButtonStyle { - - func makeBody(configuration: Configuration) -> some View { - return configuration.label - } -} diff --git a/core/Sources/Common/SwiftUI/View/PressedButtonStyle.swift b/core/Sources/Common/SwiftUI/View/PressedButtonStyle.swift deleted file mode 100644 index 362793c18..000000000 --- a/core/Sources/Common/SwiftUI/View/PressedButtonStyle.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// PressedButtonStyle.swift -// SparkCore -// -// Created by Michael Zimmermann on 04.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A Button style which updates the passed binding when the button is pressed. -/// The change to the button will be animated by default. To deactivate the animation, set the animationDuration to 0.0 -struct PressedButtonStyle: ButtonStyle { - @Binding var isPressed: Bool - let duration: CGFloat - - // MARK: - Init - init(isPressed: Binding, animationDuration duration: CGFloat = 0.2) { - self._isPressed = isPressed - self.duration = duration - } - - func makeBody(configuration: Configuration) -> some View { - let animation: Animation? = self.duration <= 0 ? .none : .easeInOut(duration: self.duration) - - return configuration.label - .onChange(of: configuration.isPressed) { isPressed in - if isPressed != self.isPressed { - self.isPressed = isPressed - } - } - .animation(animation, value: configuration.isPressed) - } -} diff --git a/core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManager.swift b/core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManager.swift deleted file mode 100644 index 3b2829fc8..000000000 --- a/core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManager.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// AccessibilityLabelManager.swift -// SparkCore -// -// Created by robin.lemaire on 24/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// This struct can be implemented in the components that contain at least one subview (label, ...) -/// -/// The goal of this struct is to manage the accessibilityLabel of the UIKit component. -/// There is two possibilities. -/// The default one, using the subviews accessibilityLabel, concatenate them to create one accessibilityLabel using by the component. -/// In this case, the accessibilityLabel can change if the subview accessibilityLabel changes. -/// Or, -/// The consumer set a value. In this case, In this case, the accessibilityLabel will always be the one set by the consumer -struct AccessibilityLabelManager { - - // MARK: - Private Properties - - /// A Boolean value indicating whether the consumer set an accessibilityLabel. - private var isSetExternally = false - - /// A String value indicating the current accessibilityLabel of the component. - private var _accessibilityLabel: String? - - // MARK: - Properties - - /// A String value indicating whether **the component** set an accessibilityLabel. - /// When the value changes, the accessibilityLabel can be updated. - var internalValue: String? { - didSet { - self.value = self.internalValue - } - } - - /// A String value indicating the current accessibilityLabel of the component. - var value: String? { - get { - return self._accessibilityLabel - } - set { - // If the value is set by the consumer OR if the default value (given by the internalValue) is nil or empty - if newValue != self.internalValue || self.internalValue.isEmptyOrNil { - if !newValue.isEmptyOrNil, newValue != self._accessibilityLabel { - self._accessibilityLabel = newValue - } else if newValue != self.internalValue { // Set the value with the internal value - self._accessibilityLabel = self.internalValue - } - self.isSetExternally = !newValue.isEmptyOrNil - - } else if !self.isSetExternally, newValue != self._accessibilityLabel { // If the value isn't set by the consumer - self._accessibilityLabel = newValue - } - } - } -} diff --git a/core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManagerTests.swift b/core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManagerTests.swift deleted file mode 100644 index bef9948e5..000000000 --- a/core/Sources/Common/UIKit/Accessibility/AccessibilityLabelManagerTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// AccessibilityLabelManagerTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 24/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class AccessibilityLabelManagerTests: XCTestCase { - - // MARK: - Tests - - func test_default_value() { - // GIVEN / WHEN - let manager = AccessibilityLabelManager() - - // THEN - XCTAssertNil(manager.value) - } - - func test_value_after_set_internalValue() { - // GIVEN - let expectedValue = "My value" - - var manager = AccessibilityLabelManager() - - // WHEN - manager.internalValue = expectedValue - - // THEN - XCTAssertEqual(manager.value, expectedValue) - } - - func test_value_after_set_value_without_set_internaValue() { - // GIVEN - let expectedValue = "My value" - - var manager = AccessibilityLabelManager() - - // WHEN - manager.value = expectedValue - - // THEN - XCTAssertEqual(manager.value, expectedValue) - } - - func test_value_after_set_internalValue_then_set_value() { - // GIVEN - let expectedValue = "My value" - - var manager = AccessibilityLabelManager() - - // WHEN - manager.internalValue = "My other value" - manager.value = expectedValue - - // THEN - XCTAssertEqual(manager.value, expectedValue) - } - - func test_value_after_set_value_then_set_internalValue() { - // GIVEN - let expectedValue = "My value" - - var manager = AccessibilityLabelManager() - - // WHEN - manager.value = expectedValue - manager.internalValue = "My other value" - - // THEN - XCTAssertEqual(manager.value, expectedValue) - } - - func test_value_after_set_internalValue_then_set_nil_value() { - // GIVEN - let expectedValue = "My value" - - var manager = AccessibilityLabelManager() - - // WHEN - manager.internalValue = expectedValue - manager.value = nil - - // THEN - XCTAssertEqual(manager.value, expectedValue) - } - - func test_value_after_set_nil_value_then_set_internalValue() { - // GIVEN - let expectedValue = "My value" - - var manager = AccessibilityLabelManager() - - // WHEN - manager.value = nil - manager.internalValue = expectedValue - - // THEN - XCTAssertEqual(manager.value, expectedValue) - } -} diff --git a/core/Sources/Common/UIKit/Extension/Animation/UIView+ExecuteExtension.swift b/core/Sources/Common/UIKit/Extension/Animation/UIView+ExecuteExtension.swift deleted file mode 100644 index 628508020..000000000 --- a/core/Sources/Common/UIKit/Extension/Animation/UIView+ExecuteExtension.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// UIView+ExecuteExtension.swift -// SparkCore -// -// Created by robin.lemaire on 26/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -enum UIExecuteAnimationType { - case unanimated - case animated(duration: TimeInterval) -} - -extension UIView { - - /// Execute a code with or without animation. - static func execute( - animationType: UIExecuteAnimationType, - instructions: @escaping () -> Void, - completion: ((Bool) -> Void)? = nil - ) { - switch animationType { - case .unanimated: - instructions() - completion?(true) - - case .animated(let duration): - UIView.animate( - withDuration: duration, - animations: instructions, - completion: completion) - } - } - - /// Execute a code with or without transition animation. - static func execute( - with view: UIView, - animationType: UIExecuteAnimationType, - options: UIView.AnimationOptions = [], - instructions: @escaping () -> Void, - completion: ((Bool) -> Void)? = nil - ) { - switch animationType { - case .unanimated: - instructions() - completion?(true) - - case .animated(let duration): - UIView.transition( - with: view, - duration: duration, - options: options, - animations: instructions, - completion: completion) - } - } -} diff --git a/core/Sources/Common/UIKit/Extension/CGSize/CGSize+TraitCollection.swift b/core/Sources/Common/UIKit/Extension/CGSize/CGSize+TraitCollection.swift deleted file mode 100644 index c5156b0ec..000000000 --- a/core/Sources/Common/UIKit/Extension/CGSize/CGSize+TraitCollection.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CGSize+TraitCollection.swift -// Spark -// -// Created by janniklas.freundt.ext on 25.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension CGSize { - func scaled(for traitCollection: UITraitCollection) -> CGSize { - let bodyFontMetrics = UIFontMetrics(forTextStyle: .body) - - let scaledWidth = bodyFontMetrics.scaledValue(for: width, compatibleWith: traitCollection) - let scaledHeight = bodyFontMetrics.scaledValue(for: height, compatibleWith: traitCollection) - return CGSize(width: scaledWidth, height: scaledHeight) - } -} diff --git a/core/Sources/Common/UIKit/Extension/NSLayoutConstraint/NSLayoutConstraint+MultiplierExtension.swift b/core/Sources/Common/UIKit/Extension/NSLayoutConstraint/NSLayoutConstraint+MultiplierExtension.swift deleted file mode 100644 index 0e8018ddc..000000000 --- a/core/Sources/Common/UIKit/Extension/NSLayoutConstraint/NSLayoutConstraint+MultiplierExtension.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// NSLayoutConstraint+MultiplierExtension.swift -// SparkCore -// -// Created by robin.lemaire on 26/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension NSLayoutConstraint { - - /// There is no native possibility to update the multiplier - /// So we need to recreate the constraint with the new multiplier - static func updateMultiplier( - on constraint: inout NSLayoutConstraint?, - multiplier: CGFloat, - layout: NSLayoutDimension, - equalTo: NSLayoutDimension - ) { - constraint?.isActive = false - - constraint = layout.constraint( - equalTo: equalTo, - multiplier: multiplier - ) - - constraint?.isActive = true - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIControl/UIControl+Extensions.swift b/core/Sources/Common/UIKit/Extension/UIControl/UIControl+Extensions.swift deleted file mode 100644 index f5676fae7..000000000 --- a/core/Sources/Common/UIKit/Extension/UIControl/UIControl+Extensions.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UIControl+Extensions.swift -// SparkCore -// -// Created by Michael Zimmermann on 21.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -extension UIControl { - // Add a default tap gesture recognizer without any action to detect the action/publisher/target action even if the parent view has a gesture recognizer - // Why? UIControl action/publisher/target doesn't work if the parent contains a gesture recognizer. - // Note: Native UIButton add the same default recognizer to manage this use case. - func enableTouch() { - let gestureRecognizer = UITapGestureRecognizer() - gestureRecognizer.cancelsTouchesInView = false - self.addGestureRecognizer(gestureRecognizer) - } - - /// Fixes conflict with other swipes (scrollViews, bottomSheets...) by adding a panGesture to prevent cancelTracking from being called - @discardableResult - func addPanGestureToPreventCancelTracking() -> UIPanGestureRecognizer { - let panGesture = UIPanGestureRecognizer(target: nil, action: nil) - panGesture.cancelsTouchesInView = false - self.addGestureRecognizer(panGesture) - return panGesture - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+Extension.swift b/core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+Extension.swift deleted file mode 100644 index 5fdf5b3bf..000000000 --- a/core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+Extension.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UIEdgeInsets+Extension.swift -// SparkCore -// -// Created by robin.lemaire on 06/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension UIEdgeInsets { - - // MARK: - Init - - /// Init UIEdgeInsets with same value for top, left, bottom and right - /// - Parameters: - /// - all: inset value - init(all value: CGFloat) { - self = .init(top: value, left: value, bottom: value, right: value) - } - - /// Init UIEdgeInsets with vertical (use to set top and bottom insets) and horizontal value (use to set left and right insets) - /// - Parameters: - /// - vertical: horizontal inset value use to set left and right insets. Default value is .brikkeSpacingNone - /// - horizontal: horizontal inset value use to set left and right insets. Default value is .brikkeSpacingNone - init(vertical: CGFloat, horizontal: CGFloat) { - self = .init(top: vertical, left: horizontal, bottom: vertical, right: horizontal) - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+ExtensionTests.swift b/core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+ExtensionTests.swift deleted file mode 100644 index 2883917fd..000000000 --- a/core/Sources/Common/UIKit/Extension/UIEdgeInsets/UIEdgeInsets+ExtensionTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// UIEdgeInsets+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 06/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class UIEdgeInsetsExtensionTests: XCTestCase { - - // MARK: - Tests - - func test_init_all() { - // GIVEN/WHEN - let edgetInsets = UIEdgeInsets(all: 99) - - // THEN - XCTAssertEqual(edgetInsets.top, 99, "Wrong top value") - XCTAssertEqual(edgetInsets.left, 99, "Wrong left value") - XCTAssertEqual(edgetInsets.bottom, 99, "Wrong bottom value") - XCTAssertEqual(edgetInsets.right, 99, "Wrong right value") - } - - func test_init_vertical_and_horizontal() { - // GIVEN/WHEN - let edgetInsets = UIEdgeInsets(vertical: 13, horizontal: 44) - - // THEN - XCTAssertEqual(edgetInsets.top, 13, "Wrong top value") - XCTAssertEqual(edgetInsets.left, 44, "Wrong left value") - XCTAssertEqual(edgetInsets.bottom, 13, "Wrong bottom value") - XCTAssertEqual(edgetInsets.right, 44, "Wrong right value") - } -} diff --git a/core/Sources/Common/UIKit/Extension/UITraitCollection/UITraitCollection-SizeAppearance.swift b/core/Sources/Common/UIKit/Extension/UITraitCollection/UITraitCollection-SizeAppearance.swift deleted file mode 100644 index 87fefc727..000000000 --- a/core/Sources/Common/UIKit/Extension/UITraitCollection/UITraitCollection-SizeAppearance.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// UITraitCollection-SizeAppearance.swift -// SparkCore -// -// Created by Michael Zimmermann on 07.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -extension UITraitCollection { - func hasDifferentSizeCategory(comparedTo traitCollection: UITraitCollection?) -> Bool { - self.preferredContentSizeCategory != traitCollection?.preferredContentSizeCategory - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIView/UIStackView-RemoveAll.swift b/core/Sources/Common/UIKit/Extension/UIView/UIStackView-RemoveAll.swift deleted file mode 100644 index ce7902681..000000000 --- a/core/Sources/Common/UIKit/Extension/UIView/UIStackView-RemoveAll.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UIStackView-RemoveAll.swift -// SparkCore -// -// Created by michael.zimmermann on 09.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -import UIKit - -extension UIStackView { - @discardableResult - func removeArrangedSubviews() -> [UIView] { - return self.arrangedSubviews.reduce([UIView]()) { $0 + [self.detachArrangedSubview($1)] } - } - - @discardableResult - func detachArrangedSubview(_ view: UIView) -> UIView { - self.removeArrangedSubview(view) - view.removeFromSuperview() - return view - } - - func addArrangedSubviews(_ subviews: [UIView]) { - for view in subviews { - self.addArrangedSubview(view) - } - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIView/UIView+AccessibilityExtension.swift b/core/Sources/Common/UIKit/Extension/UIView/UIView+AccessibilityExtension.swift deleted file mode 100644 index 75293e97f..000000000 --- a/core/Sources/Common/UIKit/Extension/UIView/UIView+AccessibilityExtension.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// UIView+AccessibilityExtension.swift -// SparkCore -// -// Created by robin.lemaire on 23/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -extension UIView { - - /// Insert or remove the trait on the current accessibilityTraits - func accessibilityTraits(manage trait: UIAccessibilityTraits, insert: Bool) { - if insert { - self.accessibilityTraits.insert(trait) - } else { - self.accessibilityTraits.remove(trait) - } - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIView/UIView+LayerExtension.swift b/core/Sources/Common/UIKit/Extension/UIView/UIView+LayerExtension.swift deleted file mode 100644 index c7aa75a2d..000000000 --- a/core/Sources/Common/UIKit/Extension/UIView/UIView+LayerExtension.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UIView+LayerExtension.swift -// SparkCore -// -// Created by robin.lemaire on 18/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension UIView { - - /// CGColors need to be refreshed on trait changes - func setBorderColor(from colorToken: any ColorToken) { - self.layer.borderColor = colorToken.uiColor.cgColor - } - - func setBorderWidth(_ borderWidth: CGFloat) { - self.layer.borderWidth = borderWidth - } - - func setCornerRadius(_ cornerRadius: CGFloat) { - self.layer.cornerRadius = cornerRadius.isInfinite ? self.frame.height / 2 : cornerRadius - } - - func setMasksToBounds(_ masksToBounds: Bool) { - self.layer.masksToBounds = masksToBounds - } -} diff --git a/core/Sources/Common/UIKit/Extension/UIView/UIView-Attributes.swift b/core/Sources/Common/UIKit/Extension/UIView/UIView-Attributes.swift deleted file mode 100644 index db6135ec5..000000000 --- a/core/Sources/Common/UIKit/Extension/UIView/UIView-Attributes.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// UIView-Attributes.swift -// SparkCore -// -// Created by michael.zimmermann on 16.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension UIView { - var isNotHidden: Bool { - return !self.isHidden - } -} diff --git a/core/Sources/Common/UIKit/GlobalExtension/NSLayoutConstraint/NSLayoutConstraint+Extension.swift b/core/Sources/Common/UIKit/GlobalExtension/NSLayoutConstraint/NSLayoutConstraint+Extension.swift deleted file mode 100644 index be4bca941..000000000 --- a/core/Sources/Common/UIKit/GlobalExtension/NSLayoutConstraint/NSLayoutConstraint+Extension.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// NSLayoutConstraint+Extension.swift -// Spark -// -// Created by robin.lemaire on 17/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -struct EdgeSet: OptionSet { - let rawValue: UInt - - static let top = EdgeSet(rawValue: 1 << 0) - static let trailing = EdgeSet(rawValue: 2 << 0) - static let bottom = EdgeSet(rawValue: 3 << 0) - static let leading = EdgeSet(rawValue: 4 << 0) - - static let all: EdgeSet = [.top, .trailing, .bottom, .leading] -} - -extension NSLayoutConstraint { - /// Make the view stick to the edges of an other view - /// - /// - Parameters: - /// - from: the source view - /// - to: the destination view - /// - insets: All component (top / left / bottom / right) should be positiv (.zero by default) - static func stickEdges(from: UIView, to: UIView, insets: UIEdgeInsets = .zero) { - NSLayoutConstraint.activate([ - from.topAnchor.constraint(equalTo: to.topAnchor, constant: insets.top), - from.leadingAnchor.constraint(equalTo: to.leadingAnchor, constant: insets.left), - from.bottomAnchor.constraint(equalTo: to.bottomAnchor, constant: -insets.bottom), - from.trailingAnchor.constraint(equalTo: to.trailingAnchor, constant: -insets.right) - ]) - } - - static func edgeConstraints(from: UIView, to: UIView, insets: UIEdgeInsets = .zero, edge: EdgeSet = .all) -> [NSLayoutConstraint] { - - var constraints = [NSLayoutConstraint]() - if edge.contains(.top) { - constraints.append(from.topAnchor.constraint(equalTo: to.topAnchor, constant: insets.top)) - } - if edge.contains(.leading) { - constraints.append(from.leadingAnchor.constraint(equalTo: to.leadingAnchor, constant: insets.left)) - } - if edge.contains(.bottom) { - constraints.append(from.bottomAnchor.constraint(equalTo: to.bottomAnchor, constant: -insets.bottom)) - } - if edge.contains(.trailing) { - constraints.append(from.trailingAnchor.constraint(equalTo: to.trailingAnchor, constant: -insets.right)) - } - - return constraints - } - - static func center(from: UIView, to: UIView) { - NSLayoutConstraint.activate([ - from.centerXAnchor.constraint(equalTo: to.centerXAnchor), - from.centerYAnchor.constraint(equalTo: to.centerYAnchor) - ]) - } -} diff --git a/core/Sources/Common/UIKit/GlobalExtension/UIView/UIView+Layout.swift b/core/Sources/Common/UIKit/GlobalExtension/UIView/UIView+Layout.swift deleted file mode 100644 index a2e9e7928..000000000 --- a/core/Sources/Common/UIKit/GlobalExtension/UIView/UIView+Layout.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// UIView+Layout.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension UIView { - - func addSubviewCentered(_ subview: UIView) { - self.translatesAutoresizingMaskIntoConstraints = false - subview.translatesAutoresizingMaskIntoConstraints = false - addSubview(subview) - - NSLayoutConstraint.center(from: subview, to: self) - } - /// Adds a subview with the same size as the view. - /// - Parameter subview: subview to be added - func addSubviewSizedEqually(_ subview: UIView) { - self.translatesAutoresizingMaskIntoConstraints = false - subview.translatesAutoresizingMaskIntoConstraints = false - addSubview(subview) - - NSLayoutConstraint.stickEdges(from: subview, to: self) - } - - /// Activate a constraint of this view in relation to another view. - /// - Parameters: - /// - attribute1: anchor of caller view - /// - attribute2: anchor of the other view - /// - ofView: view that relates to this view - /// - relation: relation of constraint, .equal by default - /// - constant: constant of constraint, 0.0 by default - /// - multiplier: multiplier of constraint, 1.0 by default - func activateConstraint( - from attribute1: NSLayoutConstraint.Attribute, - to attribute2: NSLayoutConstraint.Attribute, - ofView: UIView, - relation: NSLayoutConstraint.Relation = .equal, - constant: CGFloat = 0.0, - multiplier: CGFloat = 1.0 - ) { - self.translatesAutoresizingMaskIntoConstraints = false - ofView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - self.constraint( - from: attribute1, - to: attribute2, - ofView: ofView, - relation: relation, - constant: constant, - multiplier: multiplier - ) - ]) - } - - /// Return an NSConstraint for the caller view. - /// - Parameters: - /// - attribute1: anchor of caller view - /// - attribute2: anchor of the other view - /// - ofView: view that relates to this view - /// - relation: relation of constraint, .equal by default - /// - constant: constant of constraint, 0.0 by default - /// - multiplier: multiplier of constraint, 10.0 by default - /// - Returns: NSLayoutConstraint - func constraint( - from attribute1: NSLayoutConstraint.Attribute, - to attribute2: NSLayoutConstraint.Attribute, - ofView: UIView, - relation: NSLayoutConstraint.Relation = .equal, - constant: CGFloat = 0.0, - multiplier: CGFloat = 1.0 - ) -> NSLayoutConstraint { - NSLayoutConstraint( - item: self, - attribute: attribute1, - relatedBy: relation, - toItem: ofView, - attribute: attribute2, - multiplier: multiplier, - constant: constant - ) - } -} diff --git a/core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift b/core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift deleted file mode 100644 index b074f5df1..000000000 --- a/core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// BadgeAccessibilityIdentifier.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum BadgeAccessibilityIdentifier { - - // MARK: - Properties - - public static let text = "spark-badge-text" -} diff --git a/core/Sources/Components/Badge/Constants/BadgeConstants.swift b/core/Sources/Components/Badge/Constants/BadgeConstants.swift deleted file mode 100644 index 74e7cd3f8..000000000 --- a/core/Sources/Components/Badge/Constants/BadgeConstants.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// BadgeConstants.swift -// SparkCore -// -// Created by alex.vecherov on 22.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum BadgeConstants { - static let emptySize = CGSize(width: 8, height: 8) - enum height { - static let medium: CGFloat = 24 - static let small: CGFloat = 16 - } -} diff --git a/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift b/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift deleted file mode 100644 index e49625e06..000000000 --- a/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// BadgeColors.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -struct BadgeColors { - - // MARK: - Properties - - let backgroundColor: any ColorToken - let borderColor: any ColorToken - let foregroundColor: any ColorToken -} - diff --git a/core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift b/core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift deleted file mode 100644 index 08635a866..000000000 --- a/core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// BadgeSizeDependentAttributes.swift -// SparkCore -// -// Created by michael.zimmermann on 03.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -struct BadgeSizeDependentAttributes: Equatable { - - let offset: EdgeInsets - let height: CGFloat - let font: TypographyFontToken - - static func == (lhs: BadgeSizeDependentAttributes, rhs: BadgeSizeDependentAttributes) -> Bool { - return lhs.offset == rhs.offset && - lhs.height == rhs.height && - lhs.font.font == rhs.font.font && - lhs.font.uiFont == rhs.font.uiFont - } -} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift deleted file mode 100644 index 1a33e964a..000000000 --- a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// BadgeBorder.swift -// SparkDemo -// -// Created by alex.vecherov on 17.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Structure that is used for configuring border of ``BadgeView`` -/// -/// List of properties: -/// - width -/// - radius -/// - color returned as ColorToken -public struct BadgeBorder { - var width: CGFloat - let radius: CGFloat - var color: any ColorToken - - mutating func setWidth(_ width: CGFloat) { - self.width = width - } - - mutating func setColor(_ color: any ColorToken) { - self.color = color - } -} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift deleted file mode 100644 index 906e55176..000000000 --- a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// BadgeFormat.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Protocol that defines custom behaviour of ``BadgeView`` text -/// Create your own implementation of badge text by using it -public protocol BadgeFormatting { - func formatText(for value: Int?) -> String -} - -/// With this formatter you can define behaviour of Badge label. -/// available formats: -/// - ``default`` -/// - ``overflowCounter(maxValue:)`` -/// - ``custom(formatter:)`` -public enum BadgeFormat { - - // MARK: - Properties - - /// Use **default** for regular counting behavior with numbers. - case `default` - - /// Use **overflowCounter(maxValue)** - /// If badge **value** would be greater than passed **maxValue** into formatter - /// then badge will show **maxValue+** - case overflowCounter(maxValue: Int) - - /// You can define your custom behavior by using **custom** type. But in that case - /// Formatter should be implemented and conform to **BadgeFormatting** protocol - /// For example you can define thousand counter to show 96k instead of 96000 - case custom(formatter: BadgeFormatting) - - // MARK: - Getting text - - /// This function will return text value for your badge - /// wiht conformation to the selected **BadgeFormat** type - func text(_ value: Int?) -> String { - switch self { - case .overflowCounter(let maxValue): - guard let value else { - return "" - } - return value > maxValue ? "\(maxValue)+" : "\(value)" - case .custom(let formatter): - return formatter.formatText(for: value) - default: - guard let value else { - return "" - } - return "\(value)" - } - } -} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift b/core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift deleted file mode 100644 index 6956bf7ba..000000000 --- a/core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// BadgeIntentType.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -/// **BadgeIntentType** defines color of ``BadgeView`` -public enum BadgeIntentType: CaseIterable { - case accent - case basic - case alert - case danger - case info - case neutral - case main - case support - case success -} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgePosition.swift b/core/Sources/Components/Badge/Properties/Public/BadgePosition.swift deleted file mode 100644 index 7ab2654cb..000000000 --- a/core/Sources/Components/Badge/Properties/Public/BadgePosition.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// BadgePosition.swift -// SparkCore -// -// Created by louis.borlee on 16/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Enum that represents the position where the ``BadgeView`` can be attached on another view -/// -/// There are two possible positions: -/// - topTrailingCorner -/// - trailing -public enum BadgePosition: CaseIterable { - case topTrailingCorner - case trailing -} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift deleted file mode 100644 index 414e107db..000000000 --- a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// BadgeSize.swift -// SparkDemo -// -// Created by alex.vecherov on 17.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Enum that sets ``BadgeView`` size -/// -/// There are two possible sizes: -/// - medium -/// - small -public enum BadgeSize: CaseIterable { - case medium - case small -} diff --git a/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift b/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift deleted file mode 100644 index 4ef3d5edb..000000000 --- a/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// BadgeGetSizeUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 03.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -// sourcery: AutoMockable -protocol BadgeGetSizeAttributesUseCaseable { - func execute(theme: Theme, size: BadgeSize) -> BadgeSizeDependentAttributes -} - -/// A use case that returns size specific attributes according to the theme -struct BadgeGetSizeAttributesUseCase: BadgeGetSizeAttributesUseCaseable { - - // MARK: - Functions - func execute(theme: Theme, size: BadgeSize) -> BadgeSizeDependentAttributes { - return .init(offset: size.offset(spacing: theme.layout.spacing), - height: size.badgeHeight(), - font: size.font(typography: theme.typography)) - } -} - -// MARK: - Private helper extension -private extension BadgeSize { - func offset(spacing: LayoutSpacing) -> EdgeInsets { - switch self { - case .medium: return .init(vertical: spacing.small, - horizontal: spacing.medium) - case .small: return .init(vertical: 0, - horizontal: spacing.small) - } - } - - func badgeHeight() -> CGFloat { - switch self { - case .medium: - return BadgeConstants.height.medium - case .small: - return BadgeConstants.height.small - } - } - - func font(typography: Typography) -> TypographyFontToken { - switch self { - case .medium: - return typography.captionHighlight - case .small: - return typography.smallHighlight - } - } - -} diff --git a/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift b/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift deleted file mode 100644 index d203f82e4..000000000 --- a/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// BadgeGetSizeAttributesUseCase.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 03.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class BadgeGetSizeAttributesUseCaseTests: XCTestCase { - - // MARK: - Properties - var sut: BadgeGetSizeAttributesUseCase! - var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.theme = .mocked() - self.sut = BadgeGetSizeAttributesUseCase() - } - - // MARK: - Tests - func test_size_small() throws { - let attributes = sut.execute(theme: self.theme, size: .small) - - let expectedAttributes = BadgeSizeDependentAttributes( - offset: .init(vertical: 0, horizontal: 3), - height: 16, - font: self.theme.typography.smallHighlight) - - XCTAssertEqual(attributes, expectedAttributes) - } - - func test_size_normal() throws { - let attributes = sut.execute(theme: self.theme, size: .medium) - - let expectedAttributes = BadgeSizeDependentAttributes( - offset: .init(vertical: 3, horizontal: 5), - height: 24, - font: self.theme.typography.captionHighlight) - - XCTAssertEqual(attributes, expectedAttributes) - } -} diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift deleted file mode 100644 index d4de0f39a..000000000 --- a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// BadgeGetIntentColorsUseCase.swift -// Spark -// -// Created by alex.vecherov on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol BadgeGetIntentColorsUseCaseable { - func execute(intentType: BadgeIntentType, - on colors: Colors) -> BadgeColors -} - -final class BadgeGetIntentColorsUseCase: BadgeGetIntentColorsUseCaseable { - - // MARK: - Methods - - func execute(intentType: BadgeIntentType, - on colors: Colors) -> BadgeColors { - let surfaceColor = colors.base.surface - - switch intentType { - case .accent: - return BadgeColors( - backgroundColor: colors.accent.accent, - borderColor: surfaceColor, - foregroundColor: colors.accent.onAccent - ) - case .basic: - return BadgeColors( - backgroundColor: colors.basic.basic, - borderColor: surfaceColor, - foregroundColor: colors.basic.onBasic - ) - case .alert: - return BadgeColors( - backgroundColor: colors.feedback.alert, - borderColor: surfaceColor, - foregroundColor: colors.feedback.onAlert - ) - case .danger: - return BadgeColors( - backgroundColor: colors.feedback.error, - borderColor: surfaceColor, - foregroundColor: colors.feedback.onError - ) - case .info: - return BadgeColors( - backgroundColor: colors.feedback.info, - borderColor: surfaceColor, - foregroundColor: colors.feedback.onInfo - ) - case .neutral: - return BadgeColors( - backgroundColor: colors.feedback.neutral, - borderColor: surfaceColor, - foregroundColor: colors.feedback.onNeutral - ) - case .main: - return BadgeColors( - backgroundColor: colors.main.main, - borderColor: surfaceColor, - foregroundColor: colors.main.onMain - ) - case .support: - return BadgeColors( - backgroundColor: colors.support.support, - borderColor: surfaceColor, - foregroundColor: colors.support.onSupport - ) - case .success: - return BadgeColors( - backgroundColor: colors.feedback.success, - borderColor: surfaceColor, - foregroundColor: colors.feedback.onSuccess - ) - - } - } -} diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift deleted file mode 100644 index 23d8f6bd4..000000000 --- a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// BadgeGetColorsUseCaseTests.swift -// SparkDemo -// -// Created by alex.vecherov on 15.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class BadgeGetColorsUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_for_all_variant_cases() throws { - // Given - - let mockedExpectedColors = ColorsGeneratedMock.mocked() - let mockedExpectedSurfaceColor = mockedExpectedColors.base.surface - - let items: [BadgeGetColors] = [ - .init( - givenIntent: .accent, - expectedBackgroundToken: mockedExpectedColors.accent.accent, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.accent.onAccent - ), - .init( - givenIntent: .basic, - expectedBackgroundToken: mockedExpectedColors.basic.basic, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.basic.onBasic - ), - .init( - givenIntent: .alert, - expectedBackgroundToken: mockedExpectedColors.feedback.alert, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.feedback.onAlert - ), - .init( - givenIntent: .danger, - expectedBackgroundToken: mockedExpectedColors.feedback.error, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.feedback.onError - ), - .init( - givenIntent: .info, - expectedBackgroundToken: mockedExpectedColors.feedback.info, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.feedback.onInfo - ), - .init( - givenIntent: .neutral, - expectedBackgroundToken: mockedExpectedColors.feedback.neutral, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.feedback.onNeutral - ), - .init( - givenIntent: .main, - expectedBackgroundToken: mockedExpectedColors.main.main, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.main.onMain - ), - .init( - givenIntent: .support, - expectedBackgroundToken: mockedExpectedColors.support.support, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.support.onSupport - ), - .init( - givenIntent: .success, - expectedBackgroundToken: mockedExpectedColors.feedback.success, - expectedBorderToken: mockedExpectedSurfaceColor, - expectedTextToken: mockedExpectedColors.feedback.onSuccess - ) - ] - - for item in items { - let useCase = BadgeGetIntentColorsUseCase() - - // When - let colors = useCase.execute(intentType: item.givenIntent, on: mockedExpectedColors) - - try Tester.testColorsProperties( - givenColors: colors, - getColors: item - ) - } - } -} - -private struct Tester { - - static func testColorsProperties( - givenColors: BadgeColors, - getColors: BadgeGetColors - ) throws { - // Background Color - try self.testColor( - givenColorProperty: givenColors.backgroundColor, - givenPropertyName: "backgroundColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedBackgroundToken - ) - - // Border Color - try self.testColor( - givenColorProperty: givenColors.borderColor, - givenPropertyName: "borderColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedBorderToken - ) - - // Foreground Color - try self.testColor( - givenColorProperty: givenColors.foregroundColor, - givenPropertyName: "foregroundColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedTextToken - ) - } - - private static func testColor( - givenColorProperty: (any ColorToken)?, - givenPropertyName: String, - givenIntent: BadgeIntentType, - expectedColorToken: (any ColorToken)? - ) throws { - let errorPrefixMessage = "\(givenPropertyName) for .\(givenIntent) case" - - if let givenColorProperty { - let color = try XCTUnwrap(givenColorProperty as? ColorTokenGeneratedMock, "Wrong " + errorPrefixMessage) - XCTAssertIdentical(color, expectedColorToken as? ColorTokenGeneratedMock, "Wrong value " + errorPrefixMessage) - } else { - XCTAssertNil(givenColorProperty, "Should be nil" + errorPrefixMessage) - } - } -} - -private struct BadgeGetColors { - let givenIntent: BadgeIntentType - - let expectedBackgroundToken: any ColorToken - let expectedBorderToken: any ColorToken - let expectedTextToken: any ColorToken -} diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift deleted file mode 100644 index 4b1569527..000000000 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// BadgeView.swift -// SparkCore -// -// Created by alex.vecherov on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// This is SwiftUI badge view to show notifications count -/// -/// Badge border and offsets of it's text are **@ScaledMetric** variables and alligned to user's **Accessibility** -/// -/// **Example** -/// This example shows how to create view with horizontal alignment of Badge -/// ```swift -/// @State var value: Int? = 3 -/// var body: any View { -/// HStack { -/// Text("Some text") -/// BadgeView(theme: YourTheme.shared, intent: .alert, value: value) -/// } -/// } -/// ``` -public struct BadgeView: View { - @ObservedObject private var viewModel: BadgeViewModel - @ScaledMetric private var horizontalOffset: CGFloat - @ScaledMetric private var verticalOffset: CGFloat - @ScaledMetric private var emptySize: CGFloat - @ScaledMetric private var borderWidth: CGFloat - - public var body: some View { - if self.viewModel.isBadgeEmpty { - Circle() - .foregroundColor(self.viewModel.backgroundColor.color) - .frame(width: self.emptySize, height: self.emptySize) - .border( - width: self.viewModel.isBorderVisible ? borderWidth : 0, - radius: self.viewModel.border.radius, - colorToken: self.viewModel.border.color - ) - .fixedSize() - } else { - Text(self.viewModel.text) - .font(self.viewModel.textFont.font) - .foregroundColor(self.viewModel.textColor.color) - .padding(.init(vertical: self.verticalOffset, horizontal: self.horizontalOffset)) - .background(self.viewModel.backgroundColor.color) - .border( - width: self.viewModel.isBorderVisible ? borderWidth : 0, - radius: self.viewModel.border.radius, - colorToken: self.viewModel.border.color - ) - .fixedSize() - .accessibilityIdentifier(BadgeAccessibilityIdentifier.text) - } - } - - /// - Parameter theme: ``Theme`` - /// - Parameter intent: ``BadgeIntentType`` - /// - Parameter value: **Int?** You can set value to nil, to make ``BadgeView`` without text - public init(theme: Theme, intent: BadgeIntentType, value: Int? = nil) { - let viewModel = BadgeViewModel(theme: theme, intent: intent, value: value) - self.viewModel = viewModel - - self._horizontalOffset = - .init(wrappedValue: - viewModel.offset.leading - ) - self._verticalOffset = - .init(wrappedValue: - viewModel.offset.top - ) - self._emptySize = .init(wrappedValue: BadgeConstants.emptySize.width) - self._borderWidth = .init(wrappedValue: viewModel.border.width) - } - - // MARK: - Badge Modification Functions - - /// Controlls outline state of the Badge. - /// By default Badge has an outline based on current ``Theme``. - /// - /// Use @State variable to control outline based on this variable. - public func borderVisible(_ isBorderVisible: Bool) -> Self { - self.viewModel.isBorderVisible = isBorderVisible - return self - } - - /// Controlls text size of the Badge. By ``BadgeSize`` is *.medium*. - /// - /// Text font size is based on ``BadgeSize`` value and current ``Theme``. - /// Use @State variable to control ``BadgeSize`` based on this variable. - public func size(_ size: BadgeSize) -> Self { - self.viewModel.size = size - return self - } - - /// Controlls text format of the Badge. See more details in ``BadgeFormat``. - /// - /// Use @State variable to control ``BadgeFormat`` based on this variable. - public func format(_ format: BadgeFormat) -> Self { - self.viewModel.format = format - return self - } - - /// Controlls spark theme of the Badge. See more details in ``Theme``. - /// - /// Use @State variable to control ``Theme`` based on this variable. - public func theme(_ theme: Theme) -> Self { - self.viewModel.theme = theme - return self - } - - /// Controlls badge intent type. See more details in ``BadgeIntentType`` - /// - /// Use @State variable to control ``BadgeIntentType`` based on this variable. - public func value(_ value: Int?) -> Self { - self.viewModel.value = value - return self - } -} diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift deleted file mode 100644 index d324a3283..000000000 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ /dev/null @@ -1,427 +0,0 @@ -// -// BadgeUIView.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// This is the UIKit version for the ``BadgeView`` -public class BadgeUIView: UIView { - - private var viewModel: BadgeViewModel - - // Dynamicaly sized properties for badge - // emptyBadgeSize represents size of the circle in empty state of Badge - // horizontalSpacing is the padding to the left and right of the text - // borderWidth is the width of the border when it's shown - // badgeHeight is the height of the badge - @ScaledUIMetric private var emptyBadgeSize: CGFloat = 0 - @ScaledUIMetric private var horizontalSpacing: CGFloat = 0 - @ScaledUIMetric private var borderWidth: CGFloat = 0 - @ScaledUIMetric private var badgeHeight: CGFloat = 0 - - // Constraints for badge size - // Thess constraints containes text size with - // vertical and horizontal offsets - private var widthConstraint: NSLayoutConstraint? - private var heightConstraint: NSLayoutConstraint? - - // Constraints for attach / detach - private var attachLeadingAnchorConstraint: NSLayoutConstraint? - private var attachCenterXAnchorConstraint: NSLayoutConstraint? - private var attachCenterYAnchorConstraint: NSLayoutConstraint? - private var attachConstraints: [NSLayoutConstraint?] { - [attachLeadingAnchorConstraint, attachCenterXAnchorConstraint, attachCenterYAnchorConstraint] - } - - // MARK: - Badge Text Label properties - private var textLabel: UILabel = UILabel() - - // Constraints for badge text label. - // All of these are applied to the badge text label - private var labelLeadingConstraint: NSLayoutConstraint? - private var labelTrailingConstraint: NSLayoutConstraint? - - // Bool property that determines wether we should - // install and activate text label constraints or not - private var shouldSetupLabelConstrains: Bool { - self.labelLeadingConstraint == nil || - self.labelTrailingConstraint == nil - } - - private var cancellables = Set() - - // MARK: - Public variables - /// The current theme of the view - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// The current intent - public var intent: BadgeIntentType { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - /// The badge size - public var size: BadgeSize { - get { - return self.viewModel.size - } - set { - self.viewModel.size = newValue - } - } - - /// The current value of the badge - public var value: Int? { - get { - return self.viewModel.value - } - set { - self.viewModel.value = newValue - } - } - - /// The formatter of the badge - public var format: BadgeFormat { - get { - return self.viewModel.format - } - set { - self.viewModel.format = newValue - } - } - - /// Shows/hides the border around the badge - public var isBorderVisible: Bool { - get { - return self.viewModel.isBorderVisible - } - set { - self.viewModel.isBorderVisible = newValue - } - } - - public override var intrinsicContentSize: CGSize { - if self.viewModel.isBadgeEmpty { - return CGSize(width: self.emptyBadgeSize, height: self.emptyBadgeSize) - } else { - let height = self.badgeHeight - let contentWidth = self.textLabel.intrinsicContentSize.width + (self.horizontalSpacing * 2) - - let width = max(contentWidth, height) - return CGSize(width: width, height: height) - } - } - - // MARK: - Init - public init(theme: Theme, intent: BadgeIntentType, size: BadgeSize = .medium, value: Int? = nil, format: BadgeFormat = .default, isBorderVisible: Bool = false) { - self.viewModel = BadgeViewModel(theme: theme, intent: intent, size: size, value: value, format: format, isBorderVisible: isBorderVisible) - - super.init(frame: .zero) - - self.setupBadge() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - - // MARK: - Badge configuration - - private func setupBadge() { - setupScalables(isBorderVisible: self.viewModel.isBorderVisible) - setupBadgeText() - setupAppearance() - setupLayouts(isBadgeEmpty: self.viewModel.isBadgeEmpty) - subscribe() - } - - private func setupScalables(isBorderVisible: Bool) { - self.emptyBadgeSize = BadgeConstants.emptySize.width - self.badgeHeight = self.viewModel.badgeHeight - self.horizontalSpacing = self.viewModel.offset.leading - self.borderWidth = isBorderVisible ? self.viewModel.border.width : .zero - } - - private func setupBadgeText() { - self.addSubview(textLabel) - self.textLabel.setContentCompressionResistancePriority(.required, for: .vertical) - self.textLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - self.textLabel.accessibilityIdentifier = BadgeAccessibilityIdentifier.text - self.textLabel.adjustsFontForContentSizeCategory = true - self.textLabel.textAlignment = .center - self.textLabel.text = self.viewModel.text - self.textLabel.textColor = self.viewModel.textColor.uiColor - self.textLabel.font = self.viewModel.textFont.uiFont - self.textLabel.translatesAutoresizingMaskIntoConstraints = false - } - - private func setupAppearance() { - self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = self.viewModel.backgroundColor.uiColor - self.layer.borderWidth = self.borderWidth - self.layer.borderColor = self.viewModel.border.color.uiColor.cgColor - self.clipsToBounds = true - } - - // MARK: - Layouts setup - - private func setupLayouts(isBadgeEmpty: Bool) { - self.setupSizeConstraint(isBadgeEmpty: isBadgeEmpty) - self.updateLeadingConstraintsIfNeeded(isBadgeEmpty: isBadgeEmpty) - self.setupBadgeConstraintsIfNeeded() - } - - private func setupSizeConstraint(isBadgeEmpty: Bool) { - let badgeSize = isBadgeEmpty ? self.emptyBadgeSize : self.badgeHeight - - if let heightConstraint = self.heightConstraint, let widthConstraint = self.widthConstraint { - widthConstraint.constant = badgeSize - heightConstraint.constant = badgeSize - } else { - let widthConstraint = self.widthAnchor.constraint(greaterThanOrEqualToConstant: badgeSize) - widthConstraint.priority = .required - let heightConstraint = self.heightAnchor.constraint(equalToConstant: badgeSize) - heightConstraint.priority = .required - NSLayoutConstraint.activate([widthConstraint, heightConstraint]) - self.widthConstraint = widthConstraint - self.heightConstraint = heightConstraint - heightConstraint.isActive = true - } - } - - private func updateLeadingConstraintsIfNeeded(isBadgeEmpty: Bool) { - guard let leadingConstraint = self.labelLeadingConstraint, - let trailingConstraint = self.labelTrailingConstraint else { return } - - let spacing: CGFloat = isBadgeEmpty ? 0 : self.horizontalSpacing - leadingConstraint.constant = spacing - trailingConstraint.constant = -spacing - } - - private func setupBadgeConstraintsIfNeeded() { - guard self.shouldSetupLabelConstrains else { - return - } - - let labelLeadingConstraint = self.textLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: self.horizontalSpacing) - let labelTrailingConstraint = self.textLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.horizontalSpacing) - let centerYConstraint = self.textLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) - NSLayoutConstraint.activate([labelLeadingConstraint, labelTrailingConstraint, centerYConstraint]) - self.labelLeadingConstraint = labelLeadingConstraint - self.labelTrailingConstraint = labelTrailingConstraint - } - - public override func layoutSubviews() { - super.layoutSubviews() - - self.layer.cornerRadius = min(frame.width, frame.height) / 2.0 - } - - // MARK: - Attach / Detach - - /// Remove constraints from the view the badge was attached onto - public func detach() { - self.attachConstraints.compactMap { $0 }.forEach { - self.removeConstraint($0) - } - } - - /// Attach badge to another view by using constraints - /// Triggers detach() if it was already attached to a view - /// - Parameters: - /// - view: the targeted view to attach the badge onto - /// - position: position where the ``BadgeView`` can be attached - public func attach(to view: UIView, position: BadgePosition) { - self.detach() - - switch position { - case .topTrailingCorner: - self.attachCenterXAnchorConstraint = self.centerXAnchor.constraint( - equalTo: view.trailingAnchor) - self.attachCenterYAnchorConstraint = self.centerYAnchor.constraint( - equalTo: view.topAnchor) - case .trailing: - self.attachLeadingAnchorConstraint = self.leadingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: self.viewModel.theme.layout.spacing.small) - self.attachCenterYAnchorConstraint = self.centerYAnchor.constraint( - equalTo: view.centerYAnchor) - } - - NSLayoutConstraint.activate(self.attachConstraints.compactMap { $0 }) - } -} - -// MARK: - Badge Subscribers -extension BadgeUIView { - private func subscribe() { - self.subscribeToTextChanges() - self.subscribeToBorderChanges() - self.subscribeToColorChanges() - self.subscribeToSizeChanges() - } - - private func subscribeToTextChanges() { - self.viewModel.$text - .subscribe(in: &self.cancellables) { [weak self] text in - guard let self = self else { return } - self.textLabel.text = text - self.reloadUISize() - self.setupLayouts(isBadgeEmpty: self.viewModel.isBadgeEmpty) - self.invalidateIntrinsicContentSize() - } - self.viewModel.$textFont - .subscribe(in: &self.cancellables) { [weak self] textFont in - guard let self = self else { return } - self.textLabel.font = textFont.uiFont - self.reloadUISize() - self.setupLayouts(isBadgeEmpty: self.viewModel.isBadgeEmpty) - } - self.viewModel.$isBadgeEmpty - .subscribe(in: &self.cancellables) { [weak self] isBadgeEmpty in - self?.textLabel.text = self?.viewModel.text - self?.reloadUISize() - self?.setupLayouts(isBadgeEmpty: isBadgeEmpty) - } - } - - private func subscribeToBorderChanges() { - self.viewModel.$isBorderVisible - .subscribe(in: &self.cancellables) { [weak self] isBorderVisible in - guard let self else { - return - } - self.updateBorder(self.viewModel.border, isBorderVisible: isBorderVisible) - } - self.viewModel.$border - .subscribe(in: &self.cancellables) { [weak self] badgeBorder in - guard let self = self else { return } - self.updateBorder(badgeBorder, isBorderVisible: self.viewModel.isBorderVisible) - } - } - - private func subscribeToColorChanges() { - self.viewModel.$textColor - .subscribe(in: &self.cancellables) { [weak self] textColor in - self?.textLabel.textColor = textColor.uiColor - } - self.viewModel.$backgroundColor - .subscribe(in: &self.cancellables) { [weak self] backgroundColor in - self?.backgroundColor = backgroundColor.uiColor - } - } - - private func subscribeToSizeChanges() { - self.viewModel.$badgeHeight - .subscribe(in: &self.cancellables) { [weak self] badgeHeight in - guard let self = self else { return } - self.badgeHeight = badgeHeight - self.setupLayouts(isBadgeEmpty: self.viewModel.isBadgeEmpty) - self.invalidateIntrinsicContentSize() - } - } -} - -// MARK: - Updates on Trait Collection Change -extension BadgeUIView { - private func updateBorder(_ badgeBorder: BadgeBorder, isBorderVisible: Bool) { - self.layer.borderColor = badgeBorder.color.uiColor.cgColor - self.setupScalables(isBorderVisible: isBorderVisible) - self.reloadBorderWidth() - } - - private func reloadColors() { - self.backgroundColor = self.viewModel.backgroundColor.uiColor - self.textLabel.textColor = self.viewModel.textColor.uiColor - self.layer.borderColor = self.viewModel.border.color.uiColor.cgColor - } - - private func reloadBadgeFontIfNeeded() { - guard !self.viewModel.isBadgeEmpty else { - return - } - self.textLabel.font = self.viewModel.textFont.uiFont - } - - private func reloadUISize() { - self._emptyBadgeSize.update(traitCollection: self.traitCollection) - self._horizontalSpacing.update(traitCollection: self.traitCollection) - self._badgeHeight.update(traitCollection: traitCollection) - } - - private func reloadBorderWidth() { - self._borderWidth.update(traitCollection: self.traitCollection) - self.layer.borderWidth = self.borderWidth - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.reloadColors() - } - - self.invalidateIntrinsicContentSize() - self.reloadBadgeFontIfNeeded() - self.reloadUISize() - self.reloadBorderWidth() - self.setupLayouts(isBadgeEmpty: self.viewModel.isBadgeEmpty) - } -} - -// MARK: - Label priorities -public extension BadgeUIView { - func setLabelContentCompressionResistancePriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentCompressionResistancePriority(priority, for: axis) - } - - func setLabelContentHuggingPriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentHuggingPriority(priority, for: axis) - } -} - -// MARK: - Badge Update Functions -public extension BadgeUIView { - func setIntent(_ intent: BadgeIntentType) { - self.viewModel.intent = intent - } - - func setBorderVisible(_ isBorderVisible: Bool) { - self.viewModel.isBorderVisible = isBorderVisible - } - - func setValue(_ value: Int?) { - self.viewModel.value = value - } - - func setFormat(_ format: BadgeFormat) { - self.viewModel.format = format - } - - func setSize(_ badgeSize: BadgeSize) { - self.viewModel.size = badgeSize - } - - func setTheme(_ theme: Theme) { - self.viewModel.theme = theme - } -} diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift deleted file mode 100644 index 2a903df1e..000000000 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// BadgeViewModel.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine -import SwiftUI - -/// **BadgeViewModel** is a view model that is required for -/// configuring ``BadgeView`` and changing it's properties. -/// -/// List of properties: -/// - value -- property that represents **Int?** displayed in ``BadgeView``. -/// Set *value* to nil to show empty Badge as circle -/// -/// - intent -- changes ``BadgeIntentType`` -/// -/// - isBorderVisible -- ``Bool``, changes outline of the Badge -/// -/// - size -- changes ``BadgeSize`` and text font -/// -/// - format -- see ``BadgeFormat`` as a formater of **Badge Text** -/// -/// - theme -- represents ``Theme`` used in the app -final class BadgeViewModel: ObservableObject { - - // MARK: - Badge Configuration Public Properties - var value: Int? { - didSet { - self.updateText() - } - } - var intent: BadgeIntentType { - didSet { - self.updateColors() - } - } - var size: BadgeSize { - didSet { - self.updateFont() - self.updateScalings() - } - } - var format: BadgeFormat { - didSet { - self.updateText() - } - } - var theme: Theme { - didSet { - self.updateColors() - self.updateFont() - self.updateScalings() - } - } - - // MARK: - Internal Published Properties - @Published var text: String - @Published var textFont: TypographyFontToken - @Published var textColor: any ColorToken - @Published var isBadgeEmpty: Bool - @Published var backgroundColor: any ColorToken - @Published var border: BadgeBorder - @Published var isBorderVisible: Bool - @Published var badgeHeight: CGFloat - @Published var offset: EdgeInsets - - // MARK: - Internal Appearance Properties - var colorsUseCase: BadgeGetIntentColorsUseCaseable - var sizeAttributesUseCase: BadgeGetSizeAttributesUseCaseable - - // MARK: - Initializer - - init(theme: Theme, - intent: BadgeIntentType, - size: BadgeSize = .medium, - value: Int? = nil, - format: BadgeFormat = .default, - isBorderVisible: Bool = true, - colorsUseCase: BadgeGetIntentColorsUseCaseable = BadgeGetIntentColorsUseCase(), - sizeAttributesUseCase: BadgeGetSizeAttributesUseCaseable = BadgeGetSizeAttributesUseCase() - ) { - let colors = colorsUseCase.execute(intentType: intent, on: theme.colors) - - self.value = value - - self.text = format.text(value) - self.isBadgeEmpty = format.text(value).isEmpty - self.textColor = colors.foregroundColor - - self.backgroundColor = colors.backgroundColor - - self.border = BadgeBorder( - width: theme.border.width.medium, - radius: theme.border.radius.full, - color: colors.borderColor - ) - - self.theme = theme - - self.format = format - self.size = size - self.intent = intent - self.isBorderVisible = isBorderVisible - self.colorsUseCase = colorsUseCase - self.sizeAttributesUseCase = sizeAttributesUseCase - - let sizeAttributes = sizeAttributesUseCase.execute(theme: theme, size: size) - self.textFont = sizeAttributes.font - self.badgeHeight = sizeAttributes.height - self.offset = sizeAttributes.offset - } - - private func updateColors() { - let colors = self.colorsUseCase.execute(intentType: self.intent, on: self.theme.colors) - - self.textColor = colors.foregroundColor - self.backgroundColor = colors.backgroundColor - self.border.setColor(colors.borderColor) - } - - private func updateText() { - self.text = self.format.text(self.value) - self.isBadgeEmpty = self.text.isEmpty - } - - private func updateFont() { - let sizeAttributes = self.sizeAttributesUseCase.execute(theme: self.theme, size: self.size) - self.textFont = sizeAttributes.font - } - - private func updateScalings() { - let sizeAttributes = self.sizeAttributesUseCase.execute(theme: self.theme, size: self.size) - self.offset = sizeAttributes.offset - self.border.setWidth(self.theme.border.width.medium) - self.badgeHeight = sizeAttributes.height - } -} diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift deleted file mode 100644 index 2055becc7..000000000 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// BadgeViewModelTests.swift -// SparkCore -// -// Created by alex.vecherov on 17.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import SwiftUI -import XCTest - -final class BadgeViewModelTests: XCTestCase { - - var theme: ThemeGeneratedMock = ThemeGeneratedMock.mocked() - var subscriptions = Set() - - // MARK: - Tests - func test_init() throws { - for badgeIntent in BadgeIntentType.allCases { - // Given - - let viewModel = BadgeViewModel(theme: theme, intent: badgeIntent) - - let badgeExpectedColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeIntent, on: theme.colors) - - // Then - - XCTAssertIdentical(viewModel.textColor as? ColorTokenGeneratedMock, badgeExpectedColors.foregroundColor as? ColorTokenGeneratedMock, "Text color doesn't match expected foreground") - - XCTAssertIdentical(viewModel.theme as? ThemeGeneratedMock, theme, "Badge theme doesn't match expected theme") - - XCTAssertTrue(viewModel.border.isEqual(to: theme, isOutlined: true), "Border border doesn't match expected") - } - } - - func test_set_value() throws { - for badgeIntent in BadgeIntentType.allCases { - // Given - - let expectedInitText = "20" - let expectedUpdatedText = "233" - let viewModel = BadgeViewModel(theme: theme, intent: badgeIntent, value: 20) - - // Then - - XCTAssertEqual(expectedInitText, viewModel.text, "Text doesn't match init value with standart format") - - viewModel.value = 233 - - XCTAssertEqual(expectedUpdatedText, viewModel.text, "Text doesn't match incremented value with standart format") - - XCTAssertEqual(viewModel.textFont.font, theme.typography.captionHighlight.font, "Font is wrong") - - viewModel.size = .small - - XCTAssertEqual(viewModel.textFont.font, theme.typography.smallHighlight.font, "Font is wrong") - } - } - - func test_update_size() throws { - for badgeIntent in BadgeIntentType.allCases { - // Given - - let viewModel = BadgeViewModel(theme: theme, intent: badgeIntent, value: 20) - - // Then - - XCTAssertEqual(viewModel.size, .medium, "Badge should be .normal sized by default") - - XCTAssertEqual(viewModel.textFont.font, theme.typography.captionHighlight.font, "Font is wrong") - - viewModel.size = .small - - XCTAssertEqual(viewModel.textFont.font, theme.typography.smallHighlight.font, "Font is wrong") - } - } - - func test_update_intent() throws { - for badgeIntent in BadgeIntentType.allCases { - // Given - - let viewModel = BadgeViewModel(theme: theme, intent: badgeIntent, value: 20) - - // Then - - XCTAssertEqual(viewModel.intent, badgeIntent, "Intent type was set wrong") - - viewModel.intent = randomizeIntentAndExceptingCurrent(badgeIntent) - - XCTAssertNotEqual(viewModel.intent, badgeIntent, "Intent type was set wrong") - } - } - - func test_theme_change_publishes_values() { - // Given - let sut = BadgeViewModel(theme: self.theme, intent: .danger) - let updateExpectation = expectation(description: "Attributes updated") - updateExpectation.expectedFulfillmentCount = 2 - - let publishers = Publishers.Zip4(sut.$offset, - sut.$textColor, - sut.$backgroundColor, - sut.$border) - - let allPublishers = Publishers.Zip(publishers, sut.$textFont) - - allPublishers.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.theme = ThemeGeneratedMock.mocked() - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_size_change_publishes_values() { - // Given - let sut = BadgeViewModel(theme: self.theme, intent: .danger, size: .medium) - let updateExpectation = expectation(description: "Attributes updated") - updateExpectation.expectedFulfillmentCount = 2 - - let publishers = Publishers.Zip3(sut.$textFont, - sut.$badgeHeight, - sut.$offset) - - publishers.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.size = .small - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_intent_change_publishes_values() { - // Given - let sut = BadgeViewModel(theme: self.theme, intent: .danger, size: .medium) - let updateExpectation = expectation(description: "Attributes updated") - updateExpectation.expectedFulfillmentCount = 2 - - let publishers = Publishers.Zip(sut.$textColor, sut.$backgroundColor) - - publishers.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.intent = .alert - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_value_change_publishes_values() { - // Given - let sut = BadgeViewModel(theme: self.theme, intent: .danger, size: .medium, value: 9) - let updateExpectation = expectation(description: "Attributes updated") - updateExpectation.expectedFulfillmentCount = 2 - - sut.$text.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.value = 99 - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_formater_change_publishes_values() { - // Given - let sut = BadgeViewModel(theme: self.theme, intent: .danger, value: 9999, format: .default) - let updateExpectation = expectation(description: "Attributes updated") - updateExpectation.expectedFulfillmentCount = 2 - - sut.$text.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.format = .overflowCounter(maxValue: 99) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - // MARK: - Private functions - private func randomizeIntentAndExceptingCurrent(_ currentIntentType: BadgeIntentType) -> BadgeIntentType { - let filteredIntentTypes = BadgeIntentType.allCases.filter { $0 != currentIntentType } - let randomIndex = Int.random(in: 0...filteredIntentTypes.count - 1) - - return filteredIntentTypes[randomIndex] - } -} - -// MARK: - Private extensions -private extension BadgeBorder { - func isEqual(to theme: Theme, isOutlined: Bool) -> Bool { - return (isOutlined ? width == theme.border.width.medium : width == theme.border.width.none) && - radius == theme.border.radius.full && - color.color == theme.colors.base.surface.color - } -} diff --git a/core/Sources/Components/BottomSheet/SwiftUI/View-Height.swift b/core/Sources/Components/BottomSheet/SwiftUI/View-Height.swift deleted file mode 100644 index f585cc748..000000000 --- a/core/Sources/Components/BottomSheet/SwiftUI/View-Height.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// BottomSheetDetentViewModifier.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.05.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -struct ViewHeightPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat? - - static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { - guard let nextValue = nextValue() else { return } - value = nextValue - } -} - -struct ViewHeightModifier: ViewModifier { - let height: Binding - func body(content: Content) -> some View { - content - .fixedSize(horizontal: true, vertical: true) - .background( - GeometryReader{ geometry in - Color.clear - .preference(key: ViewHeightPreferenceKey.self, value: geometry.size.height) - } - ) - .onPreferenceChange(ViewHeightPreferenceKey.self) { viewHeight in - if let viewHeight { - height.wrappedValue = viewHeight - } - } - } -} - -public extension View { - /// A view modifier for reading the height of a view. The height will be set in the given binding. - func readHeight(_ height: Binding) -> some View { - self - .modifier(ViewHeightModifier(height: height)) - } - -} diff --git a/core/Sources/Components/BottomSheet/UIKit/UISheetPresentationController-customHeightDetent.swift b/core/Sources/Components/BottomSheet/UIKit/UISheetPresentationController-customHeightDetent.swift deleted file mode 100644 index f13359331..000000000 --- a/core/Sources/Components/BottomSheet/UIKit/UISheetPresentationController-customHeightDetent.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// UISheetPresentationController-customHeightDetent.swift -// SparkCore -// -// Created by Michael Zimmermann on 14.05.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -public extension UISheetPresentationController.Detent { - /// Return a custom sheet detent with the view height fitting the expanded size as the custom height. - /// ``` - /// let controller = ExampleBottomSheetViewController() - /// if #available(iOS 16.0, *) { - /// if let sheet = controller.sheetPresentationController { - /// sheet.detents = [.medium(), .large(), .expandedHeight(of: controller.view)] - /// } - /// } - /// present(controller, animated: true) - /// ``` - @available(iOS 16.0, *) - static func expandedHeight(of view: UIView) -> UISheetPresentationController.Detent { - return .custom { context in - return min( - view.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize).height, - context.maximumDetentValue - 1) - } - } - - /// Return a custom sheet detent with the view height fitting the compressed size as the custom height. - /// ``` - /// let controller = ExampleBottomSheetViewController() - /// if #available(iOS 16.0, *) { - /// if let sheet = controller.sheetPresentationController { - /// sheet.detents = [.medium(), .large(), .compressedHeight(of: controller.view)] - /// } - /// } - /// present(controller, animated: true) - /// ``` - @available(iOS 16.0, *) - static func compressedHeight(of view: UIView) -> UISheetPresentationController.Detent { - return .custom { context in - return min( - view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height, - context.maximumDetentValue - 1) - } - } - - @available(iOS 16.0, *) - /// A custom detent which is almos the size of the large detent but avoids that the background is scaled. - static func maxHeight() -> UISheetPresentationController.Detent { - return .custom { context in - return context.maximumDetentValue - 1 - } - } -} diff --git a/core/Sources/Components/Button/AccessibilityIdentifier/ButtonAccessibilityIdentifier.swift b/core/Sources/Components/Button/AccessibilityIdentifier/ButtonAccessibilityIdentifier.swift deleted file mode 100644 index 9186a3b1d..000000000 --- a/core/Sources/Components/Button/AccessibilityIdentifier/ButtonAccessibilityIdentifier.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ButtonAccessibilityIdentifier.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The accessibility identifiers for the button. -public enum ButtonAccessibilityIdentifier { - - // MARK: - Properties - - /// The default view accessibility identifier. Can be changed by the consumer - public static let button = "spark-button" - /// The default icon button view accessibility identifier. Can be changed by the consumer - public static let iconButton = "spark-icon-button" - /// The default content stackView accessibility identifier. - static let contentStackView = "spark-button-content-stackView" - /// The icon view accessibility identifier. - public static let imageContentView = "spark-button-image-contentView" - /// The icon image accessibility identifier. - public static let imageView = "spark-button-image" - /// The text accessibility identifier. - public static let text = "spark-button-text" - /// The default clear button accessibility identifier. - static let clearButton = "spark-button-clear-button" -} diff --git a/core/Sources/Components/Button/Constants/ButtonConstants.swift b/core/Sources/Components/Button/Constants/ButtonConstants.swift deleted file mode 100644 index ad6e88341..000000000 --- a/core/Sources/Components/Button/Constants/ButtonConstants.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ButtonConstants.swift -// SparkCore -// -// Created by robin.lemaire on 28/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum ButtonConstants { - enum Animation { - /// Slow animation duration is 100ms - static let fastDuration = 0.1 - /// Fast animation duration is 300ms - static let slowDuration = 0.3 - } -} diff --git a/core/Sources/Components/Button/Enum/Internal/ButtonType.swift b/core/Sources/Components/Button/Enum/Internal/ButtonType.swift deleted file mode 100644 index f48711c64..000000000 --- a/core/Sources/Components/Button/Enum/Internal/ButtonType.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ButtonType.swift -// SparkCore -// -// Created by robin.lemaire on 09/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -internal enum ButtonType { - case button - case iconButton -} diff --git a/core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignment.swift b/core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignment.swift deleted file mode 100644 index 0038d6d3c..000000000 --- a/core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignment.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ButtonAlignment.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The alignment of the switch. -public enum ButtonAlignment: CaseIterable { - /// Image on the leading edge of the button. - /// Text on the trailing edge of the button. - /// Not interpreted if button contains only just image or just text. - case leadingImage - /// Image on the trailing edge of the button. - /// Text on the leading edge of the button - /// Not interpreted if button contains only just image or just text. - case trailingImage - - // MARK: - Properties - - var isTrailingImage: Bool { - return self == .trailingImage - } -} diff --git a/core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignmentTests.swift b/core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignmentTests.swift deleted file mode 100644 index 8f9176360..000000000 --- a/core/Sources/Components/Button/Enum/Public/Alignment/ButtonAlignmentTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ButtonAlignmentTests.swift -// SparkCore -// -// Created by robin.lemaire on 17/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class ButtonAlignmentTests: XCTestCase { - - // MARK: - Tests - - func test_isTrailingImage_for_all_cases() { - // GIVEN - let items: [(givenAlignment: ButtonAlignment, expectedIsTrailingImage: Bool)] = [ - (givenAlignment: .leadingImage, expectedIsTrailingImage: false), - (givenAlignment: .trailingImage, expectedIsTrailingImage: true) - ] - - for item in items { - // WHEN - let isTrailingImage = item.givenAlignment.isTrailingImage - - // THEN - XCTAssertEqual( - isTrailingImage, - item.expectedIsTrailingImage, - "Wrong isTrailingImage for .\(item.givenAlignment) cases" - ) - } - } -} diff --git a/core/Sources/Components/Button/Enum/Public/ButtonIntent.swift b/core/Sources/Components/Button/Enum/Public/ButtonIntent.swift deleted file mode 100644 index 247aaffad..000000000 --- a/core/Sources/Components/Button/Enum/Public/ButtonIntent.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ButtonIntent.swift -// Spark -// -// Created by janniklas.freundt.ext on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// A button intent is used to apply a color scheme to a button. -@frozen -public enum ButtonIntent: CaseIterable { - case accent - case alert - case basic - case danger - case info - case main - case neutral - case success - case support - case surface -} diff --git a/core/Sources/Components/Button/Enum/Public/ButtonShape.swift b/core/Sources/Components/Button/Enum/Public/ButtonShape.swift deleted file mode 100644 index d46d35907..000000000 --- a/core/Sources/Components/Button/Enum/Public/ButtonShape.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ButtonShape.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 08.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Buttons can have different shapes. -public enum ButtonShape: CaseIterable { - /// Button with pill-like shape. - case pill - - /// Button with rounded corners. - case rounded - - /// Square button with no rounded corners. - case square -} diff --git a/core/Sources/Components/Button/Enum/Public/ButtonSize.swift b/core/Sources/Components/Button/Enum/Public/ButtonSize.swift deleted file mode 100644 index 865344589..000000000 --- a/core/Sources/Components/Button/Enum/Public/ButtonSize.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ButtonSize.swift -// Spark -// -// Created by janniklas.freundt.ext on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Buttons come in different heights. -public enum ButtonSize: CaseIterable { - /// A small button with a base height of 32 points. - case small - - /// A medium button with a base height of 44 points. - case medium - - /// A large button with a base height of 56 points. - case large -} diff --git a/core/Sources/Components/Button/Enum/Public/ButtonVariant.swift b/core/Sources/Components/Button/Enum/Public/ButtonVariant.swift deleted file mode 100644 index 4cfbfc327..000000000 --- a/core/Sources/Components/Button/Enum/Public/ButtonVariant.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ButtonVariant.swift -// Spark -// -// Created by janniklas.freundt.ext on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// A button variant is used to distinguish between different design and appearance options. -public enum ButtonVariant: CaseIterable { - /// A contrast button with a solid background for better readability. - case contrast - - /// A filled button with a solid background. - case filled - - /// A ghost button with no background at all. - case ghost - - /// A transparent button with an outline-border. - case outlined - - /// A tinted button with a solid background. - case tinted -} diff --git a/core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder+ExtensionTests.swift b/core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder+ExtensionTests.swift deleted file mode 100644 index 6d39541b2..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ButtonBorder.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension ButtonBorder { - - // MARK: - Properties - - static func mocked( - width: CGFloat = 2, - radius: CGFloat = 8 - ) -> Self { - return .init( - width: width, - radius: radius - ) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder.swift b/core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder.swift deleted file mode 100644 index d821486f9..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Border/ButtonBorder.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ButtonBorder.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonBorder: Equatable { - - // MARK: - Properties - - let width: CGFloat - let radius: CGFloat -} diff --git a/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors+ExtensionTests.swift b/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors+ExtensionTests.swift deleted file mode 100644 index 35bbae3a6..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors+ExtensionTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ButtonColors.swift -// Spark -// -// Created by janniklas.freundt.ext on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ButtonColors { - - // MARK: - Properties - - static func mocked( - foregroundColor: any ColorToken = ColorTokenGeneratedMock.random(), - backgroundColor: any ColorToken = ColorTokenGeneratedMock.random(), - pressedBackgroundColor: any ColorToken = ColorTokenGeneratedMock.random(), - borderColor: any ColorToken = ColorTokenGeneratedMock.random(), - pressedBorderColor: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - foregroundColor: foregroundColor, - backgroundColor: backgroundColor, - pressedBackgroundColor: pressedBackgroundColor, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors.swift b/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors.swift deleted file mode 100644 index 6b29c52cc..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColors.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ButtonColors.swift -// Spark -// -// Created by janniklas.freundt.ext on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// All Button Colors from a theme, variant and intents -struct ButtonColors { - - // MARK: - Properties - - let foregroundColor: any ColorToken - let backgroundColor: any ColorToken - let pressedBackgroundColor: any ColorToken - let borderColor: any ColorToken - let pressedBorderColor: any ColorToken -} - -// MARK: Hashable & Equatable - -extension ButtonColors: Hashable, Equatable { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.foregroundColor) - hasher.combine(self.backgroundColor) - hasher.combine(self.pressedBackgroundColor) - hasher.combine(self.borderColor) - hasher.combine(self.pressedBorderColor) - } - - static func == (lhs: ButtonColors, rhs: ButtonColors) -> Bool { - return lhs.backgroundColor.equals(rhs.backgroundColor) && - lhs.borderColor.equals(rhs.borderColor) && - lhs.foregroundColor.equals(rhs.foregroundColor) && - lhs.pressedBorderColor.equals(rhs.pressedBorderColor) && - lhs.pressedBackgroundColor.equals(rhs.pressedBackgroundColor) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColorsTests.swift b/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColorsTests.swift deleted file mode 100644 index 7c1964b62..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Colors/ButtonColorsTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// ButtonColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ButtonColorsTests: XCTestCase { - - // MARK: - Tests - - func test_buttonColors_equal() { - let colors = SparkTheme.shared.colors - let given1 = ButtonColors( - foregroundColor: colors.main.main, - backgroundColor: colors.base.background, - pressedBackgroundColor: colors.base.backgroundVariant, - borderColor: colors.base.background, - pressedBorderColor: colors.states.mainPressed) - let given2 = ButtonColors( - foregroundColor: colors.main.main, - backgroundColor: colors.base.background, - pressedBackgroundColor: colors.base.backgroundVariant, - borderColor: colors.base.background, - pressedBorderColor: colors.states.mainPressed) - - XCTAssertEqual(given1, given2) - } - - func test_buttonColors_not_equal() { - let colors = SparkTheme.shared.colors - let given1 = ButtonColors( - foregroundColor: colors.main.main, - backgroundColor: colors.base.background, - pressedBackgroundColor: colors.base.backgroundVariant, - borderColor: colors.base.background, - pressedBorderColor: colors.states.mainPressed) - let given2 = ButtonColors( - foregroundColor: colors.main.main, - backgroundColor: colors.base.background, - pressedBackgroundColor: colors.base.backgroundVariant, - borderColor: colors.base.background, - pressedBorderColor: colors.states.alertPressed) - - XCTAssertNotEqual(given1, given2) - } - -} diff --git a/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors+ExtensionTests.swift b/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors+ExtensionTests.swift deleted file mode 100644 index cce5ec4c1..000000000 --- a/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors+ExtensionTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ButtonCurrentColors.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ButtonCurrentColors { - - // MARK: - Properties - - static func mocked( - imageTintColor: any ColorToken = ColorTokenGeneratedMock.random(), - titleColor: (any ColorToken)? = ColorTokenGeneratedMock.random(), - backgroundColor: any ColorToken = ColorTokenGeneratedMock.random(), - borderColor: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - imageTintColor: imageTintColor, - titleColor: titleColor, - backgroundColor: backgroundColor, - borderColor: borderColor - ) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors.swift b/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors.swift deleted file mode 100644 index 367ed1bbf..000000000 --- a/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColors.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ButtonCurrentColors.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// Current Button Colors properties from a button colors and state -struct ButtonCurrentColors { - - // MARK: - Properties - - let imageTintColor: any ColorToken - let titleColor: (any ColorToken)? - let backgroundColor: any ColorToken - let borderColor: any ColorToken -} - -// MARK: Hashable & Equatable - -extension ButtonCurrentColors: Hashable, Equatable { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.imageTintColor) - if let titleColor = self.titleColor { - hasher.combine(titleColor) - } - hasher.combine(self.backgroundColor) - hasher.combine(self.borderColor) - } - - static func == (lhs: ButtonCurrentColors, rhs: ButtonCurrentColors) -> Bool { - return lhs.imageTintColor.equals(rhs.imageTintColor) && - lhs.titleColor.equals(rhs.titleColor) == true && - lhs.backgroundColor.equals(rhs.backgroundColor) && - lhs.borderColor.equals(rhs.borderColor) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColorsTests.swift b/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColorsTests.swift deleted file mode 100644 index 08f1866d2..000000000 --- a/core/Sources/Components/Button/Properties/Internal/CurrentColors/ButtonCurrentColorsTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ButtonCurrentColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ButtonCurrentColorsTests: XCTestCase { - - // MARK: - Tests - - func test_buttonCurrentColors_equal() { - let colors = SparkTheme.shared.colors - - let given1 = ButtonCurrentColors( - imageTintColor: colors.base.onSurface, - titleColor: colors.base.onSurfaceInverse, - backgroundColor: colors.base.backgroundVariant, - borderColor: colors.main.main) - - let given2 = ButtonCurrentColors( - imageTintColor: colors.base.onSurface, - titleColor: colors.base.onSurfaceInverse, - backgroundColor: colors.base.backgroundVariant, - borderColor: colors.main.main) - - XCTAssertEqual(given1, given2) - } - - func test_buttonColorColors_not_equal() { - let colors = SparkTheme.shared.colors - - let given1 = ButtonCurrentColors( - imageTintColor: colors.base.surface, - titleColor: colors.base.surfaceInverse, - backgroundColor: colors.base.backgroundVariant, - borderColor: colors.main.main) - - let given2 = ButtonCurrentColors( - imageTintColor: colors.base.onSurface, - titleColor: colors.base.onSurfaceInverse, - backgroundColor: colors.base.backgroundVariant, - borderColor: colors.main.onMain) - - XCTAssertNotEqual(given1, given2) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes+ExtensionTests.swift b/core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes+ExtensionTests.swift deleted file mode 100644 index e47a32ae0..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ButtonSizes.swift -// SparkCore -// -// Created by robin.lemaire on 30/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension ButtonSizes { - - // MARK: - Properties - - static func mocked( - height: CGFloat = 30, - imageSize: CGFloat = 20 - ) -> Self { - return .init( - height: height, - imageSize: imageSize - ) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes.swift b/core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes.swift deleted file mode 100644 index 58cc87d3d..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Sizes/ButtonSizes.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ButtonSizes.swift -// SparkCore -// -// Created by robin.lemaire on 30/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonSizes: Equatable { - - // MARK: - Properties - - let height: CGFloat - let imageSize: CGFloat -} diff --git a/core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings+ExtensionTests.swift b/core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings+ExtensionTests.swift deleted file mode 100644 index 4ee5e3da7..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ButtonSpacings.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension ButtonSpacings { - - // MARK: - Properties - - static func mocked( - horizontalSpacing: CGFloat = 11, - horizontalPadding: CGFloat = 12 - ) -> Self { - return .init( - horizontalSpacing: horizontalSpacing, - horizontalPadding: horizontalPadding - ) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings.swift b/core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings.swift deleted file mode 100644 index 804f5dfbe..000000000 --- a/core/Sources/Components/Button/Properties/Internal/Spacings/ButtonSpacings.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ButtonSpacings.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonSpacings: Equatable { - - // MARK: - Properties - - let horizontalSpacing: CGFloat - let horizontalPadding: CGFloat -} diff --git a/core/Sources/Components/Button/Properties/Internal/State/ButtonState+ExtensionTests.swift b/core/Sources/Components/Button/Properties/Internal/State/ButtonState+ExtensionTests.swift deleted file mode 100644 index 1c38c7817..000000000 --- a/core/Sources/Components/Button/Properties/Internal/State/ButtonState+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ButtonState.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension ButtonState { - - // MARK: - Properties - - static func mocked( - isUserInteractionEnabled: Bool = true, - opacity: CGFloat = 1.0 - ) -> Self { - return .init( - isUserInteractionEnabled: isUserInteractionEnabled, - opacity: opacity - ) - } -} diff --git a/core/Sources/Components/Button/Properties/Internal/State/ButtonState.swift b/core/Sources/Components/Button/Properties/Internal/State/ButtonState.swift deleted file mode 100644 index 7289489c2..000000000 --- a/core/Sources/Components/Button/Properties/Internal/State/ButtonState.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ButtonState.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonState: Equatable { - - // MARK: - Properties - - let isUserInteractionEnabled: Bool - let opacity: Double -} diff --git a/core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCase.swift b/core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCase.swift deleted file mode 100644 index c176a01cc..000000000 --- a/core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCase.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ButtonGetBorderUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetBorderUseCaseable { - // sourcery: border = "Identical" - func execute(shape: ButtonShape, - border: Border, - variant: ButtonVariant) -> ButtonBorder -} - -struct ButtonGetBorderUseCase: ButtonGetBorderUseCaseable { - - // MARK: - Methods - - func execute( - shape: ButtonShape, - border: Border, - variant: ButtonVariant - ) -> ButtonBorder { - let radius: CGFloat - switch shape { - case .square: - radius = 0 - case .rounded: - radius = border.radius.large - case .pill: - radius = border.radius.full - } - - let width = (variant == .outlined) ? border.width.small : 0 - - return .init( - width: width, - radius: radius - ) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCaseTests.swift deleted file mode 100644 index a2c8ba87f..000000000 --- a/core/Sources/Components/Button/UseCase/GetBorder/ButtonGetBorderUseCaseTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// ButtonGetBorderUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetBorderUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let borderMock = BorderGeneratedMock.mocked() - - // MARK: - Tests Radius from all shapes cases - - func test_execute_radius_when_shape_is_square_case() { - self.testExecute( - givenShape: .square, - expectedRadius: 0 - ) - } - - func test_execute_radius_when_shape_is_rounded_case() { - self.testExecute( - givenShape: .rounded, - expectedRadius: self.borderMock.radius.large - ) - } - - func test_execute_radius_when_shape_is_pill_case() { - self.testExecute( - givenShape: .pill, - expectedRadius: self.borderMock.radius.full - ) - } - - // MARK: - Tests Width drom all variants cases - - func test_execute_width_when_variant_is_contrast_case() { - self.testExecute( - givenVariant: .contrast, - expectedWidth: 0 - ) - } - - func test_execute_width_when_variant_is_filled_case() { - self.testExecute( - givenVariant: .filled, - expectedWidth: 0 - ) - } - - func test_execute_width_when_variant_is_ghost_case() { - self.testExecute( - givenVariant: .ghost, - expectedWidth: 0 - ) - } - - func test_execute_width_when_variant_is_outlined_case() { - self.testExecute( - givenVariant: .outlined, - expectedWidth: self.borderMock.width.small - ) - } - - func test_execute_width_when_variant_is_tinted_case() { - self.testExecute( - givenVariant: .tinted, - expectedWidth: 0 - ) - } -} - -// MARK: - Execute Testing - -private extension ButtonGetBorderUseCaseTests { - - func testExecute( - givenShape: ButtonShape, - expectedRadius: CGFloat - ) { - // GIVEN - let errorSuffixMessage = " for .\(givenShape) shape case" - - let useCase = ButtonGetBorderUseCase() - - // WHEN - let border = useCase.execute( - shape: givenShape, - border: self.borderMock, - variant: .filled - ) - - // THEN - XCTAssertEqual(border.radius, - expectedRadius, - "Wrong radius" + errorSuffixMessage) - } - - func testExecute( - givenVariant: ButtonVariant, - expectedWidth: CGFloat - ) { - // GIVEN - let errorSuffixMessage = " for .\(givenVariant) variant case" - - let useCase = ButtonGetBorderUseCase() - - // WHEN - let border = useCase.execute( - shape: .pill, - border: self.borderMock, - variant: givenVariant - ) - - // THEN - XCTAssertEqual(border.width, - expectedWidth, - "Wrong width" + errorSuffixMessage) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCase.swift b/core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCase.swift deleted file mode 100644 index 46f262ab7..000000000 --- a/core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCase.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ButtonColorsUseCase.swift -// Spark -// -// Created by janniklas.freundt.ext on 19.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetColorsUseCaseable { - // sourcery: theme = "Identical" - func execute(theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant) -> ButtonColors -} - -struct ButtonGetColorsUseCase: ButtonGetColorsUseCaseable { - - // MARK: - Private properties - - private let getContrastUseCase: ButtonGetVariantUseCaseable - private let getFilledUseCase: ButtonGetVariantUseCaseable - private let getGhostUseCase: ButtonGetVariantUseCaseable - private let getOutlinedUseCase: ButtonGetVariantUseCaseable - private let getTintedUseCase: ButtonGetVariantUseCaseable - - // MARK: - Initialization - - init( - getContrastUseCase: ButtonGetVariantUseCaseable = ButtonVariantGetContrastUseCase(), - getFilledUseCase: ButtonGetVariantUseCaseable = ButtonGetVariantFilledUseCase(), - getGhostUseCase: ButtonGetVariantUseCaseable = ButtonGetVariantGhostUseCase(), - getOutlinedUseCase: ButtonGetVariantUseCaseable = ButtonGetVariantOutlinedUseCase(), - getTintedUseCase: ButtonGetVariantUseCaseable = ButtonGetVariantTintedUseCase() - ) { - self.getContrastUseCase = getContrastUseCase - self.getFilledUseCase = getFilledUseCase - self.getGhostUseCase = getGhostUseCase - self.getOutlinedUseCase = getOutlinedUseCase - self.getTintedUseCase = getTintedUseCase - } - - // MARK: - Methods - func execute( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant - ) -> ButtonColors { - let colors = theme.colors - let dims = theme.dims - - let useCase: ButtonGetVariantUseCaseable - switch variant { - case .contrast: - useCase = self.getContrastUseCase - case .filled: - useCase = self.getFilledUseCase - case .ghost: - useCase = self.getGhostUseCase - case .outlined: - useCase = self.getOutlinedUseCase - case .tinted: - useCase = self.getTintedUseCase - } - - return useCase.execute( - intent: intent, - colors: colors, - dims: dims - ) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCaseTests.swift deleted file mode 100644 index 2764d066a..000000000 --- a/core/Sources/Components/Button/UseCase/GetColors/ButtonGetColorsUseCaseTests.swift +++ /dev/null @@ -1,200 +0,0 @@ -// -// ButtonGetColorsUseCaseTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 24.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetColorsUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_for_all_variant_cases() throws { - // GIVEN - let variants: [ButtonVariant] = [.filled, .outlined, .contrast, .tinted, .ghost] - let items = variants.map { - let intentsMock = ButtonColors.mocked() - - return GetColors( - givenIntent: .main, - givenVariant: $0, - givenColors: intentsMock, - expectedforegroundColorToken: intentsMock.foregroundColor, - expectedBackgroundToken: intentsMock.backgroundColor, - expectedPressedBackgroundToken: intentsMock.pressedBackgroundColor, - expectedBorderToken: intentsMock.borderColor, - expectedPressedBorderToken: intentsMock.pressedBorderColor - ) - } - - for item in items { - let themeColorsMock = ColorsGeneratedMock() - themeColorsMock.main = ColorsMainGeneratedMock.mocked() - themeColorsMock.states = ColorsStatesGeneratedMock.mocked() - - let dimsMock = DimsGeneratedMock.mocked() - let themeMock = ThemeGeneratedMock() - themeMock.underlyingColors = themeColorsMock - themeMock.underlyingDims = dimsMock - - let getContrastUseCaseMock = ButtonGetVariantUseCaseableGeneratedMock() - let getFilledUseCaseMock = ButtonGetVariantUseCaseableGeneratedMock() - let getGhostUseCaseMock = ButtonGetVariantUseCaseableGeneratedMock() - let getOutlinedUseCaseMock = ButtonGetVariantUseCaseableGeneratedMock() - let getTintedUseCaseMock = ButtonGetVariantUseCaseableGeneratedMock() - - let mockedUseCase: ButtonGetVariantUseCaseableGeneratedMock - switch item.givenVariant { - case .contrast: - mockedUseCase = getContrastUseCaseMock - case .filled: - mockedUseCase = getFilledUseCaseMock - case .ghost: - mockedUseCase = getGhostUseCaseMock - case .outlined: - mockedUseCase = getOutlinedUseCaseMock - case .tinted: - mockedUseCase = getTintedUseCaseMock - } - mockedUseCase.executeWithIntentAndColorsAndDimsReturnValue = item.givenColors - - let getIntentsUseCaseMock = ButtonGetColorsUseCase( - getContrastUseCase: getContrastUseCaseMock, - getFilledUseCase: getFilledUseCaseMock, - getGhostUseCase: getGhostUseCaseMock, - getOutlinedUseCase: getOutlinedUseCaseMock, - getTintedUseCase: getTintedUseCaseMock - ) - - // WHEN - let colors = getIntentsUseCaseMock.execute( - theme: themeMock, - intent: item.givenIntent, - variant: item.givenVariant - ) - - // Other UseCase - Tester.testColorsUseCaseExecuteCalling( - givenColorsUseCase: mockedUseCase, - givenIntent: item.givenIntent, - givenColors: themeColorsMock, - givenDims: dimsMock - ) - - // Colors Properties - try Tester.testColorsProperties(givenColors: colors, - getColors: item) - } - } -} - -// MARK: - Tester - -private struct Tester { - - static func testColorsUseCaseExecuteCalling( - givenColorsUseCase: ButtonGetVariantUseCaseableGeneratedMock, - givenIntent: ButtonIntent, - givenColors: ColorsGeneratedMock, - givenDims: DimsGeneratedMock - ) { - let arguments = givenColorsUseCase.executeWithIntentAndColorsAndDimsReceivedArguments - XCTAssertEqual(givenColorsUseCase.executeWithIntentAndColorsAndDimsCallsCount, - 1, - "Wrong call number on execute") - XCTAssertEqual(arguments?.intent, - givenIntent, - "Wrong intent parameter on execute") - XCTAssertIdentical(arguments?.colors as? ColorsGeneratedMock, - givenColors, - "Wrong colors parameter on execute") - XCTAssertIdentical(arguments?.dims as? DimsGeneratedMock, - givenDims, - "Wrong dims parameter on execute") - } - - static func testColorsProperties( - givenColors: ButtonColors, - getColors: GetColors - ) throws { - // Text Color - try self.testColor( - givenColorProperty: givenColors.foregroundColor, - givenPropertyName: "foregroundColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedforegroundColorToken - ) - - // Background Color - try self.testColor( - givenColorProperty: givenColors.backgroundColor, - givenPropertyName: "backgroundColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedBackgroundToken - ) - - // Pressed Background Color - try self.testColor( - givenColorProperty: givenColors.pressedBackgroundColor, - givenPropertyName: "pressedBackgroundColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedPressedBackgroundToken - ) - - // Border Color - try self.testColor( - givenColorProperty: givenColors.borderColor, - givenPropertyName: "borderColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedBorderToken - ) - - // Pressed Border Color - try self.testColor( - givenColorProperty: givenColors.pressedBorderColor, - givenPropertyName: "pressedBorderColor", - givenIntent: getColors.givenIntent, - expectedColorToken: getColors.expectedPressedBorderToken - ) - } - - private static func testColor( - givenColorProperty: (any ColorToken)?, - givenPropertyName: String, - givenIntent: ButtonIntent, - expectedColorToken: (any ColorToken)? - ) throws { - let errorSuffixMessage = " \(givenPropertyName) for .\(givenIntent) case" - - if let givenColorProperty { - let color = try XCTUnwrap(givenColorProperty as? ColorTokenGeneratedMock, - "Wrong" + errorSuffixMessage) - XCTAssertIdentical(color, - expectedColorToken as? ColorTokenGeneratedMock, - "Wrong value" + errorSuffixMessage) - - } else { - XCTAssertNil(givenColorProperty, - "Should be nil" + errorSuffixMessage) - } - } -} - -// MARK: - Others Strucs - -private struct GetColors { - let givenIntent: ButtonIntent - let givenVariant: ButtonVariant - let givenColors: ButtonColors - - let expectedforegroundColorToken: any ColorToken - let expectedBackgroundToken: any ColorToken - let expectedPressedBackgroundToken: any ColorToken - let expectedBorderToken: any ColorToken - let expectedPressedBorderToken: any ColorToken -} diff --git a/core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCase.swift b/core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCase.swift deleted file mode 100644 index b06fa8dd7..000000000 --- a/core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCase.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ButtonGetCurrentColorsUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetCurrentColorsUseCaseable { - func execute(colors: ButtonColors, - isPressed: Bool) -> ButtonCurrentColors -} - -struct ButtonGetCurrentColorsUseCase: ButtonGetCurrentColorsUseCaseable { - - // MARK: - Methods - - func execute( - colors: ButtonColors, - isPressed: Bool - ) -> ButtonCurrentColors { - if isPressed { - return .init( - imageTintColor: colors.foregroundColor, - titleColor: colors.foregroundColor, - backgroundColor: colors.pressedBackgroundColor, - borderColor: colors.pressedBorderColor - ) - } else { - return .init( - imageTintColor: colors.foregroundColor, - titleColor: colors.foregroundColor, - backgroundColor: colors.backgroundColor, - borderColor: colors.borderColor - ) - } - } -} diff --git a/core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCaseTests.swift deleted file mode 100644 index f71939034..000000000 --- a/core/Sources/Components/Button/UseCase/GetCurrentColors/ButtonGetCurrentColorsUseCaseTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// ButtonGetCurrentColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetCurrentColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let foregroundColorMock = ColorTokenGeneratedMock() - private let backgroundColorMock = ColorTokenGeneratedMock() - private let pressedBackgroundColorMock = ColorTokenGeneratedMock() - private let borderColorMock = ColorTokenGeneratedMock() - private let pressedBorderColorMock = ColorTokenGeneratedMock() - - private lazy var colorsMock: ButtonColors = { - return .init( - foregroundColor: self.foregroundColorMock, - backgroundColor: self.backgroundColorMock, - pressedBackgroundColor: self.pressedBackgroundColorMock, - borderColor: self.borderColorMock, - pressedBorderColor: self.pressedBorderColorMock - ) - }() - - // MARK: - IsPressed Tests - - func test_execute_when_isPressed_is_true() throws { - try self.testExecute( - givenIsPressed: true, - expectedImageTintColor: self.foregroundColorMock, - expectedTitleColor: self.foregroundColorMock, - expectedBackgroundColor: self.pressedBackgroundColorMock, - expectedBorderColor: self.pressedBorderColorMock - ) - } - - func test_execute_when_isPressed_is_false() throws { - try self.testExecute( - givenIsPressed: false, - expectedImageTintColor: self.foregroundColorMock, - expectedTitleColor: self.foregroundColorMock, - expectedBackgroundColor: self.backgroundColorMock, - expectedBorderColor: self.borderColorMock - ) - } -} - -// MARK: - Execute Testing - -private extension ButtonGetCurrentColorsUseCaseTests { - - func testExecute( - givenIsPressed: Bool, - expectedImageTintColor: ColorTokenGeneratedMock, - expectedTitleColor: ColorTokenGeneratedMock, - expectedBackgroundColor: ColorTokenGeneratedMock, - expectedBorderColor: ColorTokenGeneratedMock - ) throws { - // GIVEN - let errorSuffixMessage = " for \(givenIsPressed) givenIsPressed" - - let useCase = ButtonGetCurrentColorsUseCase() - - // GIVEN - let currentColors = useCase.execute( - colors: self.colorsMock, - isPressed: givenIsPressed - ) - - // THEN - XCTAssertIdentical(currentColors.imageTintColor as? ColorTokenGeneratedMock, - expectedImageTintColor, - "Wrong imageTintColor" + errorSuffixMessage) - XCTAssertIdentical(currentColors.titleColor as? ColorTokenGeneratedMock, - expectedTitleColor, - "Wrong titleColor" + errorSuffixMessage) - XCTAssertIdentical(currentColors.backgroundColor as? ColorTokenGeneratedMock, - expectedBackgroundColor, - "Wrong foregroundColor" + errorSuffixMessage) - XCTAssertIdentical(currentColors.borderColor as? ColorTokenGeneratedMock, - expectedBorderColor, - "Wrong foregroundColor" + errorSuffixMessage) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCase.swift b/core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCase.swift deleted file mode 100644 index 84f251b31..000000000 --- a/core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCase.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// ButtonGetSizesUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetSizesUseCaseable { - func execute(size: ButtonSize, - type: ButtonType) -> ButtonSizes -} - -struct ButtonGetSizesUseCase: ButtonGetSizesUseCaseable { - - // MARK: - Constants - - private enum Constants { - enum Height { - static var small: CGFloat = 32 - static var medium: CGFloat = 44 - static var large: CGFloat = 56 - } - - enum ImageSize { - static var medium: CGFloat = 16 - static var large: CGFloat = 24 - } - } - - // MARK: - Methods - - func execute( - size: ButtonSize, - type: ButtonType - ) -> ButtonSizes { - let height: CGFloat - switch size { - case .small: - height = Constants.Height.small - case .medium: - height = Constants.Height.medium - case .large: - height = Constants.Height.large - } - - // The value is differente only when there is only an image and the size is large - let imageSize: CGFloat = (type == .iconButton && size == .large) ? Constants.ImageSize.large : Constants.ImageSize.medium - - return .init(height: height, imageSize: imageSize) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCaseTests.swift deleted file mode 100644 index 819bc9b88..000000000 --- a/core/Sources/Components/Button/UseCase/GetSizes/ButtonGetSizesUseCaseTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// ButtonGetSizesUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetSizesUseCaseTests: XCTestCase { - - // MARK: - Test Type is .iconButton - - func test_execute_when_switchSize_is_small_case_and_type_is_iconButton() { - // GIVEN - let useCase = ButtonGetSizesUseCase() - - // WHEN - let sizes = useCase.execute( - size: .small, - type: .iconButton - ) - - // THEN - XCTAssertEqual(sizes.height, 32, "Wrong height value") - XCTAssertEqual(sizes.imageSize, 16, "Wrong imageSize value") - } - - func test_execute_when_switchSize_is_medium_case_and_type_is_iconButton() { - // GIVEN - let useCase = ButtonGetSizesUseCase() - - // WHEN - let sizes = useCase.execute( - size: .medium, - type: .iconButton - ) - - // THEN - XCTAssertEqual(sizes.height, 44, "Wrong height value") - XCTAssertEqual(sizes.imageSize, 16, "Wrong imageSize value") - } - - func test_execute_when_switchSize_is_large_case_and_type_is_iconButton() { - // GIVEN - let useCase = ButtonGetSizesUseCase() - - // WHEN - let sizes = useCase.execute( - size: .large, - type: .iconButton - ) - - // THEN - XCTAssertEqual(sizes.height, 56, "Wrong height value") - XCTAssertEqual(sizes.imageSize, 24, "Wrong imageSize value") - } - - // MARK: - Test Type is .button - - func test_execute_when_switchSize_is_small_case_and_type_is_button() { - // GIVEN - let useCase = ButtonGetSizesUseCase() - - // WHEN - let sizes = useCase.execute( - size: .small, - type: .button - ) - - // THEN - XCTAssertEqual(sizes.height, 32, "Wrong height value") - XCTAssertEqual(sizes.imageSize, 16, "Wrong imageSize value") - } - - func test_execute_when_switchSize_is_medium_case_and_type_is_button() { - // GIVEN - let useCase = ButtonGetSizesUseCase() - - // WHEN - let sizes = useCase.execute( - size: .medium, - type: .button - ) - - // THEN - XCTAssertEqual(sizes.height, 44, "Wrong height value") - XCTAssertEqual(sizes.imageSize, 16, "Wrong imageSize value") - } - - func test_execute_when_switchSize_is_large_case_and_type_is_button() { - // GIVEN - let useCase = ButtonGetSizesUseCase() - - // WHEN - let sizes = useCase.execute( - size: .large, - type: .button - ) - - // THEN - XCTAssertEqual(sizes.height, 56, "Wrong height value") - XCTAssertEqual(sizes.imageSize, 16, "Wrong imageSize value") - } -} diff --git a/core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCase.swift b/core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCase.swift deleted file mode 100644 index a0163a5c0..000000000 --- a/core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCase.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ButtonGetSpacingsUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetSpacingsUseCaseable { - // sourcery: spacing = "Identical" - func execute(spacing: LayoutSpacing) -> ButtonSpacings -} - -struct ButtonGetSpacingsUseCase: ButtonGetSpacingsUseCaseable { - - // MARK: - Methods - - func execute(spacing: LayoutSpacing) -> ButtonSpacings { - return .init( - horizontalSpacing: spacing.large, - horizontalPadding: spacing.medium - ) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCaseTests.swift deleted file mode 100644 index 8d5b67fcf..000000000 --- a/core/Sources/Components/Button/UseCase/GetSpacings/ButtonGetSpacingsUseCaseTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ButtonGetSpacingsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 23/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetSpacingsUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute() { - // GIVEN - let spacingMock = LayoutSpacingGeneratedMock.mocked() - - let useCase = ButtonGetSpacingsUseCase() - - // WHEN - let spacings = useCase.execute( - spacing: spacingMock - ) - - // THEN - XCTAssertEqual(spacings.horizontalSpacing, - spacingMock.large, - "Wrong horizontalSpacing value") - XCTAssertEqual(spacings.horizontalPadding, - spacingMock.medium, - "Wrong horizontalPadding value") - } -} diff --git a/core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCase.swift b/core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCase.swift deleted file mode 100644 index aeff7fcf2..000000000 --- a/core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCase.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ButtonGetStateUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetStateUseCaseable { - // sourcery: dims = "Identical" - func execute(isEnabled: Bool, - dims: Dims) -> ButtonState -} - -struct ButtonGetStateUseCase: ButtonGetStateUseCaseable { - - // MARK: - Methods - - func execute( - isEnabled: Bool, - dims: Dims - ) -> ButtonState { - let opacity = isEnabled ? dims.none : dims.dim3 - - return .init( - isUserInteractionEnabled: isEnabled, - opacity: opacity - ) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCaseTests.swift deleted file mode 100644 index 79f3e86fe..000000000 --- a/core/Sources/Components/Button/UseCase/GetState/ButtonGetStateUseCaseTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// ButtonGetStateUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 27/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetStateUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let dimsMock = DimsGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_when_isEnabled_is_true() { - self.testExecute( - givenIsEnabled: true, - expectedIsInteractionState: .init(isUserInteractionEnabled: true, opacity: self.dimsMock.none) - ) - } - - func test_execute_when_isEnabled_is_false() { - self.testExecute( - givenIsEnabled: false, - expectedIsInteractionState: .init(isUserInteractionEnabled: false, opacity: self.dimsMock.dim3) - ) - } -} - -// MARK: - Execute Testing - -private extension ButtonGetStateUseCaseTests { - - func testExecute( - givenIsEnabled: Bool, - expectedIsInteractionState: ButtonState - ) { - // GIVEN - let errorSuffixMessage = " for \(givenIsEnabled) givenIsEnabled" - - let useCase = ButtonGetStateUseCase() - - // GIVEN - let interactionState = useCase.execute( - isEnabled: givenIsEnabled, - dims: self.dimsMock - ) - - // THEN - XCTAssertEqual(interactionState.isUserInteractionEnabled, - expectedIsInteractionState.isUserInteractionEnabled, - "Wrong isUserInteractionEnabled" + errorSuffixMessage) - XCTAssertEqual(interactionState.opacity, - expectedIsInteractionState.opacity, - "Wrong opacity" + errorSuffixMessage) - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCase.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCase.swift deleted file mode 100644 index e5a05e11a..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCase.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// ButtonVariantGetContrastUseCase.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonVariantGetContrastUseCase: ButtonGetVariantUseCaseable { - - // MARK: - Methods - - func execute( - intent: ButtonIntent, - colors: Colors, - dims: Dims - ) -> ButtonColors { - let borderColor = ColorTokenDefault.clear - let pressedBorderColor = ColorTokenDefault.clear - let backgroundColor = colors.base.surface - - switch intent { - case .accent: - return .init( - foregroundColor: colors.accent.accent, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.accentContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .basic: - return .init( - foregroundColor: colors.basic.basic, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.basicContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .main: - return .init( - foregroundColor: colors.main.main, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.mainContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .support: - return .init( - foregroundColor: colors.support.support, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.supportContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .neutral: - return .init( - foregroundColor: colors.feedback.neutral, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.neutralContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .alert: - return .init( - foregroundColor: colors.feedback.alert, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.alertContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .success: - return .init( - foregroundColor: colors.feedback.success, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.successContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .danger: - return .init( - foregroundColor: colors.feedback.error, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.errorContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .surface: - return .init( - foregroundColor: colors.base.onSurface, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.surfacePressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .info: - return .init( - foregroundColor: colors.feedback.info, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.states.infoContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - } - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCaseTests.swift deleted file mode 100644 index cfcc6eba5..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantContrastUseCaseTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ButtonVariantGetContrastUseCaseTests.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonVariantGetContrastUseCaseTests: ButtonVariantUseCaseTests { - - // MARK: - Tests - func test_main_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .main, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.main.main, - self.theme.colors.base.surface, - self.theme.colors.states.mainContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_support_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .support, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.support.support, - self.theme.colors.base.surface, - self.theme.colors.states.supportContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_neutral_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .neutral, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.neutral, - self.theme.colors.base.surface, - self.theme.colors.states.neutralContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_alert_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .alert, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.alert, - self.theme.colors.base.surface, - self.theme.colors.states.alertContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_success_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .success, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.success, - self.theme.colors.base.surface, - self.theme.colors.states.successContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_danger_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .danger, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.error, - self.theme.colors.base.surface, - self.theme.colors.states.errorContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_surface_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .surface, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.base.onSurface, - self.theme.colors.base.surface, - self.theme.colors.states.surfacePressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_info_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .info, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.info, - self.theme.colors.base.surface, - self.theme.colors.states.infoContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - // MARK: - Helper Functions - func sut() -> ButtonVariantGetContrastUseCase { - return ButtonVariantGetContrastUseCase() - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCase.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCase.swift deleted file mode 100644 index 67ff48fd5..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCase.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// ButtonGetVariantFilledUseCase.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonGetVariantFilledUseCase: ButtonGetVariantUseCaseable { - - // MARK: - Methods - - func execute( - intent: ButtonIntent, - colors: Colors, - dims: Dims - ) -> ButtonColors { - let borderColor = ColorTokenDefault.clear - let pressedBorderColor = ColorTokenDefault.clear - - switch intent { - case .accent: - return .init( - foregroundColor: colors.accent.onAccent, - backgroundColor: colors.accent.accent, - pressedBackgroundColor: colors.states.accentPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .basic: - return .init( - foregroundColor: colors.basic.onBasic, - backgroundColor: colors.basic.basic, - pressedBackgroundColor: colors.states.basicPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .main: - return .init( - foregroundColor: colors.main.onMain, - backgroundColor: colors.main.main, - pressedBackgroundColor: colors.states.mainPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .support: - return .init( - foregroundColor: colors.support.onSupport, - backgroundColor: colors.support.support, - pressedBackgroundColor: colors.states.supportPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .neutral: - return .init( - foregroundColor: colors.feedback.onNeutral, - backgroundColor: colors.feedback.neutral, - pressedBackgroundColor: colors.states.neutralPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .alert: - return .init( - foregroundColor: colors.feedback.onAlert, - backgroundColor: colors.feedback.alert, - pressedBackgroundColor: colors.states.alertPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .success: - return .init( - foregroundColor: colors.feedback.onSuccess, - backgroundColor: colors.feedback.success, - pressedBackgroundColor: colors.states.successPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .danger: - return .init( - foregroundColor: colors.feedback.onError, - backgroundColor: colors.feedback.error, - pressedBackgroundColor: colors.states.errorPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .surface: - return .init( - foregroundColor: colors.base.onSurface, - backgroundColor: colors.base.surface, - pressedBackgroundColor: colors.states.surfacePressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .info: - return .init( - foregroundColor: colors.feedback.onInfo, - backgroundColor: colors.feedback.info, - pressedBackgroundColor: colors.states.infoPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - } - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCaseTests.swift deleted file mode 100644 index 51b1b5a68..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantFilledUseCaseTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ButtonGetVariantFilledUseCaseTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetVariantFilledUseCaseTests: ButtonVariantUseCaseTests { - - // MARK: - Tests - func test_main_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .main, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.main.onMain, - self.theme.colors.main.main, - self.theme.colors.states.mainPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_support_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .support, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.support.onSupport, - self.theme.colors.support.support, - self.theme.colors.states.supportPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_neutral_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .neutral, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onNeutral, - self.theme.colors.feedback.neutral, - self.theme.colors.states.neutralPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_alert_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .alert, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onAlert, - self.theme.colors.feedback.alert, - self.theme.colors.states.alertPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_success_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .success, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onSuccess, - self.theme.colors.feedback.success, - self.theme.colors.states.successPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_danger_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .danger, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onError, - self.theme.colors.feedback.error, - self.theme.colors.states.errorPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_surface_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .surface, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.base.onSurface, - self.theme.colors.base.surface, - self.theme.colors.states.surfacePressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_info_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .info, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onInfo, - self.theme.colors.feedback.info, - self.theme.colors.states.infoPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - // MARK: - Helper Functions - func sut() -> ButtonGetVariantFilledUseCase { - return ButtonGetVariantFilledUseCase() - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCase.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCase.swift deleted file mode 100644 index f7c24cc11..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCase.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// ButtonGetVariantGhostUseCase.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonGetVariantGhostUseCase: ButtonGetVariantUseCaseable { - - // MARK: - Methods - - func execute( - intent: ButtonIntent, - colors: Colors, - dims: Dims - ) -> ButtonColors { - let borderColor = ColorTokenDefault.clear - let pressedBorderColor = ColorTokenDefault.clear - - let dim5 = dims.dim5 - - switch intent { - case .accent: - return .init( - foregroundColor: colors.accent.accent, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.accent.accent.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .basic: - return .init( - foregroundColor: colors.basic.basic, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.basic.basic.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .main: - return .init( - foregroundColor: colors.main.main, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.main.main.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .support: - return .init( - foregroundColor: colors.support.support, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.support.support.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .neutral: - return .init( - foregroundColor: colors.feedback.neutral, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.feedback.neutral.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .alert: - return .init( - foregroundColor: colors.feedback.alert, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.feedback.alert.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .success: - return .init( - foregroundColor: colors.feedback.success, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.feedback.success.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .danger: - return .init( - foregroundColor: colors.feedback.error, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.feedback.error.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .surface: - return .init( - foregroundColor: colors.base.surface, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.base.surface.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .info: - return .init( - foregroundColor: colors.feedback.info, - backgroundColor: ColorTokenDefault.clear, - pressedBackgroundColor: colors.feedback.info.opacity(dim5), - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - } - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCaseTests.swift deleted file mode 100644 index 26ed12f2c..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantGhostUseCaseTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ButtonGetVariantGhostUseCaseTests.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetVariantGhostUseCaseTests: ButtonVariantUseCaseTests { - - // MARK: - Tests - func test_main_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .main, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.main.main, - ColorTokenDefault.clear, - self.theme.colors.main.main.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_support_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .support, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.support.support, - ColorTokenDefault.clear, - self.theme.colors.support.support.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_neutral_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .neutral, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.neutral, - ColorTokenDefault.clear, - self.theme.colors.feedback.neutral.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_alert_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .alert, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.alert, - ColorTokenDefault.clear, - self.theme.colors.feedback.alert.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_success_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .success, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.success, - ColorTokenDefault.clear, - self.theme.colors.feedback.success.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_danger_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .danger, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.error, - ColorTokenDefault.clear, - self.theme.colors.feedback.error.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_surface_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .surface, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.base.surface, - ColorTokenDefault.clear, - self.theme.colors.base.surface.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_info_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .info, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.info, - ColorTokenDefault.clear, - self.theme.colors.feedback.info.opacity(self.theme.dims.dim5), - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - // MARK: - Helper Functions - func sut() -> ButtonGetVariantGhostUseCase { - return ButtonGetVariantGhostUseCase() - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCase.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCase.swift deleted file mode 100644 index fd71db293..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCase.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// ButtonGetVariantOutlinedUseCase.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonGetVariantOutlinedUseCase: ButtonGetVariantUseCaseable { - - // MARK: - Methods - - func execute( - intent: ButtonIntent, - colors: Colors, - dims: Dims - ) -> ButtonColors { - let dim5 = dims.dim5 - - let backgroundColor = ColorTokenDefault.clear - - switch intent { - case .accent: - return .init( - foregroundColor: colors.accent.accent, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.accent.accent.opacity(dim5), - borderColor: colors.accent.accent, - pressedBorderColor: colors.accent.accent - ) - case .basic: - return .init( - foregroundColor: colors.basic.basic, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.basic.basic.opacity(dim5), - borderColor: colors.basic.basic, - pressedBorderColor: colors.basic.basic - ) - case .main: - return .init( - foregroundColor: colors.main.main, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.main.main.opacity(dim5), - borderColor: colors.main.main, - pressedBorderColor: colors.main.main - ) - case .support: - return .init( - foregroundColor: colors.support.support, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.support.support.opacity(dim5), - borderColor: colors.support.support, - pressedBorderColor: colors.support.support - ) - case .neutral: - return .init( - foregroundColor: colors.feedback.neutral, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.feedback.neutral.opacity(dim5), - borderColor: colors.feedback.neutral, - pressedBorderColor: colors.feedback.neutral - ) - case .alert: - return .init( - foregroundColor: colors.feedback.alert, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.feedback.alert.opacity(dim5), - borderColor: colors.feedback.alert, - pressedBorderColor: colors.feedback.alert - ) - case .success: - return .init( - foregroundColor: colors.feedback.success, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.feedback.success.opacity(dim5), - borderColor: colors.feedback.success, - pressedBorderColor: colors.feedback.success - ) - case .danger: - return .init( - foregroundColor: colors.feedback.error, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.feedback.error.opacity(dim5), - borderColor: colors.feedback.error, - pressedBorderColor: colors.feedback.error - ) - case .surface: - return .init( - foregroundColor: colors.base.surface, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.base.surface.opacity(dim5), - borderColor: colors.base.surface, - pressedBorderColor: colors.base.surface - ) - case .info: - return .init( - foregroundColor: colors.feedback.info, - backgroundColor: backgroundColor, - pressedBackgroundColor: colors.feedback.info.opacity(dim5), - borderColor: colors.feedback.info, - pressedBorderColor: colors.feedback.info - ) - } - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCaseTests.swift deleted file mode 100644 index 9a00c7410..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantOutlinedUseCaseTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ButtonGetVariantOutlinedUseCaseTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetVariantOutlinedUseCaseTests: ButtonVariantUseCaseTests { - - // MARK: - Tests - func test_main_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .main, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.main.main, - ColorTokenDefault.clear, - self.theme.colors.main.main.opacity(self.theme.dims.dim5), - self.theme.colors.main.main, - self.theme.colors.main.main - ].map(\.color)) - } - - func test_support_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .support, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.support.support, - ColorTokenDefault.clear, - self.theme.colors.support.support.opacity(self.theme.dims.dim5), - self.theme.colors.support.support, - self.theme.colors.support.support - ].map(\.color)) - } - - func test_neutral_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .neutral, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.neutral, - ColorTokenDefault.clear, - self.theme.colors.feedback.neutral.opacity(self.theme.dims.dim5), - self.theme.colors.feedback.neutral, - self.theme.colors.feedback.neutral - ].map(\.color)) - } - - func test_alert_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .alert, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.alert, - ColorTokenDefault.clear, - self.theme.colors.feedback.alert.opacity(self.theme.dims.dim5), - self.theme.colors.feedback.alert, - self.theme.colors.feedback.alert - ].map(\.color)) - } - - func test_success_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .success, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.success, - ColorTokenDefault.clear, - self.theme.colors.feedback.success.opacity(self.theme.dims.dim5), - self.theme.colors.feedback.success, - self.theme.colors.feedback.success - ].map(\.color)) - } - - func test_danger_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .danger, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.error, - ColorTokenDefault.clear, - self.theme.colors.feedback.error.opacity(self.theme.dims.dim5), - self.theme.colors.feedback.error, - self.theme.colors.feedback.error - ].map(\.color)) - } - - func test_surface_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .surface, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.base.surface, - ColorTokenDefault.clear, - self.theme.colors.base.surface.opacity(self.theme.dims.dim5), - self.theme.colors.base.surface, - self.theme.colors.base.surface - ].map(\.color)) - } - - func test_info_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .info, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.info, - ColorTokenDefault.clear, - self.theme.colors.feedback.info.opacity(self.theme.dims.dim5), - self.theme.colors.feedback.info, - self.theme.colors.feedback.info - ].map(\.color)) - } - - // MARK: - Helper Functions - func sut() -> ButtonGetVariantOutlinedUseCase { - return ButtonGetVariantOutlinedUseCase() - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCase.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCase.swift deleted file mode 100644 index 393873fa2..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCase.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// ButtonGetVariantTintedUseCase.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ButtonGetVariantTintedUseCase: ButtonGetVariantUseCaseable { - - // MARK: - Methods - - func execute( - intent: ButtonIntent, - colors: Colors, - dims: Dims - ) -> ButtonColors { - let borderColor = ColorTokenDefault.clear - let pressedBorderColor = ColorTokenDefault.clear - - switch intent { - case .accent: - return .init( - foregroundColor: colors.accent.onAccentContainer, - backgroundColor: colors.accent.accentContainer, - pressedBackgroundColor: colors.states.accentContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .basic: - return .init( - foregroundColor: colors.basic.onBasicContainer, - backgroundColor: colors.basic.basicContainer, - pressedBackgroundColor: colors.states.accentContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .main: - return .init( - foregroundColor: colors.main.onMainContainer, - backgroundColor: colors.main.mainContainer, - pressedBackgroundColor: colors.states.mainContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .support: - return .init( - foregroundColor: colors.support.onSupportContainer, - backgroundColor: colors.support.supportContainer, - pressedBackgroundColor: colors.states.supportContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .neutral: - return .init( - foregroundColor: colors.feedback.onNeutralContainer, - backgroundColor: colors.feedback.neutralContainer, - pressedBackgroundColor: colors.states.neutralContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .alert: - return .init( - foregroundColor: colors.feedback.onAlertContainer, - backgroundColor: colors.feedback.alertContainer, - pressedBackgroundColor: colors.states.alertContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .success: - return .init( - foregroundColor: colors.feedback.onSuccessContainer, - backgroundColor: colors.feedback.successContainer, - pressedBackgroundColor: colors.states.successContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .danger: - return .init( - foregroundColor: colors.feedback.onErrorContainer, - backgroundColor: colors.feedback.errorContainer, - pressedBackgroundColor: colors.states.errorContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .surface: - return .init( - foregroundColor: colors.base.onBackgroundVariant, - backgroundColor: colors.base.backgroundVariant, - pressedBackgroundColor: colors.states.surfacePressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - case .info: - return .init( - foregroundColor: colors.feedback.onInfoContainer, - backgroundColor: colors.feedback.infoContainer, - pressedBackgroundColor: colors.states.infoContainerPressed, - borderColor: borderColor, - pressedBorderColor: pressedBorderColor - ) - } - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCaseTests.swift deleted file mode 100644 index cc00b7919..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantTintedUseCaseTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ButtonGetVariantTintedUseCaseTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ButtonGetVariantTintedUseCaseTests: ButtonVariantUseCaseTests { - - // MARK: - Tests - func test_main_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .main, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.main.onMainContainer, - self.theme.colors.main.mainContainer, - self.theme.colors.states.mainContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_support_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .support, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.support.onSupportContainer, - self.theme.colors.support.supportContainer, - self.theme.colors.states.supportContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_neutral_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .neutral, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onNeutralContainer, - self.theme.colors.feedback.neutralContainer, - self.theme.colors.states.neutralContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_alert_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .alert, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onAlertContainer, - self.theme.colors.feedback.alertContainer, - self.theme.colors.states.alertContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_success_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .success, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onSuccessContainer, - self.theme.colors.feedback.successContainer, - self.theme.colors.states.successContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_danger_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .danger, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onErrorContainer, - self.theme.colors.feedback.errorContainer, - self.theme.colors.states.errorContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_surface_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .surface, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.base.onBackgroundVariant, - self.theme.colors.base.backgroundVariant, - self.theme.colors.states.surfacePressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - func test_info_colors() throws { - // Given - let sut = self.sut() - - // When - let colors = sut.execute(intent: .info, colors: self.theme.colors, dims: self.theme.dims) - - // Then - XCTAssertEqual( - [colors.foregroundColor, - colors.backgroundColor, - colors.pressedBackgroundColor, - colors.borderColor, - colors.pressedBorderColor].map(\.color), - [self.theme.colors.feedback.onInfoContainer, - self.theme.colors.feedback.infoContainer, - self.theme.colors.states.infoContainerPressed, - ColorTokenDefault.clear, - ColorTokenDefault.clear - ].map(\.color)) - } - - // MARK: - Helper Functions - func sut() -> ButtonGetVariantTintedUseCase { - return ButtonGetVariantTintedUseCase() - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseTests.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseTests.swift deleted file mode 100644 index 3a39ea52b..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ButtonVariantUseCaseTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -/// Base class for variant tests. -class ButtonVariantUseCaseTests: XCTestCase { - - // MARK: - Properties - var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - self.theme = ThemeGeneratedMock.mock - } -} - -// MARK: - Helper Extensions -private extension Theme where Self == ThemeGeneratedMock { - static var mock: Self { - let theme = ThemeGeneratedMock() - theme.colors = .mock - theme.dims = DimsGeneratedMock.mocked() - return theme - } -} - -private extension Colors where Self == ColorsGeneratedMock { - static var mock: Self { - let colors = ColorsGeneratedMock() - colors.base = ColorsBaseGeneratedMock.mocked() - colors.main = ColorsMainGeneratedMock.mocked() - colors.support = ColorsSupportGeneratedMock.mocked() - colors.feedback = ColorsFeedbackGeneratedMock.mocked() - colors.states = ColorsStatesGeneratedMock.mocked() - - return colors - } -} diff --git a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseable.swift b/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseable.swift deleted file mode 100644 index 3792487a4..000000000 --- a/core/Sources/Components/Button/UseCase/GetVariants/ButtonGetVariantUseCaseable.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ButtonGetVariantUseCaseable.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -// sourcery: AutoMockable, AutoMockTest -protocol ButtonGetVariantUseCaseable { - // sourcery: colors = "Identical", dims = "Identical" - func execute(intent: ButtonIntent, colors: Colors, dims: Dims) -> ButtonColors -} diff --git a/core/Sources/Components/Button/View/Common/ButtonConfigurationSnapshotTests.swift b/core/Sources/Components/Button/View/Common/ButtonConfigurationSnapshotTests.swift deleted file mode 100644 index 9b7d81f2d..000000000 --- a/core/Sources/Components/Button/View/Common/ButtonConfigurationSnapshotTests.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// ButtonConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -struct ButtonConfigurationSnapshotTests { - - // MARK: - Type Alias - - private typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Properties - - let scenario: ButtonScenarioSnapshotTests - - let intent: ButtonIntent - let alignment: ButtonAlignment - let shape: ButtonShape - let size: ButtonSize - let variant: ButtonVariant - - let content: ButtonContentType - let state: ControlState - - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Initialization - - init( - scenario: ButtonScenarioSnapshotTests, - intent: ButtonIntent = .main, - alignment: ButtonAlignment = .leadingImage, - shape: ButtonShape = .rounded, - size: ButtonSize = .medium, - variant: ButtonVariant = .filled, - content: ButtonContentType = .title("My Title"), - state: ControlState = .normal, - modes: [ComponentSnapshotTestMode] = Constants.Modes.default, - sizes: [UIContentSizeCategory] = Constants.Sizes.default - ) { - self.scenario = scenario - self.intent = intent - self.alignment = alignment - self.shape = shape - self.size = size - self.variant = variant - self.content = content - self.state = state - self.modes = modes - self.sizes = sizes - } - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.alignment)" + "Alignment", - "\(self.shape)" + "Shape", - "\(self.size)" + "Size", - "\(self.variant)" + "Variant", - "\(self.content.name)" + "Content", - "\(self.state)" + "State" - ].joined(separator: "-") - } -} - -// MARK: - Enum - -enum ButtonContentType { - case title(_ value: String) - case attributedTitle(_ value: AttributedStringEither) - case titleAndImage(_ value: String, _ image: ImageEither) - case attributedTitleAndImage(_ value: AttributedStringEither, _ image: ImageEither) - - // MARK: - Properties - - var name: String { - switch self { - case .title: - return "title" - case .attributedTitle: - return "attributedTitle" - case .titleAndImage: - return "titleAndImage" - case .attributedTitleAndImage: - return "attributedTitleAndImage" - } - } - - // MARK: - All Cases - - static func allCases(attributedTitle: AttributedStringEither, image: ImageEither) -> [Self] { - let title = "My title" - return [ - .title(title), - .attributedTitle(attributedTitle), - .titleAndImage(title, image), - .attributedTitleAndImage(attributedTitle, image) - ] - } -} diff --git a/core/Sources/Components/Button/View/Common/ButtonScenarioSnapshotTests.swift b/core/Sources/Components/Button/View/Common/ButtonScenarioSnapshotTests.swift deleted file mode 100644 index 2651f3069..000000000 --- a/core/Sources/Components/Button/View/Common/ButtonScenarioSnapshotTests.swift +++ /dev/null @@ -1,262 +0,0 @@ -// -// ButtonScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI - -enum ButtonScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - case test6 - case test7 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) throws -> [ButtonConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1() - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3() - case .test4: - return self.test4() - case .test5: - return self.test5() - case .test6: - return self.test6(isSwiftUIComponent: isSwiftUIComponent) - case .test7: - return self.test7(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - **intents: all** - /// - alignment: default - /// - shape: default - /// - size: default - /// - variant: default - /// - content: default - /// - state: default - /// - mode: all - /// - a11y: default - private func test1() -> [ButtonConfigurationSnapshotTests] { - let intents = ButtonIntent.allCases - - return intents.map { intent -> ButtonConfigurationSnapshotTests in - .init( - scenario: self, - intent: intent, - modes: Constants.Modes.all - ) - } - } - - /// Test 2 - /// - /// Description: To test all alignments - /// - /// Content: - /// - intent: default - /// - **alignments: all** - /// - shapes: default - /// - size: default - /// - variant: default - /// - content: default - /// - state: default - /// - mode: default - /// - a11y: default - private func test2(isSwiftUIComponent: Bool) -> [ButtonConfigurationSnapshotTests] { - let alignments = ButtonAlignment.allCases - - return alignments.map { alignment -> ButtonConfigurationSnapshotTests in - .init( - scenario: self, - alignment: alignment, - content: .titleAndImage( - "My Title", - .mock(isSwiftUIComponent: isSwiftUIComponent) - ) - ) - } - } - - /// Test 3 - /// - /// Description: To test all shapes for all a11y sizes - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - **shapes: all** - /// - size: default - /// - variant: default - /// - content: default - /// - state: default - /// - mode: default - /// - **a11y: all** - private func test3() -> [ButtonConfigurationSnapshotTests] { - let shapes = ButtonShape.allCases - - return shapes.map { shape -> ButtonConfigurationSnapshotTests in - .init( - scenario: self, - shape: shape, - sizes: Constants.Sizes.all - ) - } - } - - /// Test 4 - /// - /// Description: To test all sizes for all a11y sizes - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - **sizes: all** - /// - variant: default - /// - content: default - /// - state: default - /// - mode: default - /// - **a11y: all** - private func test4() -> [ButtonConfigurationSnapshotTests] { - let sizes = ButtonSize.allCases - - return sizes.map { size -> ButtonConfigurationSnapshotTests in - .init( - scenario: self, - size: size, - sizes: Constants.Sizes.all - ) - } - } - - /// Test 5 - /// - /// Description: To test all variants - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - size: default - /// - **variants: all** - /// - content: default - /// - state: default - /// - mode: default - /// - a11y: default - private func test5() -> [ButtonConfigurationSnapshotTests] { - let variants = ButtonVariant.allCases - - return variants.map { variant -> ButtonConfigurationSnapshotTests in - .init( - scenario: self, - variant: variant - ) - } - } - - /// Test 6 - /// - /// Description: To test all contents - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - size: default - /// - variants: default - /// - **contents: all** - /// - state: default - /// - mode: default - /// - a11y: default - private func test6(isSwiftUIComponent: Bool) -> [ButtonConfigurationSnapshotTests] { - let contents = ButtonContentType.allCases(isSwiftUIComponent: isSwiftUIComponent) - - return contents.map { content -> ButtonConfigurationSnapshotTests in - .init( - scenario: self, - content: content - ) - } - } - - /// Test 7 - /// - /// Description: To test all states - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - size: default - /// - variant: default - /// - content: default - /// - **states: all** - /// - mode: default - /// - a11y: default - private func test7(isSwiftUIComponent: Bool) -> [ButtonConfigurationSnapshotTests] { - let states = ControlState.allCases - - return states.compactMap { state -> ButtonConfigurationSnapshotTests? in - - let title: String? - switch state { - case .normal: - title = "Normal" - case .highlighted: - // We can't test highlighted for SwiftUI - title = isSwiftUIComponent ? nil : "Highlighted" - case .disabled: - title = "Disabled" - case .selected: - title = "Selected" - } - - guard let title else { return nil } - - return .init( - scenario: self, - content: .title(title), - state: state - ) - } - } -} - -// MARK: - Extension - -extension ButtonContentType { - - static func allCases(isSwiftUIComponent: Bool) -> [Self] { - let image = ImageEither.mock(isSwiftUIComponent: isSwiftUIComponent) - let attributedString = AttributedStringEither.mock(isSwiftUIComponent: isSwiftUIComponent) - - return self.allCases( - attributedTitle: attributedString, - image: image - ) - } -} diff --git a/core/Sources/Components/Button/View/Common/IconButtonConfigurationSnapshotTests.swift b/core/Sources/Components/Button/View/Common/IconButtonConfigurationSnapshotTests.swift deleted file mode 100644 index d8e5b6b19..000000000 --- a/core/Sources/Components/Button/View/Common/IconButtonConfigurationSnapshotTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// IconButtonConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -struct IconButtonConfigurationSnapshotTests { - - // MARK: - Type Alias - - private typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Properties - - let scenario: IconButtonScenarioSnapshotTests - - let intent: ButtonIntent - let shape: ButtonShape - let size: ButtonSize - let variant: ButtonVariant - - let image: ImageEither - let state: ControlState - - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Initialization - - init( - scenario: IconButtonScenarioSnapshotTests, - intent: ButtonIntent = .main, - shape: ButtonShape = .rounded, - size: ButtonSize = .medium, - variant: ButtonVariant = .filled, - image: ImageEither, - state: ControlState = .normal, - modes: [ComponentSnapshotTestMode] = Constants.Modes.default, - sizes: [UIContentSizeCategory] = Constants.Sizes.default - ) { - self.scenario = scenario - self.intent = intent - self.shape = shape - self.size = size - self.variant = variant - self.image = image - self.state = state - self.modes = modes - self.sizes = sizes - } - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.shape)" + "Shape", - "\(self.size)" + "Size", - "\(self.variant)" + "Variant", - "\(self.state)" + "State" - ].joined(separator: "-") - } -} diff --git a/core/Sources/Components/Button/View/Common/IconButtonScenarioSnapshotTests.swift b/core/Sources/Components/Button/View/Common/IconButtonScenarioSnapshotTests.swift deleted file mode 100644 index a14915545..000000000 --- a/core/Sources/Components/Button/View/Common/IconButtonScenarioSnapshotTests.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// IconButtonScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI - -enum IconButtonScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) throws -> [IconButtonConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1(isSwiftUIComponent: isSwiftUIComponent) - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3(isSwiftUIComponent: isSwiftUIComponent) - case .test4: - return self.test4(isSwiftUIComponent: isSwiftUIComponent) - case .test5: - return self.test5(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - **intents: all** - /// - alignment: default - /// - shape: default - /// - size: default - /// - variant: default - /// - content: default - /// - state: default - /// - mode: all - /// - a11y: default - private func test1(isSwiftUIComponent: Bool) -> [IconButtonConfigurationSnapshotTests] { - let intents = ButtonIntent.allCases - - return intents.compactMap { intent -> IconButtonConfigurationSnapshotTests? in - guard let image = ImageEither.mock( - isSwiftUIComponent: isSwiftUIComponent, - for: .normal - ) else { - return nil - } - - return .init( - scenario: self, - intent: intent, - image: image, - modes: Constants.Modes.all - ) - } - } - - /// Test 2 - /// - /// Description: To test all shapes for all a11y sizes - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - **shapes: all** - /// - size: default - /// - variant: default - /// - content: default - /// - state: default - /// - mode: default - /// - **a11y: all** - private func test2(isSwiftUIComponent: Bool) -> [IconButtonConfigurationSnapshotTests] { - let shapes = ButtonShape.allCases - - return shapes.compactMap { shape -> IconButtonConfigurationSnapshotTests? in - guard let image = ImageEither.mock( - isSwiftUIComponent: isSwiftUIComponent, - for: .normal - ) else { - return nil - } - - return .init( - scenario: self, - shape: shape, - image: image, - sizes: Constants.Sizes.all - ) - } - } - - /// Test 3 - /// - /// Description: To test all sizes for all a11y sizes - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - **sizes: all** - /// - variant: default - /// - content: default - /// - state: default - /// - mode: default - /// - **a11y: all** - private func test3(isSwiftUIComponent: Bool) -> [IconButtonConfigurationSnapshotTests] { - let sizes = ButtonSize.allCases - - return sizes.compactMap { size -> IconButtonConfigurationSnapshotTests? in - guard let image = ImageEither.mock( - isSwiftUIComponent: isSwiftUIComponent, - for: .normal - ) else { - return nil - } - - return .init( - scenario: self, - size: size, - image: image, - sizes: Constants.Sizes.all - ) - } - } - - /// Test 4 - /// - /// Description: To test all variants - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - size: default - /// - **variants: all** - /// - content: default - /// - state: default - /// - mode: default - /// - a11y: default - private func test4(isSwiftUIComponent: Bool) -> [IconButtonConfigurationSnapshotTests] { - let variants = ButtonVariant.allCases - - return variants.compactMap { variant -> IconButtonConfigurationSnapshotTests? in - guard let image = ImageEither.mock( - isSwiftUIComponent: isSwiftUIComponent, - for: .normal - ) else { - return nil - } - - return .init( - scenario: self, - variant: variant, - image: image - ) - } - } - - /// Test 5 - /// - /// Description: To test all states - /// - /// Content: - /// - intent: default - /// - alignment: default - /// - shape: default - /// - size: default - /// - variant: default - /// - content: default - /// - **states: all** - /// - mode: default - /// - a11y: default - private func test5(isSwiftUIComponent: Bool) -> [IconButtonConfigurationSnapshotTests] { - let states = ControlState.allCases - - return states.compactMap { state -> IconButtonConfigurationSnapshotTests? in - guard let image = ImageEither.mock( - isSwiftUIComponent: isSwiftUIComponent, - for: state - ) else { return nil } - - return .init( - scenario: self, - image: image, - state: state - ) - } - } -} - -// MARK: - Extension - -extension ImageEither { - - static func mock( - isSwiftUIComponent: Bool, - for state: ControlState - ) -> Self? { - switch state { - case .normal: - return isSwiftUIComponent ? .right(.normalMock) : .left(.normalMock) - case .highlighted: - return isSwiftUIComponent ? nil : .left(.highlightedMock) - case .disabled: - return isSwiftUIComponent ? .right(.disabledMock) : .left(.disabledMock) - case .selected: - return isSwiftUIComponent ? .right(.selectedMock) : .left(.selectedMock) - } - } -} - -private extension Image { - static var normalMock = Image(systemName: "arrow.right.square") - static let disabledMock = Image(systemName: "arrow.up.square") - static let selectedMock = Image(systemName: "arrow.down.square") -} - -private extension UIImage { - static var normalMock = IconographyTests.shared.arrow - static var highlightedMock = IconographyTests.shared.checkmark - static var disabledMock = IconographyTests.shared.switchOn - static var selectedMock = IconographyTests.shared.switchOff -} diff --git a/core/Sources/Components/Button/View/SwiftUI/Internal/ButtonContainerView.swift b/core/Sources/Components/Button/View/SwiftUI/Internal/ButtonContainerView.swift deleted file mode 100644 index 9112efc6e..000000000 --- a/core/Sources/Components/Button/View/SwiftUI/Internal/ButtonContainerView.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// ButtonContainerView.swift -// SparkCore -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import Foundation - -struct ButtonContainerView: View { - - // MARK: - Properties - - @ObservedObject private var viewModel: ViewModel - - @ScaledMetric private var height: CGFloat - @ScaledMetric private var borderWidth: CGFloat - @ScaledMetric private var borderRadius: CGFloat - private let padding: EdgeInsets? - - @State private var isPressed: Bool = false - - private let action: () -> Void - - // MARK: - Components - - private var contentView: () -> ContainerView - - // MARK: - Initialization - - init( - viewModel: ViewModel, - padding: EdgeInsets? = nil, - action: @escaping () -> Void, - contentView: @escaping () -> ContainerView - ) { - self.viewModel = viewModel - - self._height = .init(wrappedValue: viewModel.sizes?.height ?? .zero) - self._borderWidth = .init(wrappedValue: viewModel.border?.width ?? .zero) - self._borderRadius = .init(wrappedValue: viewModel.border?.radius ?? .zero) - self.padding = padding - - self.action = action - self.contentView = contentView - } - - // MARK: - View - - var body: some View { - Button(action: self.action) { - self.contentView() - .padding(self.padding) - .frame(height: self.height) - .frame(minWidth: self.height) - .background(self.viewModel.currentColors?.backgroundColor.color ?? .clear) - .contentShape(Rectangle()) - .border( - width: self.borderWidth, - radius: self.borderRadius, - colorToken: self.viewModel.currentColors?.borderColor ?? ColorTokenDefault.clear - ) - } - .buttonStyle(PressedButtonStyle( - isPressed: self.$isPressed - )) - .compositingGroup() - .disabled(self.viewModel.state?.isUserInteractionEnabled == false) - .opacity(self.viewModel.state?.opacity ?? .zero) - .accessibilityIdentifier(ButtonAccessibilityIdentifier.button) - .accessibilityAddTraits(.isButton) - .onChange(of: self.isPressed) { isPressed in - self.viewModel.setIsPressed(isPressed) - } - } -} - -// MARK: - Modifier - -private struct ButtonOptionalPaddingModifier: ViewModifier { - - // MARK: - Properties - - var padding: EdgeInsets? - - // MARK: - Initialization - - func body(content: Content) -> some View { - if let padding = self.padding { - content.padding(padding) - } else { - content - } - } -} - -private extension View { - - func padding(_ padding: EdgeInsets?) -> some View { - self.modifier(ButtonOptionalPaddingModifier( - padding: padding - )) - } -} diff --git a/core/Sources/Components/Button/View/SwiftUI/Internal/ButtonImageView.swift b/core/Sources/Components/Button/View/SwiftUI/Internal/ButtonImageView.swift deleted file mode 100644 index eccdd700e..000000000 --- a/core/Sources/Components/Button/View/SwiftUI/Internal/ButtonImageView.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ButtonImageView.swift -// SparkCore -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -struct ButtonImageView: View { - - // MARK: - Properties - - let image: Image? - @ScaledMetric private var size: CGFloat - let foregroundColor: (any ColorToken)? - - // MARK: - Initialization - - init(viewModel: ViewModel) { - self.image = viewModel.controlStateImage.image - self._size = .init(wrappedValue: viewModel.sizes?.imageSize ?? .zero) - self.foregroundColor = viewModel.currentColors?.imageTintColor - } - - // MARK: - View - - var body: some View { - if let image = self.image { - image.resizable() - .aspectRatio(contentMode: .fit) - .frame( - width: self.size, - height: self.size, - alignment: .center - ) - .foregroundStyle(self.foregroundColor?.color ?? ColorTokenDefault.clear.color) - .accessibilityIdentifier(ButtonAccessibilityIdentifier.imageView) - } - } -} diff --git a/core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonView.swift b/core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonView.swift deleted file mode 100644 index bfc31b8a6..000000000 --- a/core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonView.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// ButtonView.swift -// SparkCore -// -// Created by robin.lemaire on 20/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import Foundation - -public struct ButtonView: View { - - // MARK: - Private Properties - - @ObservedObject private var viewModel: ButtonSUIViewModel - - @ScaledMetric private var horizontalSpacing: CGFloat - @ScaledMetric private var horizontalPadding: CGFloat - - private var action: () -> Void - - // MARK: - Initialization - - /// Initialize a new button view. - /// - Parameters: - /// - theme: The spark theme of the button. - /// - intent: The intent of the button. - /// - variant: The variant of the button. - /// - size: The size of the button. - /// - shape: The shape of the button. - /// - alignment: The alignment of the button. - /// - action: The action of the button. - public init( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape, - alignment: ButtonAlignment, - action: @escaping () -> Void - ) { - let viewModel = ButtonSUIViewModel( - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape, - alignment: alignment - ) - self.viewModel = viewModel - - // ** - // Scaled Metric - self._horizontalSpacing = .init(wrappedValue: viewModel.spacings?.horizontalSpacing ?? .zero) - self._horizontalPadding = .init(wrappedValue: viewModel.spacings?.horizontalPadding ?? .zero) - // ** - - self.action = action - } - - // MARK: - View - - public var body: some View { - ButtonContainerView( - viewModel: self.viewModel, - padding: .init( - horizontal: self.horizontalSpacing - ), - action: self.action - ) { - self.content() - } - } - - // MARK: - View Builder - - @ViewBuilder - private func content() -> some View { - HStack( - alignment: .center, - spacing: self.horizontalPadding - ) { - if self.viewModel.isImageTrailing { - self.title() - self.image() - } else { - self.image() - self.title() - } - } - .frame( - maxWidth: self.viewModel.maxWidth, - alignment: self.viewModel.frameAlignment - ) - .animation(nil, value: self.viewModel.maxWidth) - .animation(nil, value: self.viewModel.frameAlignment) - } - - @ViewBuilder - private func image() -> some View { - ButtonImageView(viewModel: self.viewModel) - } - - @ViewBuilder - private func title() -> some View { - if let text = self.viewModel.controlStateText?.text { - Text(text) - .foregroundStyle(self.viewModel.currentColors?.titleColor?.color ?? ColorTokenDefault.clear.color) - .font(self.viewModel.titleFontToken?.font) - .accessibilityIdentifier(ButtonAccessibilityIdentifier.text) - .animation(nil, value: text) - } else if let attributedText = self.viewModel.controlStateText?.attributedText { - Text(attributedText) - .accessibilityIdentifier(ButtonAccessibilityIdentifier.text) - .animation(nil, value: attributedText) - } - } - - // MARK: - Modifier - - /// Update the frame of the button for a state. - /// - Parameters: - /// - maxWidth: The maximum width of the resulting frame. - /// - alignment: The alignment of this view inside the resulting frame. - /// Note that most alignment values have no apparent effect when the - /// size of the frame happens to match that of this view. - /// - /// - Returns: A view with flexible dimensions given by the call's non-`nil` - /// parameters. - public func frame(maxWidth: CGFloat? = nil, alignment: Alignment = .center) -> Self { - self.viewModel.maxWidth = maxWidth - self.viewModel.frameAlignment = alignment - return self - } - - /// Set the image of the button for a state. - /// - parameter image: new image of the button - /// - parameter state: state of the image - /// - Returns: Current Button View. - public func image(_ image: Image?, for state: ControlState) -> Self { - self.viewModel.setImage(image, for: state) - return self - } - - /// Set the title of the button for a state. - /// - parameter title: new title of the button - /// - parameter state: state of the title - /// - Returns: Current Button View. - public func title(_ title: String?, for state: ControlState) -> Self { - self.viewModel.setTitle( - title, - for: state - ) - - return self - } - - /// Set the attributedTitle of the button for a state. - /// - parameter attributedTitle: new attributedTitle of the button - /// - parameter state: state of the attributedTitle - /// - Returns: Current Button View. - public func attributedTitle(_ attributedTitle: AttributedString?, for state: ControlState) -> Self { - self.viewModel.setAttributedTitle( - attributedTitle, - for: state - ) - - return self - } - - /// Set the button to disabled. - /// - Parameters: - /// - text: The button is disabled or not. - /// - Returns: Current Button View. - public func disabled(_ isDisabled: Bool) -> Self { - self.viewModel.setIsDisabled(isDisabled) - - return self - } - - /// Set the button to selected. - /// - Parameters: - /// - text: The switch is selected or not. - /// - Returns: Current Button View. - public func selected(_ isSelected: Bool) -> Self { - self.viewModel.setIsSelected(isSelected) - - return self - } -} diff --git a/core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonViewSnapshotTests.swift b/core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonViewSnapshotTests.swift deleted file mode 100644 index d485318f9..000000000 --- a/core/Sources/Components/Button/View/SwiftUI/Public/Button/ButtonViewSnapshotTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// ButtonViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore -import SwiftUI - -final class ButtonViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = ButtonScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [ButtonConfigurationSnapshotTests] = try scenario.configuration( - isSwiftUIComponent: true - ) - - for configuration in configurations { - let view = ButtonView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - size: configuration.size, - shape: configuration.shape, - alignment: configuration.alignment, - action: {} - ) - .disabled(configuration.state == .disabled) - .selected(configuration.state == .selected) - .content(configuration) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} - -// MARK: - Extension - -private extension ButtonView { - - @ViewBuilder - func content(_ configuration: ButtonConfigurationSnapshotTests) -> some View { - let state = configuration.state - switch configuration.content { - case .title(let title): - self.title(title, for: state) - - case .attributedTitle(let attributedTitle): - self.attributedTitle(attributedTitle.rightValue, for: state) - - case .titleAndImage(let title, let image): - self.title(title, for: state) - .image(image.rightValue, for: state) - - case .attributedTitleAndImage(let attributedTitle, let image): - self.attributedTitle(attributedTitle.rightValue, for: state) - .image(image.rightValue, for: state) - } - } -} diff --git a/core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonView.swift b/core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonView.swift deleted file mode 100644 index af2ab3ea2..000000000 --- a/core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonView.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// IconButtonView.swift -// SparkCore -// -// Created by robin.lemaire on 24/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import Foundation - -public struct IconButtonView: View { - - // MARK: - Private Properties - - @ObservedObject private var viewModel: IconButtonSUIViewModel - - private var action: () -> Void - - // MARK: - Initialization - - /// Initialize a new button view. - /// - Parameters: - /// - theme: The spark theme of the button. - /// - intent: The intent of the button. - /// - variant: The variant of the button. - /// - size: The size of the button. - /// - shape: The shape of the button. - /// - action: The action of the button. - public init( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape, - action: @escaping () -> Void - ) { - let viewModel = IconButtonSUIViewModel( - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape - ) - self.viewModel = viewModel - - self.action = action - } - - // MARK: - View - - public var body: some View { - ButtonContainerView( - viewModel: self.viewModel, - action: self.action - ) { - ButtonImageView(viewModel: self.viewModel) - } - } - - // MARK: - Modifier - - /// Set the image of the button for a state. - /// - parameter image: new image of the button - /// - parameter state: state of the image - /// - Returns: Current Button View. - public func image(_ image: Image?, for state: ControlState) -> Self { - self.viewModel.setImage(image, for: state) - return self - } - - /// Set the button to disabled. - /// - Parameters: - /// - text: The button is disabled or not. - /// - Returns: Current Button View. - public func disabled(_ isDisabled: Bool) -> Self { - self.viewModel.setIsDisabled(isDisabled) - - return self - } - - /// Set the switch to selected. - /// - Parameters: - /// - text: The switch is selected or not. - /// - Returns: Current Button View. - public func selected(_ isSelected: Bool) -> Self { - self.viewModel.setIsSelected(isSelected) - - return self - } -} diff --git a/core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonViewSnapshotTests.swift b/core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonViewSnapshotTests.swift deleted file mode 100644 index 4ce789e3b..000000000 --- a/core/Sources/Components/Button/View/SwiftUI/Public/Icon/IconButtonViewSnapshotTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// IconButtonViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore -import SwiftUI - -final class IconButtonViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = IconButtonScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [IconButtonConfigurationSnapshotTests] = try scenario.configuration( - isSwiftUIComponent: true - ) - - for configuration in configurations { - let view = IconButtonView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - size: configuration.size, - shape: configuration.shape, - action: {} - ) - .disabled(configuration.state == .disabled) - .selected(configuration.state == .selected) - .image(configuration.image.rightValue, for: configuration.state) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} - -// MARK: - Extension - -private extension ButtonView { - - @ViewBuilder - func content(_ configuration: ButtonConfigurationSnapshotTests) -> some View { - let state = configuration.state - switch configuration.content { - case .title(let title): - self.title(title, for: state) - - case .attributedTitle(let attributedTitle): - self.attributedTitle(attributedTitle.rightValue, for: state) - - case .titleAndImage(let title, let image): - self.title(title, for: state) - .image(image.rightValue, for: state) - - case .attributedTitleAndImage(let attributedTitle, let image): - self.attributedTitle(attributedTitle.rightValue, for: state) - .image(image.rightValue, for: state) - } - } -} diff --git a/core/Sources/Components/Button/View/UIKit/Button/ButtonUIView.swift b/core/Sources/Components/Button/View/UIKit/Button/ButtonUIView.swift deleted file mode 100644 index 199ca6f2f..000000000 --- a/core/Sources/Components/Button/View/UIKit/Button/ButtonUIView.swift +++ /dev/null @@ -1,382 +0,0 @@ -// -// ButtonUIView.swift -// SparkCore -// -// Created by robin.lemaire on 10/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// The UIKit version for the button. -public final class ButtonUIView: ButtonMainUIView { - - // MARK: - Type alias - - private typealias Animation = ButtonConstants.Animation - - // MARK: - Components - - private lazy var contentStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - - self.imageContentView, - self.titleLabel - ] - ) - stackView.axis = .horizontal - stackView.accessibilityIdentifier = ButtonAccessibilityIdentifier.contentStackView - stackView.isUserInteractionEnabled = false - return stackView - }() - - private lazy var imageContentView: UIView = { - let view = UIView() - view.addSubview(self.imageView) - view.accessibilityIdentifier = ButtonAccessibilityIdentifier.imageContentView - return view - }() - - public var titleLabel: UILabel { - return self.titleStateLabel - } - - private var titleStateLabel: UIControlStateLabel = { - let label = UIControlStateLabel() - label.numberOfLines = 1 - label.lineBreakMode = .byWordWrapping - label.textAlignment = .left - label.adjustsFontForContentSizeCategory = true - label.accessibilityIdentifier = ButtonAccessibilityIdentifier.text - label.lineBreakMode = .byTruncatingMiddle - label.setContentCompressionResistancePriority(.required, for: .vertical) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - label.setContentHuggingPriority(.defaultLow, for: .vertical) - label.setContentHuggingPriority(.defaultLow, for: .horizontal) - return label - }() - - // MARK: - Public Properties - - /// The alignment of the button. - public var alignment: ButtonAlignment { - get { - return self.viewModel.alignment - } - set { - self.viewModel.alignment = newValue - } - } - - /// A Boolean value indicating whether the button is in the enabled state. - public override var isEnabled: Bool { - get { - return super.isEnabled - } - set { - super.isEnabled = newValue - self.titleStateLabel.updateContent(from: self) - self.updateAccessibilityLabel() - } - } - - /// A Boolean value indicating whether the button is in the selected state. - public override var isSelected: Bool { - get { - return super.isSelected - } - set { - super.isSelected = newValue - self.titleStateLabel.updateContent(from: self) - self.updateAccessibilityLabel() - } - } - - /// A Boolean value indicating whether the button draws a highlight. - public override var isHighlighted: Bool { - get { - return super.isHighlighted - } - set { - super.isHighlighted = newValue - self.titleStateLabel.updateContent(from: self) - } - } - - public override var accessibilityLabel: String? { - get { - return self.accessibilityLabelManager.value - } - set { - self.accessibilityLabelManager.value = newValue - } - } - - // MARK: - Private Properties - - private let viewModel: ButtonViewModel - - private var contentStackViewLeadingConstraint: NSLayoutConstraint? - - @ScaledUIMetric private var horizontalSpacing: CGFloat = 0 - @ScaledUIMetric private var horizontalPadding: CGFloat = 0 - - private var firstContentStackViewAnimation: Bool = true - - private var accessibilityLabelManager = AccessibilityLabelManager() - - private var subscriptions = Set() - - // MARK: - Initialization - - /// Initialize a new button view. - /// - Parameters: - /// - theme: The spark theme of the button. - /// - intent: The intent of the button. - /// - variant: The variant of the button. - /// - size: The size of the button. - /// - shape: The shape of the button. - /// - alignment: The alignment of the button. - public init( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape, - alignment: ButtonAlignment - ) { - let viewModel = ButtonViewModel( - for: .uiKit, - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape, - alignment: alignment - ) - - self.viewModel = viewModel - - super.init(viewModel: viewModel) - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - internal override func setupView() { - // Accessibility - self.accessibilityIdentifier = ButtonAccessibilityIdentifier.button - - // Add subviews - self.addSubview(self.contentStackView) - - super.setupView() - } - - // MARK: - Constraints - - internal override func setupConstraints() { - super.setupConstraints() - - self.setupContentStackViewConstraints() - self.setupImageContentViewConstraints() - } - - internal override func setupViewConstraints() { - super.setupViewConstraints() - - self.widthAnchor.constraint(greaterThanOrEqualTo: self.heightAnchor).isActive = true - } - - private func setupContentStackViewConstraints() { - self.contentStackView.translatesAutoresizingMaskIntoConstraints = false - - self.contentStackViewLeadingConstraint = self.contentStackView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor) - let contentStackViewTopConstraint = self.contentStackView.topAnchor.constraint(equalTo: self.topAnchor) - let contentStackViewCenterXAnchor = self.contentStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor) - let contentStackViewBottomConstraint = self.contentStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor) - - NSLayoutConstraint.activate([ - self.contentStackViewLeadingConstraint, - contentStackViewTopConstraint, - contentStackViewCenterXAnchor, - contentStackViewBottomConstraint, - ].compactMap { $0 }) - } - - private func setupImageContentViewConstraints() { - self.imageContentView.translatesAutoresizingMaskIntoConstraints = false - self.imageContentView.widthAnchor.constraint(greaterThanOrEqualTo: self.imageView.widthAnchor).isActive = true - } - - internal override func setupImageViewConstraints() { - super.setupImageViewConstraints() - - NSLayoutConstraint.activate([ - self.imageView.centerXAnchor.constraint(equalTo: self.imageContentView.centerXAnchor), - self.imageView.centerYAnchor.constraint(equalTo: self.imageContentView.centerYAnchor) - ]) - } - - // MARK: - Instrinsic Content Size - - public override var intrinsicContentSize: CGSize { - var width: CGFloat = self.horizontalSpacing * 2 - - let isTitle = !self.titleLabel.isHidden - let isImage = !self.imageContentView.isHidden - - if isTitle { - width += self.titleLabel.intrinsicContentSize.width - } - if isImage { - width += self.imageHeight // It is always a square - } - if isTitle && isImage { - width += self.contentStackView.spacing - } - - return CGSize( - width: width, - height: self.height - ) - } - - // MARK: - Setter & Getter - - /// The title of the button for a state. - /// - parameter state: state of the title - public func title(for state: ControlState) -> String? { - return self.titleStateLabel.text(for: state) - } - - /// Set the title of the button for a state. - /// - parameter title: new title of the button - /// - parameter state: state of the title - public func setTitle(_ title: String?, for state: ControlState) { - self.titleStateLabel.setText(title, for: state, on: self) - self.updateAccessibilityLabel() - } - - /// The title of the button for a state. - /// - parameter state: state of the title - public func attributedTitle(for state: ControlState) -> NSAttributedString? { - return self.titleStateLabel.attributedText(for: state) - } - - /// Set the attributedTitle of the button for a state. - /// - parameter attributedTitle: new attributedTitle of the button - /// - parameter state: state of the attributedTitle - public func setAttributedTitle(_ attributedTitle: NSAttributedString?, for state: ControlState) { - self.titleStateLabel.setAttributedText(attributedTitle, for: state, on: self) - self.updateAccessibilityLabel() - } - - // MARK: - Update UI - - private func updateSpacings() { - // Reload spacing only if value changed - let horizontalSpacing = self._horizontalSpacing.wrappedValue - let horizontalPadding = self._horizontalPadding.wrappedValue - - if horizontalSpacing != self.contentStackViewLeadingConstraint?.constant || - horizontalPadding != self.contentStackView.spacing { - - let isAnimated = self.isAnimated && !self.firstContentStackViewAnimation - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Animation.slowDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - guard let self else { return } - - self.firstContentStackViewAnimation = false - - self.contentStackViewLeadingConstraint?.constant = horizontalSpacing - self.contentStackView.updateConstraintsIfNeeded() - - self.contentStackView.spacing = horizontalPadding - } - } - } - - // MARK: - Data Did Update - - private func spacingsDidUpdate(_ spacings: ButtonSpacings) { - self.horizontalSpacing = spacings.horizontalSpacing - self.horizontalPadding = spacings.horizontalPadding - - self.updateSpacings() - } - - internal override func colorsDidUpdate(_ colors: ButtonCurrentColors) { - super.colorsDidUpdate(colors) - - if let titleColor = colors.titleColor { - self.titleLabel.textColor = titleColor.uiColor - } - } - - internal override func isImageOnStateViewDidUpdate(_ isImage: Bool) { - super.isImageOnStateViewDidUpdate(isImage) - - self.imageContentView.isHidden = !isImage - } - - private func updateAccessibilityLabel() { - self.accessibilityLabelManager.internalValue = self.titleLabel.accessibilityLabel - } - - // MARK: - Subscribe - - internal override func setupSubscriptions() { - super.setupSubscriptions() - - // ** - // Spacings - self.viewModel.$spacings.subscribe(in: &self.subscriptions) { [weak self] spacings in - guard let self, let spacings else { return } - - self.spacingsDidUpdate(spacings) - } - // ** - - // ** - // Content - self.viewModel.$isImageTrailing.subscribe(in: &self.subscriptions) { [weak self] isImageTrailing in - guard let self else { return } - - self.contentStackView.semanticContentAttribute = isImageTrailing ? .forceRightToLeft : .forceLeftToRight - } - // ** - - // ** - // Title Font - self.viewModel.$titleFontToken.subscribe(in: &self.subscriptions) { [weak self] titleFontToken in - guard let self, let titleFontToken else { return } - - self.titleLabel.font = titleFontToken.uiFont - } - // ** - - // ** - // Is Text ? - self.titleStateLabel.$isText.subscribe(in: &self.subscriptions) { [weak self] isText in - self?.titleLabel.isHidden = !isText - } - // ** - } - - // MARK: - Trait Collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // Update spacings - self._horizontalSpacing.update(traitCollection: self.traitCollection) - self._horizontalPadding.update(traitCollection: self.traitCollection) - self.updateSpacings() - } -} diff --git a/core/Sources/Components/Button/View/UIKit/Button/ButtonUIViewSnapshotTests.swift b/core/Sources/Components/Button/View/UIKit/Button/ButtonUIViewSnapshotTests.swift deleted file mode 100644 index 30f4e6b22..000000000 --- a/core/Sources/Components/Button/View/UIKit/Button/ButtonUIViewSnapshotTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// ButtonUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore - -final class ButtonUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = ButtonScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [ButtonConfigurationSnapshotTests] = try scenario.configuration( - isSwiftUIComponent: false - ) - for configuration in configurations { - - let view: ButtonUIView = .init( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - size: configuration.size, - shape: configuration.shape, - alignment: configuration.alignment - ) - view.isHighlighted = configuration.state == .highlighted - view.isEnabled = configuration.state != .disabled - view.isSelected = configuration.state == .selected - - let state = configuration.state - switch configuration.content { - case .title(let title): - view.setTitle(title, for: state) - - case .attributedTitle(let attributedTitle): - view.setAttributedTitle(attributedTitle.leftValue, for: state) - - case .titleAndImage(let title, let image): - view.setTitle(title, for: state) - view.setImage(image.leftValue, for: state) - - case .attributedTitleAndImage(let attributedTitle, let image): - view.setAttributedTitle(attributedTitle.leftValue, for: state) - view.setImage(image.leftValue, for: state) - } - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIView.swift b/core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIView.swift deleted file mode 100644 index 06e2441d8..000000000 --- a/core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// IconIconButtonUIView.swift -// SparkCore -// -// Created by robin.lemaire on 10/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// The UIKit version for the icon button. -public final class IconButtonUIView: ButtonMainUIView { - - // MARK: - Initialization - - /// Initialize a new button view. - /// - Parameters: - /// - theme: The spark theme of the button. - /// - intent: The intent of the button. - /// - variant: The variant of the button. - /// - size: The size of the button. - /// - shape: The shape of the button. - public init( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape - ) { - let viewModel = IconButtonViewModel( - for: .uiKit, - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape - ) - - super.init(viewModel: viewModel) - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - internal override func setupView() { - // Accessibility - self.accessibilityIdentifier = ButtonAccessibilityIdentifier.iconButton - - // Add subviews - self.addSubview(self.imageView) - - super.setupView() - } - - // MARK: - Constraints - - internal override func setupViewConstraints() { - super.setupViewConstraints() - - self.widthAnchor.constraint(equalTo: self.heightAnchor).isActive = true - } - - internal override func setupImageViewConstraints() { - super.setupImageViewConstraints() - - NSLayoutConstraint.activate([ - self.imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - self.imageView.centerYAnchor.constraint(equalTo: self.centerYAnchor) - ]) - } - - // MARK: - Instrinsic Content Size - - public override var intrinsicContentSize: CGSize { - return CGSize( - width: self.height, // It is always a square - height: self.height - ) - } -} diff --git a/core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIViewSnapshotTests.swift b/core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIViewSnapshotTests.swift deleted file mode 100644 index 45a755a77..000000000 --- a/core/Sources/Components/Button/View/UIKit/Icon/IconButtonUIViewSnapshotTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// IconButtonUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore - -final class IconButtonUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = IconButtonScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [IconButtonConfigurationSnapshotTests] = try scenario.configuration( - isSwiftUIComponent: false - ) - for configuration in configurations { - - let view: IconButtonUIView = .init( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - size: configuration.size, - shape: configuration.shape - ) - view.isHighlighted = configuration.state == .highlighted - view.isEnabled = configuration.state != .disabled - view.isSelected = configuration.state == .selected - - view.setImage(configuration.image.leftValue, for: configuration.state) - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Button/View/UIKit/Main/ButtonMainUIView.swift b/core/Sources/Components/Button/View/UIKit/Main/ButtonMainUIView.swift deleted file mode 100644 index 80303a58f..000000000 --- a/core/Sources/Components/Button/View/UIKit/Main/ButtonMainUIView.swift +++ /dev/null @@ -1,418 +0,0 @@ -// -// ButtonMainUIView.swift -// SparkCore -// -// Created by robin.lemaire on 10/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// This ButtonMainUIView view contains all communs subviews (imageView), rules, styles, constraints, ... for all buttons. -/// This view doesn't have a public init. -public class ButtonMainUIView: UIControl { - - // MARK: - Type alias - - private typealias Animation = ButtonConstants.Animation - - // MARK: - Components - - public var imageView: UIImageView { - return self.imageStateView - } - - private var imageStateView: UIControlStateImageView = { - let imageView = UIControlStateImageView() - imageView.contentMode = .scaleAspectFit - imageView.tintAdjustmentMode = .normal - imageView.accessibilityIdentifier = ButtonAccessibilityIdentifier.imageView - imageView.setContentCompressionResistancePriority(.required, for: .vertical) - imageView.setContentCompressionResistancePriority(.required, for: .horizontal) - imageView.setContentHuggingPriority(.defaultLow, for: .vertical) - imageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - return imageView - }() - - // MARK: - Public Properties - - /// The tap publisher. Alternatively, you can use the native **action** (addAction) or **target** (addTarget). - public var tapPublisher: UIControl.EventPublisher { - return self.publisher(for: .touchUpInside) - } - - /// Publishes when a touch was cancelled (e.g. by the system). - public var touchCancelPublisher: UIControl.EventPublisher { - return self.publisher(for: .touchCancel) - } - - /// Publishes when a touch was started but the touch ended outside of the button view bounds. - public var touchUpOutsidePublisher: UIControl.EventPublisher { - return self.publisher(for: .touchUpOutside) - } - - /// Publishes instantly when the button is touched down. - /// - warning: This should not trigger a user action and should only be used for things like tracking. - public var touchDownPublisher: UIControl.EventPublisher { - return self.publisher(for: .touchDown) - } - - /// The spark theme of the button. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// The intent of the button. - public var intent: ButtonIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - /// The variant of the button. - public var variant: ButtonVariant { - get { - return self.viewModel.variant - } - set { - self.viewModel.variant = newValue - } - } - - /// The size of the button. - public var size: ButtonSize { - get { - return self.viewModel.size - } - set { - self.viewModel.size = newValue - } - } - - /// The shape of the button. - public var shape: ButtonShape { - get { - return self.viewModel.shape - } - set { - self.viewModel.shape = newValue - } - } - - /// A Boolean value indicating whether the button is in the enabled state. - public override var isEnabled: Bool { - get { - return self.viewModel.isEnabled - } - set { - super.isEnabled = newValue - self.viewModel.isEnabled = newValue - - self.imageStateView.updateContent(from: self) - } - } - - /// A Boolean value indicating whether the button is in the selected state. - public override var isSelected: Bool { - didSet { - self.imageStateView.updateContent(from: self) - } - } - - /// A Boolean value indicating whether the button draws a highlight. - public override var isHighlighted: Bool { - didSet { - self.imageStateView.updateContent(from: self) - self.viewModel.pressedAction(self.isHighlighted) - } - } - - /// Button modifications should be animated or not. **False** by default. - public var isAnimated: Bool = false - - // MARK: - Internal Properties - - private let viewModel: ButtonMainViewModel - - // MARK: - Private Properties - - private var heightConstraint: NSLayoutConstraint? - private var imageViewHeightConstraint: NSLayoutConstraint? - - @ScaledUIMetric var height: CGFloat = 0 - @ScaledUIMetric var imageHeight: CGFloat = 0 - - @ScaledUIMetric private var cornerRadius: CGFloat = 0 - @ScaledUIMetric private var borderWidth: CGFloat = 0 - - private var subscriptions = Set() - - // MARK: - Initialization - - internal init(viewModel: ButtonMainViewModel) { - self.viewModel = viewModel - - super.init(frame: .zero) - - // Setup - self.setupView() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - /// Setup the all needed data for this view and all subviews. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupView() { - // Accessibility - self.accessibilityTraits = [.button] - self.isAccessibilityElement = true - - // Needed values from viewModel (important for superview) - self.height = self.viewModel.sizes?.height ?? 0 - - // Setup constraints - self.setupConstraints() - - // Setup gesture - self.enableTouch() - - // Setup publisher subcriptions - self.setupSubscriptions() - - // Load view model - self.viewModel.load() - } - - // MARK: - Layout - - public override func layoutSubviews() { - super.layoutSubviews() - - self.updateBorderRadius() - } - - // MARK: - Constraints - - /// Setup the constraints for this view and all subviews. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupConstraints() { - self.setupViewConstraints() - self.setupImageViewConstraints() - } - - /// Setup the size constraints for this view. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupViewConstraints() { - self.translatesAutoresizingMaskIntoConstraints = false - - self.heightConstraint = self.heightAnchor.constraint(equalToConstant: self.height) - self.heightConstraint?.isActive = true - } - - /// Setup the imageView constraints. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupImageViewConstraints() { - self.imageView.translatesAutoresizingMaskIntoConstraints = false - - self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.imageHeight) - self.imageViewHeightConstraint?.isActive = true - - self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor).isActive = true - } - - // MARK: - Setter & Getter - - /// The image of the button for a state. - /// - parameter state: state of the image - public func image(for state: ControlState) -> UIImage? { - return self.imageStateView.image(for: state) - } - - /// Set the image of the button for a state. - /// - parameter image: new image of the button - /// - parameter state: state of the image - public func setImage(_ image: UIImage?, for state: ControlState) { - self.imageStateView.setImage(image, for: state, on: self) - } - - // MARK: - Update UI - - private func updateBorderRadius() { - self.setCornerRadius(self.cornerRadius) - } - - private func updateBorderWidth() { - self.setBorderWidth(self.borderWidth) - } - - private func updateHeight() { - // Reload height only if value changed - if self.heightConstraint?.constant != self.height { - self.heightConstraint?.constant = self.height - self.updateConstraintsIfNeeded() - } - } - - private func updateImageViewHeight() { - // Reload height only if value changed - if self.imageViewHeightConstraint?.constant != self.imageHeight { - self.imageViewHeightConstraint?.constant = self.imageHeight - self.imageView.updateConstraintsIfNeeded() - } - } - - // MARK: - Data Did Update - - func stateDidUpdate(_ state: ButtonState) { - // Update the user interaction enabled - self.isUserInteractionEnabled = state.isUserInteractionEnabled - if !state.isUserInteractionEnabled { - self.accessibilityTraits.insert(.notEnabled) - } else { - self.accessibilityTraits.remove(.notEnabled) - } - - // Animate only if new alpha is different from current alpha - let alpha = state.opacity - - let isAnimated = self.isAnimated && self.alpha != alpha - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Animation.slowDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - self?.alpha = alpha - } - } - - /// UI must be update when colors are updated on ViewModel. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func colorsDidUpdate(_ colors: ButtonCurrentColors) { - // Background Color - let isAnimated = self.isAnimated && self.backgroundColor != colors.backgroundColor.uiColor - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Animation.fastDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - self?.backgroundColor = colors.backgroundColor.uiColor - } - - // Border Color - self.borderColorDidUpdate(from: colors) - - // Foreground Color - self.imageView.tintColor = colors.imageTintColor.uiColor - } - - private func borderColorDidUpdate(from colors: ButtonCurrentColors? = nil) { - guard let colors = colors ?? self.viewModel.currentColors else { - return - } - - // Border Color - self.setBorderColor(from: colors.borderColor) - } - - /// UI must be update when image change on imageStateView. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func isImageOnStateViewDidUpdate(_ isImage: Bool) { - } - - private func sizesDidUpdate(_ sizes: ButtonSizes) { - // Height - self.height = sizes.height - self.updateHeight() - - // ImageView height - self.imageHeight = sizes.imageSize - self.updateImageViewHeight() - } - - private func borderDidUpdate(_ border: ButtonBorder) { - // Radius - self.cornerRadius = border.radius - self.updateBorderRadius() - - // Width - self.borderWidth = border.width - self.updateBorderWidth() - } - - // MARK: - Subscribe - - /// Subscribe to the published properties on ViewModel. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupSubscriptions() { - // ** - // State - self.viewModel.$state.subscribe(in: &self.subscriptions) { [weak self] state in - guard let self, let state else { return } - - self.stateDidUpdate(state) - } - // ** - - // ** - // Colors - self.viewModel.$currentColors.subscribe(in: &self.subscriptions) { [weak self] colors in - guard let self, let colors else { return } - - self.colorsDidUpdate(colors) - } - - // ** - // Sizes - self.viewModel.$sizes.subscribe(in: &self.subscriptions) { [weak self] sizes in - guard let self, let sizes else { return } - - self.sizesDidUpdate(sizes) - } - // ** - - // ** - // Border - self.viewModel.$border.subscribe(in: &self.subscriptions) { [weak self] border in - guard let self, let border else { return } - - self.borderDidUpdate(border) - } - // ** - - // ** - // Is Image ? - self.imageStateView.$isImage.subscribe(in: &self.subscriptions) { [weak self] isImage in - self?.isImageOnStateViewDidUpdate(isImage) - } - } - - // MARK: - Trait Collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // Reload colors ? - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.borderColorDidUpdate() - } - - // Update heights - self._height.update(traitCollection: self.traitCollection) - self.updateHeight() - self._imageHeight.update(traitCollection: self.traitCollection) - self.updateImageViewHeight() - - // Corner - self._cornerRadius.update(traitCollection: self.traitCollection) - self.updateBorderRadius() - self._borderWidth.update(traitCollection: self.traitCollection) - self.updateBorderWidth() - } -} diff --git a/core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModel.swift b/core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModel.swift deleted file mode 100644 index 6596f43f9..000000000 --- a/core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModel.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ButtonSUIViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 15/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -final class ButtonSUIViewModel: ButtonViewModel, ButtonMainSUIViewModel { - - // MARK: - Properties - - var controlStatus: ControlStatus = .init() - - // MARK: - Published Properties - - @Published private(set) var controlStateImage: ControlStateImage = .init() - @Published private(set) var controlStateText: ControlStateText? = .init() - @Published var maxWidth: CGFloat? - @Published var frameAlignment: Alignment = .center - - // MARK: - Initialization - - init( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape, - alignment: ButtonAlignment - ) { - super.init( - for: .swiftUI, - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape, - alignment: alignment - ) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModelTests.swift b/core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModelTests.swift deleted file mode 100644 index 5635cd0f2..000000000 --- a/core/Sources/Components/Button/ViewModel/Button/ButtonSUIViewModelTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ButtonSUIViewModelTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 15/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class ButtonSUIViewModelTests: XCTestCase { - - // MARK: - Init Tests - - func test_default_properties_on_init() { - // GIVEN / WHEN - let viewModel = ButtonSUIViewModel( - theme: ThemeGeneratedMock.mocked(), - intent: .main, - variant: .filled, - size: .medium, - shape: .rounded, - alignment: .leadingImage - ) - - // THEN - XCTAssertEqual( - viewModel.controlStatus, - .init(), - "Wrong constrol status" - ) - XCTAssertNotNil( - viewModel.controlStateImage, - "Wrong constrol state text" - ) - XCTAssertNotNil( - viewModel.controlStateText, - "Wrong constrol state text" - ) - XCTAssertNil( - viewModel.maxWidth, - "Wrong max Width" - ) - XCTAssertEqual( - viewModel.frameAlignment, - .center, - "Wrong frame alignment" - ) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Button/ButtonViewModel.swift b/core/Sources/Components/Button/ViewModel/Button/ButtonViewModel.swift deleted file mode 100644 index 8ddcc74a6..000000000 --- a/core/Sources/Components/Button/ViewModel/Button/ButtonViewModel.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// ButtonViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 13/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine - -// sourcery: AutoPublisherTest, AutoViewModelStub -// sourcery: titleFontToken = "Identical" -class ButtonViewModel: ButtonMainViewModel { - - // MARK: - Properties - - var alignment: ButtonAlignment { - didSet { - guard self.alignment != oldValue else { return } - self.alignmentDidUpdate() - } - } - - // MARK: - Published Properties - - @Published private(set) var spacings: ButtonSpacings? - @Published private(set) var isImageTrailing: Bool = false - @Published private(set) var titleFontToken: TypographyFontToken? - - // MARK: - Private Properties - - private let getSpacingsUseCase: ButtonGetSpacingsUseCaseable - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape, - alignment: ButtonAlignment, - getSpacingsUseCase: ButtonGetSpacingsUseCaseable = ButtonGetSpacingsUseCase() - ) { - self.alignment = alignment - self.getSpacingsUseCase = getSpacingsUseCase - - super.init( - for: frameworkType, - type: .button, - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape - ) - } - - // MARK: - Update - - override func updateAll() { - super.updateAll() - - self.alignmentDidUpdate() - self.spacingsDidUpdate() - self.titleFontDidUpdate() - } - - private func alignmentDidUpdate() { - self.isImageTrailing = self.alignment.isTrailingImage - } - - private func spacingsDidUpdate() { - self.spacings = self.getSpacingsUseCase.execute( - spacing: self.theme.layout.spacing - ) - } - - private func titleFontDidUpdate() { - self.titleFontToken = self.theme.typography.callout - } -} diff --git a/core/Sources/Components/Button/ViewModel/Button/ButtonViewModelTests.swift b/core/Sources/Components/Button/ViewModel/Button/ButtonViewModelTests.swift deleted file mode 100644 index 0014ff258..000000000 --- a/core/Sources/Components/Button/ViewModel/Button/ButtonViewModelTests.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// ButtonViewModelTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 13/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import XCTest -@testable import SparkCore -import Combine - -final class ButtonViewModelTests: XCTestCase { - - // MARK: - Properties - - private var subscriptions = Set() - - // MARK: - Setup - - override func tearDown() { - super.tearDown() - - // Clear publishers - self.subscriptions.removeAll() - } - - // MARK: - Init Tests - - func test_properties_on_init_when_frameworkType_is_UIKit() { - self.testPropertiesOnInit(givenFrameworkType: .uiKit) - } - - func test_properties_on_init_when_frameworkType_is_SwiftUI() { - self.testPropertiesOnInit(givenFrameworkType: .swiftUI) - } - - private func testPropertiesOnInit( - givenFrameworkType: FrameworkType - ) { - // GIVEN - let givenAlignment: ButtonAlignment = .trailingImage - - let isUIKit = givenFrameworkType == .uiKit - - // WHEN - let stub = Stub( - frameworkType: givenFrameworkType, - alignment: givenAlignment - ) - - stub.subscribePublishers(on: &self.subscriptions) - - // THEN - - // Properties - XCTAssertEqual( - stub.viewModel.alignment, - givenAlignment, - "Wrong alignment value" - ) - - // ** - // Published properties - - // Spacings - ButtonViewModelPublisherTest.XCTAssert( - spacings: stub.spacingsPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: !isUIKit ? stub.spacings : nil - ) - - // Is Image Trailing - ButtonViewModelPublisherTest.XCTAssert( - isImageTrailing: stub.isImageTrailingPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: !isUIKit ? givenAlignment.isTrailingImage : false - ) - - // Title Font Token - ButtonViewModelPublisherTest.XCTAssert( - titleFontToken: stub.titleFontTokenPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: !isUIKit ? stub.themeMock.typography.callout as? TypographyFontTokenGeneratedMock : nil - ) - // ** - - // Use Cases - ButtonGetSpacingsUseCaseableMockTest.XCTAssert( - stub.getSpacingsUseCaseMock, - expectedNumberOfCalls: (isUIKit ? 0 : 1), - givenSpacing: stub.themeMock.layout.spacing as? LayoutSpacingGeneratedMock, - expectedReturnValue: stub.spacings - ) - } - - // MARK: - Setter Tests - - func test_set_alignment_with_different_new_value() { - self.testSetAlignment( - givenIsDifferentNewValue: true - ) - } - - func test_set_alignment_with_same_new_value() { - self.testSetAlignment( - givenIsDifferentNewValue: false - ) - } - - private func testSetAlignment( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: ButtonAlignment = .leadingImage - let newValue: ButtonAlignment = givenIsDifferentNewValue ? .trailingImage : defaultValue - - let stub = Stub( - alignment: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.alignment = newValue - - // THEN - - // Properties - XCTAssertEqual( - stub.viewModel.alignment, - newValue, - "Wrong alignment value" - ) - - // ** - // Published properties - - // Spacings - ButtonViewModelPublisherTest.XCTSinksCount( - spacings: stub.spacingsPublisherMock, - expectedNumberOfSinks: 0 - ) - - // Is Image Trailing - ButtonViewModelPublisherTest.XCTAssert( - isImageTrailing: stub.isImageTrailingPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: newValue.isTrailingImage - ) - - // Title Font Token - ButtonViewModelPublisherTest.XCTSinksCount( - titleFontToken: stub.titleFontTokenPublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // Use Cases - ButtonGetSpacingsUseCaseableMockTest.XCTCallsCount( - stub.getSpacingsUseCaseMock, - executeWithSpacingNumberOfCalls: 0 - ) - } - - // MARK: - Update Tests - - func test_updateAll() { - // GIVEN - let givenAlignment: ButtonAlignment = .trailingImage - - let stub = Stub( - alignment: givenAlignment - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.updateAll() - - // THEN - - // ** - // Published properties - - // Spacings - ButtonViewModelPublisherTest.XCTAssert( - spacings: stub.spacingsPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: stub.spacings - ) - - // Is Image Trailing - ButtonViewModelPublisherTest.XCTAssert( - isImageTrailing: stub.isImageTrailingPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: givenAlignment.isTrailingImage - ) - - // Title Font Token - ButtonViewModelPublisherTest.XCTAssert( - titleFontToken: stub.titleFontTokenPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: stub.themeMock.typography.callout as? TypographyFontTokenGeneratedMock - ) - // ** - - // Use Cases - ButtonGetSpacingsUseCaseableMockTest.XCTAssert( - stub.getSpacingsUseCaseMock, - expectedNumberOfCalls: 1, - givenSpacing: stub.themeMock.layout.spacing as? LayoutSpacingGeneratedMock, - expectedReturnValue: stub.spacings - ) - } -} - -private final class Stub: ButtonViewModelStub { - - // MARK: - Properties - - let spacings = ButtonSpacings.mocked() - - let themeMock = ThemeGeneratedMock.mocked() - - // MARK: - Initialization - - init( - frameworkType: FrameworkType = .uiKit, - alignment: ButtonAlignment - ) { - // ** - // Use Cases - let getSpacingsUseCaseMock = ButtonGetSpacingsUseCaseableGeneratedMock() - getSpacingsUseCaseMock.executeWithSpacingReturnValue = self.spacings - // ** - - // ** - // View Model - let viewModel = ButtonViewModel( - for: frameworkType, - theme: self.themeMock, - intent: .main, - variant: .filled, - size: .medium, - shape: .pill, - alignment: alignment, - getSpacingsUseCase: getSpacingsUseCaseMock - ) - // ** - - super.init( - viewModel: viewModel, - getSpacingsUseCaseMock: getSpacingsUseCaseMock - ) - } -} - diff --git a/core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModel.swift b/core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModel.swift deleted file mode 100644 index 4578ad943..000000000 --- a/core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModel.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// IconButtonSUIViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 15/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -final class IconButtonSUIViewModel: IconButtonViewModel, ButtonMainSUIViewModel { - - // MARK: - Properties - - var controlStatus: ControlStatus = .init() - - // MARK: - Published Properties - - @Published private(set) var controlStateImage: ControlStateImage = .init() - @Published private(set) var controlStateText: ControlStateText? - - // MARK: - Initialization - - init( - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape - ) { - super.init( - for: .swiftUI, - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape - ) - } -} - diff --git a/core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModelTests.swift b/core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModelTests.swift deleted file mode 100644 index b50234648..000000000 --- a/core/Sources/Components/Button/ViewModel/Icon/IconButtonSUIViewModelTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// IconButtonSUIViewModelTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 15/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class IconButtonSUIViewModelTests: XCTestCase { - - // MARK: - Init Tests - - func test_default_properties_on_init() { - // GIVEN / WHEN - let viewModel = IconButtonSUIViewModel( - theme: ThemeGeneratedMock.mocked(), - intent: .main, - variant: .filled, - size: .medium, - shape: .rounded - ) - - // THEN - XCTAssertEqual( - viewModel.controlStatus, - .init(), - "Wrong constrol status" - ) - XCTAssertNotNil( - viewModel.controlStateImage, - "Wrong constrol state text" - ) - XCTAssertNil( - viewModel.controlStateText, - "Wrong constrol state text" - ) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Icon/IconButtonViewModel.swift b/core/Sources/Components/Button/ViewModel/Icon/IconButtonViewModel.swift deleted file mode 100644 index 537664371..000000000 --- a/core/Sources/Components/Button/ViewModel/Icon/IconButtonViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// IconButtonViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 13/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -class IconButtonViewModel: ButtonMainViewModel { - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape - ) { - super.init( - for: frameworkType, - type: .iconButton, - theme: theme, - intent: intent, - variant: variant, - size: size, - shape: shape - ) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModel.swift b/core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModel.swift deleted file mode 100644 index 29f1dadbf..000000000 --- a/core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModel.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// ButtonMainSUIViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 15/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -// sourcery: AutoMockable -protocol ButtonMainSUIViewModel { - - // MARK: - Properties - - var controlStatus: ControlStatus { get set } - var controlStateImage: ControlStateImage { get } - var controlStateText: ControlStateText? { get } -} - -extension ButtonMainSUIViewModel where Self: ButtonMainViewModel { - - // MARK: - Setter - - func setImage(_ image: Image?, for state: ControlState) { - self.controlStateImage.setImage( - image, - for: state, - on: self.controlStatus - ) - } - - func setTitle(_ title: String?, for state: ControlState) { - self.controlStateText?.setText( - title, - for: state, - on: self.controlStatus - ) - } - - func setAttributedTitle(_ attributedTitle: AttributedString?, for state: ControlState) { - self.controlStateText?.setAttributedText( - attributedTitle, - for: state, - on: self.controlStatus - ) - } - - func setIsPressed(_ isPressed: Bool) { - self.controlStatus.isHighlighted = isPressed - - self.pressedAction(isPressed) - - self.controlStateImage.updateContent(from: self.controlStatus) - self.controlStateText?.updateContent(from: self.controlStatus) - } - - func setIsDisabled(_ isDisabled: Bool) { - self.controlStatus.isEnabled = !isDisabled - - self.isEnabled = !isDisabled - - self.controlStateImage.updateContent(from: self.controlStatus) - self.controlStateText?.updateContent(from: self.controlStatus) - } - - func setIsSelected(_ isSelected: Bool) { - self.controlStatus.isSelected = isSelected - - self.controlStateImage.updateContent(from: self.controlStatus) - self.controlStateText?.updateContent(from: self.controlStatus) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModelTests.swift b/core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModelTests.swift deleted file mode 100644 index f8df91e6a..000000000 --- a/core/Sources/Components/Button/ViewModel/Main/ButtonMainSUIViewModelTests.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// ButtonMainSUIViewModelTests.swift -// SparkCore -// -// Created by robin.lemaire on 15/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore -import SwiftUI - -final class ButtonMainSUIViewModelTests: XCTestCase { - - // MARK: - Init Tests - - func test_setImage() { - // GIVEN - let imageMock = Image("switchOff") - - let viewModel = ButtonMainSUIViewModelMock() - - // WHEN - viewModel.setImage(imageMock, for: .selected) - viewModel.setIsSelected(true) - - // THEN - XCTAssertEqual( - viewModel.controlStateImage.image, - imageMock, - "Wrong image" - ) - } - - func test_setTitle() { - // GIVEN - let titleMock = "Title" - - let viewModel = ButtonMainSUIViewModelMock() - - // WHEN - viewModel.setTitle(titleMock, for: .selected) - viewModel.setIsSelected(true) - - // THEN - XCTAssertEqual( - viewModel.controlStateText?.text, - titleMock, - "Wrong title" - ) - } - - func test_setAttributedTitle() { - // GIVEN - let titleMock = AttributedString("Title") - - let viewModel = ButtonMainSUIViewModelMock() - - // WHEN - viewModel.setAttributedTitle(titleMock, for: .selected) - viewModel.setIsSelected(true) - - // THEN - XCTAssertEqual( - viewModel.controlStateText?.attributedText, - titleMock, - "Wrong attributed title" - ) - } - - func test_setIsPressed() { - // GIVEN - let isHighlightedMock = true - - let isHighlightedImageMock = Image("switchOff") - let isHighlightedTitleMock = "Title" - - let viewModel = ButtonMainSUIViewModelMock() - - // WHEN - viewModel.setImage(isHighlightedImageMock, for: .highlighted) - viewModel.setTitle(isHighlightedTitleMock, for: .highlighted) - - viewModel.setIsPressed(isHighlightedMock) - - // THEN - XCTAssertEqual( - viewModel.controlStatus.isHighlighted, - isHighlightedMock, - "Wrong controlStatus isHighlighted value on test 1" - ) - XCTAssertEqual( - viewModel.controlStateImage.image, - isHighlightedImageMock, - "Wrong image on test 1" - ) - XCTAssertEqual( - viewModel.controlStateText?.text, - isHighlightedTitleMock, - "Wrong title on test 1" - ) - - // ** - // Reverse the isPressed value - // ** - - // WHEN - viewModel.setIsPressed(!isHighlightedMock) - - // THEN - XCTAssertEqual( - viewModel.controlStatus.isHighlighted, - !isHighlightedMock, - "Wrong controlStatus isHighlighted value on test 2" - ) - XCTAssertNil( - viewModel.controlStateImage.image, - "Wrong image on test 2" - ) - XCTAssertNil( - viewModel.controlStateText?.text, - "Wrong title on test 2" - ) - } - - func test_setIsDisabled() { - // GIVEN - let isEnableddMock = false - - let isDisabledImageMock = Image("switchOff") - let isDisabledTitleMock = "Title" - - let viewModel = ButtonMainSUIViewModelMock() - - // WHEN - viewModel.setImage(isDisabledImageMock, for: .disabled) - viewModel.setTitle(isDisabledTitleMock, for: .disabled) - - viewModel.setIsDisabled(!isEnableddMock) - - // THEN - XCTAssertEqual( - viewModel.isEnabled, - isEnableddMock, - "Wrong isEnabled value on test 1" - ) - XCTAssertEqual( - viewModel.controlStatus.isEnabled, - isEnableddMock, - "Wrong controlStatus isEnabled value on test 1" - ) - XCTAssertEqual( - viewModel.controlStateImage.image, - isDisabledImageMock, - "Wrong image on test 1" - ) - XCTAssertEqual( - viewModel.controlStateText?.text, - isDisabledTitleMock, - "Wrong title on test 1" - ) - - // ** - // Reverse the isDisabled value - // ** - - // WHEN - viewModel.setIsDisabled(isEnableddMock) - - // THEN - XCTAssertEqual( - viewModel.isEnabled, - !isEnableddMock, - "Wrong isEnabled value on test 1" - ) - XCTAssertEqual( - viewModel.controlStatus.isEnabled, - !isEnableddMock, - "Wrong controlStatus isDisabled value on test 2" - ) - XCTAssertNil( - viewModel.controlStateImage.image, - "Wrong image on test 2" - ) - XCTAssertNil( - viewModel.controlStateText?.text, - "Wrong title on test 2" - ) - } - - func test_setIsSelected() { - // GIVEN - let isSelectedMock = true - - let isSelectedImageMock = Image("switchOff") - let isSelectedTitleMock = "Title" - - let viewModel = ButtonMainSUIViewModelMock() - - // WHEN - viewModel.setImage(isSelectedImageMock, for: .selected) - viewModel.setTitle(isSelectedTitleMock, for: .selected) - - viewModel.setIsSelected(isSelectedMock) - - // THEN - XCTAssertEqual( - viewModel.controlStatus.isSelected, - isSelectedMock, - "Wrong controlStatus isSelected value on test 1" - ) - XCTAssertEqual( - viewModel.controlStateImage.image, - isSelectedImageMock, - "Wrong image on test 1" - ) - XCTAssertEqual( - viewModel.controlStateText?.text, - isSelectedTitleMock, - "Wrong title on test 1" - ) - - // ** - // Reverse the isSelected value - // ** - - // WHEN - viewModel.setIsSelected(!isSelectedMock) - - // THEN - XCTAssertEqual( - viewModel.controlStatus.isSelected, - !isSelectedMock, - "Wrong controlStatus isSelected value on test 2" - ) - XCTAssertNil( - viewModel.controlStateImage.image, - "Wrong image on test 2" - ) - XCTAssertNil( - viewModel.controlStateText?.text, - "Wrong title on test 2" - ) - } -} - -// MARK: - Mock - -final class ButtonMainSUIViewModelMock: ButtonMainViewModel, ButtonMainSUIViewModel { - - // MARK: - Properties - - var controlStatus: ControlStatus = .init() - var controlStateImage: ControlStateImage = .init() - var controlStateText: ControlStateText? = .init() - - // MARK: - Initialization - - init() { - super.init( - for: .swiftUI, - type: .button, - theme: ThemeGeneratedMock.mocked(), - intent: .main, - variant: .filled, - size: .medium, - shape: .rounded - ) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModel.swift b/core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModel.swift deleted file mode 100644 index 4a598a9c4..000000000 --- a/core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModel.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// ButtonMainViewModel.swift -// Spark -// -// Created by janniklas.freundt.ext on 09.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine - -/// This ButtonMainViewModel view contains all communs properties and rules for all buttons viewModel. -// sourcery: AutoPublisherTest, AutoViewModelStub -// sourcery: titleFontToken = "Identical" -class ButtonMainViewModel: ObservableObject { - - // MARK: - Properties - - var theme: Theme { - didSet { - self.updateAll() - } - } - - var intent: ButtonIntent { - didSet { - guard self.intent != oldValue else { return } - self.colorsDidUpdate(reloadColorsFromUseCase: true) - } - } - - var variant: ButtonVariant { - didSet { - guard self.variant != oldValue else { return } - self.colorsDidUpdate(reloadColorsFromUseCase: true) - self.borderDidUpdate() - } - } - - var size: ButtonSize { - didSet { - guard self.size != oldValue else { return } - self.sizesDidUpdate() - } - } - - var shape: ButtonShape { - didSet { - guard self.shape != oldValue else { return } - self.borderDidUpdate() - } - } - - var isEnabled: Bool = true { - didSet { - guard self.isEnabled != oldValue else { return } - self.stateDidUpdate() - } - } - - // MARK: - Published Properties - - @Published private(set) var state: ButtonState? - - @Published private(set) var currentColors: ButtonCurrentColors? - - @Published private(set) var sizes: ButtonSizes? - @Published private(set) var border: ButtonBorder? - - // MARK: - Private Properties - - private let frameworkType: FrameworkType - private let type: ButtonType - - private var colors: ButtonColors? - - private var isPressed: Bool = false - - // MARK: - UseCases - - let getBorderUseCase: ButtonGetBorderUseCaseable - let getColorsUseCase: ButtonGetColorsUseCaseable - let getCurrentColorsUseCase: ButtonGetCurrentColorsUseCaseable - let getSizesUseCase: ButtonGetSizesUseCaseable - let getStateUseCase: ButtonGetStateUseCaseable - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - type: ButtonType, - theme: Theme, - intent: ButtonIntent, - variant: ButtonVariant, - size: ButtonSize, - shape: ButtonShape, - getBorderUseCase: ButtonGetBorderUseCaseable = ButtonGetBorderUseCase(), - getColorsUseCase: ButtonGetColorsUseCaseable = ButtonGetColorsUseCase(), - getCurrentColorsUseCase: ButtonGetCurrentColorsUseCaseable = ButtonGetCurrentColorsUseCase(), - getSizesUseCase: ButtonGetSizesUseCaseable = ButtonGetSizesUseCase(), - getStateUseCase: ButtonGetStateUseCaseable = ButtonGetStateUseCase() - ) { - self.frameworkType = frameworkType - self.type = type - self.theme = theme - self.intent = intent - self.variant = variant - self.size = size - self.shape = shape - - self.getBorderUseCase = getBorderUseCase - self.getColorsUseCase = getColorsUseCase - self.getCurrentColorsUseCase = getCurrentColorsUseCase - self.getSizesUseCase = getSizesUseCase - self.getStateUseCase = getStateUseCase - - // Load the values directly on init just for SwiftUI - if frameworkType == .swiftUI { - self.updateAll() - } - } - - // MARK: - Load - - /// Load all published values. Should be called when all published values are subscribed by the view - func load() { - // Update all values when view is ready to receive published values - self.updateAll() - } - - // MARK: - Actions - - func pressedAction(_ isPressed: Bool) { - if self.isPressed != isPressed { - self.isPressed.toggle() - - self.colorsDidUpdate(reloadColorsFromUseCase: false) - } - } - - // MARK: - Update - - internal func updateAll() { - self.stateDidUpdate() - self.colorsDidUpdate(reloadColorsFromUseCase: true) - self.sizesDidUpdate() - self.borderDidUpdate() - } - - private func stateDidUpdate() { - self.state = self.getStateUseCase.execute( - isEnabled: self.isEnabled, - dims: self.theme.dims - ) - } - - private func colorsDidUpdate( - reloadColorsFromUseCase: Bool - ) { - if reloadColorsFromUseCase { - self.colors = self.getColorsUseCase.execute( - theme: self.theme, - intent: self.intent, - variant: self.variant - ) - } - - guard let colors = self.colors else { - return - } - - self.currentColors = self.getCurrentColorsUseCase.execute( - colors: colors, - isPressed: self.isPressed - ) - } - - private func sizesDidUpdate() { - self.sizes = self.getSizesUseCase.execute( - size: self.size, - type: self.type - ) - } - - private func borderDidUpdate() { - self.border = self.getBorderUseCase.execute( - shape: self.shape, - border: self.theme.border, - variant: self.variant - ) - } -} diff --git a/core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModelTests.swift b/core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModelTests.swift deleted file mode 100644 index 949e29237..000000000 --- a/core/Sources/Components/Button/ViewModel/Main/ButtonMainViewModelTests.swift +++ /dev/null @@ -1,907 +0,0 @@ -// -// ButtonMainViewModelTests.swift -// Spark -// -// Created by janniklas.freundt.ext on 25.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore -import Combine - -final class ButtonMainViewModelTests: XCTestCase { - - // MARK: - Properties - - private var subscriptions = Set() - - // MARK: - Setup - - override func tearDown() { - super.tearDown() - - // Clear publishers - self.subscriptions.removeAll() - } - - // MARK: - Init Tests - - func test_init_when_frameworkType_is_UIKit() { - self.testAllData(for: .initForUIKit) - } - - func test_init_when_frameworkType_is_SwiftUI() { - self.testAllData(for: .initForSwiftUI) - } - - // MARK: - Load Tests - - func test_load() { - self.testAllData(for: .load) - } - - // MARK: - Actions Tests - - func test_pressedAction_when_viewModel_is_not_already_pressed_state() { - self.testPressedAction( - isPressedAction: true, - isAlreadyOnPressedState: false - ) - } - - func test_pressedAction_when_viewModel_is_already_pressed_state() { - self.testPressedAction( - isPressedAction: true, - isAlreadyOnPressedState: true - ) - } - - func test_unpressedAction_when_viewModel_is_not_already_unpressed_state() { - self.testPressedAction( - isPressedAction: false, - isAlreadyOnPressedState: false - ) - } - - func test_unpressedAction_when_viewModel_is_already_unpressed_state() { - self.testPressedAction( - isPressedAction: false, - isAlreadyOnPressedState: true - ) - } - - private func testPressedAction( - isPressedAction: Bool, - isAlreadyOnPressedState: Bool - ) { - // GIVEN - let stub = Stub() - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load()// Needed to get colors from usecase one time - - // Simulate action to toggle the isPressed value on viewModel - if isPressedAction { - if isAlreadyOnPressedState { - viewModel.pressedAction(true) - } - } else { - viewModel.pressedAction(!isAlreadyOnPressedState) - } - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - viewModel.pressedAction(isPressedAction) - - // THEN - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTSinksCount( - state: stub.statePublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTAssert( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: !isAlreadyOnPressedState ? 1 : 0, - expectedValue: stub.currentColorsMock - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - border: stub.borderPublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTCallsCount( - stub.getBorderUseCaseMock, - executeWithShapeAndBorderAndVariantNumberOfCalls: 0 - ) - ButtonGetColorsUseCaseableMockTest.XCTCallsCount( - stub.getColorsUseCaseMock, - executeWithThemeAndIntentAndVariantNumberOfCalls: 0 - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTAssert( - stub.getCurrentColorsUseCaseMock, - expectedNumberOfCalls: !isAlreadyOnPressedState ? 1 : 0, - givenColors: stub.colorsMock, - givenIsPressed: isPressedAction, - expectedReturnValue: stub.currentColorsMock - ) - ButtonGetSizesUseCaseableMockTest.XCTCallsCount( - stub.getSizesUseCaseMock, - executeWithSizeAndTypeNumberOfCalls: 0 - ) - ButtonGetStateUseCaseableMockTest.XCTCallsCount( - stub.getStateUseCaseMock, - executeWithIsEnabledAndDimsNumberOfCalls: 0 - ) - // ** - } - - // MARK: - Update All - - func test_updateAll() { - self.testAllData(for: .updateAll) - } - - // MARK: - Setter Tests - - func test_set_theme() { - self.testAllData(for: .setTheme) - } - - func test_set_intent_with_different_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: true - ) - } - - func test_set_intent_with_same_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: false - ) - } - - private func testSetIntent( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: ButtonIntent = .alert - let newValue: ButtonIntent = givenIsDifferentNewValue ? .accent : defaultValue - - let variantMock: ButtonVariant = .outlined - - let stub = Stub( - intent: defaultValue, - variant: variantMock - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - viewModel.intent = newValue - - // THEN - XCTAssertEqual( - viewModel.intent, - newValue, - "Wrong intent value" - ) - - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTSinksCount( - state: stub.statePublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTAssert( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: stub.currentColorsMock - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - border: stub.borderPublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTCallsCount( - stub.getBorderUseCaseMock, - executeWithShapeAndBorderAndVariantNumberOfCalls: 0 - ) - ButtonGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenTheme: stub.themeMock, - givenIntent: newValue, - givenVariant: variantMock, - expectedReturnValue: stub.colorsMock - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTAssert( - stub.getCurrentColorsUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenColors: stub.colorsMock, - givenIsPressed: false, - expectedReturnValue: stub.currentColorsMock - ) - ButtonGetSizesUseCaseableMockTest.XCTCallsCount( - stub.getSizesUseCaseMock, - executeWithSizeAndTypeNumberOfCalls: 0 - ) - ButtonGetStateUseCaseableMockTest.XCTCallsCount( - stub.getStateUseCaseMock, - executeWithIsEnabledAndDimsNumberOfCalls: 0 - ) - // ** - } - - func test_set_variant_with_different_new_value() { - self.testSetVariant( - givenIsDifferentNewValue: true - ) - } - - func test_set_variant_with_same_new_value() { - self.testSetVariant( - givenIsDifferentNewValue: false - ) - } - - private func testSetVariant( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: ButtonVariant = .contrast - let newValue: ButtonVariant = givenIsDifferentNewValue ? .outlined : defaultValue - - let intentMock: ButtonIntent = .success - let shapeMock: ButtonShape = .square - - let stub = Stub( - intent: intentMock, - variant: defaultValue, - shape: shapeMock - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - viewModel.variant = newValue - - // THEN - XCTAssertEqual( - viewModel.variant, - newValue, - "Wrong variant value" - ) - - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTSinksCount( - state: stub.statePublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTAssert( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: stub.currentColorsMock - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTAssert( - border: stub.borderPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: stub.borderMock - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTAssert( - stub.getBorderUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenShape: shapeMock, - givenBorder: stub.themeMock.border as? BorderGeneratedMock, - givenVariant: newValue, - expectedReturnValue: stub.borderMock - ) - ButtonGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenTheme: stub.themeMock, - givenIntent: intentMock, - givenVariant: newValue, - expectedReturnValue: stub.colorsMock - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTAssert( - stub.getCurrentColorsUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenColors: stub.colorsMock, - givenIsPressed: false, - expectedReturnValue: stub.currentColorsMock - ) - ButtonGetSizesUseCaseableMockTest.XCTCallsCount( - stub.getSizesUseCaseMock, - executeWithSizeAndTypeNumberOfCalls: 0 - ) - ButtonGetStateUseCaseableMockTest.XCTCallsCount( - stub.getStateUseCaseMock, - executeWithIsEnabledAndDimsNumberOfCalls: 0 - ) - // ** - } - - func test_set_size_with_different_new_value() { - self.testSetSize( - givenIsDifferentNewValue: true - ) - } - - func test_set_size_with_same_new_value() { - self.testSetSize( - givenIsDifferentNewValue: false - ) - } - - private func testSetSize( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: ButtonSize = .large - let newValue: ButtonSize = givenIsDifferentNewValue ? .small : defaultValue - - let typeMock: ButtonType = .button - - let stub = Stub( - type: typeMock, - size: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - viewModel.size = newValue - - // THEN - XCTAssertEqual( - viewModel.size, - newValue, - "Wrong size value" - ) - - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTSinksCount( - state: stub.statePublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTAssert( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: stub.sizesMock - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - border: stub.borderPublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTCallsCount( - stub.getBorderUseCaseMock, - executeWithShapeAndBorderAndVariantNumberOfCalls: 0 - ) - ButtonGetColorsUseCaseableMockTest.XCTCallsCount( - stub.getColorsUseCaseMock, - executeWithThemeAndIntentAndVariantNumberOfCalls: 0 - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTCallsCount( - stub.getCurrentColorsUseCaseMock, - executeWithColorsAndIsPressedNumberOfCalls: 0 - ) - ButtonGetSizesUseCaseableMockTest.XCTAssert( - stub.getSizesUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenSize: newValue, - givenType: typeMock, - expectedReturnValue: stub.sizesMock - ) - ButtonGetStateUseCaseableMockTest.XCTCallsCount( - stub.getStateUseCaseMock, - executeWithIsEnabledAndDimsNumberOfCalls: 0 - ) - // ** - } - - func test_set_shape_with_different_new_value() { - self.testSetShape( - givenIsDifferentNewValue: true - ) - } - - func test_set_shape_with_same_new_value() { - self.testSetShape( - givenIsDifferentNewValue: false - ) - } - - private func testSetShape( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: ButtonShape = .pill - let newValue: ButtonShape = givenIsDifferentNewValue ? .rounded : defaultValue - - let variantMock: ButtonVariant = .tinted - - let stub = Stub( - variant: variantMock, - shape: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - viewModel.shape = newValue - - // THEN - XCTAssertEqual( - viewModel.shape, - newValue, - "Wrong shape value" - ) - - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTSinksCount( - state: stub.statePublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTAssert( - border: stub.borderPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: stub.borderMock - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTAssert( - stub.getBorderUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenShape: newValue, - givenBorder: stub.themeMock.border as? BorderGeneratedMock, - givenVariant: variantMock, - expectedReturnValue: stub.borderMock - ) - ButtonGetColorsUseCaseableMockTest.XCTCallsCount( - stub.getColorsUseCaseMock, - executeWithThemeAndIntentAndVariantNumberOfCalls: 0 - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTCallsCount( - stub.getCurrentColorsUseCaseMock, - executeWithColorsAndIsPressedNumberOfCalls: 0 - ) - ButtonGetSizesUseCaseableMockTest.XCTCallsCount( - stub.getSizesUseCaseMock, - executeWithSizeAndTypeNumberOfCalls: 0 - ) - ButtonGetStateUseCaseableMockTest.XCTCallsCount( - stub.getStateUseCaseMock, - executeWithIsEnabledAndDimsNumberOfCalls: 0 - ) - // ** - } - - func test_set_isEnabled_with_different_new_value() { - self.testSetIsEnabled( - givenIsDifferentNewValue: true - ) - } - - func test_set_isEnabled_with_same_new_value() { - self.testSetIsEnabled( - givenIsDifferentNewValue: false - ) - } - - private func testSetIsEnabled( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: Bool = true - let newValue: Bool = givenIsDifferentNewValue ? false : defaultValue - - let stub = Stub() - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - viewModel.isEnabled = newValue - - // THEN - XCTAssertEqual( - viewModel.isEnabled, - newValue, - "Wrong isEnabled value" - ) - - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTAssert( - state: stub.statePublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: stub.stateMock - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: 0 - ) - ButtonMainViewModelPublisherTest.XCTSinksCount( - border: stub.borderPublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTCallsCount( - stub.getBorderUseCaseMock, - executeWithShapeAndBorderAndVariantNumberOfCalls: 0 - ) - ButtonGetColorsUseCaseableMockTest.XCTCallsCount( - stub.getColorsUseCaseMock, - executeWithThemeAndIntentAndVariantNumberOfCalls: 0 - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTCallsCount( - stub.getCurrentColorsUseCaseMock, - executeWithColorsAndIsPressedNumberOfCalls: 0 - ) - ButtonGetSizesUseCaseableMockTest.XCTCallsCount( - stub.getSizesUseCaseMock, - executeWithSizeAndTypeNumberOfCalls: 0 - ) - ButtonGetStateUseCaseableMockTest.XCTAssert( - stub.getStateUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenIsEnabled: newValue, - givenDims: stub.themeMock.dims as? DimsGeneratedMock, - expectedReturnValue: stub.stateMock - ) - // ** - } - - // MARK: - All Data - - private func testAllData(for testAllDataType: TestAllDataType) { - // GIVEN - let typeMock: ButtonType = .button - let intentMock: ButtonIntent = .success - let variantMock: ButtonVariant = .outlined - let sizeMock: ButtonSize = .large - let shapeMock: ButtonShape = .square - - let stub = Stub( - for: testAllDataType.frameworkType, - type: typeMock, - intent: intentMock, - variant: variantMock, - size: sizeMock, - shape: shapeMock - ) - let viewModel = stub.viewModel - - let themeMock = (testAllDataType == .setTheme) ? ThemeGeneratedMock.mocked() : stub.themeMock - - stub.subscribePublishers(on: &self.subscriptions) - - // WHEN - if testAllDataType.callResetMockedDataBeforeLoad { - stub.resetMockedData() - } - - if testAllDataType.callLoad { - viewModel.load() - } - - if testAllDataType.callResetMockedDataAfterLoad { - stub.resetMockedData() - } - - switch testAllDataType { - case .updateAll: - viewModel.updateAll() - case .setTheme: - viewModel.theme = themeMock - default: - break - } - - // THEN - XCTAssertIdentical( - viewModel.theme as? ThemeGeneratedMock, - themeMock, - "Wrong theme value" - ) - XCTAssertEqual( - viewModel.intent, - intentMock, - "Wrong intent value" - ) - XCTAssertEqual( - viewModel.variant, - variantMock, - "Wrong variant value" - ) - XCTAssertEqual( - viewModel.size, - sizeMock, - "Wrong size value" - ) - XCTAssertEqual( - viewModel.shape, - shapeMock, - "Wrong shape value" - ) - XCTAssertEqual( - viewModel.isEnabled, - true, - "Wrong default isEnabled value" - ) - - // ** - // Published properties - ButtonMainViewModelPublisherTest.XCTAssert( - state: stub.statePublisherMock, - expectedNumberOfSinks: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - expectedValue: stub.stateMock - ) - ButtonMainViewModelPublisherTest.XCTAssert( - currentColors: stub.currentColorsPublisherMock, - expectedNumberOfSinks: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - expectedValue: stub.currentColorsMock - ) - ButtonMainViewModelPublisherTest.XCTAssert( - sizes: stub.sizesPublisherMock, - expectedNumberOfSinks: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - expectedValue: stub.sizesMock - ) - ButtonMainViewModelPublisherTest.XCTAssert( - border: stub.borderPublisherMock, - expectedNumberOfSinks: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - expectedValue: stub.borderMock - ) - // ** - - // ** - // Use Cases - ButtonGetBorderUseCaseableMockTest.XCTAssert( - stub.getBorderUseCaseMock, - expectedNumberOfCalls: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - givenShape: shapeMock, - givenBorder: themeMock.border as? BorderGeneratedMock, - givenVariant: variantMock, - expectedReturnValue: stub.borderMock - ) - ButtonGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - givenTheme: themeMock, - givenIntent: intentMock, - givenVariant: variantMock, - expectedReturnValue: stub.colorsMock - ) - ButtonGetCurrentColorsUseCaseableMockTest.XCTAssert( - stub.getCurrentColorsUseCaseMock, - expectedNumberOfCalls: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - givenColors: stub.colorsMock, - givenIsPressed: false, - expectedReturnValue: stub.currentColorsMock - ) - ButtonGetSizesUseCaseableMockTest.XCTAssert( - stub.getSizesUseCaseMock, - expectedNumberOfCalls: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - givenSize: sizeMock, - givenType: typeMock, - expectedReturnValue: stub.sizesMock - ) - ButtonGetStateUseCaseableMockTest.XCTAssert( - stub.getStateUseCaseMock, - expectedNumberOfCalls: testAllDataType.expectedCalledPropertiesAndUseCases ? 1 : 0, - givenIsEnabled: true, - givenDims: themeMock.dims as? DimsGeneratedMock, - expectedReturnValue: stub.stateMock - ) - // ** - } -} - -// MARK: - Stub - -private final class Stub: ButtonMainViewModelStub { - - // MARK: - Data Properties - - let themeMock = ThemeGeneratedMock.mocked() - - let borderMock = ButtonBorder.mocked() - let colorsMock = ButtonColors.mocked() - let currentColorsMock = ButtonCurrentColors.mocked() - let sizesMock = ButtonSizes.mocked() - let stateMock = ButtonState.mocked() - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType = .uiKit, - type: ButtonType = .button, - intent: ButtonIntent = .main, - variant: ButtonVariant = .tinted, - size: ButtonSize = .medium, - shape: ButtonShape = .rounded - ) { - // ** - // Use Cases - let getBorderUseCaseMock = ButtonGetBorderUseCaseableGeneratedMock() - getBorderUseCaseMock.executeWithShapeAndBorderAndVariantReturnValue = self.borderMock - - let getColorsUseCaseMock = ButtonGetColorsUseCaseableGeneratedMock() - getColorsUseCaseMock.executeWithThemeAndIntentAndVariantReturnValue = self.colorsMock - - let getCurrentColorsUseCaseMock = ButtonGetCurrentColorsUseCaseableGeneratedMock() - getCurrentColorsUseCaseMock.executeWithColorsAndIsPressedReturnValue = self.currentColorsMock - - let getSizesUseCaseMock = ButtonGetSizesUseCaseableGeneratedMock() - getSizesUseCaseMock.executeWithSizeAndTypeReturnValue = self.sizesMock - - let getStateUseCaseMock = ButtonGetStateUseCaseableGeneratedMock() - getStateUseCaseMock.executeWithIsEnabledAndDimsReturnValue = self.stateMock - // ** - - let viewModel = ButtonMainViewModel( - for: frameworkType, - type: .button, - theme: self.themeMock, - intent: intent, - variant: variant, - size: size, - shape: shape, - getBorderUseCase: getBorderUseCaseMock, - getColorsUseCase: getColorsUseCaseMock, - getCurrentColorsUseCase: getCurrentColorsUseCaseMock, - getSizesUseCase: getSizesUseCaseMock, - getStateUseCase: getStateUseCaseMock - ) - - super.init( - viewModel: viewModel, - getBorderUseCaseMock: getBorderUseCaseMock, - getColorsUseCaseMock: getColorsUseCaseMock, - getCurrentColorsUseCaseMock: getCurrentColorsUseCaseMock, - getSizesUseCaseMock: getSizesUseCaseMock, - getStateUseCaseMock: getStateUseCaseMock - ) - } -} - -// MARK: - Enum - -private enum TestAllDataType { - case initForSwiftUI - case initForUIKit - case load - case updateAll - case setTheme - - // MARK: - Properties - - var frameworkType: FrameworkType { - switch self { - case .initForSwiftUI: - return .swiftUI - default: - return .uiKit - } - } - - var callLoad: Bool { - switch self { - case .initForUIKit, .load: - return true - default: - return false - } - } - - var callResetMockedDataBeforeLoad: Bool { - switch self { - case .load: - return true - default: - return false - } - } - - var callResetMockedDataAfterLoad: Bool { - switch self { - case .initForSwiftUI, .load: - return false - default: - return true - } - } - - var expectedCalledPropertiesAndUseCases: Bool { - switch self { - case .initForUIKit: - return false - default: - return true - } - } -} diff --git a/core/Sources/Components/Checkbox/AccessibilityIdentifier/CheckboxAccessibilityIdentifier.swift b/core/Sources/Components/Checkbox/AccessibilityIdentifier/CheckboxAccessibilityIdentifier.swift deleted file mode 100644 index 04bfd76f9..000000000 --- a/core/Sources/Components/Checkbox/AccessibilityIdentifier/CheckboxAccessibilityIdentifier.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CheckboxAccessibilityIdentifier.swift -// SparkCore -// -// Created by michael.zimmermann on 15.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the checkbox. -public enum CheckboxAccessibilityIdentifier { - /// The default checkbox accessibility identifier. - public static let checkbox = "spark-check-box" - /// The default checkbox group accessibility identifier. - public static let checkboxGroup = "spark-check-box-group" - /// The identifier of checkbox group ui view title - public static let checkboxGroupTitle = "spark-check-box-group-title" - /// The default checkbox group item accessibility identifier. - public static func checkboxGroupItem(_ id: String) -> String { - Self.checkbox + "-\(id)" - } -} - -public enum CheckboxAccessibilityValue { - public static let checked = "1" - public static let indeterminate = "0.5" - public static let unchecked = "0" -} diff --git a/core/Sources/Components/Checkbox/Enum/CheckboxAlignment.swift b/core/Sources/Components/Checkbox/Enum/CheckboxAlignment.swift deleted file mode 100644 index 977bb14db..000000000 --- a/core/Sources/Components/Checkbox/Enum/CheckboxAlignment.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CheckboxAlignment.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 17.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The checkbox can be either on the leading or trailing edge of the view. -public enum CheckboxAlignment: CaseIterable { - /// Checkbox on leading edge. - case left - - /// Checkbox on trailing edge. - case right -} diff --git a/core/Sources/Components/Checkbox/Enum/CheckboxGroupLayout.swift b/core/Sources/Components/Checkbox/Enum/CheckboxGroupLayout.swift deleted file mode 100644 index 154be0a04..000000000 --- a/core/Sources/Components/Checkbox/Enum/CheckboxGroupLayout.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CheckboxGroupLayout.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 11.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Enum describing layout options for checkbox groups. Currently horizontal and vertical layouts are supported. -@frozen -public enum CheckboxGroupLayout { - /// Horizontal layout. - case horizontal - - /// Vertical layout. - case vertical -} diff --git a/core/Sources/Components/Checkbox/Enum/CheckboxIntent.swift b/core/Sources/Components/Checkbox/Enum/CheckboxIntent.swift deleted file mode 100644 index 47f27b2cf..000000000 --- a/core/Sources/Components/Checkbox/Enum/CheckboxIntent.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CheckboxIntent.swift -// SparkCore -// -// Created by alican.aycil on 12.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The various intent color a checkbox may have. -public enum CheckboxIntent: CaseIterable { - case basic - case accent - case error - case success - case alert - case info - case neutral - case support - case main -} diff --git a/core/Sources/Components/Checkbox/Enum/CheckboxSelectionState.swift b/core/Sources/Components/Checkbox/Enum/CheckboxSelectionState.swift deleted file mode 100644 index 63bb2dc4d..000000000 --- a/core/Sources/Components/Checkbox/Enum/CheckboxSelectionState.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CheckboxSelectionState.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 04.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Enum describing Checkbox selection states. -@frozen -public enum CheckboxSelectionState: CaseIterable { - /// Checkbox is selected. - case selected - - /// Checkbox is partly selected (indeterminate). (E.g. of a given category only a subset of sub-categories is selected.) - case indeterminate - - /// Checkbox is unselected. - case unselected -} diff --git a/core/Sources/Components/Checkbox/Enum/SelectButtonState.swift b/core/Sources/Components/Checkbox/Enum/SelectButtonState.swift deleted file mode 100644 index 712fb68cc..000000000 --- a/core/Sources/Components/Checkbox/Enum/SelectButtonState.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CheckboxState.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 11.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -@available(*, deprecated, message: "isEnabled: Bool parameter will be used instead of this.") -/// "isEnabled" Bool parameter is used instead of this enum. -public enum SelectButtonState: CaseIterable { - case enabled - case disabled -} diff --git a/core/Sources/Components/Checkbox/Model/CheckboxColors.swift b/core/Sources/Components/Checkbox/Model/CheckboxColors.swift deleted file mode 100644 index 23b2b74dc..000000000 --- a/core/Sources/Components/Checkbox/Model/CheckboxColors.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CheckboxColorables.swift -// Spark -// -// Created by janniklas.freundt.ext on 04.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct CheckboxColors { - let textColor: any ColorToken - let borderColor: any ColorToken - let tintColor: any ColorToken - let iconColor: any ColorToken - let pressedBorderColor: any ColorToken -} diff --git a/core/Sources/Components/Checkbox/Model/CheckboxGroupItemProtocol.swift b/core/Sources/Components/Checkbox/Model/CheckboxGroupItemProtocol.swift deleted file mode 100644 index f16f987d7..000000000 --- a/core/Sources/Components/Checkbox/Model/CheckboxGroupItemProtocol.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CheckboxGroupItemProtocol.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 12.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The protocol is used for items in checkbox groups. It describes a single item within a checkbox group. -public protocol CheckboxGroupItemProtocol: Hashable { - /// The checkbox title. - /// Either a standard title or an attributed title can be set on the UIKit checkbox. If both are set here in the model, then the standard title has precedence. - /// In SwiftUI only a standard title is accepted. The attributed title will be ignored. - var title: String? { get set } - - /// The attributed checkbox title. - /// The attributed title can not be set in the SwiftUI component. - var attributedTitle: NSAttributedString? { get set } - - /// The checkbox identifier. - var id: String { get set } - - /// The current selection state of the checkbox. - var selectionState: CheckboxSelectionState { get set } - - /// The current control state of the checkbox. - var isEnabled: Bool { get set } -} diff --git a/core/Sources/Components/Checkbox/Model/CheckboxGroupViewModel.swift b/core/Sources/Components/Checkbox/Model/CheckboxGroupViewModel.swift deleted file mode 100644 index cb38f024d..000000000 --- a/core/Sources/Components/Checkbox/Model/CheckboxGroupViewModel.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// CheckboxGroupViewModel.swift -// SparkCore -// -// Created by alican.aycil on 22.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -final class CheckboxGroupViewModel: ObservableObject { - - // MARK: - Internal properties - @Published var title: String? - @Published var checkedImage: Image - @Published var layout: CheckboxGroupLayout - @Published var spacing: LayoutSpacing - @Published var titleFont: TypographyFontToken - @Published var titleColor: any ColorToken - @Published var intent: CheckboxIntent - @Published var alignment: CheckboxAlignment - var theme: Theme - - // MARK: - Init - init( - title: String?, - checkedImage: Image, - theme: Theme, - intent: CheckboxIntent = .main, - alignment: CheckboxAlignment = .left, - layout: CheckboxGroupLayout = .vertical - ) { - self.title = title - self.checkedImage = checkedImage - self.theme = theme - self.intent = intent - self.alignment = alignment - self.layout = layout - self.spacing = theme.layout.spacing - self.titleFont = theme.typography.subhead - self.titleColor = theme.colors.base.onSurface - } - - func calculateSingleCheckboxWidth(string: String?) -> CGFloat { - let font: UIFont = self.theme.typography.body1.uiFont - let textWidth: CGFloat = string?.widthOfString(usingFont: font) ?? 0 - let spacing: CGFloat = CheckboxGetSpacingUseCase().execute(layoutSpacing: self.theme.layout.spacing, alignment: self.alignment) - let checkboxControlSize: CGFloat = CheckboxView.Constants.checkboxSize - return checkboxControlSize + spacing + textWidth as CGFloat - } -} - -private extension String { - func widthOfString(usingFont font: UIFont?) -> CGFloat { - if let font = font { - let fontAttributes = [NSAttributedString.Key.font: font] - let size = self.size(withAttributes: fontAttributes) - return size.width - } - return 0 - } -} diff --git a/core/Sources/Components/Checkbox/Model/CheckboxGroupViewModelTests.swift b/core/Sources/Components/Checkbox/Model/CheckboxGroupViewModelTests.swift deleted file mode 100644 index cfa6affaf..000000000 --- a/core/Sources/Components/Checkbox/Model/CheckboxGroupViewModelTests.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// CheckboxGroupViewModelTests.swift -// SparkCoreUnitTests -// -// Created by alican.aycil on 22.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import UIKit -import SwiftUI -@testable import SparkCore - -final class CheckboxGroupViewModelTests: XCTestCase { - - var theme: ThemeGeneratedMock! - var checkedImage = IconographyTests.shared.checkmark - var sut: CheckboxGroupViewModel! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.theme = ThemeGeneratedMock.mock - self.sut = CheckboxGroupViewModel( - title: "Title", - checkedImage: Image(uiImage: self.checkedImage), - theme: self.theme - ) - } - - // MARK: - Tests - func test_init() throws { - - // Then - XCTAssertIdentical(sut.theme as? ThemeGeneratedMock, - self.theme, - "Wrong typography value") - - XCTAssertEqual(sut.title, "Title", "text does not match") - XCTAssertEqual(sut.checkedImage, Image(uiImage: self.checkedImage), "Checked image does not match") - XCTAssertEqual(sut.alignment, .left, "Alignment does not match") - XCTAssertEqual(sut.layout, .vertical, "Layout does not match") - XCTAssertEqual(sut.intent, .main, "Intent does not match") - XCTAssertEqual(sut.titleFont.uiFont, self.theme.typography.subhead.uiFont, "Title font does not match" ) - XCTAssertEqual(sut.titleColor.uiColor, self.theme.colors.base.onSurface.uiColor, "Title color does not match" ) - } - - func test_singleCheckbox_width() throws { - // Given - let text = "This is the way" - let width: CGFloat = self.calculateCheckboxWidth(string: text) - - // Then - let viewModelWidth: CGFloat = self.sut.calculateSingleCheckboxWidth(string: text) - - XCTAssertEqual(width, viewModelWidth, "Single checkbox calculation is wrong for SwiftUI Side") - } - - func calculateCheckboxWidth(string: String?) -> CGFloat { - - let checkboxViewModel = CheckboxViewModel( - text: .left(NSAttributedString(string: "Text")), - checkedImage: .left(self.checkedImage), - theme: self.theme, - selectionState: .selected - ) - - let spacing: CGFloat = checkboxViewModel.spacing - let checkboxControlSize: CGFloat = CheckboxView.Constants.checkboxSize - let font: UIFont = checkboxViewModel.font.uiFont - let fontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] - let textSize: CGSize? = (string as? NSString)?.size(withAttributes: fontAttributes) - let textWidth: CGFloat = textSize?.width ?? 0 - return checkboxControlSize + spacing + textWidth - } -} - -private extension Theme where Self == ThemeGeneratedMock { - static var mock: Self { - let theme = ThemeGeneratedMock() - - theme.colors = ColorsGeneratedMock.mocked() - theme.layout = LayoutGeneratedMock.mocked() - theme.dims = DimsGeneratedMock.mocked() - theme.border = BorderGeneratedMock.mocked() - - let typography = TypographyGeneratedMock() - typography.body1 = TypographyFontTokenGeneratedMock.mocked(.systemFont(ofSize: 14)) - typography.subhead = TypographyFontTokenGeneratedMock.mocked(.subheadline) - theme.typography = typography - - return theme - } -} diff --git a/core/Sources/Components/Checkbox/Model/CheckboxViewModel.swift b/core/Sources/Components/Checkbox/Model/CheckboxViewModel.swift deleted file mode 100644 index 7740b7ccc..000000000 --- a/core/Sources/Components/Checkbox/Model/CheckboxViewModel.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// CheckboxViewModel.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 05.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -final class CheckboxViewModel: ObservableObject { - - // MARK: - Internal properties - @Published var text: Either - @Published var checkedImage: Either - @Published var colors: CheckboxColors - @Published var alignment: CheckboxAlignment { - didSet { - self.updateSpacing() - } - } - - @Published var selectionState: CheckboxSelectionState - @Published var opacity: CGFloat - @Published var spacing: CGFloat - @Published var font: TypographyFontToken - - @Published var intent: CheckboxIntent { - didSet { - guard oldValue != intent else { return } - self.updateColors() - } - } - - var isEnabled: Bool { - didSet { - guard self.isEnabled != oldValue else { return } - self.updateOpacity() - } - } - - var theme: Theme { - didSet { - self.font = self.theme.typography.body1 - self.updateColors() - self.updateOpacity() - self.updateSpacing() - } - } - - // MARK: - Private properties - private let colorsUseCase: CheckboxColorsUseCaseable - private let spacingUseCase: CheckboxGetSpacingUseCaseable - - // MARK: - Init - - init( - text: Either, - checkedImage: Either, - theme: Theme, - intent: CheckboxIntent = .main, - colorsUseCase: CheckboxColorsUseCaseable = CheckboxColorsUseCase(), - spacingUseCase: CheckboxGetSpacingUseCaseable = CheckboxGetSpacingUseCase(), - isEnabled: Bool = true, - alignment: CheckboxAlignment = .left, - selectionState: CheckboxSelectionState - ) { - self.text = text - self.checkedImage = checkedImage - self.theme = theme - self.isEnabled = isEnabled - self.colorsUseCase = colorsUseCase - self.colors = colorsUseCase.execute( - from: theme.colors, - intent: intent - ) - self.intent = intent - self.alignment = alignment - self.selectionState = selectionState - self.opacity = self.isEnabled ? self.theme.dims.none : self.theme.dims.dim3 - self.spacing = spacingUseCase.execute(layoutSpacing: theme.layout.spacing, alignment: alignment) - self.spacingUseCase = spacingUseCase - self.font = self.theme.typography.body1 - } - - // MARK: - Methods - - private func updateColors() { - self.colors = self.colorsUseCase.execute( - from: self.theme.colors, - intent: self.intent - ) - } - - private func updateOpacity() { - self.opacity = self.isEnabled ? self.theme.dims.none : self.theme.dims.dim3 - } - - private func updateSpacing() { - self.spacing = spacingUseCase.execute(layoutSpacing: self.theme.layout.spacing, alignment: self.alignment) - } -} diff --git a/core/Sources/Components/Checkbox/Model/CheckboxViewModelTests.swift b/core/Sources/Components/Checkbox/Model/CheckboxViewModelTests.swift deleted file mode 100644 index 69ed9f210..000000000 --- a/core/Sources/Components/Checkbox/Model/CheckboxViewModelTests.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// CheckboxViewModelTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 17.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import XCTest -@testable import SparkCore - -// swiftlint:disable force_unwrapping -final class CheckboxViewModelTests: XCTestCase { - - var theme: ThemeGeneratedMock! - var cancellable = Set() - var checkedImage = IconographyTests.shared.checkmark - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.theme = ThemeGeneratedMock.mocked() - } - - // MARK: - Tests - func test_init() throws { - let states = [true, false] - - for state in states { - // Given - let viewModel = sut(isEnabled: state) - - // Then - XCTAssertEqual(state, viewModel.isEnabled, "wrong state") - XCTAssertNotNil(viewModel.theme, "no theme set") - XCTAssertNotNil(viewModel.colors, "no colors set") - XCTAssertNotNil(viewModel.intent, "no intents set") - XCTAssertNotNil(viewModel.alignment, "no alignment set") - - XCTAssertIdentical(viewModel.theme as? ThemeGeneratedMock, - self.theme, - "Wrong typography value") - - XCTAssertEqual(viewModel.text.leftValue?.string, "Text", "text does not match") - XCTAssertEqual(viewModel.checkedImage.leftValue, self.checkedImage, "Checked image does not match") - XCTAssertEqual(viewModel.alignment, .left, "Alignment does not match") - XCTAssertEqual(viewModel.intent, .main, "Intent does not match") - XCTAssertEqual(viewModel.selectionState, .unselected, "Selection state does not match") - } - } - - func test_opacity() throws { - // Given - let opacities = self.sutValues(for: \.opacity) - - // Then - XCTAssertEqual(opacities, [self.theme.dims.none, self.theme.dims.dim3]) - } - - func test_isEnabled() { - // Given - let disabledStates = self.sutValues(for: \.isEnabled) - - // Then - XCTAssertEqual(disabledStates, [true, false]) - } - - func test_text() { - // Given - let sut = self.sut(isEnabled: true, attributeText: NSAttributedString("Text")) - - // When - sut.text = .right("Text") - - // Then - XCTAssertNotNil(sut.text.rightValue) - } - - func test_attributeText() { - // Given - let sut = self.sut(isEnabled: true) - - // When - sut.text = .left(NSAttributedString("Text")) - - // Then - XCTAssertNotNil(sut.text.leftValue) - } - - func test_updateColorsMethod_afterIntentIsSet() async { - // Given - let sut = self.sut(isEnabled: true, attributeText: NSAttributedString("Text")) - var isColorsUpdated = false - // When - sut.intent = .basic - - let expectation = expectation(description: "Colors are updates") - - sut.$colors.sink(receiveValue: { _ in - isColorsUpdated = true - expectation.fulfill() - }) - .store(in: &cancellable) - - await fulfillment(of: [expectation], timeout: 2.0) - - // Then - XCTAssertTrue(isColorsUpdated) - } - - // MARK: - Private Helper Functions - private func sutValues(for keyPath: KeyPath) -> [T] { - // Given - let statesToTest: [Bool] = [true, false] - - return statesToTest - .map{ self.sut(isEnabled: $0)} - .map{ $0[keyPath: keyPath] } - } - - private func sut(isEnabled: Bool, attributeText: NSAttributedString? = nil) -> CheckboxViewModel { - return CheckboxViewModel( - text: attributeText == nil ? .left(NSAttributedString("Text")) : .left(attributeText!), - checkedImage: .left(self.checkedImage), - theme: self.theme, - isEnabled: isEnabled, - selectionState: .unselected - ) - } -} - -private extension Theme where Self == ThemeGeneratedMock { - static var mock: Self { - let theme = ThemeGeneratedMock() - let colors = ColorsGeneratedMock() - - colors.base = ColorsBaseGeneratedMock.mocked() - colors.main = ColorsMainGeneratedMock.mocked() - colors.feedback = ColorsFeedbackGeneratedMock.mocked() - theme.colors = colors - theme.dims = DimsGeneratedMock.mocked() - - return theme - } -} diff --git a/core/Sources/Components/Checkbox/TestHelper/CheckboxConfigurationSnapshotTests.swift b/core/Sources/Components/Checkbox/TestHelper/CheckboxConfigurationSnapshotTests.swift deleted file mode 100644 index 517cea2a3..000000000 --- a/core/Sources/Components/Checkbox/TestHelper/CheckboxConfigurationSnapshotTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// CheckboxConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 16.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -@testable import SparkCore - -struct CheckboxConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: CheckboxScenarioSnapshotTests - let intent: CheckboxIntent - let selectionState: CheckboxSelectionState - let state: CheckboxState - let alignment: CheckboxAlignment - let text: String - let image: UIImage - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.selectionState)", - "\(self.state)", - "\(self.alignment)" - ].joined(separator: "-") - } -} - -enum CheckboxState: CaseIterable { - case enabled - case disabled - case pressed -} diff --git a/core/Sources/Components/Checkbox/TestHelper/CheckboxGroupConfigurationSnapshotTests.swift b/core/Sources/Components/Checkbox/TestHelper/CheckboxGroupConfigurationSnapshotTests.swift deleted file mode 100644 index 5890abecf..000000000 --- a/core/Sources/Components/Checkbox/TestHelper/CheckboxGroupConfigurationSnapshotTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// CheckboxGroupConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 16.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -@testable import SparkCore - -struct CheckboxGroupConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: CheckboxGroupScenarioSnapshotTests - let intent: CheckboxIntent - let alignment: CheckboxAlignment - let axis: CheckboxGroupLayout - let items: [any CheckboxGroupItemProtocol] - let image: UIImage - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.alignment)", - "\(self.axis)" - ].joined(separator: "-") - } -} diff --git a/core/Sources/Components/Checkbox/TestHelper/CheckboxGroupScenarioSnapshotTests.swift b/core/Sources/Components/Checkbox/TestHelper/CheckboxGroupScenarioSnapshotTests.swift deleted file mode 100644 index 4d15c47d8..000000000 --- a/core/Sources/Components/Checkbox/TestHelper/CheckboxGroupScenarioSnapshotTests.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// CheckboxGroupScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 16.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// -import UIKit - -@testable import SparkCore - -enum CheckboxGroupScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration() -> [CheckboxGroupConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1() - case .test2: - return self.test2() - case .test3: - return self.test3() - case .test4: - return self.test4() - case .test5: - return self.test5() - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test spacing - /// - /// Content: - /// - intent: basic - /// - alignment: all - /// - axis: vertical - /// - items: contents of single checkbox component - /// - image: checkbox checked image - /// - modes: light - /// - sizes (accessibility): default - private func test1() -> [CheckboxGroupConfigurationSnapshotTests] { - - let alignments = CheckboxAlignment.allCases - let items = [ - CheckboxGroupItemDefault(title: "Hello World", id: "1", selectionState: .selected, isEnabled: true), - CheckboxGroupItemDefault(title: "This is the way.", id: "2", selectionState: .selected, isEnabled: true) - ] - - return alignments.map { alignment in - .init( - scenario: self, - intent: .basic, - alignment: alignment, - axis: .vertical, - items: items, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test all aliggnments - /// - /// Content: - /// - intent: accent - /// - alignment: all - /// - axis: all - /// - items: contents of single checkbox component - /// - image: checkbox checked image - /// - modes: light - /// - sizes (accessibility): default - private func test2() -> [CheckboxGroupConfigurationSnapshotTests] { - - let alignments = CheckboxAlignment.allCases - let layouts: [CheckboxGroupLayout] = [.vertical, .horizontal] - let items = [ - CheckboxGroupItemDefault(title: "Hello World", id: "1", selectionState: .unselected, isEnabled: true), - CheckboxGroupItemDefault(title: "This is the way.", id: "2", selectionState: .unselected, isEnabled: true) - ] - - return alignments.flatMap { alignment in - layouts.map { layout in - return .init( - scenario: self, - intent: .accent, - alignment: alignment, - axis: layout, - items: items, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - } - - /// Test 3 - /// - /// Description: To test labels content resilience (label of checkboxes) - /// - /// Content: - /// - intent: accent - /// - alignment: all - /// - axis: all - /// - items: contents of single checkbox component - /// - image: checkbox checked image - /// - modes: light - /// - sizes (accessibility): default - private func test3() -> [CheckboxGroupConfigurationSnapshotTests] { - - let alignments = CheckboxAlignment.allCases - let layouts: [CheckboxGroupLayout] = [.vertical, .horizontal] - let items = [ - CheckboxGroupItemDefault(title: "Hello World", id: "1", selectionState: .indeterminate, isEnabled: true), - CheckboxGroupItemDefault(title: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", id: "2", selectionState: .indeterminate, isEnabled: true) - ] - - return alignments.flatMap { alignment in - layouts.map { layout in - return .init( - scenario: self, - intent: .main, - alignment: alignment, - axis: layout, - items: items, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - } - - /// Test 4 - /// - /// Description: To test label group content resilience - /// - /// Content: - /// - intent: support - /// - alignment: left - /// - axis: all - /// - items: contents of single checkbox component - /// - image: checkbox checked image - /// - modes: light - /// - sizes (accessibility): default - private func test4() -> [CheckboxGroupConfigurationSnapshotTests] { - let layouts: [CheckboxGroupLayout] = [.vertical, .horizontal] - let itemsArray = [[CheckboxGroupItemDefault(title: "Hello World", id: "1", selectionState: .selected, isEnabled: true), - CheckboxGroupItemDefault(title: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", id: "2", selectionState: .selected, isEnabled: true)], - [CheckboxGroupItemDefault(title: "Hello World", id: "1", selectionState: .selected, isEnabled: true), - CheckboxGroupItemDefault(title: "This is the way.", id: "2", selectionState: .selected, isEnabled: true)]] - - return layouts.flatMap { layout in - itemsArray.map { items in - return .init( - scenario: self, - intent: .support, - alignment: .left, - axis: layout, - items: items, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - } - - /// Test 5 - /// - /// Description: To test a11y sizes - /// - /// Content: - /// - intent: support - /// - alignment: left - /// - axis: all - /// - items: contents of single checkbox component - /// - image: checkbox checked image - /// - modes: light - /// - sizes (accessibility): default - private func test5() -> [CheckboxGroupConfigurationSnapshotTests] { - let layouts: [CheckboxGroupLayout] = [.vertical, .horizontal] - let items = [CheckboxGroupItemDefault(title: "Hello World", id: "1", selectionState: .selected, isEnabled: true), CheckboxGroupItemDefault(title: "This is the way.", id: "2", selectionState: .selected, isEnabled: true)] - - return layouts.map { layout in - .init( - scenario: self, - intent: .support, - alignment: .left, - axis: layout, - items: items, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - ) - } - } - -} - -private extension UIImage { - static let mock: UIImage = UIImage(systemName: "checkmark")?.withRenderingMode(.alwaysTemplate) ?? UIImage() -} diff --git a/core/Sources/Components/Checkbox/TestHelper/CheckboxScenarioSnapshotTests.swift b/core/Sources/Components/Checkbox/TestHelper/CheckboxScenarioSnapshotTests.swift deleted file mode 100644 index 5c7860199..000000000 --- a/core/Sources/Components/Checkbox/TestHelper/CheckboxScenarioSnapshotTests.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// CheckboxScenarioSnapshotTests.swift -// Spark -// -// Created by alican.aycil on 12.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -enum CheckboxScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool = false) -> [CheckboxConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1() - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3() - case .test4: - return self.test4() - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - intent: all - /// - selectionState: selected - /// - state: enabled - /// - alignment: left - /// - text: normal text - /// - modes: all - /// - sizes (accessibility): default - private func test1() -> [CheckboxConfigurationSnapshotTests] { - let intents = CheckboxIntent.allCases - - return intents.map { intent in - return .init( - scenario: self, - intent: intent, - selectionState: .selected, - state: .enabled, - alignment: .left, - text: "Hello World", - image: UIImage.mock, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test all states (content and component) - /// - /// Content: - /// - intent: all - /// - selectionState: selected - /// - state: enabled - /// - alignment: left - /// - text: normal text - /// - modes: all - /// - sizes (accessibility): default - private func test2(isSwiftUIComponent: Bool) -> [CheckboxConfigurationSnapshotTests] { - let selectionStates = CheckboxSelectionState.allCases - let states = isSwiftUIComponent ? [.enabled, .disabled] : CheckboxState.allCases - - return selectionStates.flatMap { selectionState in - states.map { state in - return CheckboxConfigurationSnapshotTests.init( - scenario: self, - intent: .basic, - selectionState: selectionState, - state: state, - alignment: .left, - text: "Hello World", - image: UIImage.mock, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - } - - /// Test 3 - /// - /// Description: To test label resilience - /// - /// Content: - /// - intent: all - /// - selectionState: selected - /// - state: enabled - /// - alignment: left - /// - text: normal text - /// - modes: all - /// - sizes (accessibility): default - private func test3() -> [CheckboxConfigurationSnapshotTests] { - let texts = ["Hello World", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."] - let alignments = CheckboxAlignment.allCases - - return texts.flatMap { text in - alignments.map { alignment in - return CheckboxConfigurationSnapshotTests.init( - scenario: self, - intent: .main, - selectionState: .selected, - state: .enabled, - alignment: alignment, - text: text, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - } - - /// Test 4 - /// - /// Description: To test a11y sizes - /// - /// Content: - /// - intent: all - /// - selectionState: selected - /// - state: enabled - /// - alignment: left - /// - text: normal text - /// - modes: all - /// - sizes (accessibility): default - private func test4() -> [CheckboxConfigurationSnapshotTests] { - let text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." - - return [.init( - scenario: self, - intent: .main, - selectionState: .unselected, - state: .enabled, - alignment: .right, - text: text, - image: UIImage.mock, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - )] - } -} - -private extension UIImage { - static let mock: UIImage = UIImage(systemName: "checkmark")?.withRenderingMode(.alwaysTemplate) ?? UIImage() -} diff --git a/core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCase.swift b/core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCase.swift deleted file mode 100644 index 9aa687378..000000000 --- a/core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCase.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CheckboxGetSpacingUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -protocol CheckboxGetSpacingUseCaseable { - func execute(layoutSpacing: LayoutSpacing, alignment: CheckboxAlignment) -> CGFloat -} - -struct CheckboxGetSpacingUseCase: CheckboxGetSpacingUseCaseable { - func execute(layoutSpacing: LayoutSpacing, alignment: CheckboxAlignment) -> CGFloat { - switch alignment { - case .left: return layoutSpacing.medium - case .right: return layoutSpacing.xxxLarge - } - } -} diff --git a/core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCaseTests.swift b/core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCaseTests.swift deleted file mode 100644 index 769feb868..000000000 --- a/core/Sources/Components/Checkbox/UseCase/CheckboxGetSpacingUseCaseTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// CheckboxGetSpacingUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class CheckboxGetSpacingUseCaseTests: XCTestCase { - - var sut: CheckboxGetSpacingUseCase! - var layoutSpacing: LayoutSpacingGeneratedMock! - - override func setUp() { - self.layoutSpacing = LayoutSpacingGeneratedMock.mocked() - self.sut = CheckboxGetSpacingUseCase() - } - - func test_left_alignment_spacing() { - let spacing = sut.execute(layoutSpacing: self.layoutSpacing, alignment: .left) - - XCTAssertEqual(spacing, self.layoutSpacing.medium) - } - - func test_right_alignment_spacing() { - let spacing = sut.execute(layoutSpacing: self.layoutSpacing, alignment: .right) - - XCTAssertEqual(spacing, self.layoutSpacing.xxxLarge) - } -} diff --git a/core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCase.swift b/core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCase.swift deleted file mode 100644 index 4ea50a34c..000000000 --- a/core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCase.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// CheckboxColorsUseCase.swift -// Spark -// -// Created by janniklas.freundt.ext on 04.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol CheckboxColorsUseCaseable { - func execute(from color: Colors, intent: CheckboxIntent) -> CheckboxColors -} - -struct CheckboxColorsUseCase: CheckboxColorsUseCaseable { - - func execute(from colors: Colors, intent: CheckboxIntent) -> CheckboxColors { - switch intent { - case .basic: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.basic.basic, - iconColor: colors.basic.onBasic, - pressedBorderColor: colors.basic.basicContainer - ) - case .accent: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.accent.accent, - iconColor: colors.accent.onAccent, - pressedBorderColor: colors.accent.accentContainer - ) - case .error: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.feedback.error, - iconColor: colors.feedback.onError, - pressedBorderColor: colors.feedback.errorContainer - ) - case .success: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.feedback.success, - iconColor: colors.feedback.onSuccess, - pressedBorderColor: colors.feedback.successContainer - ) - case .alert: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.feedback.alert, - iconColor: colors.feedback.onAlert, - pressedBorderColor: colors.feedback.alertContainer - ) - case .info: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.feedback.info, - iconColor: colors.feedback.onInfo, - pressedBorderColor: colors.feedback.infoContainer - ) - case .neutral: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.feedback.neutral, - iconColor: colors.feedback.onNeutral, - pressedBorderColor: colors.feedback.neutralContainer - ) - case .support: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.support.support, - iconColor: colors.support.onSupport, - pressedBorderColor: colors.support.supportContainer - ) - case .main: - return CheckboxColors( - textColor: colors.base.onSurface, - borderColor: colors.base.outline, - tintColor: colors.main.main, - iconColor: colors.main.onMain, - pressedBorderColor: colors.main.mainContainer - ) - } - } -} diff --git a/core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCaseTests.swift b/core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCaseTests.swift deleted file mode 100644 index 45feceb88..000000000 --- a/core/Sources/Components/Checkbox/UseCase/Colors/CheckboxColorsUseCaseTests.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// CheckboxColorsUseCaseTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 17.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class CheckboxColorsUseCaseTests: XCTestCase { - - var sut: CheckboxColorsUseCase! - var theme: ThemeGeneratedMock! - - override func setUp() { - super.setUp() - - self.sut = .init() - self.theme = .mocked() - } - - // MARK: - Tests - - func test_execute_for_all_intent_cases() { - let intents = CheckboxIntent.allCases - - intents.forEach { - - let checkboxColors = sut.execute(from: theme.colors, intent: $0) - - let expectedColors: CheckboxColors - - switch $0 { - case .basic: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.basic.basic, - iconColor: theme.colors.basic.onBasic, - pressedBorderColor: theme.colors.basic.basicContainer - ) - case .accent: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.accent.accent, - iconColor: theme.colors.accent.onAccent, - pressedBorderColor: theme.colors.accent.accentContainer - ) - case .error: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.feedback.error, - iconColor: theme.colors.feedback.onError, - pressedBorderColor: theme.colors.feedback.errorContainer - ) - case .success: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.feedback.success, - iconColor: theme.colors.feedback.onSuccess, - pressedBorderColor: theme.colors.feedback.successContainer - ) - case .alert: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.feedback.alert, - iconColor: theme.colors.feedback.onAlert, - pressedBorderColor: theme.colors.feedback.alertContainer - ) - case .info: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.feedback.info, - iconColor: theme.colors.feedback.onInfo, - pressedBorderColor: theme.colors.feedback.infoContainer - ) - case .neutral: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.feedback.neutral, - iconColor: theme.colors.feedback.onNeutral, - pressedBorderColor: theme.colors.feedback.neutralContainer - ) - case .support: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.support.support, - iconColor: theme.colors.support.onSupport, - pressedBorderColor: theme.colors.support.supportContainer - ) - case .main: - expectedColors = CheckboxColors( - textColor: theme.colors.base.onSurface, - borderColor: theme.colors.base.outline, - tintColor: theme.colors.main.main, - iconColor: theme.colors.main.onMain, - pressedBorderColor: theme.colors.main.mainContainer - ) - } - - XCTAssertEqual(checkboxColors.textColor.uiColor, expectedColors.textColor.uiColor) - XCTAssertEqual(checkboxColors.borderColor.uiColor, expectedColors.borderColor.uiColor) - XCTAssertEqual(checkboxColors.tintColor.uiColor, expectedColors.tintColor.uiColor) - XCTAssertEqual(checkboxColors.iconColor.uiColor, expectedColors.iconColor.uiColor) - XCTAssertEqual(checkboxColors.pressedBorderColor.uiColor, expectedColors.pressedBorderColor.uiColor) - } - } -} diff --git a/core/Sources/Components/Checkbox/View/CheckboxGroupItemDefault.swift b/core/Sources/Components/Checkbox/View/CheckboxGroupItemDefault.swift deleted file mode 100644 index 766418e74..000000000 --- a/core/Sources/Components/Checkbox/View/CheckboxGroupItemDefault.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// CheckboxGroupItemDefault.swift -// SparkCore -// -// Created by louis.borlee on 22/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Default struct extending CheckboxGroupItemProtocol used for items in checkbox groups. It describes a single item within a checkbox group. -public struct CheckboxGroupItemDefault: CheckboxGroupItemProtocol, Hashable { - - /// The checkbox title. - public var title: String? - /// The attributed checkbox title. - public var attributedTitle: NSAttributedString? - /// The checkbox identifier. - public var id: String - /// The current selection state of the checkbox. - public var selectionState: CheckboxSelectionState - /// The current control state of the checkbox. - public var state: SelectButtonState - /// The current control state of the checkbox. - public var isEnabled: Bool - - /// CheckboxGroupItemDefault initializer - /// - Parameters: - /// - title: The checkbox title. Default value is `nil` - /// - attributedTitle: The attributed checkbox title. Default value is `nil` - /// - id: The checkbox identifier. - /// - selectionState: The current selection state of the checkbox. - /// - state: The current control state of the checkbox. - @available(*, deprecated, message: "state parameter was changed with isEnabled") - public init( - title: String? = nil, - attributedTitle: NSAttributedString? = nil, - id: String, - selectionState: CheckboxSelectionState, - state: SelectButtonState = .enabled - ) { - self.title = title - self.attributedTitle = attributedTitle - self.id = id - self.selectionState = selectionState - self.state = state - self.isEnabled = state == .enabled - } - - /// CheckboxGroupItemDefault initializer - /// - Parameters: - /// - title: The checkbox title. Default value is `nil` - /// - attributedTitle: The attributed checkbox title. Default value is `nil` - /// - id: The checkbox identifier. - /// - selectionState: The current selection state of the checkbox. - /// - isEnabled: The current control state of the checkbox. - public init( - title: String? = nil, - attributedTitle: NSAttributedString? = nil, - id: String, - selectionState: CheckboxSelectionState, - isEnabled: Bool = true - ) { - self.title = title - self.attributedTitle = attributedTitle - self.id = id - self.selectionState = selectionState - self.isEnabled = isEnabled - self.state = isEnabled ? .enabled : .disabled - } - - public static func == (lhs: CheckboxGroupItemDefault, rhs: CheckboxGroupItemDefault) -> Bool { - lhs.id == rhs.id - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } -} - diff --git a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupView.swift b/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupView.swift deleted file mode 100644 index 9673baf2d..000000000 --- a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupView.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// CheckboxGroupView.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 06.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// The `CheckboxGroupView` renders a group containing of multiple`CheckboxView`-views. It supports a title, different layout and positioning options. -public struct CheckboxGroupView: View { - - // MARK: - Private properties - - @Binding private var items: [any CheckboxGroupItemProtocol] - private var itemContents: [String] { - return self.items.map { $0.id + ($0.title ?? "") } - } - @ObservedObject var viewModel: CheckboxGroupViewModel - - @ScaledMetric private var spacingSmall: CGFloat - @ScaledMetric private var spacingLarge: CGFloat - @ScaledMetric private var checkboxSelectedBorderWidth: CGFloat - - @State private var viewWidth: CGFloat = 0 - @State private var isScrollableHStack: Bool = true - - // MARK: - Initialization - - /// Initialize a group of one or multiple checkboxes. - /// - Parameters: - /// - title: An optional group title displayed on top of the checkbox group.. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - items: An array containing of multiple `CheckboxGroupItemProtocol`. Each array item is used to render a single checkbox. - /// - layout: The layout of the group can be horizontal or vertical. - /// - checkboxAlignment: The checkbox is positioned on the leading or trailing edge of the view. - /// - theme: The Spark-Theme. - /// - accessibilityIdentifierPrefix: All checkbox-views are prefixed by this identifier followed by the `CheckboxGroupItemProtocol`-identifier. - @available(*, deprecated, message: "Please use init without accessibilityIdentifierPrefix. It was given as a static string.") - public init( - title: String? = nil, - checkedImage: Image, - items: Binding<[any CheckboxGroupItemProtocol]>, - layout: CheckboxGroupLayout = .vertical, - alignment: CheckboxAlignment, - theme: Theme, - intent: CheckboxIntent = .main, - accessibilityIdentifierPrefix: String - ) { - self.init( - title: title, - checkedImage: checkedImage, - items: items, - layout: layout, - alignment: alignment, - theme: theme, - intent: intent - ) - } - - /// Initialize a group of one or multiple checkboxes. - /// - Parameters: - /// - title: An optional group title displayed on top of the checkbox group.. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - items: An array containing of multiple `CheckboxGroupItemProtocol`. Each array item is used to render a single checkbox. - /// - layout: The layout of the group can be horizontal or vertical. - /// - checkboxAlignment: The checkbox is positioned on the leading or trailing edge of the view. - /// - theme: The Spark-Theme. - public init( - title: String? = nil, - checkedImage: Image, - items: Binding<[any CheckboxGroupItemProtocol]>, - layout: CheckboxGroupLayout = .vertical, - alignment: CheckboxAlignment, - theme: Theme, - intent: CheckboxIntent = .main - ) { - let viewModel = CheckboxGroupViewModel( - title: title, - checkedImage: checkedImage, - theme: theme, - intent: intent, - alignment: alignment, - layout: layout - ) - self.viewModel = viewModel - - self._items = items - self._spacingSmall = .init(wrappedValue: viewModel.spacing.small) - self._spacingLarge = .init(wrappedValue: viewModel.spacing.large) - self._checkboxSelectedBorderWidth = .init(wrappedValue: CheckboxView.Constants.checkboxSelectedBorderWidth) - } - - // MARK: - Body - - /// Returns the rendered checkbox group view. - public var body: some View { - VStack(alignment: .leading, spacing: 0) { - if let title = self.viewModel.title, !title.isEmpty { - Text(title) - .foregroundColor(self.viewModel.titleColor.color) - .font(self.viewModel.titleFont.font) - .padding(.bottom, self.spacingLarge - self.spacingSmall) - .accessibilityIdentifier(CheckboxAccessibilityIdentifier.checkboxGroupTitle) - } - switch self.viewModel.layout { - case .horizontal: - self.makeHStackView() - case .vertical: - self.makeVStackView() - } - } - .overlay( - GeometryReader { geo in - Color.clear.onAppear { - self.viewWidth = geo.size.width - } - } - ) - .onChange(of: self.itemContents) { newValue in - self.isScrollableHStack = true - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier(CheckboxAccessibilityIdentifier.checkboxGroup) - } - - @ViewBuilder - private func makeHStackView() -> some View { - if self.isScrollableHStack { - self.makeScrollHStackView() - } else { - self.makeDefaultHStackView() - } - } - - @ViewBuilder - private func makeScrollHStackView() -> some View { - ScrollView (.horizontal, showsIndicators: false) { - HStack(alignment: .top, spacing: self.spacingLarge) { - self.makeContentView(maxWidth: self.viewWidth) - .fixedSize(horizontal: false, vertical: true) - } - .overlay( - GeometryReader { geo in - Color.clear.preference(key: ScrollViewWidthPreferenceKey.self, value: geo.size.width) - } - ) - .onPreferenceChange(ScrollViewWidthPreferenceKey.self) { newValue in - self.isScrollableHStack = self.viewWidth < newValue ?? .zero - } - .padding(checkboxSelectedBorderWidth) - } - .padding(-checkboxSelectedBorderWidth) - } - - @ViewBuilder - private func makeDefaultHStackView() -> some View { - if (self.items.count < 2) { - self.makeVStackView() - } else { - HStack(spacing: self.spacingLarge) { - self.makeContentView() - } - .fixedSize(horizontal: true, vertical: false) - } - } - - private func makeVStackView() -> some View { - VStack(alignment: .leading, spacing: self.spacingLarge) { - self.makeContentView() - } - .fixedSize(horizontal: false, vertical: true) - } - - private func makeContentView(maxWidth: CGFloat? = nil) -> some View { - return ForEach(self.$items, id: \.id) { item in - let checkboxWidth = self.viewModel.calculateSingleCheckboxWidth(string: item.title.wrappedValue) - - if checkboxWidth > maxWidth ?? 0 { - self.checkBoxView(item: item) - .frame(width: maxWidth) - .fixedSize(horizontal: false, vertical: true) - } else { - self.checkBoxView(item: item) - .fixedSize() - } - } - } - - private func checkBoxView(item: Binding) -> some View { - return CheckboxView( - text: item.title.wrappedValue, - checkedImage: self.viewModel.checkedImage, - alignment: self.viewModel.alignment, - theme: self.viewModel.theme, - intent: self.viewModel.intent, - isEnabled: item.isEnabled.wrappedValue, - selectionState: item.selectionState - ) - .disabled(!item.isEnabled.wrappedValue) - .accessibilityIdentifier(CheckboxAccessibilityIdentifier.checkboxGroupItem(item.id.wrappedValue)) - } -} - -// MARK: - PreferenceKeys -private struct CheckboxWidthPreferenceKey: PreferenceKey { - static let defaultValue: CGFloat? = nil - static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { - value = value ?? nextValue() - } -} - -private struct ScrollViewWidthPreferenceKey: PreferenceKey { - static let defaultValue: CGFloat? = nil - static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { - value = value ?? nextValue() - } -} diff --git a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupViewSnapshotTests.swift b/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupViewSnapshotTests.swift deleted file mode 100644 index a7658dcd0..000000000 --- a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxGroupViewSnapshotTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// CheckboxGroupViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 16.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -@testable import SparkCore - -final class CheckboxGroupViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Tests - - func test() { - let scenarios = CheckboxGroupScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration() - - for configuration in configurations { - - let view = CheckboxGroupContainerView(configuration: configuration) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} - -private struct CheckboxGroupContainerView: View { - - private let theme: Theme = SparkTheme.shared - let configuration: CheckboxGroupConfigurationSnapshotTests - - @Binding var items: [any CheckboxGroupItemProtocol] - @State var viewHeight: CGFloat = 0 - - init(configuration: CheckboxGroupConfigurationSnapshotTests) { - self.configuration = configuration - self._items = .constant(configuration.items) - } - - var body: some View { - VStack { - CheckboxGroupView( - checkedImage: Image(uiImage: configuration.image), - items: self.$items, - layout: configuration.axis, - alignment: configuration.alignment, - theme: self.theme, - intent: configuration.intent, - accessibilityIdentifierPrefix: "id" - ) - .background(Color.systemBackground) - .frame(width: UIScreen.main.bounds.width) - .overlay( - GeometryReader { geo in - Color.clear.onAppear { - self.viewHeight = geo.size.height - } - } - ) - } - .frame(height: self.viewHeight) - } -} diff --git a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxView.swift b/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxView.swift deleted file mode 100644 index 23179bf91..000000000 --- a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxView.swift +++ /dev/null @@ -1,248 +0,0 @@ -// -// CheckboxView.swift -// Spark -// -// Created by janniklas.freundt.ext on 04.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// The `CheckboxView`renders a single checkbox. -public struct CheckboxView: View { - - // MARK: - Constants - - enum Constants { - static var checkboxSize: CGFloat = 24 - static var checkboxBorderRadius: CGFloat = 4 - static var checkboxBorderWidth: CGFloat = 2 - - static var checkboxSelectedSize: CGFloat = 17 - static var checkboxSelectedBorderWidth: CGFloat = 4 - - static var checkboxIndeterminateWidth: CGFloat = 14 - static var checkboxIndeterminateHeight: CGFloat = 2 - } - - // MARK: - Public Properties - - /// The current Spark theme. - public var theme: Theme { - return self.viewModel.theme - } - - /// A binding for the selection state of the checkbox (`.selected`, `.unselected` or `.indeterminate`). The value will update when the control is tapped. - @Binding public var selectionState: CheckboxSelectionState - - // MARK: - Internal Properties - - @State var isPressed: Bool = false - - @ObservedObject var viewModel: CheckboxViewModel - - // MARK: - Private Properties - - @Namespace private var namespace - - @ScaledMetric var checkboxSize: CGFloat = Constants.checkboxSize - @ScaledMetric private var checkboxBorderRadius: CGFloat = Constants.checkboxBorderRadius - @ScaledMetric private var checkboxBorderWidth: CGFloat = Constants.checkboxBorderWidth - - @ScaledMetric private var checkboxSelectedSize: CGFloat = Constants.checkboxSelectedSize - @ScaledMetric var checkboxSelectedBorderWidth: CGFloat = Constants.checkboxSelectedBorderWidth - - @ScaledMetric private var checkboxIndeterminateWidth: CGFloat = Constants.checkboxIndeterminateWidth - @ScaledMetric private var checkboxIndeterminateHeight: CGFloat = Constants.checkboxIndeterminateHeight - - @ScaledMetric private var horizontalSpacing: CGFloat - - // MARK: - Initialization - - /// Initialize a new checkbox. - /// - Parameters: - /// - text: The checkbox text. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - alignment: Positions the checkbox on the leading or trailing edge of the view. - /// - theme: The current Spark-Theme. - /// - intent: The current Intent. - /// - state: The control state describes whether the checkbox is enabled or disabled as well as options for displaying success and error messages. - /// - selectionState: `CheckboxSelectionState` is either selected, unselected or indeterminate. - public init( - text: String?, - checkedImage: Image, - alignment: CheckboxAlignment = .left, - theme: Theme, - intent: CheckboxIntent = .main, - isEnabled: Bool = true, - selectionState: Binding - ) { - self._selectionState = selectionState - let viewModel = CheckboxViewModel( - text: .right(text), - checkedImage: .right(checkedImage), - theme: theme, - intent: intent, - isEnabled: isEnabled, - alignment: alignment, - selectionState: selectionState.wrappedValue - ) - self.viewModel = viewModel - self._horizontalSpacing = .init(wrappedValue: viewModel.spacing) - } - - // MARK: - Body - - /// Returns a single rendered checkbox. - public var body: some View { - Button( - action: { - self.tapped() - }, - label: { - self.contentView - } - ) - .buttonStyle(PressedButtonStyle(isPressed: self.$isPressed)) - .isEnabledChanged { isEnabled in - self.viewModel.isEnabled = isEnabled - } - .fixedSize(horizontal: false, vertical: true) - .accessibilityIdentifier(CheckboxAccessibilityIdentifier.checkbox) - .accessibilityValue(setAccessibilityValue(selectionState: self.viewModel.selectionState)) - .accessibilityRemoveTraits(.isSelected) - } - - private func setAccessibilityValue(selectionState: CheckboxSelectionState) -> String { - switch selectionState { - case .selected: - return CheckboxAccessibilityValue.checked - case .indeterminate: - return CheckboxAccessibilityValue.indeterminate - case .unselected: - return CheckboxAccessibilityValue.unchecked - } - } - - @ViewBuilder - private var checkboxView: some View { - if self.selectionState == .selected { - self.checkbox().accessibilityAddTraits(.isSelected) - } else { - self.checkbox() - } - } - - @ViewBuilder - private func checkbox() -> some View { - let iconColor = self.viewModel.colors.iconColor.color - - ZStack { - self.stateFullCheckboxRectangle() - - switch self.selectionState { - case .selected: - self.viewModel.checkedImage.rightValue - .resizable() - .scaledToFit() - .foregroundColor(iconColor) - .frame(width: self.checkboxSelectedSize, height: self.checkboxSelectedSize) - - case .unselected: - EmptyView() - case .indeterminate: - Capsule() - .fill(iconColor) - .frame(width: self.checkboxIndeterminateWidth, height: self.checkboxIndeterminateHeight) - } - } - .id(Identifier.checkbox.rawValue) - .matchedGeometryEffect(id: Identifier.checkbox.rawValue, in: self.namespace) - } - - @ViewBuilder - private func stateFullCheckboxRectangle() -> some View { - if self.isPressed && self.viewModel.isEnabled { - self.checkboxRectangle() - .overlay( - RoundedRectangle(cornerRadius: self.checkboxBorderRadius) - .inset(by: -self.checkboxSelectedBorderWidth / 2) - .stroke(self.viewModel.colors.pressedBorderColor.color, lineWidth: self.checkboxSelectedBorderWidth) - .animation(.easeInOut(duration: 0.1), value: self.isPressed) - ) - } else { - self.checkboxRectangle() - } - } - - @ViewBuilder - private func checkboxRectangle() -> some View { - let tintColor = self.viewModel.colors.tintColor.color - let borderColor = self.viewModel.colors.borderColor.color - - if self.selectionState == .selected || self.selectionState == .indeterminate { - RoundedRectangle(cornerRadius: self.checkboxBorderRadius) - .fill(tintColor) - .frame(width: self.checkboxSize, height: self.checkboxSize) - - } else { - RoundedRectangle(cornerRadius: self.checkboxBorderRadius) - .strokeBorder(borderColor, lineWidth: self.checkboxBorderWidth) - .frame(width: self.checkboxSize, height: self.checkboxSize) - } - } - - @ViewBuilder - private var contentView: some View { - HStack(spacing: 0) { - switch self.viewModel.alignment { - case .left: - VStack { - self.checkboxView.padding(.trailing, self.horizontalSpacing) - Spacer(minLength: 0) - } - self.labelView - Spacer(minLength: 0) - case .right: - self.labelView.padding(.trailing, self.horizontalSpacing) - Spacer(minLength: 0) - VStack { - self.checkboxView - Spacer(minLength: 0) - } - } - } - .opacity(self.viewModel.opacity) - .allowsHitTesting(self.viewModel.isEnabled) - .contentShape(Rectangle()) - } - - private var labelView: some View { - VStack(alignment: .leading, spacing: 0) { - Text(self.viewModel.text.rightValue ?? "") - .font(self.viewModel.font.font) - .foregroundColor(self.viewModel.colors.textColor.color) - .fixedSize(horizontal: false, vertical: true) - } - .id(Identifier.content.rawValue) - .matchedGeometryEffect(id: Identifier.content.rawValue, in: self.namespace) - } - - // MARK: - Action - - func tapped() { - guard self.viewModel.isEnabled else { return } - - switch self.selectionState { - case .selected: - self.selectionState = .unselected - case .unselected, .indeterminate: - self.selectionState = .selected - } - } - - private enum Identifier: String { - case checkbox - case content - } -} diff --git a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxViewSnapshotTests.swift b/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxViewSnapshotTests.swift deleted file mode 100644 index 24dde3055..000000000 --- a/core/Sources/Components/Checkbox/View/SwiftUI/CheckboxViewSnapshotTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// CheckboxViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 12.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -@testable import SparkCore - -final class CheckboxViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - private var selectionState: CheckboxSelectionState = .selected - - private lazy var _selectionState: Binding = { - return Binding( - get: { return self.selectionState }, - set: { newValue, transaction in - self.selectionState = newValue - } - ) - }() - - // MARK: - Tests - - func test() { - let scenarios = CheckboxScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: true) - - for configuration in configurations { - self.selectionState = configuration.selectionState - - let checkboxView = CheckboxView( - text: configuration.text, - checkedImage: Image(uiImage: configuration.image), - alignment: configuration.alignment, - theme: self.theme, - intent: configuration.intent, - isEnabled: configuration.state == .disabled ? false : true, - selectionState: self._selectionState - ) - .background(Color.systemBackground) - - let view = self.view(text: configuration.text, checkbox: checkboxView) - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } - - func view(text: String, checkbox: some View) -> AnyView { - if text != "Hello World" { - let view = VStack { checkbox } - .frame(width: UIScreen.main.bounds.width) - return AnyView(view) - } else { - return AnyView(checkbox.fixedSize()) - } - - } -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxControlUIView.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxControlUIView.swift deleted file mode 100644 index edac1007a..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxControlUIView.swift +++ /dev/null @@ -1,199 +0,0 @@ -// -// CheckboxControlUIView.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 18.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit - -class CheckboxControlUIView: UIView { - - // MARK: - Constants - - enum Constants { - static var cornerRadius: CGFloat = 4 - static var cornerRadiusPressed: CGFloat = 7 - static var lineWidth: CGFloat = 2 - static var lineWidthPressed: CGFloat = 4 - static var size: CGFloat = 24 - static var selectedIconSize: CGSize = CGSize(width: 17, height: 17) - static var indeterminateIconSize: CGSize = CGSize(width: 14, height: 2) - } - - // MARK: - Properties. - - var selectionIcon: UIImage { - didSet { - self.setNeedsDisplay() - } - } - - var isHighlighted: Bool { - didSet { - self.setNeedsDisplay() - } - } - - var selectionState: CheckboxSelectionState { - didSet { - self.setNeedsDisplay() - } - } - - var colors: CheckboxColors { - didSet { - self.setNeedsDisplay() - } - } - - var isEnabled: Bool { - didSet { - self.setNeedsDisplay() - } - } - - // MARK: - Private Properties. - @ScaledUIMetric private var cornerRadius: CGFloat = Constants.cornerRadius - @ScaledUIMetric private var cornerRadiusPressed: CGFloat = Constants.cornerRadiusPressed - @ScaledUIMetric private var lineWidth: CGFloat = Constants.lineWidth - @ScaledUIMetric private var lineWidthPressed: CGFloat = Constants.lineWidthPressed - @ScaledUIMetric private var controlSize: CGFloat = Constants.size - - private lazy var pressedBorderView: UIView = { - let view = UIView() - self.addBorderToView(for: view) - return view - }() - - private var iconSize: CGSize { - let iconSize: CGSize - switch self.selectionState { - case .unselected: - return .zero - case .selected: - iconSize = Constants.selectedIconSize - case .indeterminate: - iconSize = Constants.indeterminateIconSize - } - return iconSize.scaled(for: self.traitCollection) - } - - // MARK: - Initialization - init( - selectionIcon: UIImage, - colors: CheckboxColors, - isEnabled: Bool, - selectionState: CheckboxSelectionState, - isHighlighted: Bool - ) { - self.selectionIcon = selectionIcon - self.isEnabled = isEnabled - self.selectionState = selectionState - self.isHighlighted = isHighlighted - self.colors = colors - super.init(frame: .zero) - - self.backgroundColor = .clear - self.addSubview(pressedBorderView) - } - - private func addBorderToView(for view: UIView) { - view.frame = CGRect( - x: -self.lineWidthPressed, - y: -self.lineWidthPressed, - width: self.controlSize + 2 * self.lineWidthPressed, - height: self.controlSize + 2 * self.lineWidthPressed - ) - view.layer.borderWidth = self.lineWidthPressed - view.layer.borderColor = self.colors.pressedBorderColor.uiColor.cgColor - view.layer.cornerRadius = self.cornerRadiusPressed - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { - let traitCollection = self.traitCollection - self._cornerRadius.update(traitCollection: traitCollection) - self._cornerRadiusPressed.update(traitCollection: traitCollection) - self._lineWidth.update(traitCollection: traitCollection) - self._lineWidthPressed.update(traitCollection: traitCollection) - self._controlSize.update(traitCollection: traitCollection) - } - - self.addBorderToView(for: self.pressedBorderView) - } - - required init?(coder: NSCoder) { - fatalError("not implemented") - } - - override func draw(_ rect: CGRect) { - super.draw(rect) - - self.pressedBorderView.isHidden = !self.isHighlighted - self.pressedBorderView.layer.borderColor = self.colors.pressedBorderColor.uiColor.cgColor - - guard let ctx = UIGraphicsGetCurrentContext() else { return } - - let bodyFontMetrics = UIFontMetrics(forTextStyle: .body) - let rect = CGRect(x: 0, y: 0, width: self.controlSize, height: self.controlSize) - - let fillPath = UIBezierPath(roundedRect: rect, cornerRadius: self.cornerRadius) - let fillColor = self.colors.tintColor.uiColor - fillColor.setFill() - ctx.setFillColor(fillColor.cgColor) - - if self.isHighlighted { - let path = UIBezierPath(roundedRect: rect, cornerRadius: self.cornerRadius) - let color = self.colors.pressedBorderColor.uiColor - path.lineWidth = self.lineWidth / 2 - color.setStroke() - ctx.setStrokeColor(color.cgColor) - path.stroke() - } - - switch self.selectionState { - case .unselected: - let strokeRectangle = rect.insetBy(dx: self.lineWidth / 2, dy: self.lineWidth / 2) - let strokePath = UIBezierPath(roundedRect: strokeRectangle, cornerRadius: self.cornerRadius) - let strokeColor = self.colors.borderColor.uiColor - strokePath.lineWidth = self.lineWidth - strokeColor.setStroke() - ctx.setStrokeColor(strokeColor.cgColor) - strokePath.stroke() - - case .indeterminate: - fillPath.fill() - - let iconPath = UIBezierPath( - roundedRect: self.iconRect(for: rect), - cornerRadius: bodyFontMetrics.scaledValue( - for: self.iconSize.height / 2, - compatibleWith: self.traitCollection - ) - ) - let iconColor = self.colors.iconColor.uiColor - iconColor.setFill() - iconPath.fill() - - case .selected: - fillPath.fill() - - let iconColor = self.colors.iconColor.uiColor - iconColor.set() - self.selectionIcon.draw(in: self.iconRect(for: rect)) - } - } - - private func iconRect(for rectangle: CGRect) -> CGRect { - let origin = CGPoint( - x: rectangle.origin.x + rectangle.width / 2 - self.iconSize.width / 2, - y: rectangle.origin.y + rectangle.height / 2 - self.iconSize.height / 2 - ) - return CGRect(origin: origin, size: self.iconSize) - } -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIView.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIView.swift deleted file mode 100644 index fb8a4999a..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIView.swift +++ /dev/null @@ -1,366 +0,0 @@ -// -// CheckboxGroupUIView.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 19.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -/// The `CheckboxGroupUIView` renders a group containing of multiple`CheckboxUIView`-views. It supports a title, different layout and positioning options. -public final class CheckboxGroupUIView: UIControl { - // MARK: - Private properties. - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - label.text = self.title - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = self.theme.colors.base.onSurface.uiColor - label.font = self.theme.typography.subhead.uiFont - label.accessibilityIdentifier = CheckboxAccessibilityIdentifier.checkboxGroupTitle - return label - }() - - private lazy var spacingView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private lazy var titleStackView: UIStackView = { - let view = UIStackView(arrangedSubviews: [self.titleLabel, self.spacingView]) - view.axis = .vertical - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private lazy var itemsStackView: UIStackView = { - let view = UIStackView() - view.spacing = self.spacingLarge - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private lazy var scrollView: UIScrollView = { - let view = UIScrollView() - view.showsHorizontalScrollIndicator = false - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private var subscriptions = Set() - private var items: [any CheckboxGroupItemProtocol] - private var subject = PassthroughSubject<[any CheckboxGroupItemProtocol], Never>() - - @ScaledUIMetric private var spacingLarge: CGFloat - @ScaledUIMetric private var padding: CGFloat = CheckboxControlUIView.Constants.lineWidthPressed - @ScaledUIMetric private var spacingSmall: CGFloat - - // MARK: - Public properties. - - /// The delegate CheckboxGroupUIViewDelegate` which may be set to retrieve changes to the checkboxes. - public weak var delegate: CheckboxGroupUIViewDelegate? - - /// Changes to the checkboxgroup are published to the publisher. - public var publisher: some Publisher<[any CheckboxGroupItemProtocol], Never> { - return self.subject - } - - @Published public var theme: Theme { - didSet { - self.updateTheme() - } - } - - /// The title of the checkbox group displayed on top of the group. - public var title: String? { - didSet { - self.updateTitle() - } - } - - /// The tick-checkbox-icon for the selected state. - public var checkedImage: UIImage { - didSet { - self.updateImage() - } - } - - /// The layout of the checkbox - public var layout: CheckboxGroupLayout { - didSet { - self.updateLayout() - } - } - /// The checkbox is positioned on the leading or trailing edge of the view. - @available(*, deprecated, message: "alignment will be used instead of this") - public var checkboxAlignment: CheckboxAlignment { - didSet { - self.alignment = self.checkboxAlignment - } - } - - public var alignment: CheckboxAlignment { - didSet { - self.updateAlignment() - } - } - - /// The checkbox is positioned on the leading or trailing edge of the view. - public var intent: CheckboxIntent { - didSet { - self.updateIntent() - } - } - - /// The checkboxes are items of checkbox group which are created by CheckboxGroupItemProtocol - public var checkboxes: [CheckboxUIView] { - self.itemsStackView.arrangedSubviews.compactMap { $0 as? CheckboxUIView } - } - - /// A Boolean value indicating whether the component is in the enabled state. - public override var isEnabled: Bool { - didSet{ - guard isEnabled != oldValue else { return } - if isEnabled { - self.checkboxes.enumerated().forEach { index, item in - item.isEnabled = self.items.indices.contains(index) ? self.items[index].isEnabled : true - } - } else { - self.checkboxes.forEach { $0.isEnabled = false } - } - } - } - - // MARK: - Initialization - - /// Not implemented. Please use another init. - /// - Parameter coder: the coder. - public required init?(coder: NSCoder) { - fatalError("not implemented") - } - - /// Initialize a group of one or multiple checkboxes. - /// - Parameters: - /// - title: An optional group title displayed on top of the checkbox group.. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - items: An array containing of multiple `CheckboxGroupItemProtocol`. Each array item is used to render a single checkbox. - /// - layout: The layout of the group can be horizontal or vertical. - /// - checkboxAlignment: The checkbox is positioned on the leading or trailing edge of the view. - /// - theme: The Spark-Theme. - /// - intent: Current intent of checkbox group - /// - accessibilityIdentifierPrefix: All checkbox-views are prefixed by this identifier followed by the `CheckboxGroupItemProtocol`-identifier. - @available(*, deprecated, message: "Please use init without accessibilityIdentifierPrefix. It was given as a static string.") - public convenience init( - title: String? = nil, - checkedImage: UIImage, - items: [any CheckboxGroupItemProtocol], - layout: CheckboxGroupLayout = .vertical, - alignment: CheckboxAlignment = .left, - theme: Theme, - intent: CheckboxIntent = .main, - accessibilityIdentifierPrefix: String - ) { - self.init( - title: title, - checkedImage: checkedImage, - items: items, - layout: layout, - alignment: alignment, - theme: theme, - intent: intent - ) - } - - /// Initialize a group of one or multiple checkboxes. - /// - Parameters: - /// - title: An optional group title displayed on top of the checkbox group.. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - items: An array containing of multiple `CheckboxGroupItemProtocol`. Each array item is used to render a single checkbox. - /// - layout: The layout of the group can be horizontal or vertical. - /// - checkboxAlignment: The checkbox is positioned on the leading or trailing edge of the view. - /// - theme: The Spark-Theme. - /// - intent: Current intent of checkbox group - public init( - title: String? = nil, - checkedImage: UIImage, - items: [any CheckboxGroupItemProtocol], - layout: CheckboxGroupLayout = .vertical, - alignment: CheckboxAlignment = .left, - theme: Theme, - intent: CheckboxIntent = .main - ) { - self.title = title - self.checkedImage = checkedImage - self.items = items - self.layout = layout - self.alignment = alignment - self.checkboxAlignment = alignment - self.theme = theme - self.intent = intent - self.spacingLarge = theme.layout.spacing.large - self.spacingSmall = theme.layout.spacing.small - super.init(frame: .zero) - self.commonInit() - } - - private func commonInit() { - self.setupItemsStackView() - self.setupView() - self.enableTouch() - self.updateTitle() - self.updateAccessibility() - } - - // MARK: - Methods - - /// The trait collection was updated causing the view to update its constraints (e.g. dynamic content size change). - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self._spacingLarge.update(traitCollection: self.traitCollection) - self._spacingSmall.update(traitCollection: self.traitCollection) - self._padding.update(traitCollection: self.traitCollection) - } - - private func updateAccessibility() { - self.accessibilityIdentifier = CheckboxAccessibilityIdentifier.checkboxGroup - self.isAccessibilityElement = false - self.accessibilityContainerType = .semanticGroup - } - - private func setupItemsStackView() { - self.updateLayout() - - for item in self.items { - - var content: Either - - if let text = item.title { - content = .left(NSAttributedString(string: text)) - } else { - content = .left(item.attributedTitle) - } - - let checkbox = CheckboxUIView( - theme: theme, - intent: intent, - content: content, - checkedImage: .left(self.checkedImage), - isEnabled: item.isEnabled, - selectionState: item.selectionState, - alignment: self.alignment - ) - checkbox.accessibilityIdentifier = CheckboxAccessibilityIdentifier.checkboxGroupItem(item.id) - - checkbox.publisher.sink(receiveValue: { [weak self] in - guard - let self, - let index = self.items.firstIndex(where: { $0.id == item.id}) - else { return } - - var item = self.items[index] - item.selectionState = $0 - self.items[index] = item - self.delegate?.checkboxGroup(self, didChangeSelection: self.items) - self.subject.send(self.items) - self.sendActions(for: .valueChanged) - }) - .store(in: &self.subscriptions) - - self.itemsStackView.addArrangedSubview(checkbox) - } - } - - private func setupView() { - - self.addSubview(self.titleStackView) - self.scrollView.addSubview(self.itemsStackView) - self.addSubview(self.scrollView) - - self.itemsStackView.arrangedSubviews.forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.widthAnchor.constraint(lessThanOrEqualTo: self.widthAnchor).isActive = true - } - - NSLayoutConstraint.stickEdges( - from: self.itemsStackView, - to: self.scrollView, - insets: UIEdgeInsets(top: self.padding, left: self.padding, bottom: self.padding, right: self.padding) - ) - - let constraint = self.itemsStackView.centerXAnchor.constraint(equalTo: self.scrollView.centerXAnchor) - constraint.priority = .defaultHigh - - NSLayoutConstraint.activate([ - self.titleStackView.topAnchor.constraint(equalTo: self.topAnchor), - self.titleStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - self.titleStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - - self.spacingView.heightAnchor.constraint(equalToConstant: self.spacingSmall), - - self.itemsStackView.heightAnchor.constraint(equalTo: self.scrollView.heightAnchor, constant: -2 * self.padding), - constraint, - - self.scrollView.topAnchor.constraint(equalTo: self.titleStackView.bottomAnchor, constant: -self.padding), - self.scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -self.padding), - self.scrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: self.padding), - self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: self.padding), - ]) - } -} - -// MARK: - Updates -extension CheckboxGroupUIView { - - public func updateItems(_ items: [any CheckboxGroupItemProtocol]) { - self.items.removeAll() - self.itemsStackView.removeArrangedSubviews() - self.items = items - self.setupItemsStackView() - self.itemsStackView.arrangedSubviews.forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.widthAnchor.constraint(lessThanOrEqualTo: self.widthAnchor).isActive = true - } - } - - private func updateTheme() { - self.spacingLarge = self.theme.layout.spacing.large - self.spacingSmall = self.theme.layout.spacing.small - self.checkboxes.forEach { $0.theme = theme } - } - - private func updateTitle() { - if let title = self.title, !title.isEmpty { - self.titleLabel.text = title - self.spacingView.isHidden = false - self.titleLabel.isHidden = false - } else { - self.spacingView.isHidden = true - self.titleLabel.isHidden = true - } - } - - private func updateImage() { - self.checkboxes.forEach { $0.checkedImage = self.checkedImage } - } - - private func updateLayout() { - self.itemsStackView.axis = self.layout == .horizontal ? .horizontal : .vertical - self.itemsStackView.alignment = self.layout == .horizontal ? .top : .fill - } - - private func updateAlignment() { - self.checkboxes.forEach { $0.alignment = self.alignment } - self.layoutIfNeeded() - } - - private func updateIntent() { - self.checkboxes.forEach { $0.intent = self.intent } - } -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewActionTests.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewActionTests.swift deleted file mode 100644 index 847ee1d1b..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewActionTests.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// CheckboxGroupUIViewActionTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 14.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class CheckboxGroupUIViewActionTests: TestCase { - // MARK: Private Properties - private var theme: Theme! - private var subscriptions: Set! - // swiftlint:disable weak_delegate - private var delegate: CheckboxGroupUIViewDelegateGeneratedMock! - private var items: [any CheckboxGroupItemProtocol] = [ - CheckboxGroupItemDefault(title: "Apple", id: "1", selectionState: .selected, isEnabled: true), - CheckboxGroupItemDefault(title: "Cake", id: "2", selectionState: .indeterminate, isEnabled: true), - CheckboxGroupItemDefault(title: "Fish", id: "3", selectionState: .unselected, isEnabled: true), - CheckboxGroupItemDefault(title: "Fruit", id: "4", selectionState: .unselected, isEnabled: true), - CheckboxGroupItemDefault(title: "Vegetables", id: "5", selectionState: .selected, isEnabled: false) - ] - - // MARK: - Setup - override func setUp() { - super.setUp() - self.theme = SparkTheme.shared - self.subscriptions = .init() - self.delegate = .init() - } - - // MARK: - Tests - func test_action_from_checbox_item_touchUpInside() { - let sut = sut() - - let exp = expectation(description: "Checkbox change should be published") - - var selectionState: CheckboxSelectionState = .indeterminate - - sut.publisher.sink(receiveValue: { items in - selectionState = items[0].selectionState - exp.fulfill() - }) - .store(in: &self.subscriptions) - - sut.checkboxes[0].sendActions(for: .touchUpInside) - - wait(for: [exp], timeout: 3.0) - - XCTAssertEqual(selectionState, .unselected) - XCTAssertEqual(self.delegate.checkboxGroupWithCheckboxGroupAndStatesCallsCount, 1) - XCTAssertEqual(self.delegate.checkboxGroupWithCheckboxGroupAndStatesReceivedArguments?.states[0].selectionState, .unselected) - } - - // MARK: Private Functions - private func sut() -> CheckboxGroupUIView { - let sut = CheckboxGroupUIView( - checkedImage: IconographyTests.shared.checkmark, - items: self.items, - alignment: .left, - theme: self.theme, - accessibilityIdentifierPrefix: "XX" - ) - - sut.delegate = self.delegate - return sut - } -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewDelegate.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewDelegate.swift deleted file mode 100644 index c23c21b2f..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewDelegate.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CheckboxGroupUIViewDelegate.swift -// SparkCore -// -// Created by michael.zimmermann on 13.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The checkbox groupe delegate informs about a changes to any of the checkbox selection state. -// sourcery: AutoMockable -public protocol CheckboxGroupUIViewDelegate: AnyObject { - /// The checkbox group selection was changed. - /// - Parameters: - /// - _: The updated checkbox group. - /// - didChangeSelection: It will return items. - - func checkboxGroup(_ checkboxGroup: CheckboxGroupUIView, didChangeSelection states: [any CheckboxGroupItemProtocol]) -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewSnapshotTests.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewSnapshotTests.swift deleted file mode 100644 index c74e38861..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxGroupUIViewSnapshotTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// CheckboxGroupUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 16.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -final class CheckboxGroupUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = CheckboxGroupScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration() - - for (index, configuration) in configurations.enumerated() { - - let view = CheckboxGroupUIView( - checkedImage: configuration.image, - items: configuration.items, - layout: configuration.axis, - alignment: configuration.alignment, - theme: self.theme, - intent: configuration.intent, - accessibilityIdentifierPrefix: "\(index)" - ) - view.translatesAutoresizingMaskIntoConstraints = false - - let containerView = UIView() - containerView.backgroundColor = UIColor.systemBackground - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(view) - - NSLayoutConstraint.stickEdges(from: view, to: containerView) - - if configuration.axis == .vertical { - containerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width).isActive = true - } else { - containerView.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.size.width).isActive = true - } - - self.assertSnapshot( - matching: containerView, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIView.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIView.swift deleted file mode 100644 index c6c2abc3f..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIView.swift +++ /dev/null @@ -1,428 +0,0 @@ -// -// CheckboxUIView.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 17.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -/// The `CheckboxUIView`renders a single checkbox using UIKit. -public final class CheckboxUIView: UIControl { - - // MARK: - Private Properties. - private let textLabel: UILabel = { - let label = UILabel() - label.isAccessibilityElement = false - label.backgroundColor = .clear - label.numberOfLines = 0 - label.setContentHuggingPriority(.defaultHigh, for: .vertical) - label.adjustsFontForContentSizeCategory = true - return label - }() - - private lazy var controlView: CheckboxControlUIView = { - let controlView = CheckboxControlUIView( - selectionIcon: self.checkedImage, - colors: self.viewModel.colors, - isEnabled: self.isEnabled, - selectionState: self.selectionState, - isHighlighted: self.isHighlighted - ) - controlView.isAccessibilityElement = false - return controlView - }() - - private lazy var stackView: UIStackView = { - let arrangedSubviews = self.alignment == .left ? [controlView, textLabel] : [textLabel, controlView] - let stackView = UIStackView(arrangedSubviews: arrangedSubviews) - stackView.axis = .horizontal - stackView.spacing = self.spacing - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .top - stackView.isUserInteractionEnabled = false - return stackView - }() - - private var cancellables = Set() - private var checkboxSelectionStateSubject = PassthroughSubject() - @ScaledUIMetric private var checkboxSize: CGFloat = CheckboxControlUIView.Constants.size - @ScaledUIMetric private var spacing: CGFloat - - private var checkboxSizeConstraint: NSLayoutConstraint? - private var textObserver: NSKeyValueObservation? - private var attributedTextObserver: NSKeyValueObservation? - - // MARK: - Public properties. - - /// Changes to the checbox state are published to the publisher. - public var publisher: some Publisher { - return self.checkboxSelectionStateSubject - } - - /// Set a delegate to receive selection state change callbacks. Alternatively, you can use bindings. - public weak var delegate: CheckboxUIViewDelegate? - - /// The text displayed in the checkbox. - public var text: String? { - get { - return self.textLabel.text - } - set { - self.viewModel.text = .left(newValue.map(NSAttributedString.init)) - } - } - - /// The attributed text displayed in the checkbox. - public var attributedText: NSAttributedString? { - get { - return self.textLabel.attributedText - } - set { - self.viewModel.text = .left(newValue) - } - } - - /// The checkedImage displayed in the checkbox when status is selected. - public var checkedImage: UIImage { - get { - return self.viewModel.checkedImage.leftValue - } - set { - self.viewModel.checkedImage = .left(newValue) - } - } - - /// The current selection state of the checkbox. - public var selectionState: CheckboxSelectionState { - get { - return self.viewModel.selectionState - } - set { - self.viewModel.selectionState = newValue - } - } - - /// Returns the theme of the checkbox. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// Returns the intent of the checkbox. - public var intent: CheckboxIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - /// Returns the alignment of the checkbox. - public var alignment: CheckboxAlignment { - get { - return self.viewModel.alignment - } - set { - self.viewModel.alignment = newValue - } - } - - /// The current state of the checkbox. - public override var isEnabled: Bool { - get { - return self.viewModel.isEnabled - } - set { - self.viewModel.isEnabled = newValue - } - } - - public override var isHighlighted: Bool { - didSet { - self.controlView.isHighlighted = self.isHighlighted - } - } - - public override var isSelected: Bool { - get { - return self.selectionState == .selected - } - set { - if newValue == true { - self.selectionState = .selected - } else { - self.selectionState = .unselected - } - } - } - - var viewModel: CheckboxViewModel - - // MARK: - Initialization - - /// Not implemented. Please use another init. - /// - Parameter coder: the coder. - public required init?(coder: NSCoder) { - fatalError("not implemented") - } - - /// Initialize a new checkbox UIKit-view. - /// - Parameters: - /// - theme: The current Spark-Theme. - /// - intent: The current Intent. - /// - text: The checkbox text. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - isEnabled: IsEnabled describes whether the checkbox is enabled or disabled. - /// - selectionState: `CheckboxSelectionState` is either selected, unselected or indeterminate. - /// - alignment: Positions the checkbox on the leading or trailing edge of the view. - public convenience init( - theme: Theme, - intent: CheckboxIntent = .main, - text: String, - checkedImage: UIImage, - isEnabled: Bool = true, - selectionState: CheckboxSelectionState, - alignment: CheckboxAlignment - ) { - self.init( - theme: theme, - intent: intent, - content: .left(NSAttributedString(string: text)), - checkedImage: .left(checkedImage), - isEnabled: isEnabled, - selectionState: selectionState, - alignment: alignment - ) - } - - /// Initialize a new checkbox UIKit-view. - /// - Parameters: - /// - theme: The current Spark-Theme. - /// - intent: The current Intent. - /// - attributedText: The checkbox attributeText. - /// - checkedImage: The tick-checkbox image for checked-state. - /// - isEnabled: IsEnabled describes whether the checkbox is enabled or disabled. - /// - selectionState: `CheckboxSelectionState` is either selected, unselected or indeterminate. - /// - alignment: Positions the checkbox on the leading or trailing edge of the view. - public convenience init( - theme: Theme, - intent: CheckboxIntent = .main, - attributedText: NSAttributedString, - checkedImage: UIImage, - isEnabled: Bool = true, - selectionState: CheckboxSelectionState, - alignment: CheckboxAlignment - ) { - self.init( - theme: theme, - intent: intent, - content: .left(attributedText), - checkedImage: .left(checkedImage), - isEnabled: isEnabled, - selectionState: selectionState, - alignment: alignment - ) - } - - init( - theme: Theme, - intent: CheckboxIntent = .main, - content: Either, - checkedImage: Either, - isEnabled: Bool = true, - selectionState: CheckboxSelectionState, - alignment: CheckboxAlignment - ) { - let viewModel = CheckboxViewModel( - text: content, - checkedImage: checkedImage, - theme: theme, - intent: intent, - isEnabled: isEnabled, - alignment: alignment, - selectionState: selectionState - ) - self.spacing = viewModel.spacing - self.viewModel = viewModel - super.init(frame: .zero) - self.commonInit() - } - - private func commonInit() { - self.setupViews() - self.enableTouch() - self.subscribe() - self.updateAccessibility() - self.addActions() - self.addObservers() - } - - // MARK: - Methods - private func addActions() { - let toggleAction = UIAction { [weak self] _ in - guard let self else { return } - if self.selectionState == .indeterminate { - self.isSelected = true - } else { - self.isSelected.toggle() - } - self.delegate?.checkbox(self, didChangeSelection: self.selectionState) - self.checkboxSelectionStateSubject.send(self.selectionState) - self.sendActions(for: .valueChanged) - } - self.addAction(toggleAction, for: .touchUpInside) - } - - private func addObservers() { - self.textObserver = textLabel.observe(\UILabel.text, options: [.new, .old]) { [weak self] label, observedChange in - if let newText = observedChange.newValue, - let oldText = observedChange.oldValue, - newText != oldText { - self?.text = newText - } - } - - self.attributedTextObserver = textLabel.observe(\UILabel.attributedText, options: [.new, .old]) { [weak self] label, observedChange in - if let newText = observedChange.newValue, - let oldText = observedChange.oldValue, - newText != oldText { - self?.attributedText = newText - } - } - } - - private func setupViews() { - self.translatesAutoresizingMaskIntoConstraints = false - - self.addSubview(self.stackView) - - NSLayoutConstraint.stickEdges(from: self.stackView, to: self) - - let checkboxWidthConstraint = self.controlView.widthAnchor.constraint(equalToConstant: self.checkboxSize) - self.checkboxSizeConstraint = checkboxWidthConstraint - - NSLayoutConstraint.activate([ - checkboxWidthConstraint, - self.controlView.heightAnchor.constraint(equalTo: self.controlView.widthAnchor), - self.textLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: self.checkboxSize), - self.heightAnchor.constraint(equalTo: textLabel.heightAnchor) - ]) - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { return } - - self._checkboxSize.update(traitCollection: self.traitCollection) - self.checkboxSizeConstraint?.constant = self.checkboxSize - self._spacing.update(traitCollection: traitCollection) - self.stackView.spacing = self.spacing - } - - private func subscribe() { - self.viewModel.$colors.subscribe(in: &self.cancellables) { [weak self] colors in - guard let self else { return } - self.updateTheme(colors: colors) - } - - self.viewModel.$opacity.subscribe(in: &self.cancellables) { [weak self] opacity in - guard let self else { return } - self.layer.opacity = Float(opacity) - self.setAccessibilityEnable() - } - - self.viewModel.$selectionState.subscribe(in: &self.cancellables) { [weak self] selectionState in - guard let self else { return } - self.controlView.selectionState = selectionState - self.setAccessibilityValue(state: selectionState) - } - - self.viewModel.$alignment.subscribe(in: &self.cancellables) { [weak self] alignment in - guard let self else { return } - self.updateAlignment(alignment: alignment) - } - - self.viewModel.$text.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] text in - guard let self else { return } - let labelHidden: Bool = (text.leftValue?.string ?? "").isEmpty - self.textLabel.isHidden = labelHidden - self.textLabel.font = self.viewModel.font.uiFont - self.textLabel.attributedText = text.leftValue - self.setAccessibilityLabel(text.leftValue?.string) - } - - self.viewModel.$checkedImage.subscribe(in: &self.cancellables) { [weak self] icon in - guard let self else { return } - self.controlView.selectionIcon = icon.leftValue - } - - self.viewModel.$spacing.subscribe(in: &self.cancellables) { [weak self] spacing in - guard let self = self else { return } - self._spacing.wrappedValue = spacing - self.stackView.spacing = self.spacing - } - } - - deinit { - self.textObserver?.invalidate() - self.attributedTextObserver?.invalidate() - } -} - -// MARK: Updates -private extension CheckboxUIView { - - private func updateAccessibility() { - self.accessibilityIdentifier = CheckboxAccessibilityIdentifier.checkbox - self.isAccessibilityElement = true - self.accessibilityTraits.insert(.button) - self.accessibilityTraits.remove(.selected) - self.setAccessibilityLabel(self.textLabel.text) - self.setAccessibilityValue(state: self.selectionState) - self.setAccessibilityEnable() - } - - private func setAccessibilityLabel(_ label: String?) { - self.accessibilityLabel = label - } - - private func setAccessibilityValue(state: CheckboxSelectionState) { - switch state { - case .selected: - self.accessibilityValue = CheckboxAccessibilityValue.checked - case .indeterminate: - self.accessibilityValue = CheckboxAccessibilityValue.indeterminate - case .unselected: - self.accessibilityValue = CheckboxAccessibilityValue.unchecked - } - } - - private func setAccessibilityEnable() { - if self.isEnabled { - self.accessibilityTraits.remove(.notEnabled) - } else { - self.accessibilityTraits.insert(.notEnabled) - } - } - - private func updateTheme(colors: CheckboxColors) { - self.controlView.colors = colors - self.textLabel.textColor = self.viewModel.colors.textColor.uiColor - self.textLabel.alpha = self.viewModel.opacity - self.textLabel.attributedText = self.viewModel.text.leftValue - } - - private func updateAlignment(alignment: CheckboxAlignment) { - self.stackView.spacing = self.spacing - self.stackView.insertArrangedSubview(alignment == .left ? self.controlView : self.textLabel, at: 0) - } -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewDelegate.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewDelegate.swift deleted file mode 100644 index 4be6111ae..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewDelegate.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CheckboxUIViewDelegate.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 18.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The checkbox delegate informs about a new checkbox selection state. -// sourcery: AutoMockable -public protocol CheckboxUIViewDelegate: AnyObject { - /// The checkbox selection was changed. - /// - Parameters: - /// - checkbox: The updated checkbox. - /// - state: The new checkbox state. - func checkbox(_ checkbox: CheckboxUIView, didChangeSelection state: CheckboxSelectionState) -} diff --git a/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewSnapshotTests.swift b/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewSnapshotTests.swift deleted file mode 100644 index 88ec57020..000000000 --- a/core/Sources/Components/Checkbox/View/UIKit/CheckboxUIViewSnapshotTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// CheckboxUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 12.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -final class CheckboxUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = CheckboxScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration() - - for configuration in configurations { - - let view = CheckboxUIView( - theme: self.theme, - intent: configuration.intent, - text: configuration.text, - checkedImage: configuration.image, - isEnabled: configuration.state == .disabled ? false : true, - selectionState: configuration.selectionState, - alignment: configuration.alignment - ) - view.backgroundColor = UIColor.systemBackground - - NSLayoutConstraint.activate([ - view.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.size.width) - ]) - - if configuration.state == .pressed { - view.isHighlighted = true - - let containerView = UIView() - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(view) - - NSLayoutConstraint.activate([ - view.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5), - view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -5), - view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 5), - view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -5) - ]) - - self.assertSnapshot( - matching: containerView, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } else { - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } - } -} diff --git a/core/Sources/Components/Chip/AccessiilityIdentifier/ChipAccessibilityIdentifier.swift b/core/Sources/Components/Chip/AccessiilityIdentifier/ChipAccessibilityIdentifier.swift deleted file mode 100644 index 2f7a87663..000000000 --- a/core/Sources/Components/Chip/AccessiilityIdentifier/ChipAccessibilityIdentifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ChipAccessibilityIdentifier.swift -// SparkCore -// -// Created by michael.zimmermann on 20.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the chip. -public enum ChipAccessibilityIdentifier { - - // MARK: - Properties - - /// The accessibility identifier. - public static let identifier = "spark-chip" - /// The text label accessibility identifier. - public static let text = "spark-chip-text" - /// The icon accessibility identifier. - public static let icon = "spark-chip-icon" -} diff --git a/core/Sources/Components/Chip/Enum/ChipAlignment.swift b/core/Sources/Components/Chip/Enum/ChipAlignment.swift deleted file mode 100644 index c88fbde7a..000000000 --- a/core/Sources/Components/Chip/Enum/ChipAlignment.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ChipAlignment.swift -// SparkCore -// -// Created by michael.zimmermann on 21.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum ChipAlignment: CaseIterable { - /// Icon on the leading edge of the chip. - /// Text on the trailing edge of the chip. - /// Not interpreted if chip contains just an icon or just text. - case leadingIcon - /// Icon on the trailing edge of the chip. - /// Text on the leading edge of the chip - /// Not interpreted if the chip contains just an icon or just text. - case trailingIcon -} diff --git a/core/Sources/Components/Chip/Enum/ChipConstants.swift b/core/Sources/Components/Chip/Enum/ChipConstants.swift deleted file mode 100644 index f41e2ceac..000000000 --- a/core/Sources/Components/Chip/Enum/ChipConstants.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ChipConstants.swift -// SparkCore -// -// Created by michael.zimmermann on 17.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// MARK: - Constants - -enum ChipConstants { - static let imageSize: CGFloat = 16 - static let height: CGFloat = 32 - static let borderWidth: CGFloat = 1 - static let dashLength: CGFloat = 1.9 -} - diff --git a/core/Sources/Components/Chip/Enum/ChipIntent.swift b/core/Sources/Components/Chip/Enum/ChipIntent.swift deleted file mode 100644 index 15206f355..000000000 --- a/core/Sources/Components/Chip/Enum/ChipIntent.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ChipIntentColor.swift -// SparkCore -// -// Created by michael.zimmermann on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The various intent color a chip may have. -public enum ChipIntent: CaseIterable { - case main - case support - case surface - case success - case danger - case alert - case neutral - case info - case accent - case basic -} diff --git a/core/Sources/Components/Chip/Enum/ChipVariant.swift b/core/Sources/Components/Chip/Enum/ChipVariant.swift deleted file mode 100644 index dd4e68f44..000000000 --- a/core/Sources/Components/Chip/Enum/ChipVariant.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ChipVariant.swift -// SparkCore -// -// Created by michael.zimmermann on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The different variants of a chip -public enum ChipVariant: CaseIterable { - case outlined - case tinted - case dashed -} diff --git a/core/Sources/Components/Chip/Model/ChipContent.swift b/core/Sources/Components/Chip/Model/ChipContent.swift deleted file mode 100644 index 2fd1163ae..000000000 --- a/core/Sources/Components/Chip/Model/ChipContent.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ChipContent.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 20.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -public struct ChipContent: Updateable { - public var title: String? - public var icon: Image? - public var component: AnyView? -} diff --git a/core/Sources/Components/Chip/Model/ChipIntentColors.swift b/core/Sources/Components/Chip/Model/ChipIntentColors.swift deleted file mode 100644 index 147689472..000000000 --- a/core/Sources/Components/Chip/Model/ChipIntentColors.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ChipIntentColors.swift -// SparkCore -// -// Created by michael.zimmermann on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The intent colors a chip can have -struct ChipIntentColors { - - // MARK: - Properties - - let border: any ColorToken - let text: any ColorToken - let selectedText: any ColorToken - let background: any ColorToken - let pressedBackground: any ColorToken - let selectedBackground: any ColorToken - let disabledBackground: (any ColorToken)? - - init(border: any ColorToken, - text: any ColorToken, - selectedText: any ColorToken, - background: any ColorToken, - pressedBackground: any ColorToken, - selectedBackground: any ColorToken, - disabledBackground: (any ColorToken)? = nil) { - self.border = border - self.text = text - self.selectedText = selectedText - self.background = background - self.pressedBackground = pressedBackground - self.selectedBackground = selectedBackground - self.disabledBackground = disabledBackground - } -} diff --git a/core/Sources/Components/Chip/Model/ChipState.swift b/core/Sources/Components/Chip/Model/ChipState.swift deleted file mode 100644 index 581524b36..000000000 --- a/core/Sources/Components/Chip/Model/ChipState.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ChipState.swift -// SparkCore -// -// Created by michael.zimmermann on 21.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ChipState { - - static let `default` = ChipState(isEnabled: true, isPressed: false, isSelected: false) - - let isEnabled: Bool - let isPressed: Bool - let isSelected: Bool - - var isDisabled: Bool { - return !self.isEnabled - } -} diff --git a/core/Sources/Components/Chip/Model/ChipStateColors.swift b/core/Sources/Components/Chip/Model/ChipStateColors.swift deleted file mode 100644 index 378e85f01..000000000 --- a/core/Sources/Components/Chip/Model/ChipStateColors.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ChipStateColors.swift -// SparkCore -// -// Created by michael.zimmermann on 08.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The colors definie a chip -struct ChipStateColors { - var background: any ColorToken - let border: any ColorToken - var foreground: any ColorToken - var opacity: CGFloat - - init(background: any ColorToken = ColorTokenDefault.clear, - border: any ColorToken, - foreground: any ColorToken, - opacity: CGFloat = 1.0) { - self.background = background - self.border = border - self.foreground = foreground - self.opacity = opacity - } - -} - -extension ChipStateColors: Equatable { - static func == (lhs: ChipStateColors, rhs: ChipStateColors) -> Bool { - return lhs.background.equals(rhs.background) && - lhs.border.equals(rhs.border) && - lhs.foreground.equals(rhs.foreground) && - lhs.opacity == rhs.opacity - } -} diff --git a/core/Sources/Components/Chip/Model/ChipStateColorsTests.swift b/core/Sources/Components/Chip/Model/ChipStateColorsTests.swift deleted file mode 100644 index bf2f5de3a..000000000 --- a/core/Sources/Components/Chip/Model/ChipStateColorsTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ChipStateColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ChipStateColorsTests: XCTestCase { - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = ChipStateColors( - background: colors.base.background, - border: colors.base.onBackgroundVariant, - foreground: colors.feedback.alert) - - let colors2 = ChipStateColors( - background: colors.base.background, - border: colors.base.onBackgroundVariant, - foreground: colors.feedback.alert) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = ChipStateColors( - background: colors.base.background, - border: colors.base.onBackgroundVariant, - foreground: colors.feedback.alert) - - let colors2 = ChipStateColors( - background: colors.base.backgroundVariant, - border: colors.base.onBackgroundVariant, - foreground: colors.feedback.alert) - - XCTAssertNotEqual(colors1, colors2) - } - -} diff --git a/core/Sources/Components/Chip/Model/ChipStateTests.swift b/core/Sources/Components/Chip/Model/ChipStateTests.swift deleted file mode 100644 index 60472090e..000000000 --- a/core/Sources/Components/Chip/Model/ChipStateTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ChipStateTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 23.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class ChipStateTests: XCTestCase { - - func test_default() { - let sut = ChipState.default - - XCTAssertTrue(sut.isEnabled, "By default enabled should be true.") - XCTAssertFalse(sut.isDisabled, "Expected isDisabled to be the oposite of enabled.") - XCTAssertFalse(sut.isPressed, "By default, the pressed state is not set") - XCTAssertFalse(sut.isSelected, "By default, the selected state is not set") - } - - func test_disabled() { - let sut = ChipState(isEnabled: false, isPressed: true, isSelected: false) - - XCTAssertFalse(sut.isEnabled, "Expected the state not to be enabled.") - XCTAssertTrue(sut.isDisabled, "Expected the state to be disabled.") - XCTAssertTrue(sut.isPressed, "The pressed state should be true.") - } - -} diff --git a/core/Sources/Components/Chip/UseCase/ChipGetColorsUseCase.swift b/core/Sources/Components/Chip/UseCase/ChipGetColorsUseCase.swift deleted file mode 100644 index 5f6939931..000000000 --- a/core/Sources/Components/Chip/UseCase/ChipGetColorsUseCase.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// ChipGetColorsUseCase.swift -// SparkDemo -// -// Created by michael.zimmermann on 03.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI -import Foundation - -/// A use case to calculate the colors of a chip depending on the theme, variant and intent -// sourcery: AutoMockable -protocol ChipGetColorsUseCasable { - /// Function `execute` calculates the chip colors - /// - /// Parameters: - /// - theme: The spark theme. - /// - variant: The variant of the chip, if it is outlined, filled, etc. - /// - intent: The intent color, e.g. main, support. - /// - state: The current state of the chip - /// Returns: - /// ChipColors: all the colors used for the chip - func execute(theme: Theme, - variant: ChipVariant, - intent: ChipIntent, - state: ChipState - ) -> ChipStateColors -} - -/// ChipGetColorsUseCase: A use case to calculate the colors of a chip depending on the theme, variand and intent -struct ChipGetColorsUseCase: ChipGetColorsUseCasable { - // MARK: - Properties - private let outlinedIntentColorsUseCase: ChipGetIntentColorsUseCasable - private let tintedIntentColorsUseCase: ChipGetIntentColorsUseCasable - - // MARK: - Initializer - /// Initializer - /// - /// Parameters: - /// - outlinedIntentColorsUseCase: A use case to calculate the intent colors of outlined chips. - /// - tintedIntentColorsUseCase: A use case to calculate the intent colors of tinted chips. - init(outlinedIntentColorsUseCase: ChipGetIntentColorsUseCasable = ChipGetOutlinedIntentColorsUseCase(), - tintedIntentColorsUseCase: ChipGetIntentColorsUseCasable = ChipGetTintedIntentColorsUseCase() - ) { - self.outlinedIntentColorsUseCase = outlinedIntentColorsUseCase - self.tintedIntentColorsUseCase = tintedIntentColorsUseCase - - } - - // MARK: - Functions - - /// The funcion execute calculates the chip colors based on the parameters. - /// - /// Parameters: - /// - theme: The current theme to be used - /// - variant: The variant of the chip, whether it's filled, outlined, etc. - /// - intent: The intent color of the chip, e.g. main, support - /// - state: The current state of the chip, e.g. selected, enabled, pressed - func execute(theme: Theme, - variant: ChipVariant, - intent: ChipIntent, - state: ChipState) -> ChipStateColors { - - let intentUseCase: ChipGetIntentColorsUseCasable = variant == .tinted ? self.tintedIntentColorsUseCase : self.outlinedIntentColorsUseCase - - let colors = intentUseCase.execute(theme: theme, intent: intent) - - if state.isPressed { - return .init( - background: colors.pressedBackground, - border: colors.border, - foreground: colors.text) - } - - var stateColors = ChipStateColors( - background: colors.background, - border: colors.border, - foreground: colors.text) - - if state.isSelected { - stateColors.background = colors.selectedBackground - stateColors.foreground = colors.selectedText - } - - if state.isDisabled { - if let backgroundColor = colors.disabledBackground { - stateColors.background = backgroundColor - } - stateColors.opacity = theme.dims.dim3 - } - - return stateColors - } -} diff --git a/core/Sources/Components/Chip/UseCase/ChipGetColorsUseCaseTests.swift b/core/Sources/Components/Chip/UseCase/ChipGetColorsUseCaseTests.swift deleted file mode 100644 index eb09bada8..000000000 --- a/core/Sources/Components/Chip/UseCase/ChipGetColorsUseCaseTests.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// ChipGetColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 08.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class ChipGetColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - private var sut: ChipGetColorsUseCase! - private var outlinedIntentColorsUseCase: ChipGetIntentColorsUseCasableGeneratedMock! - private var tintedIntentColorsUseCase: ChipGetIntentColorsUseCasableGeneratedMock! - private var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.outlinedIntentColorsUseCase = .init() - self.tintedIntentColorsUseCase = .init() - self.sut = .init( - outlinedIntentColorsUseCase: self.outlinedIntentColorsUseCase, - tintedIntentColorsUseCase: self.tintedIntentColorsUseCase) - self.theme = .init() - - let dims = DimsGeneratedMock() - dims.dim3 = 0.33 - self.theme.dims = dims - } - -// MARK: - Tests - - func test_outlined_variant_uses_correct_use_case() { - // Given - let chipIntentColors = ChipIntentColors.mocked() - - self.outlinedIntentColorsUseCase.executeWithThemeAndIntentReturnValue = chipIntentColors - - let expectedColors = ChipStateColors( - background: chipIntentColors.background, - border: chipIntentColors.border, - foreground: chipIntentColors.text, - opacity: 1.0) - - // When - let colors = self.sut.execute(theme: self.theme, variant: .outlined, intent: .basic, state: .default) - - XCTAssertEqual(colors, expectedColors) - } - - func test_tinted_variant_uses_correct_use_case() { - // Given - let chipIntentColors = ChipIntentColors.mocked() - - self.tintedIntentColorsUseCase.executeWithThemeAndIntentReturnValue = chipIntentColors - - let expectedColors = ChipStateColors( - background: chipIntentColors.background, - border: chipIntentColors.border, - foreground: chipIntentColors.text, - opacity: 1.0) - - // When - let colors = self.sut.execute(theme: self.theme, variant: .tinted, intent: .basic, state: .default) - - XCTAssertEqual(colors, expectedColors) - } - - func test_pressed_has_correct_background() { - // Given - let chipIntentColors = ChipIntentColors.mocked() - - self.tintedIntentColorsUseCase.executeWithThemeAndIntentReturnValue = chipIntentColors - - let expectedColors = ChipStateColors( - background: chipIntentColors.pressedBackground, - border: chipIntentColors.border, - foreground: chipIntentColors.text, - opacity: 1.0) - - // When - let colors = self.sut.execute(theme: self.theme, variant: .tinted, intent: .basic, state: .pressed) - - XCTAssertEqual(colors, expectedColors) - } - - func test_selected_has_correct_backgorund_and_text() { - // Given - let chipIntentColors = ChipIntentColors.mocked() - - self.tintedIntentColorsUseCase.executeWithThemeAndIntentReturnValue = chipIntentColors - - let expectedColors = ChipStateColors( - background: chipIntentColors.selectedBackground, - border: chipIntentColors.border, - foreground: chipIntentColors.selectedText, - opacity: 1.0) - - // When - let colors = self.sut.execute(theme: self.theme, variant: .tinted, intent: .basic, state: .selected) - - XCTAssertEqual(colors, expectedColors) - } - - func test_disabled_has_correct_opacity() { - // Given - let chipIntentColors = ChipIntentColors.mocked() - - self.tintedIntentColorsUseCase.executeWithThemeAndIntentReturnValue = chipIntentColors - - let expectedColors = ChipStateColors( - background: chipIntentColors.background, - border: chipIntentColors.border, - foreground: chipIntentColors.text, - opacity: self.theme.dims.dim3) - - // When - let colors = self.sut.execute(theme: self.theme, variant: .tinted, intent: .basic, state: .disabled) - - XCTAssertEqual(colors, expectedColors) - } - - func test_selected_and_disabled_has_correct_background() { - // Given - let chipIntentColors = ChipIntentColors.mocked() - - self.tintedIntentColorsUseCase.executeWithThemeAndIntentReturnValue = chipIntentColors - - let expectedColors = ChipStateColors( - background: chipIntentColors.selectedBackground, - border: chipIntentColors.border, - foreground: chipIntentColors.selectedText, - opacity: self.theme.dims.dim3) - - // When - let colors = self.sut.execute(theme: self.theme, variant: .tinted, intent: .basic, state: .selectedDisabled) - - XCTAssertEqual(colors, expectedColors) - } -} - -private extension ChipIntentColors { - static func mocked() -> ChipIntentColors { - return .init( - border: ColorTokenGeneratedMock.random(), - text: ColorTokenGeneratedMock.random(), - selectedText: ColorTokenGeneratedMock.random(), - background: ColorTokenGeneratedMock.random(), - pressedBackground: ColorTokenGeneratedMock.random(), - selectedBackground: ColorTokenGeneratedMock.random()) - } -} - -private extension ChipState { - static let pressed = ChipState(isEnabled: true, isPressed: true, isSelected: false) - static let disabled = ChipState(isEnabled: false, isPressed: false, isSelected: false) - static let selected = ChipState(isEnabled: true, isPressed: false, isSelected: true) - static let selectedDisabled = ChipState(isEnabled: false, isPressed: false, isSelected: true) -} diff --git a/core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCase.swift b/core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCase.swift deleted file mode 100644 index afd14617e..000000000 --- a/core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCase.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// ChipGetIntentColorsUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol ChipGetIntentColorsUseCasable { - func execute(theme: Theme, intent: ChipIntent) -> ChipIntentColors -} - -/// ChipGetIntentColorsUseCase: A use case to calculate the colors that are needed by a chip -struct ChipGetOutlinedIntentColorsUseCase: ChipGetIntentColorsUseCasable { - - /// Function `execute` calculates the intent colors used by a chip - /// - /// Parameters: - /// - theme: The availablethee - /// - intent: The intent of the chip - /// - /// Returns: ChipIntentColors - func execute(theme: Theme, intent: ChipIntent) -> ChipIntentColors { - let colors = theme.colors - let opacity = theme.dims.dim5 - - switch intent { - case .main: return .init( - border: colors.main.main, - text: colors.main.main, - selectedText: colors.main.onMainContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.main.main.opacity(opacity), - selectedBackground: colors.main.mainContainer - ) - - case .support: return .init( - border: colors.support.support, - text: colors.support.support, - selectedText: colors.support.onSupportContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.support.support.opacity(opacity), - selectedBackground: colors.support.supportContainer - ) - - case .surface: return .init( - border: colors.base.surface, - text: colors.base.surface, - selectedText: colors.base.onSurface, - background: ColorTokenDefault.clear, - pressedBackground: colors.base.surface.opacity(opacity), - selectedBackground: colors.base.surface - ) - - case .neutral: return .init( - border: colors.feedback.neutral, - text: colors.feedback.neutral, - selectedText: colors.feedback.onNeutralContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.feedback.neutral.opacity(opacity), - selectedBackground: colors.feedback.neutralContainer - ) - - case .info: return .init( - border: colors.feedback.info, - text: colors.feedback.info, - selectedText: colors.feedback.onInfoContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.feedback.info.opacity(opacity), - selectedBackground: colors.feedback.infoContainer - ) - - case .success: return .init( - border: colors.feedback.success, - text: colors.feedback.success, - selectedText: colors.feedback.onSuccessContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.feedback.success.opacity(opacity), - selectedBackground: colors.feedback.successContainer - ) - - case .alert: return .init( - border: colors.feedback.alert, - text: colors.feedback.onAlertContainer, - selectedText: colors.feedback.onAlertContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.feedback.alert.opacity(opacity), - selectedBackground: colors.feedback.alertContainer - ) - - case .danger: return .init( - border: colors.feedback.error, - text: colors.feedback.error, - selectedText: colors.feedback.onErrorContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.feedback.error.opacity(opacity), - selectedBackground: colors.feedback.errorContainer - ) - - case .accent: return .init( - border: colors.accent.accent, - text: colors.accent.accent, - selectedText: colors.accent.onAccentContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.accent.accent.opacity(opacity), - selectedBackground: colors.accent.accentContainer - ) - - case .basic: return .init( - border: colors.basic.basic, - text: colors.basic.basic, - selectedText: colors.basic.onBasicContainer, - background: ColorTokenDefault.clear, - pressedBackground: colors.basic.basic.opacity(opacity), - selectedBackground: colors.basic.basicContainer - ) - } - } -} diff --git a/core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCaseTests.swift b/core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCaseTests.swift deleted file mode 100644 index 3715ab595..000000000 --- a/core/Sources/Components/Chip/UseCase/ChipGetOutlinedIntentColorsUseCaseTests.swift +++ /dev/null @@ -1,358 +0,0 @@ -// -// ChipGetOutlinedIntentColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 09.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ChipGetOutlinedIntentColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - private var sut: ChipGetOutlinedIntentColorsUseCase! - private var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.theme = ThemeGeneratedMock() - - self.sut = ChipGetOutlinedIntentColorsUseCase() - let dims = DimsGeneratedMock() - dims.dim5 = 0.55 - self.theme.dims = dims - } - - // MARK: - Tests - func test_main_color() { - // Given - self.setupMainColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .main) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.main.main, - colors.main.main, - colors.main.onMainContainer, - ColorTokenDefault.clear, - colors.main.main.opacity(theme.dims.dim5), - colors.main.mainContainer - ].map(\.uiColor)) - } - - func test_support_color() { - // Given - self.setupSupportColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .support) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.support.support, - colors.support.support, - colors.support.onSupportContainer, - ColorTokenDefault.clear, - colors.support.support.opacity(theme.dims.dim5), - colors.support.supportContainer - ].map(\.uiColor)) - } - - func test_basic_color() { - // Given - self.setupBasicColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .basic) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.basic.basic, - colors.basic.basic, - colors.basic.onBasicContainer, - ColorTokenDefault.clear, - colors.basic.basic.opacity(theme.dims.dim5), - colors.basic.basicContainer - ].map(\.uiColor)) - } - - func test_surface_color() { - // Given - self.setupSurfaceColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .surface) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.base.surface, - colors.base.surface, - colors.base.onSurface, - ColorTokenDefault.clear, - colors.base.surface.opacity(theme.dims.dim5), - colors.base.surface - ].map(\.uiColor)) - } - - func test_neutral_color() { - // Given - self.setupFeedbackNeutralColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .neutral) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.neutral, - colors.feedback.neutral, - colors.feedback.onNeutralContainer, - ColorTokenDefault.clear, - colors.feedback.neutral.opacity(theme.dims.dim5), - colors.feedback.neutralContainer - ].map(\.uiColor)) - } - - func test_info_color() { - // Given - self.setupFeedbackInfoColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .info) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.info, - colors.feedback.info, - colors.feedback.onInfoContainer, - ColorTokenDefault.clear, - colors.feedback.info.opacity(theme.dims.dim5), - colors.feedback.infoContainer - ].map(\.uiColor)) - } - - func test_alert_color() { - // Given - self.setupFeedbackAlertColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .alert) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.alert, - colors.feedback.onAlertContainer, - colors.feedback.onAlertContainer, - ColorTokenDefault.clear, - colors.feedback.alert.opacity(theme.dims.dim5), - colors.feedback.alertContainer - ].map(\.uiColor)) - } - - func test_danger_color() { - // Given - self.setupFeedbackDangerColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .danger) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.error, - colors.feedback.error, - colors.feedback.onErrorContainer, - ColorTokenDefault.clear, - colors.feedback.error.opacity(theme.dims.dim5), - colors.feedback.errorContainer - ].map(\.uiColor)) - } - - // MARK: - Private helpers - private func setupMainColors() { - let main = ColorsMainGeneratedMock() - main.main = ColorTokenGeneratedMock.random() - main.onMain = ColorTokenGeneratedMock.random() - main.mainContainer = ColorTokenGeneratedMock.random() - main.onMainContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.main = main - self.theme.colors = colors - } - - private func setupSupportColors() { - let support = ColorsSupportGeneratedMock() - support.support = ColorTokenGeneratedMock.random() - support.onSupport = ColorTokenGeneratedMock.random() - support.supportContainer = ColorTokenGeneratedMock.random() - support.onSupportContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.support = support - self.theme.colors = colors - } - - private func setupBasicColors() { - let basic = ColorsBasicGeneratedMock() - basic.basic = ColorTokenGeneratedMock.random() - basic.onBasic = ColorTokenGeneratedMock.random() - basic.basicContainer = ColorTokenGeneratedMock.random() - basic.onBasicContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.basic = basic - self.theme.colors = colors - } - - private func setupSurfaceColors() { - let base = ColorsBaseGeneratedMock() - base.surface = ColorTokenGeneratedMock.random() - base.onSurface = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.base = base - self.theme.colors = colors - } - - private func setupFeedbackNeutralColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.neutral = ColorTokenGeneratedMock.random() - feedback.onNeutral = ColorTokenGeneratedMock.random() - feedback.neutralContainer = ColorTokenGeneratedMock.random() - feedback.onNeutralContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - self.theme.colors = colors - } - - private func setupFeedbackInfoColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.info = ColorTokenGeneratedMock.random() - feedback.onInfo = ColorTokenGeneratedMock.random() - feedback.infoContainer = ColorTokenGeneratedMock.random() - feedback.onInfoContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - self.theme.colors = colors - } - - private func setupFeedbackAlertColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.alert = ColorTokenGeneratedMock.random() - feedback.onAlert = ColorTokenGeneratedMock.random() - feedback.alertContainer = ColorTokenGeneratedMock.random() - feedback.onAlertContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - self.theme.colors = colors - } - - private func setupFeedbackDangerColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.error = ColorTokenGeneratedMock.random() - feedback.onError = ColorTokenGeneratedMock.random() - feedback.errorContainer = ColorTokenGeneratedMock.random() - feedback.onErrorContainer = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - self.theme.colors = colors - } -} diff --git a/core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCase.swift b/core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCase.swift deleted file mode 100644 index 8dd0443ab..000000000 --- a/core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCase.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// ChipGetTintedIntentColorsUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 10.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ChipGetTintedIntentColorsUseCase: ChipGetIntentColorsUseCasable { - - /// Function `execute` calculates the intent colors used by a chip - /// - /// Parameters: - /// - theme: The availablethee - /// - intent: The intent of the chip - /// - /// Returns: ChipIntentColors - func execute(theme: Theme, intent: ChipIntent) -> ChipIntentColors { - let colors = theme.colors - - switch intent { - case .main: return .init( - border: colors.main.mainContainer, - text: colors.main.onMainContainer, - selectedText: colors.main.onMain, - background: colors.main.mainContainer, - pressedBackground: colors.states.mainContainerPressed, - selectedBackground: colors.main.main - ) - - case .support: return .init( - border: colors.support.supportContainer, - text: colors.support.onSupportContainer, - selectedText: colors.support.onSupport, - background: colors.support.supportContainer, - pressedBackground: colors.states.supportContainerPressed, - selectedBackground: colors.support.support - ) - - case .surface: return .init( - border: ColorTokenDefault.clear, - text: colors.base.surfaceInverse, - selectedText: colors.base.onSurface, - background: colors.base.surface.opacity(theme.dims.dim1), - pressedBackground: colors.states.surfacePressed, - selectedBackground: colors.base.surface, - disabledBackground: colors.base.surface - ) - - case .neutral: return .init( - border: colors.feedback.neutralContainer, - text: colors.feedback.onNeutralContainer, - selectedText: colors.feedback.onNeutral, - background: colors.feedback.neutralContainer, - pressedBackground: colors.states.neutralContainerPressed, - selectedBackground: colors.feedback.neutral - ) - - case .info: return .init( - border: colors.feedback.infoContainer, - text: colors.feedback.onInfoContainer, - selectedText: colors.feedback.onInfo, - background: colors.feedback.infoContainer, - pressedBackground: colors.states.infoContainerPressed, - selectedBackground: colors.feedback.info - ) - - case .success: return .init( - border: colors.feedback.successContainer, - text: colors.feedback.onSuccessContainer, - selectedText: colors.feedback.onSuccess, - background: colors.feedback.successContainer, - pressedBackground: colors.states.successContainerPressed, - selectedBackground: colors.feedback.success - ) - - case .alert: return .init( - border: colors.feedback.alertContainer, - text: colors.feedback.onAlertContainer, - selectedText: colors.feedback.onAlert, - background: colors.feedback.alertContainer, - pressedBackground: colors.states.alertContainerPressed, - selectedBackground: colors.feedback.alert - ) - - case .danger: return .init( - border: colors.feedback.errorContainer, - text: colors.feedback.onErrorContainer, - selectedText: colors.feedback.onError, - background: colors.feedback.errorContainer, - pressedBackground: colors.states.errorContainerPressed, - selectedBackground: colors.feedback.error - ) - - case .accent: return .init( - border: colors.accent.accentContainer, - text: colors.accent.onAccentContainer, - selectedText: colors.accent.onAccent, - background: colors.accent.accentContainer, - pressedBackground: colors.states.accentContainerPressed, - selectedBackground: colors.accent.accent - ) - - case .basic: return .init( - border: colors.basic.basicContainer, - text: colors.basic.onBasicContainer, - selectedText: colors.basic.onBasic, - background: colors.basic.basicContainer, - pressedBackground: colors.states.basicContainerPressed, - selectedBackground: colors.basic.basic - ) - } - }} diff --git a/core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCaseTests.swift b/core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCaseTests.swift deleted file mode 100644 index 4db0d5e95..000000000 --- a/core/Sources/Components/Chip/UseCase/ChipGetTintedIntentColorsUseCaseTests.swift +++ /dev/null @@ -1,392 +0,0 @@ -// -// ChipGetTintedIntentColorsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by michael.zimmermann on 11.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ChipGetTintedIntentColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - private var sut: ChipGetTintedIntentColorsUseCase! - private var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.theme = ThemeGeneratedMock() - - self.sut = ChipGetTintedIntentColorsUseCase() - let dims = DimsGeneratedMock() - dims.dim5 = 0.55 - self.theme.dims = dims - } - - // MARK: - Tests - func test_main_color() { - // Given - self.setupMainColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .main) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.main.mainContainer, - colors.main.onMainContainer, - colors.main.onMain, - colors.main.mainContainer, - colors.states.mainContainerPressed, - colors.main.main - ].map(\.uiColor)) - } - - func test_support_color() { - // Given - self.setupSupportColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .support) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.support.supportContainer, - colors.support.onSupportContainer, - colors.support.onSupport, - colors.support.supportContainer, - colors.states.supportContainerPressed, - colors.support.support - ].map(\.uiColor)) - } - - func test_basic_color() { - // Given - self.setupBasicColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .basic) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.basic.basicContainer, - colors.basic.onBasicContainer, - colors.basic.onBasic, - colors.basic.basicContainer, - colors.states.basicContainerPressed, - colors.basic.basic - ].map(\.uiColor)) - } - - func test_surface_color() { - // Given - self.setupSurfaceColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .surface) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - ColorTokenDefault.clear, - colors.base.surfaceInverse, - colors.base.onSurface, - colors.base.surface.opacity(theme.dims.dim1), - colors.states.surfacePressed, - colors.base.surface, - colors.base.surface - ].map(\.uiColor)) - } - - func test_neutral_color() { - // Given - self.setupFeedbackNeutralColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .neutral) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.neutralContainer, - colors.feedback.onNeutralContainer, - colors.feedback.onNeutral, - colors.feedback.neutralContainer, - colors.states.neutralContainerPressed, - colors.feedback.neutral - ].map(\.uiColor)) - } - - func test_info_color() { - // Given - self.setupFeedbackInfoColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .info) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.infoContainer, - colors.feedback.onInfoContainer, - colors.feedback.onInfo, - colors.feedback.infoContainer, - colors.states.infoContainerPressed, - colors.feedback.info - ].map(\.uiColor)) - } - - func test_alert_color() { - // Given - self.setupFeedbackAlertColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .alert) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.alertContainer, - colors.feedback.onAlertContainer, - colors.feedback.onAlert, - colors.feedback.alertContainer, - colors.states.alertContainerPressed, - colors.feedback.alert - ].map(\.uiColor)) - } - - func test_danger_color() { - // Given - self.setupFeedbackDangerColors() - let colors = self.theme.colors - - // When - let chipIntentColors = self.sut.execute(theme: self.theme, intent: .danger) - - // Then - XCTAssertEqual( - [ - chipIntentColors.border, - chipIntentColors.text, - chipIntentColors.selectedText, - chipIntentColors.background, - chipIntentColors.pressedBackground, - chipIntentColors.selectedBackground, - chipIntentColors.disabledBackground - ].compacted().map(\.uiColor), - [ - colors.feedback.errorContainer, - colors.feedback.onErrorContainer, - colors.feedback.onError, - colors.feedback.errorContainer, - colors.states.errorContainerPressed, - colors.feedback.error - ].map(\.uiColor)) - } - - // MARK: - Private helpers - private func setupMainColors() { - let main = ColorsMainGeneratedMock() - main.main = ColorTokenGeneratedMock.random() - main.onMain = ColorTokenGeneratedMock.random() - main.mainContainer = ColorTokenGeneratedMock.random() - main.onMainContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.mainContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.main = main - colors.states = states - self.theme.colors = colors - } - - private func setupSupportColors() { - let support = ColorsSupportGeneratedMock() - support.support = ColorTokenGeneratedMock.random() - support.onSupport = ColorTokenGeneratedMock.random() - support.supportContainer = ColorTokenGeneratedMock.random() - support.onSupportContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.supportContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.support = support - colors.states = states - self.theme.colors = colors - } - - private func setupBasicColors() { - let basic = ColorsBasicGeneratedMock() - basic.basic = ColorTokenGeneratedMock.random() - basic.onBasic = ColorTokenGeneratedMock.random() - basic.basicContainer = ColorTokenGeneratedMock.random() - basic.onBasicContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.basicContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.basic = basic - colors.states = states - self.theme.colors = colors - } - - private func setupSurfaceColors() { - let base = ColorsBaseGeneratedMock() - base.surface = ColorTokenGeneratedMock.random() - base.onSurface = ColorTokenGeneratedMock.random() - base.surfaceInverse = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.surfacePressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.base = base - colors.states = states - self.theme.colors = colors - } - - private func setupFeedbackNeutralColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.neutral = ColorTokenGeneratedMock.random() - feedback.onNeutral = ColorTokenGeneratedMock.random() - feedback.neutralContainer = ColorTokenGeneratedMock.random() - feedback.onNeutralContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.neutralContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - colors.states = states - self.theme.colors = colors - } - - private func setupFeedbackInfoColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.info = ColorTokenGeneratedMock.random() - feedback.onInfo = ColorTokenGeneratedMock.random() - feedback.infoContainer = ColorTokenGeneratedMock.random() - feedback.onInfoContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.infoContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - colors.states = states - self.theme.colors = colors - } - - private func setupFeedbackAlertColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.alert = ColorTokenGeneratedMock.random() - feedback.onAlert = ColorTokenGeneratedMock.random() - feedback.alertContainer = ColorTokenGeneratedMock.random() - feedback.onAlertContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.alertContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - colors.states = states - self.theme.colors = colors - } - - private func setupFeedbackDangerColors() { - let feedback = ColorsFeedbackGeneratedMock() - feedback.error = ColorTokenGeneratedMock.random() - feedback.onError = ColorTokenGeneratedMock.random() - feedback.errorContainer = ColorTokenGeneratedMock.random() - feedback.onErrorContainer = ColorTokenGeneratedMock.random() - - let states = ColorsStatesGeneratedMock() - states.errorContainerPressed = ColorTokenGeneratedMock.random() - - let colors = ColorsGeneratedMock() - colors.feedback = feedback - colors.states = states - self.theme.colors = colors - } -} diff --git a/core/Sources/Components/Chip/View/ChipViewModel.swift b/core/Sources/Components/Chip/View/ChipViewModel.swift deleted file mode 100644 index e0812e1b0..000000000 --- a/core/Sources/Components/Chip/View/ChipViewModel.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// ChipViewModel.swift -// SparkCore -// -// Created by michael.zimmermann on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -class ChipViewModel: ObservableObject { - - // MARK: - Properties Injected - private (set) var theme: Theme - private (set) var variant: ChipVariant - private (set) var intent: ChipIntent - private (set) var alignment: ChipAlignment - private let useCase: ChipGetColorsUseCasable - - // MARK: - State Properties - var isEnabled: Bool = true { - didSet { - guard isEnabled != oldValue else { return } - self.updateColors() - } - } - - var isPressed: Bool = false { - didSet { - guard isPressed != oldValue else { return } - self.updateColors() - } - } - - var isSelected: Bool = false { - didSet { - guard isSelected != oldValue else { return } - self.updateColors() - } - } - - // MARK: - Published Properties - @Published var spacing: CGFloat - @Published var padding: CGFloat - @Published var borderRadius: CGFloat - @Published var font: TypographyFontToken - @Published var colors: ChipStateColors - @Published var isIconLeading: Bool - @Published var content: Content - - // MARK: - Computed variables - var isBorderDashed: Bool { - return self.variant.isDashedBorder - } - var isBordered: Bool { - return self.variant.isBordered - } - var isBorderPlain: Bool { - return self.isBordered && !self.isBorderDashed - } - - // MARK: - Initializers - convenience init(theme: Theme, - variant: ChipVariant, - intent: ChipIntent, - alignment: ChipAlignment, - content: Content - ) { - self.init(theme: theme, - variant: variant, - intent: intent, - alignment: alignment, - content: content, - useCase: ChipGetColorsUseCase()) - } - - init(theme: Theme, - variant: ChipVariant, - intent: ChipIntent, - alignment: ChipAlignment, - content: Content, - useCase: ChipGetColorsUseCasable) { - self.theme = theme - self.variant = variant - self.intent = intent - self.useCase = useCase - self.alignment = alignment - self.content = content - self.colors = useCase.execute(theme: theme, variant: variant, intent: intent, state: .default) - self.spacing = self.theme.layout.spacing.small - self.padding = self.theme.layout.spacing.medium - self.borderRadius = self.theme.border.radius.medium - self.font = self.theme.bodyFont - self.isIconLeading = alignment.isIconLeading - } - - func set(theme: Theme) { - self.theme = theme - self.themeDidUpdate() - } - - func set(variant: ChipVariant) { - guard self.variant != variant else { return } - - self.variant = variant - self.variantDidUpdate() - } - - func set(intent: ChipIntent) { - guard self.intent != intent else { return } - - self.intent = intent - self.intentColorsDidUpdate() - } - - func set(alignment: ChipAlignment) { - guard self.alignment != alignment else { return } - - self.alignment = alignment - self.isIconLeading = alignment.isIconLeading - } - - func updateColors() { - let state = ChipState(isEnabled: self.isEnabled, isPressed: self.isPressed, isSelected: self.isSelected) - self.colors = self.useCase.execute(theme: self.theme, variant: self.variant, intent: self.intent, state: state) - } - - // MARK: - Private functions - private func themeDidUpdate() { - self.updateColors() - - self.spacing = self.theme.layout.spacing.small - self.padding = self.theme.layout.spacing.medium - self.borderRadius = self.theme.border.radius.medium - self.font = self.theme.bodyFont - } - - private func variantDidUpdate() { - self.updateColors() - } - - private func intentColorsDidUpdate() { - self.updateColors() - } -} - -private extension Theme { - var bodyFont: TypographyFontToken { - return self.typography.body1 - } -} - -private extension ChipVariant { - var isBordered: Bool { - return self == .dashed || self == .outlined - } - - var isDashedBorder: Bool { - return self == .dashed - } -} - -private extension ChipAlignment { - var isIconLeading: Bool { - return self == .leadingIcon - } -} diff --git a/core/Sources/Components/Chip/View/ChipViewModelTests.swift b/core/Sources/Components/Chip/View/ChipViewModelTests.swift deleted file mode 100644 index 02d15096a..000000000 --- a/core/Sources/Components/Chip/View/ChipViewModelTests.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// ChipViewModelTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import XCTest - -@testable import SparkCore - -final class ChipViewModelTests: XCTestCase { - - // MARK: - Properties - - var sut: ChipViewModel! - var useCase: ChipGetColorsUseCasableGeneratedMock! - var theme: ThemeGeneratedMock! - var subscriptions: Set! - - // MARK: - Setup - - override func setUpWithError() throws { - try super.setUpWithError() - - self.useCase = ChipGetColorsUseCasableGeneratedMock() - self.theme = ThemeGeneratedMock.mocked() - self.subscriptions = .init() - - let colorToken = ColorTokenGeneratedMock() - - self.useCase.executeWithThemeAndVariantAndIntentAndStateReturnValue = ChipStateColors(background: colorToken, border: colorToken, foreground: colorToken) - - self.sut = ChipViewModel(theme: theme, - variant: .outlined, - intent: .main, - alignment: .leadingIcon, - content: Void(), - useCase: useCase) - } - - // MARK: - Tests - - func test_variant_change_triggers_color_change() throws { - // Given - let updateExpectation = expectation(description: "Colors and border status updated") - updateExpectation.expectedFulfillmentCount = 2 - - self.sut.$colors.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - self.sut.set(variant: .dashed) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_theme_change_triggers_publishers() throws { - // Given - let updateExpectation = expectation(description: "Colors and other attributes updated") - updateExpectation.expectedFulfillmentCount = 2 - - let publishers = Publishers.Zip4(self.sut.$padding, - self.sut.$spacing, - self.sut.$borderRadius, - self.sut.$font) - - Publishers.Zip(self.sut.$colors, publishers) - .sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - self.sut.set(theme: ThemeGeneratedMock.mocked()) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_intent_change_triggers_colors() throws { - // Given - let updateExpectation = expectation(description: "Colors updated") - updateExpectation.expectedFulfillmentCount = 2 - - self.sut.$colors.sink(receiveValue: { _ in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - self.sut.set(intent: .alert) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_new_theme_with_different_spacing_triggers_change() throws { - // Given - let updateExpectation = expectation(description: "Spacing updated") - updateExpectation.expectedFulfillmentCount = 2 - - var spacings = [CGFloat]() - - self.sut.$spacing.sink(receiveValue: { spacing in - updateExpectation.fulfill() - spacings.append(spacing) - }) - .store(in: &self.subscriptions) - - // When - let newTheme = ThemeGeneratedMock.mocked() - let layout = LayoutGeneratedMock.mocked() - let spacing = LayoutSpacingGeneratedMock.mocked() - spacing.small = 20 - layout.spacing = spacing - newTheme.layout = layout - - self.sut.set(theme: newTheme) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - - XCTAssertEqual(spacings, [3, 20]) - } - - func test_new_theme_with_different_padding_triggers_change() throws { - // Given - let updateExpectation = expectation(description: "Padding updated") - updateExpectation.expectedFulfillmentCount = 2 - - var paddings = [CGFloat]() - - self.sut.$padding.sink(receiveValue: { padding in - updateExpectation.fulfill() - paddings.append(padding) - }) - .store(in: &self.subscriptions) - - // When - let newTheme = ThemeGeneratedMock.mocked() - let layout = LayoutGeneratedMock.mocked() - let spacing = LayoutSpacingGeneratedMock.mocked() - spacing.medium = 30 - layout.spacing = spacing - newTheme.layout = layout - - self.sut.set(theme: newTheme) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - - XCTAssertEqual(paddings, [5, 30]) - } - - func test_new_theme_with_different_border_radius_triggers_change() throws { - // Given - let updateExpectation = expectation(description: "Border radius updated") - updateExpectation.expectedFulfillmentCount = 2 - - var borderRadii = [CGFloat]() - - self.sut.$borderRadius.sink(receiveValue: { radius in - updateExpectation.fulfill() - borderRadii.append(radius) - }) - .store(in: &self.subscriptions) - - // When - let newTheme = ThemeGeneratedMock.mocked() - let border = BorderGeneratedMock.mocked() - let radius = BorderRadiusGeneratedMock.mocked() - radius.medium = 11 - border.radius = radius - newTheme.border = border - - self.sut.set(theme: newTheme) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - - XCTAssertEqual(borderRadii, [8, 11]) - } - - func test_new_theme_with_different_font_triggers_change() throws { - // Given - let updateExpectation = expectation(description: "Font updated") - updateExpectation.expectedFulfillmentCount = 2 - - var fonts = [TypographyFontToken]() - - self.sut.$font.sink(receiveValue: { font in - updateExpectation.fulfill() - fonts.append(font) - }) - .store(in: &self.subscriptions) - - // When - let newTheme = ThemeGeneratedMock.mocked() - let typography = TypographyGeneratedMock.mocked() - typography.body1 = TypographyFontTokenGeneratedMock.mocked(.title) - newTheme.typography = typography - - self.sut.set(theme: newTheme) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - - XCTAssertEqual(fonts.map(\.font), [.body, .title]) - } - - func test_new_theme_with_different_colors_triggers_change() throws { - // Given - let updateExpectation = expectation(description: "Colors updated") - updateExpectation.expectedFulfillmentCount = 2 - - self.sut.$colors.sink(receiveValue: { font in - updateExpectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - let newTheme = ThemeGeneratedMock.mocked() - - self.sut.set(theme: newTheme) - - // Then - wait(for: [updateExpectation], timeout: 0.1) - } - - func test_border_change_to_dashed() throws { - // Given - XCTAssertEqual(self.sut.isBorderDashed, false) - - // When - self.sut.set(variant: .dashed) - - // Then - XCTAssertEqual(self.sut.isBorderDashed, true) - } - - func test_not_bordered() throws { - // When - self.sut.set(variant: .tinted) - - // Then - XCTAssertEqual(self.sut.isBordered, false) - } - - func test_is_bordered() throws { - for variant in [ChipVariant.outlined, .dashed] { - // When - self.sut.set(variant: variant) - - // Then - XCTAssertEqual(self.sut.isBordered, true) - } - } -} diff --git a/core/Sources/Components/Chip/View/CommonTests/ChipConfigurationSnapshotTests.swift b/core/Sources/Components/Chip/View/CommonTests/ChipConfigurationSnapshotTests.swift deleted file mode 100644 index 74781495a..000000000 --- a/core/Sources/Components/Chip/View/CommonTests/ChipConfigurationSnapshotTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ChipConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -@testable import SparkCore - -struct ChipConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: ChipScenarioSnapshotTests - - let intent: ChipIntent - let variant: ChipVariant - let icon: ImageEither? - let text: String? - let badge: ViewEither? - let state: ChipState - - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.variant)", - self.icon != nil ? "withImage" : "withoutImage", - self.text != nil ? "withText" : "withoutText", - self.badge != nil ? "withBadge" : "withoutBadge", - self.state.isDisabled ? "disabled" : "enabled", - self.state.isSelected ? "selected" : "notSelected" - ].joined(separator: "-") - } -} diff --git a/core/Sources/Components/Chip/View/CommonTests/ChipScenarioSnapshotTests.swift b/core/Sources/Components/Chip/View/CommonTests/ChipScenarioSnapshotTests.swift deleted file mode 100644 index fcca9eeba..000000000 --- a/core/Sources/Components/Chip/View/CommonTests/ChipScenarioSnapshotTests.swift +++ /dev/null @@ -1,222 +0,0 @@ -// -// ChipScenarioSnapshotTests.swift -// Spark -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI - -enum ChipScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) -> [ChipConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1(isSwiftUIComponent: isSwiftUIComponent) - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3(isSwiftUIComponent: isSwiftUIComponent) - case .test4: - return self.test4(isSwiftUIComponent: isSwiftUIComponent) - case .test5: - return self.test5(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - intents: all - /// - variant: outlined - /// - content: icon + text - /// - state: default - /// - mode: all - /// - size: default - private func test1(isSwiftUIComponent: Bool) -> [ChipConfigurationSnapshotTests] { - let intents = ChipIntent.allCases - - return intents.map { - .init( - scenario: self, - intent: $0, - variant: .outlined, - icon: .mock(isSwiftUIComponent: isSwiftUIComponent), - text: "Label", - badge: nil, - state: .default, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test all variants - /// - /// Content: - /// - intent: basic - /// - variant: all - /// - content: text only - /// - state: default - /// - mode: all - /// - size: default - private func test2(isSwiftUIComponent: Bool) -> [ChipConfigurationSnapshotTests] { - let variants = ChipVariant.allCases - - return variants.map { - .init( - scenario: self, - intent: .basic, - variant: $0, - icon: nil, - text: "Label", - badge: nil, - state: .default, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 3 - /// - /// Description: To test all states - /// - /// Content: - /// - intents: all - /// - variant: all - /// - content: icon + text - /// - state: all - /// - mode: default - /// - size: default - private func test3(isSwiftUIComponent: Bool) -> [ChipConfigurationSnapshotTests] { - let variants = ChipVariant.allCases - let states = ChipState.all - - return all(variants, states).map { variant, state in - .init( - scenario: self, - intent: .main, - variant: variant, - icon: .mock(isSwiftUIComponent: isSwiftUIComponent), - text: "Label", - badge: nil, - state: state, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 4 - /// - /// Description: To test content resilience - /// - /// Content: - /// - intent: neutral - /// - variant: tinted - /// - content: text + icon + in different combinations - /// - mode: default - /// - size: default - private func test4(isSwiftUIComponent: Bool) -> [ChipConfigurationSnapshotTests] { - let contents: [(hasIcon: Bool, hasText: Bool, hasBadge: Bool)] = - [ - (true, false, false), - (true, false, true), - (false, true, true), - (true, true, true) - ] - - return contents.map { content in - .init( - scenario: self, - intent: .neutral, - variant: .tinted, - icon: content.hasIcon ? .mock(isSwiftUIComponent: isSwiftUIComponent) : nil, - text: content.hasText ? "A Very Long Label" : nil, - badge: content.hasBadge ? .mock(isSwiftUIComponent) : nil, - state: .default, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 6 - /// - /// Description: To test a11y sizes - /// - /// Content: - /// - intent: main - /// - variant: tinted - /// - content: icon + text - /// - mode: default - /// - size: all - private func test5(isSwiftUIComponent: Bool) -> [ChipConfigurationSnapshotTests] { - return [ - .init( - scenario: self, - intent: .accent, - variant: .tinted, - icon: .mock(isSwiftUIComponent: isSwiftUIComponent), - text: "Label", - badge: nil, - state: .default, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - ) - ] - } -} - -// MARK: - Private Extensions - -private extension ViewEither { - static func mock(_ isSwiftUIComponent: Bool) -> Self { - if isSwiftUIComponent { - let view = BadgeView( - theme: SparkTheme.shared, - intent: .danger, - value: 99 - ).borderVisible(false) - - return .right(AnyView(view)) - } else { - let view = BadgeUIView( - theme: SparkTheme.shared, - intent: .danger, - value: 99, - isBorderVisible: false - ) - return .left(view) - } - } -} - -private func all(_ lhs: [U], _ rhs: [V]) -> [(U, V)] { - lhs.flatMap { left in - rhs.map { right in - (left, right) - } - } -} diff --git a/core/Sources/Components/Chip/View/CommonTests/ChipStateSnapshotTests.swift b/core/Sources/Components/Chip/View/CommonTests/ChipStateSnapshotTests.swift deleted file mode 100644 index 150f2494a..000000000 --- a/core/Sources/Components/Chip/View/CommonTests/ChipStateSnapshotTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ChipStateSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -@testable import SparkCore - -extension ChipState { - static let all: [ChipState] = - [ - .init(isEnabled: true, isPressed: false, isSelected: true), - .init(isEnabled: false, isPressed: false, isSelected: true), - .init(isEnabled: false, isPressed: false, isSelected: false), - ] -} diff --git a/core/Sources/Components/Chip/View/SwiftUI/ChipView.swift b/core/Sources/Components/Chip/View/SwiftUI/ChipView.swift deleted file mode 100644 index b90184107..000000000 --- a/core/Sources/Components/Chip/View/SwiftUI/ChipView.swift +++ /dev/null @@ -1,296 +0,0 @@ -// -// ChipView.swift -// SparkCore -// -// Created by michael.zimmermann on 17.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// ChipView. -/// A chip view may contain an icon, a title and a further view of type AnyView. -/// The icon and title are the first two items of the chip and their position depends on the given alignment. -/// The extra component is always the last component in the view. -public struct ChipView: View { - - private enum Constants { - static let verticalPadding: CGFloat = 6.25 - } - - @ObservedObject private var viewModel: ChipViewModel - @ScaledMetric private var imageSize = ChipConstants.imageSize - @ScaledMetric private var height = ChipConstants.height - @ScaledMetric private var borderWidth = ChipConstants.borderWidth - @ScaledMetric private var dashLength = ChipConstants.dashLength - @ScaledMetric private var spacing: CGFloat - @ScaledMetric private var padding: CGFloat - @ScaledMetric private var paddingVertical = Constants.verticalPadding - @ScaledMetric private var borderRadius: CGFloat - - private var action: (() -> Void)? - - // MARK: - Initializers - /// Initializer of a chip containing only an icon. - /// - /// Parameters: - /// - theme: The theme. - /// - intent: The intent of the chip, e.g. main, support - /// - variant: The chip variant, e.g. outlined, filled - /// - icon: An icon - /// - action: An optional action. If the chip has an action, it will be treated like a button - public init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - icon: Image, - action: (() -> Void)? = nil) { - self.init(theme: theme, - intent: intent, - variant: variant, - alignment: alignment, - icon: icon, - title: nil, - action: action - ) - } - - /// Initializer of a chip containing only a title. - /// - /// Parameters: - /// - theme: The theme. - /// - intent: The intent of the chip, e.g. main, support - /// - variant: The chip variant, e.g. outlined, filled - /// - icon: An icon - /// - action: An optional action. If the chip has an action, it will be treated like a button - public init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - title: String, - action: (() -> Void)? = nil) { - self.init(theme: theme, - intent: intent, - variant: variant, - alignment: alignment, - icon: nil, - title: title, - action: action - ) - } - - /// Initializer of a chip with an optional title and an optional icon. - /// - /// Parameters: - /// - theme: The theme. - /// - intent: The intent of the chip, e.g. main, support - /// - variant: The chip variant, e.g. outlined, filled - /// - icon: An optional icon - /// - title: An optional title - /// - action: An optional action. If the chip has an action, it will be treated like a button - public init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - icon: Image?, - title: String?, - action: (() -> Void)? = nil) { - let viewModel = ChipViewModel( - theme: theme, - variant: variant, - intent: intent, - alignment: alignment, - content: ChipContent(title: title, icon: icon)) - - self.init(viewModel: viewModel, - action: action - ) - } - - // MARK: Internal initalizer - internal init(viewModel: ChipViewModel, - action: (() -> Void)? = nil) { - self.viewModel = viewModel - self.action = action - - self._spacing = ScaledMetric(wrappedValue: viewModel.spacing) - self._padding = ScaledMetric(wrappedValue: viewModel.padding) - self._borderRadius = ScaledMetric(wrappedValue: viewModel.borderRadius) - - } - - // MARK: - View - public var body: some View { - if (self.action == nil) { - self.borderedChipView().buttonStyle(NoButtonStyle()) - } else { - self.borderedChipView().buttonStyle(PressedButtonStyle(isPressed: self.$viewModel.isPressed)) - } - } - - @ViewBuilder - private func borderedChipView() -> some View { - if self.viewModel.isBordered { - self.chipView().chipBorder( - width: self.borderWidth, - radius: self.borderRadius, - dashLength: self.borderDashLength(), - colorToken: self.viewModel.colors.border) - } else { - self.chipView() - } - } - - @ViewBuilder - private func chipView() -> some View { - Button(action: self.action ?? {}) { - self.content() - } - .frame(height: self.height) - .background(self.viewModel.colors.background.color) - .opacity(self.viewModel.colors.opacity) - .cornerRadius(self.borderRadius) - .isEnabledChanged { isEnabled in - self.viewModel.isEnabled = isEnabled - } - .accessibilityIdentifier(ChipAccessibilityIdentifier.identifier) - } - - private func borderDashLength() -> CGFloat? { - return self.viewModel.isBorderDashed ? self.dashLength : nil - } - - @ViewBuilder - private func content() -> some View { - HStack(spacing: self.spacing) { - if self.viewModel.alignment == .leadingIcon { - self.icon() - self.title() - } else { - self.title() - self.icon() - } - if let component = self.viewModel.content.component { - component - .frame(height: self.imageSize) - } - } - .padding(EdgeInsets(vertical: self.verticalPadding(), horizontal: self.padding)) - } - - private func verticalPadding() -> CGFloat { - if self.viewModel.content.title == nil { - return self.padding - } else { - return self.paddingVertical - } - } - - @ViewBuilder - private func icon() -> some View { - if let icon = self.viewModel.content.icon { - icon - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(self.viewModel.colors.foreground.color) - .accessibilityIdentifier(ChipAccessibilityIdentifier.icon) - .frame(width: self.imageSize, height: self.imageSize) - } else { - EmptyView() - } - } - - @ViewBuilder - private func title() -> some View { - if let title = self.viewModel.content.title { - Text(title) - .font(self.viewModel.font.font) - .foregroundColor(self.viewModel.colors.foreground.color) - .accessibilityIdentifier(ChipAccessibilityIdentifier.text) - } else { - EmptyView() - } - } - - // MARK: - Modifiers - /// Set an icon - public func icon(_ icon: Image?) -> Self { - self.viewModel.content.icon = icon - return self - } - - /// Set a title - public func title(_ title: String?) -> Self { - self.viewModel.content.title = title - return self - } - - /// Set an extra component - public func component(_ component: AnyView?) -> Self { - self.viewModel.content.component = component - return self - } - - public func selected(_ selected: Bool) -> Self { - self.viewModel.isSelected = selected - return self - } -} - -// MARK: - View Border Extension -private extension View { - func chipBorder(width: CGFloat, - radius: CGFloat, - dashLength: CGFloat?, - colorToken: any ColorToken) -> some View { - self.modifier( - ChipBorderViewModifier( - width: width, - radius: radius, - dashLength: dashLength, - colorToken: colorToken)) - } -} - -private struct ChipBorderViewModifier: ViewModifier { - // MARK: - Properties - - private let width: CGFloat - private let radius: CGFloat - private let dashLength: CGFloat? - private let colorToken: any ColorToken - - // MARK: - Initialization - - init(width: CGFloat, - radius: CGFloat, - dashLength: CGFloat?, - colorToken: any ColorToken) { - self.width = width - self.radius = radius - self.dashLength = dashLength - self.colorToken = colorToken - } - - // MARK: - View - func body(content: Content) -> some View { - content - .cornerRadius(self.radius) - .overlay(self.rectangle()) - } - - @ViewBuilder - private func rectangle() -> some View { - if let dashLength = dashLength { - RoundedRectangle(cornerRadius: self.radius) - .stroke( - self.colorToken.color, - style: StrokeStyle(lineWidth: self.width, dash: [dashLength])) - } else { - RoundedRectangle(cornerRadius: self.radius) - .stroke( - self.colorToken.color, - lineWidth: self.width) - } - } -} diff --git a/core/Sources/Components/Chip/View/SwiftUI/ChipViewSnapshotTests.swift b/core/Sources/Components/Chip/View/SwiftUI/ChipViewSnapshotTests.swift deleted file mode 100644 index 1131c7ea3..000000000 --- a/core/Sources/Components/Chip/View/SwiftUI/ChipViewSnapshotTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ChipViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import SnapshotTesting - -@testable import SparkCore - -final class ChipViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = ChipScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: true) - for configuration in configurations { - let view = ChipView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - icon: configuration.icon?.rightValue, - title: configuration.text) - .component(configuration.badge?.rightValue) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Chip/View/UIKit/ChipUIView.swift b/core/Sources/Components/Chip/View/UIKit/ChipUIView.swift deleted file mode 100644 index 1bd07344c..000000000 --- a/core/Sources/Components/Chip/View/UIKit/ChipUIView.swift +++ /dev/null @@ -1,572 +0,0 @@ -// -// ChipUIView.swift -// SparkCore -// -// Created by michael.zimmermann on 02.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// ChipUIView is a control which can act like a button, if an action is attached to it, or it can act like a label. -public final class ChipUIView: UIControl { - - private enum Constants { - static let touchAreaTolerance: CGFloat = 100 - } - - // MARK: - Public properties - /// An optional icon on the Chip. The icon is always rendered to the left of the text - public var icon: UIImage? { - set { - self.imageView.image = newValue - self.imageView.isHidden = newValue == nil - self.invalidateIntrinsicContentSize() - } - get { - return self.imageView.image - } - } - - /// An optional text shown on the Chip. The text is rendered to the right of the icon. - public var text: String? { - set { - self.textLabel.text = newValue - self.textLabel.isHidden = newValue == nil - self.invalidateIntrinsicContentSize() - } - get { - return self.textLabel.text - } - } - - override public var isEnabled: Bool { - set { - self.viewModel.isEnabled = newValue - } - get { - return self.viewModel.isEnabled - } - } - - public override var isSelected: Bool { - set { - self.viewModel.isSelected = newValue - } - get { - return self.viewModel.isSelected - } - } - - public override var isHighlighted: Bool { - set { - if self.hasAction { - self.viewModel.isPressed = newValue - } - } - get { - return self.hasAction && self.viewModel.isPressed - } - } - - /// An optional action. If the action is given, the Chip will act like a button and have a pressed state. - /// As an alternative, a .touchUpInside action can be set - public var action: (() -> ())? { - didSet { - if let uiAction = self.uiAction { - self.removeAction(uiAction, for: .touchUpInside) - } - if let uiAction = self.action.map({ action in - return UIAction{ _ in action() } - }) { - self.uiAction = uiAction - self.addAction(uiAction, for: .touchUpInside) - } else { - self.uiAction = nil - } - } - } - - /// The intent of the chip. - public var intent: ChipIntent { - set { - self.viewModel.set(intent: newValue) - } - get { - return self.viewModel.intent - } - } - - /// The variant of the chip - public var variant: ChipVariant { - set { - self.viewModel.set(variant: newValue) - } - get { - return self.viewModel.variant - } - } - - public var alignment: ChipAlignment { - set { - self.viewModel.set(alignment: newValue) - } - get { - return self.viewModel.alignment - } - } - /// The theme. - public var theme: Theme { - set { - self.viewModel.set(theme: newValue) - } - get { - return self.viewModel.theme - } - } - - /// Optional component whicl will be rendered to the right of the label. - /// Note: the client must be responsible, that it fits within the chip which has a height of 32pts - public var component: UIView? { - willSet { - self.component?.removeFromSuperview() - if let component = self.component { - self.stackView.removeArrangedSubview(component) - } - } - didSet { - if let component = self.component { - self.stackView.addArrangedSubview(component) - } - self.invalidateIntrinsicContentSize() - } - } - - public override var intrinsicContentSize: CGSize { - - let width: CGFloat = { - if let component = self.component, component.intrinsicContentSize.width == UIView.noIntrinsicMetric { - return UIView.noIntrinsicMetric - } - - let width: CGFloat = (self.imageView.isHidden ? 0 : self.imageSize) - + (self.textLabel.isHidden ? 0 : self.textLabel.intrinsicContentSize.width) - + (self.component?.intrinsicContentSize.width ?? 0.0) - - let spacings = max(0, self.stackView.arrangedSubviews.filter(\.isNotHidden).count - 1) - - return width + (CGFloat(spacings) * self.spacing) + (self.padding * 2.0) - }() - - return CGSize(width: width, height: self.height) - } - - // MARK: - Private properties - - var hasAction: Bool { - return self.allControlEvents == .touchUpInside - } - - private var uiAction: UIAction? - - private let viewModel: ChipViewModel - - private var dashBorder: CAShapeLayer? - - @ScaledUIMetric private var imageSize = ChipConstants.imageSize - @ScaledUIMetric private var height = ChipConstants.height - @ScaledUIMetric private var borderWidth = ChipConstants.borderWidth - @ScaledUIMetric private var dashLength = ChipConstants.dashLength - @ScaledUIMetric private var spacing: CGFloat - @ScaledUIMetric private var padding: CGFloat - @ScaledUIMetric private var borderRadius: CGFloat - - public let textLabel: UILabel = { - let label = UILabel() - label.isUserInteractionEnabled = false - label.accessibilityIdentifier = ChipAccessibilityIdentifier.text - label.contentMode = .scaleAspectFit - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = true - label.numberOfLines = 1 - label.setContentCompressionResistancePriority(.defaultHigh, - for: .horizontal) - label.setContentCompressionResistancePriority(.required, - for: .vertical) - return label - }() - - public let imageView: UIImageView = { - let imageView = UIImageView() - imageView.isUserInteractionEnabled = false - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.tintAdjustmentMode = .normal - imageView.accessibilityIdentifier = ChipAccessibilityIdentifier.icon - imageView.setContentCompressionResistancePriority(.required, - for: .horizontal) - imageView.setContentCompressionResistancePriority(.required, - for: .vertical) - return imageView - }() - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.isUserInteractionEnabled = false - stackView.axis = .horizontal - stackView.alignment = .center - stackView.isLayoutMarginsRelativeArrangement = true - stackView.insetsLayoutMarginsFromSafeArea = false - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - private var sizeConstraints: [NSLayoutConstraint] = [] - private var heightConstraint: NSLayoutConstraint? - private var subscriptions = Set() - - // MARK: - Initializers - - /// Initializer of a chip containing only an icon. - /// - /// - Parameters: - /// - theme: The theme. - /// - intent: The intent of the chip, e.g. main, support - /// - variant: The chip variant, e.g. outlined, filled - /// - alignment: Leading or Trailing Icon - /// - iconImage: An icon - public convenience init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - iconImage: UIImage) { - self.init(theme: theme, - intent: intent, - variant: variant, - alignment: alignment, - optionalLabel: nil, - optionalIconImage: iconImage) - } - - /// Initializer of a chip containing only a text. - /// - /// Parameters: - /// - theme: The theme. - /// - intent: The intent of the chip, e.g. main, support - /// - variant: The chip variant, e.g. outlined, filled - /// - text: The text label - public convenience init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - label: String) { - self.init(theme: theme, - intent: intent, - variant: variant, - alignment: alignment, - optionalLabel: label, - optionalIconImage: nil) - } - - /// Initializer of a chip containing both a text and an icon. - /// - /// Parameters: - /// - theme: The theme. - /// - intent: The intent of the chip, e.g. main, support - /// - variant: The chip variant, e.g. outlined, filled - /// - text: The text label - /// - iconImage: An icon - public convenience init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - label: String, - iconImage: UIImage) { - self.init(theme: theme, - intent: intent, - variant: variant, - alignment: alignment, - optionalLabel: label, - optionalIconImage: iconImage) - } - - init(theme: Theme, - intent: ChipIntent, - variant: ChipVariant, - alignment: ChipAlignment = .leadingIcon, - optionalLabel: String?, - optionalIconImage: UIImage?) { - - self.viewModel = ChipViewModel( - theme: theme, - variant: variant, - intent: intent, - alignment: alignment, - content: Void() - ) - self.spacing = self.viewModel.spacing - self.padding = self.viewModel.padding - self.borderRadius = self.viewModel.borderRadius - - super.init(frame: CGRect.zero) - - self.icon = optionalIconImage - self.text = optionalLabel - self.textLabel.sizeToFit() - - self.setupView() - } - - /// Function traitCollectionDidChange: all dynamic sizing and padding will be recalculated here - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self.updateScaledMetrics() - - self.sizeConstraints.forEach{ - $0.constant = self.imageSize - } - - self.stackView.spacing = self.spacing - self.heightConstraint?.constant = self.height - - self.updateLayoutMargins() - self.updateBorder() - - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.viewModel.updateColors() - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func layoutSubviews() { - super.layoutSubviews() - self.removeDashedBorder() - - if self.viewModel.isBorderDashed { - self.addDashedBorder(borderColor: self.viewModel.colors.border) - } - } - - // MARK: - Control functions - public override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - super.touchesCancelled(touches, with: event) - self.viewModel.isPressed = false - } - - // MARK: - Private functions - /// Update all colors used - private func setChipColors(_ chipColors: ChipStateColors) { - self.stackView.backgroundColor = chipColors.background.uiColor - self.textLabel.textColor = chipColors.foreground.uiColor - self.imageView.tintColor = chipColors.foreground.uiColor - self.layer.opacity = Float(chipColors.opacity) - - self.removeDashedBorder() - - if self.viewModel.isBorderDashed { - self.removeBorder() - self.addDashedBorder(borderColor: chipColors.border) - } else if viewModel.isBordered { - self.stackView.layer.borderWidth = self.borderWidth - self.stackView.layer.borderColor = chipColors.border.uiColor.cgColor - } else { - self.stackView.layer.borderWidth = 0 - self.stackView.layer.borderColor = nil - } - } - - /// Update all scaled metrics - private func updateScaledMetrics() { - self._imageSize.update(traitCollection: self.traitCollection) - self._height.update(traitCollection: self.traitCollection) - self._spacing.update(traitCollection: self.traitCollection) - self._padding.update(traitCollection: self.traitCollection) - self._borderRadius.update(traitCollection: self.traitCollection) - self._borderWidth.update(traitCollection: self.traitCollection) - self._dashLength.update(traitCollection: self.traitCollection) - } - - private func setupView() { - self.translatesAutoresizingMaskIntoConstraints = false - - self.addSubview(self.stackView) - if self.viewModel.isIconLeading { - self.stackView.addArrangedSubviews([self.imageView, self.textLabel]) - } else { - self.stackView.addArrangedSubviews([self.textLabel, self.imageView]) - } - - self.updateFont() - self.updateSpacing() - self.updateLayoutMargins() - - self.setupConstraints() - self.setChipColors(self.viewModel.colors) - self.enableTouch() - self.setupSubscriptions() - - self.accessibilityIdentifier = ChipAccessibilityIdentifier.identifier - } - - private func updateLayoutMargins() { - self.stackView.layoutMargins = UIEdgeInsets(top: 0, left: self.padding, bottom: 0, right: self.padding) - self.invalidateIntrinsicContentSize() - } - - private func updateSpacing() { - self.stackView.spacing = self.spacing - self.invalidateIntrinsicContentSize() - } - - private func updateBorder() { - self.stackView.layer.cornerRadius = self.borderRadius - self.removeBorder() - - if self.viewModel.isBorderDashed { - self.addDashedBorder(borderColor: self.viewModel.colors.border) - } else if self.viewModel.isBordered { - self.stackView.layer.borderWidth = self.borderWidth - self.stackView.layer.borderColor = self.viewModel.colors.border.uiColor.cgColor - } - } - - private func removeDashedBorder() { - self.dashBorder?.removeFromSuperlayer() - self.dashBorder = nil - } - - private func removeBorder() { - self.stackView.layer.borderWidth = 0 - self.stackView.layer.borderColor = nil - self.removeDashedBorder() - } - - private func updateFont() { - self.textLabel.font = self.viewModel.font.uiFont - self.invalidateIntrinsicContentSize() - } - - private func setupConstraints() { - let heightConstraint = self.stackView.heightAnchor.constraint(equalToConstant: self.height) - - let sizeConstraints = [ - self.imageView.heightAnchor.constraint(equalToConstant: self.imageSize), - self.imageView.widthAnchor.constraint(equalToConstant: self.imageSize) - ] - - let stackConstraints = [ - self.stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - self.stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - heightConstraint, - self.stackView.topAnchor.constraint(equalTo: self.topAnchor), - self.stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor) - ] - - NSLayoutConstraint.activate(stackConstraints) - - self.stackView.layer.cornerRadius = self.borderRadius - self.stackView.layer.masksToBounds = true - - self.sizeConstraints = sizeConstraints - self.heightConstraint = heightConstraint - - NSLayoutConstraint.activate(self.sizeConstraints) - - self.imageView.isHidden = self.icon == nil - self.textLabel.isHidden = self.text == nil - } - - private func setupSubscriptions() { - self.viewModel.$colors.subscribe(in: &self.subscriptions) { [weak self] colors in - self?.setChipColors(colors) - } - - self.viewModel.$spacing.subscribe(in: &self.subscriptions) { [weak self] spacing in - guard let self else { return } - self.spacing = spacing - self.updateSpacing() - } - - self.viewModel.$padding.subscribe(in: &self.subscriptions) { [weak self] padding in - guard let self else { return } - self.padding = padding - self.updateLayoutMargins() - } - - self.viewModel.$borderRadius.subscribe(in: &self.subscriptions) { [weak self] borderRadius in - guard let self else { return } - self.borderRadius = borderRadius - self.updateBorder() - } - - self.viewModel.$font.subscribe(in: &self.subscriptions) { [weak self] _ in - self?.updateFont() - } - - self.viewModel.$isIconLeading.subscribe(in: &self.subscriptions) { [weak self] isLeading in - self?.updateImagePosition(isIconLeading: isLeading) - } - } - - private func addDashedBorder(borderColor: any ColorToken) { - let dashBorder = CAShapeLayer() - let bounds = self.stackView.bounds - dashBorder.lineWidth = self.borderWidth - dashBorder.strokeColor = borderColor.uiColor.cgColor - dashBorder.lineDashPattern = [self.dashLength, self.dashLength] as [NSNumber] - dashBorder.frame = bounds - dashBorder.fillColor = nil - - if borderRadius > 0 { - dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: self.borderRadius).cgPath - } else { - dashBorder.path = UIBezierPath(rect: bounds).cgPath - } - self.stackView.layer.addSublayer(dashBorder) - self.dashBorder = dashBorder - } - - private func updateImagePosition(isIconLeading: Bool) { - let newImageIndex = isIconLeading ? 0 : 1 - - guard self.stackView.arrangedSubviews.firstIndex(of: self.imageView) != newImageIndex else { return } - - self.stackView.removeArrangedSubview(imageView) - self.stackView.insertArrangedSubview(imageView, at: newImageIndex) - } - - // MARK: - Control functions - public func enableComponentUserInteraction(_ isEnabled: Bool) { - self.stackView.isUserInteractionEnabled = isEnabled - } -} - -// MARK: - Label priorities -public extension ChipUIView { - func setLabelContentCompressionResistancePriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentCompressionResistancePriority(priority, - for: axis) - } - - func setLabelContentHuggingPriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentHuggingPriority(priority, - for: axis) - } -} - -private extension CGRect { - func padded(offset: CGFloat) -> CGRect { - - return CGRect(x: self.minX - offset, - y: self.minY - offset, - width: self.width + (offset * 2), - height: self.height + (offset * 2)) - } -} diff --git a/core/Sources/Components/Chip/View/UIKit/ChipUIViewSnapshotTests.swift b/core/Sources/Components/Chip/View/UIKit/ChipUIViewSnapshotTests.swift deleted file mode 100644 index cff6a8355..000000000 --- a/core/Sources/Components/Chip/View/UIKit/ChipUIViewSnapshotTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ChipUIViewSnapshotTests.swift -// Spark -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import SnapshotTesting - -@testable import SparkCore - -final class ChipUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = ChipScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: false) - for configuration in configurations { - let view = ChipUIView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - optionalLabel: configuration.text, - optionalIconImage: configuration.icon?.leftValue) - - view.component = configuration.badge?.leftValue - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Chip/View/UIKit/ChipUIViewTests.swift b/core/Sources/Components/Chip/View/UIKit/ChipUIViewTests.swift deleted file mode 100644 index 99865e0b7..000000000 --- a/core/Sources/Components/Chip/View/UIKit/ChipUIViewTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// ChipUIViewTests.swift -// SparkCoreSnapshotTests -// -// Created by michael.zimmermann on 11.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class ChipUIViewTests: TestCase { - - var sut: ChipUIView! - - override func setUpWithError() throws { - try super.setUpWithError() - self.sut = ChipUIView(theme: SparkTheme.shared, - intent: .basic, - variant: .outlined, - label: "Title") - } - - func test_when_touch_action_set_then_has_action() throws { - let action = UIAction{ _ in } - - self.sut.addAction(action, for: .touchUpInside) - - XCTAssertTrue(self.sut.hasAction) - } - - func test_when_action_set_then_has_action() throws { - // Given - let actionExpectation = expectation(description: "Expect action to be executed") - - self.sut.action = { - actionExpectation.fulfill() - } - - // When - sut.sendActions(for: .touchUpInside) - - // Then - XCTAssertTrue(self.sut.hasAction) - - wait(for: [actionExpectation], timeout: 0.1) - } - - func test_when_no_action_set_then_has_action() throws { - sut.action = { } - sut.action = nil - XCTAssertFalse(self.sut.hasAction) - } - -} diff --git a/core/Sources/Components/FormField/AccessibilityIdentifier/FormFieldAccessibilityIdentifier.swift b/core/Sources/Components/FormField/AccessibilityIdentifier/FormFieldAccessibilityIdentifier.swift deleted file mode 100644 index ba41140fe..000000000 --- a/core/Sources/Components/FormField/AccessibilityIdentifier/FormFieldAccessibilityIdentifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FormFieldAccessibilityIdentifier.swift -// SparkCore -// -// Created by alican.aycil on 30.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -public enum FormFieldAccessibilityIdentifier { - - // MARK: - Properties - - public static let formField = "spark-formfield" - public static let formFieldLabel = "spark-formfield-label" - public static let formFieldHelperMessage = "spark-formfield-helper-message" -} diff --git a/core/Sources/Components/FormField/Enum/FormFieldFeedbackState.swift b/core/Sources/Components/FormField/Enum/FormFieldFeedbackState.swift deleted file mode 100644 index bdecf3b3f..000000000 --- a/core/Sources/Components/FormField/Enum/FormFieldFeedbackState.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FormFieldFeedbackState.swift -// SparkCore -// -// Created by alican.aycil on 31.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The various intent color a formfield may have. -public enum FormFieldFeedbackState: CaseIterable { - case `default` - case error -} diff --git a/core/Sources/Components/FormField/Model/FormFieldColors.swift b/core/Sources/Components/FormField/Model/FormFieldColors.swift deleted file mode 100644 index 023a80d0c..000000000 --- a/core/Sources/Components/FormField/Model/FormFieldColors.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FormFieldColors.swift -// SparkCore -// -// Created by alican.aycil on 31.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -struct FormFieldColors { - let title: any ColorToken - let description: any ColorToken - let asterisk: any ColorToken -} diff --git a/core/Sources/Components/FormField/Model/FormFieldViewModel.swift b/core/Sources/Components/FormField/Model/FormFieldViewModel.swift deleted file mode 100644 index d748e7827..000000000 --- a/core/Sources/Components/FormField/Model/FormFieldViewModel.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// FormFieldViewModel.swift -// SparkCore -// -// Created by alican.aycil on 30.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -final class FormFieldViewModel: ObservableObject { - - // MARK: - Internal properties - @Published private(set) var title: AS? - @Published var description: AS? - @Published var titleFont: any TypographyFontToken - @Published var descriptionFont: any TypographyFontToken - @Published var titleColor: any ColorToken - @Published var descriptionColor: any ColorToken - @Published var spacing: CGFloat - - var theme: Theme { - didSet { - self.updateColors() - self.updateFonts() - self.updateSpacing() - self.updateTitle() - } - } - - var feedbackState: FormFieldFeedbackState { - didSet { - guard feedbackState != oldValue else { return } - self.updateColors() - } - } - - var isTitleRequired: Bool { - didSet { - guard isTitleRequired != oldValue else { return } - self.updateTitle() - } - } - - var colors: FormFieldColors - - private var colorUseCase: FormFieldColorsUseCaseable - private var titleUseCase: FormFieldTitleUseCaseable - private var userDefinedTitle: AS? - - // MARK: - Init - init( - theme: Theme, - feedbackState: FormFieldFeedbackState, - title: AS?, - description: AS?, - isTitleRequired: Bool = false, - colorUseCase: FormFieldColorsUseCaseable = FormFieldColorsUseCase(), - titleUseCase: FormFieldTitleUseCaseable = FormFieldTitleUseCase() - ) { - self.theme = theme - self.feedbackState = feedbackState - self.description = description - self.isTitleRequired = isTitleRequired - self.colorUseCase = colorUseCase - self.titleUseCase = titleUseCase - self.colors = colorUseCase.execute(from: theme, feedback: feedbackState) - self.spacing = self.theme.layout.spacing.small - self.titleFont = self.theme.typography.body2 - self.descriptionFont = self.theme.typography.caption - self.titleColor = self.colors.title - self.descriptionColor = self.colors.description - self.setTitle(title) - } - - func setTitle(_ title: AS?) { - self.userDefinedTitle = title - self.updateTitle() - } - - private func updateTitle() { - self.title = self.titleUseCase.execute(title: self.userDefinedTitle, isTitleRequired: self.isTitleRequired, colors: self.colors, typography: self.theme.typography) as? AS - } - - private func updateColors() { - self.colors = colorUseCase.execute(from: self.theme, feedback: self.feedbackState) - self.titleColor = self.colors.title - self.descriptionColor = self.colors.description - } - - private func updateFonts() { - self.titleFont = self.theme.typography.body2 - self.descriptionFont = self.theme.typography.caption - } - - private func updateSpacing() { - self.spacing = self.theme.layout.spacing.small - } -} diff --git a/core/Sources/Components/FormField/Model/FormFieldViewModelTests.swift b/core/Sources/Components/FormField/Model/FormFieldViewModelTests.swift deleted file mode 100644 index c4e03a52f..000000000 --- a/core/Sources/Components/FormField/Model/FormFieldViewModelTests.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// FormFieldViewModelTests.swift -// SparkCoreUnitTests -// -// Created by alican.aycil on 26.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import XCTest -@testable import SparkCore - -final class FormFieldViewModelTests: XCTestCase { - - var theme: ThemeGeneratedMock! - var cancellable = Set() - var checkedImage = IconographyTests.shared.checkmark - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.theme = ThemeGeneratedMock.mocked() - } - - // MARK: - Tests - func test_init() throws { - - // Given - let viewModel = FormFieldViewModel( - theme: self.theme, - feedbackState: .default, - title: NSAttributedString(string: "Title"), - description: NSAttributedString(string: "Description"), - isTitleRequired: true - ) - - // Then - XCTAssertNotNil(viewModel.theme, "No theme set") - XCTAssertNotNil(viewModel.feedbackState, "No feedback state set") - XCTAssertNotNil(viewModel.isTitleRequired, "No title required set") - XCTAssertTrue(viewModel.title?.string.contains("*") ?? false) - XCTAssertEqual(viewModel.title?.string, "Title *") - XCTAssertEqual(viewModel.description?.string, "Description") - XCTAssertEqual(viewModel.spacing, self.theme.layout.spacing.small) - XCTAssertEqual(viewModel.titleFont.uiFont, self.theme.typography.body2.uiFont) - XCTAssertEqual(viewModel.descriptionFont.uiFont, self.theme.typography.caption.uiFont) - XCTAssertEqual(viewModel.titleColor.uiColor, viewModel.colors.title.uiColor) - XCTAssertEqual(viewModel.descriptionColor.uiColor, viewModel.colors.description.uiColor) - } - - func test_texts_right_value() { - // Given - let viewModel = FormFieldViewModel( - theme: self.theme, - feedbackState: .default, - title: AttributedString("Title"), - description: AttributedString("Description"), - isTitleRequired: false - ) - - // Then - XCTAssertEqual(viewModel.title, AttributedString("Title")) - XCTAssertEqual(viewModel.description, AttributedString("Description")) - } - - func test_isTitleRequired() async { - // Given - let viewModel = FormFieldViewModel( - theme: self.theme, - feedbackState: .default, - title: NSAttributedString("Title"), - description: NSAttributedString("Description"), - isTitleRequired: false - ) - - let expectation = expectation(description: "Title is updated") - expectation.expectedFulfillmentCount = 2 - var isTitleUpdated = false - - viewModel.$title.sink(receiveValue: { title in - isTitleUpdated = title?.string.contains("*") ?? false - expectation.fulfill() - }) - .store(in: &cancellable) - - // When - viewModel.isTitleRequired = true - - await fulfillment(of: [expectation]) - - // Then - XCTAssertTrue(isTitleUpdated) - } - - func test_set_title() { - // Given - let viewModel = FormFieldViewModel( - theme: self.theme, - feedbackState: .default, - title: NSAttributedString("Title"), - description: NSAttributedString("Description"), - isTitleRequired: true - ) - - // When - viewModel.setTitle(NSAttributedString("Title2")) - - // Then - XCTAssertEqual(viewModel.title?.string, "Title2 *") - } - - func test_set_feedback_state() { - // Given - let viewModel = FormFieldViewModel( - theme: self.theme, - feedbackState: .default, - title: NSAttributedString("Title"), - description: NSAttributedString("Description"), - isTitleRequired: false - ) - - // When - viewModel.feedbackState = .error - - // Then - XCTAssertEqual(viewModel.feedbackState, .error) - } -} diff --git a/core/Sources/Components/FormField/TestHelper/FormfieldConfigurationSnapshotTests.swift b/core/Sources/Components/FormField/TestHelper/FormfieldConfigurationSnapshotTests.swift deleted file mode 100644 index b2e0da15b..000000000 --- a/core/Sources/Components/FormField/TestHelper/FormfieldConfigurationSnapshotTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// FormfieldConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 08.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -struct FormfieldConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: FormfieldScenarioSnapshotTests - let feedbackState: FormFieldFeedbackState - let label: String? - let helperMessage: String? - let isRequired: Bool - let isEnabled: Bool - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.feedbackState)", - "IsRequired:\(self.isRequired)", - "IsEnabled:\(self.isEnabled)" - ].joined(separator: "-") - } -} diff --git a/core/Sources/Components/FormField/TestHelper/FormfieldScenarioSnapshotTests.swift b/core/Sources/Components/FormField/TestHelper/FormfieldScenarioSnapshotTests.swift deleted file mode 100644 index 2ffc2f1e6..000000000 --- a/core/Sources/Components/FormField/TestHelper/FormfieldScenarioSnapshotTests.swift +++ /dev/null @@ -1,252 +0,0 @@ -// -// FormfieldScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 08.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -enum FormfieldScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - case test6 - case test7 - - // MARK: - Type Alias - - private typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration() -> [FormfieldConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1() - case .test2: - return self.test2() - case .test3: - return self.test3() - case .test4: - return self.test4() - case .test5: - return self.test5() - case .test6: - return self.test6() - case .test7: - return self.test7() - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all feedback states - /// - /// Content: - /// - feedbackState: all - /// - label: short - /// - helperMessage: short - /// - isRequired: false, - /// - isEnabled: true - /// - modes: light - /// - sizes (accessibility): default - private func test1() -> [FormfieldConfigurationSnapshotTests] { - let feedbackStates = FormFieldFeedbackState.allCases - - return feedbackStates.map { feedbackState in - return .init( - scenario: self, - feedbackState: feedbackState, - label: "Agreement", - helperMessage: "Your agreement is important.", - isRequired: false, - isEnabled: true, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test label's content resilience - /// - /// Content: - /// - feedbackState: 'default' - /// - label: all - /// - helperMessage: short - /// - isRequired: false, - /// - isEnabled: true - /// - modes: light - /// - sizes (accessibility): default - private func test2() -> [FormfieldConfigurationSnapshotTests] { - let labels: [String?] = [ - "Lorem Ipsum", - "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", - nil - ] - - return labels.map { label in - return .init( - scenario: self, - feedbackState: .default, - label: label, - helperMessage: "Your agreement is important.", - isRequired: false, - isEnabled: true, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 3 - /// - /// Description: To test required option - /// - /// Content: - /// - feedbackState: 'default' - /// - label: all - /// - helperMessage: short - /// - isRequired: false, - /// - isEnabled: true - /// - modes: light - /// - sizes (accessibility): default - private func test3() -> [FormfieldConfigurationSnapshotTests] { - return [.init( - scenario: self, - feedbackState: .default, - label: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", - helperMessage: "Your agreement is important.", - isRequired: true, - isEnabled: true, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - )] - } - - /// Test 4 - /// - /// Description: To test helper text's content resilience - /// - /// Content: - /// - feedbackState: error - /// - label: short - /// - helperMessage: all - /// - isRequired: false, - /// - isEnabled: true - /// - modes: light - /// - sizes (accessibility): default - private func test4() -> [FormfieldConfigurationSnapshotTests] { - let messages: [String?] = [ - "Lorem Ipsum", - "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.", - nil - ] - - return messages.map { message in - return .init( - scenario: self, - feedbackState: .error, - label: "Agreement", - helperMessage: message, - isRequired: false, - isEnabled: true, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 5 - /// - /// Description: To test disabled state - /// - /// Content: - /// - feedbackState: 'default' - /// - label: short - /// - helperMessage: short - /// - isRequired: false, - /// - isEnabled: true - /// - modes: light - /// - sizes (accessibility): default - private func test5() -> [FormfieldConfigurationSnapshotTests] { - let feedbackStates = FormFieldFeedbackState.allCases - - return feedbackStates.map { feedbackState in - return .init( - scenario: self, - feedbackState: feedbackState, - label: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", - helperMessage: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.", - isRequired: false, - isEnabled: false, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 6 - /// - /// Description: To test dark & light mode - /// - /// Content: - /// - feedbackState: all - /// - label: short - /// - helperMessage: short - /// - isRequired: false, - /// - isEnabled: false - /// - modes: dark - /// - sizes (accessibility): default - private func test6() -> [FormfieldConfigurationSnapshotTests] { - let feedbackStates = FormFieldFeedbackState.allCases - - return feedbackStates.map { feedbackState in - return .init( - scenario: self, - feedbackState: feedbackState, - label: "Agreement", - helperMessage: "Your agreement is important.", - isRequired: false, - isEnabled: true, - modes: [.dark], - sizes: Constants.Sizes.default - ) - } - } - - /// Test 7 - /// - /// Description: To test a11y sizes - /// - /// Content: - /// - feedbackState: error - /// - label: short - /// - helperMessage: short - /// - isRequired: false, - /// - isEnabled: false - /// - modes: light - /// - sizes (accessibility): all - private func test7() -> [FormfieldConfigurationSnapshotTests] { - - return [.init( - scenario: self, - feedbackState: .error, - label: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", - helperMessage: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.", - isRequired: true, - isEnabled: true, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - )] - } -} diff --git a/core/Sources/Components/FormField/UseCase/FormFieldColorsUseCase.swift b/core/Sources/Components/FormField/UseCase/FormFieldColorsUseCase.swift deleted file mode 100644 index f58c8175a..000000000 --- a/core/Sources/Components/FormField/UseCase/FormFieldColorsUseCase.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// FormFieldColorsUseCase.swift -// SparkCore -// -// Created by alican.aycil on 31.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol FormFieldColorsUseCaseable { - func execute(from theme: Theme, feedback state: FormFieldFeedbackState) -> FormFieldColors -} - -struct FormFieldColorsUseCase: FormFieldColorsUseCaseable { - - func execute(from theme: Theme, feedback state: FormFieldFeedbackState) -> FormFieldColors { - switch state { - case .default: - return FormFieldColors( - title: theme.colors.base.onSurface, - description: theme.colors.base.onSurface.opacity(theme.dims.dim1), - asterisk: theme.colors.base.onSurface.opacity(theme.dims.dim1) - ) - case .error: - return FormFieldColors( - title: theme.colors.base.onSurface, - description: theme.colors.feedback.error, - asterisk: theme.colors.base.onSurface.opacity(theme.dims.dim1) - ) - } - } -} diff --git a/core/Sources/Components/FormField/UseCase/FormFieldColorsUseCaseTests.swift b/core/Sources/Components/FormField/UseCase/FormFieldColorsUseCaseTests.swift deleted file mode 100644 index 58d8ef550..000000000 --- a/core/Sources/Components/FormField/UseCase/FormFieldColorsUseCaseTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// FormFieldColorsUseCaseTests.swift -// SparkCore -// -// Created by alican.aycil on 26.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class FormFieldColorsUseCaseTests: XCTestCase { - - var sut: FormFieldColorsUseCase! - var theme: ThemeGeneratedMock! - - override func setUp() { - super.setUp() - - self.sut = .init() - self.theme = .mocked() - } - - // MARK: - Tests - - func test_execute_for_all_feedback_cases() { - let feedbacks = FormFieldFeedbackState.allCases - - feedbacks.forEach { - - let formfieldColors = sut.execute(from: theme, feedback: $0) - - let expectedFormfieldColor: FormFieldColors - - switch $0 { - case .default: - expectedFormfieldColor = FormFieldColors( - title: theme.colors.base.onSurface, - description: theme.colors.base.onSurface.opacity(theme.dims.dim1), - asterisk: theme.colors.base.onSurface.opacity(theme.dims.dim1) - ) - case .error: - expectedFormfieldColor = FormFieldColors( - title: theme.colors.base.onSurface, - description: theme.colors.feedback.error, - asterisk: theme.colors.base.onSurface.opacity(theme.dims.dim1) - ) - } - - XCTAssertEqual(formfieldColors.title.uiColor, expectedFormfieldColor.title.uiColor) - XCTAssertEqual(formfieldColors.description.uiColor, expectedFormfieldColor.description.uiColor) - XCTAssertEqual(formfieldColors.asterisk.uiColor, expectedFormfieldColor.asterisk.uiColor) - } - } -} diff --git a/core/Sources/Components/FormField/UseCase/FormfieldTitleUseCase.swift b/core/Sources/Components/FormField/UseCase/FormfieldTitleUseCase.swift deleted file mode 100644 index d4c4d3d91..000000000 --- a/core/Sources/Components/FormField/UseCase/FormfieldTitleUseCase.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// FormfieldTitleUseCase.swift -// SparkCore -// -// Created by alican.aycil on 09.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -// sourcery: AutoMockable -protocol FormFieldTitleUseCaseable { - func execute(title: SparkAttributedString?, isTitleRequired: Bool, colors: FormFieldColors, typography: Typography) -> SparkAttributedString? -} - -struct FormFieldTitleUseCase: FormFieldTitleUseCaseable { - - func execute(title: SparkAttributedString?, isTitleRequired: Bool, colors: FormFieldColors, typography: Typography) -> SparkAttributedString? { - - let asterisk = NSAttributedString( - string: " *", - attributes: [ - NSAttributedString.Key.foregroundColor: colors.asterisk.uiColor, - NSAttributedString.Key.font: typography.caption.uiFont - ] - ) - - if let attributedString = title as? NSAttributedString { - let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) - if isTitleRequired { - mutableAttributedString.append(asterisk) - } - return mutableAttributedString - - } else if var attributedString = title as? AttributedString { - if isTitleRequired { - attributedString.append(AttributedString(asterisk)) - } - return attributedString - } - return nil - } -} diff --git a/core/Sources/Components/FormField/UseCase/FormfieldTitleUseCaseTests.swift b/core/Sources/Components/FormField/UseCase/FormfieldTitleUseCaseTests.swift deleted file mode 100644 index 5b68396d7..000000000 --- a/core/Sources/Components/FormField/UseCase/FormfieldTitleUseCaseTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// FormfieldTitleUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by alican.aycil on 10.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class FormFieldTitleUseCaseTests: XCTestCase { - - var sut: FormFieldTitleUseCase! - var theme: ThemeGeneratedMock! - - override func setUp() { - super.setUp() - - self.sut = .init() - self.theme = .mocked() - } - - // MARK: - Tests - - func test_execute_title_required_cases() { - let isTitleRequireds = [true, false] - let titleUseCase = FormFieldColorsUseCase() - - isTitleRequireds.forEach { isTitleRequired in - - let formfieldTitle = sut.execute( - title: NSAttributedString(string: "Agreement"), - isTitleRequired: isTitleRequired, - colors: titleUseCase.execute( - from: self.theme, - feedback: .default - ), - typography: self.theme.typography - ) - let formfieldTitleString = (formfieldTitle as? NSAttributedString)?.string ?? "" - - if isTitleRequired { - XCTAssertEqual(formfieldTitleString, "Agreement *") - } else { - XCTAssertEqual(formfieldTitleString, "Agreement") - } - } - } -} diff --git a/core/Sources/Components/FormField/View/SwiftUI/FormFieldView.swift b/core/Sources/Components/FormField/View/SwiftUI/FormFieldView.swift deleted file mode 100644 index cf05563b2..000000000 --- a/core/Sources/Components/FormField/View/SwiftUI/FormFieldView.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// FormFieldView.swift -// SparkCore -// -// Created by alican.aycil on 18.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct FormFieldView: View { - - @ObservedObject private var viewModel: FormFieldViewModel - @ScaledMetric private var spacing: CGFloat - private let component: Component - - /// Initialize a new checkbox UIKit-view. - /// - Parameters: - /// - theme: The current Spark-Theme. - /// - component: The component is covered by formfield. - /// - feedbackState: The formfield feedback state. 'Default' or 'Error'. - /// - title: The formfield title. - /// - description: The formfield helper message. - /// - isTitleRequired: The asterisk symbol at the end of title. - public init( - theme: Theme, - @ViewBuilder component: @escaping () -> Component, - feedbackState: FormFieldFeedbackState = .default, - title: String? = nil, - description: String? = nil, - isTitleRequired: Bool = false - ) { - let attributedTitle: AttributedString? = title.map(AttributedString.init) - let attributedDescription: AttributedString? = description.map(AttributedString.init) - self.init( - theme: theme, - component: component, - feedbackState: feedbackState, - attributedTitle: attributedTitle, - attributedDescription: attributedDescription, - isTitleRequired: isTitleRequired - ) - } - - /// Initialize a new checkbox UIKit-view. - /// - Parameters: - /// - theme: The current Spark-Theme. - /// - component: The component is covered by formfield. - /// - feedbackState: The formfield feedback state. 'Default' or 'Error'. - /// - attributedTitle: The formfield attributedTitle. - /// - attributedDescription: The formfield attributed helper message. - /// - isTitleRequired: The asterisk symbol at the end of title. - public init( - theme: Theme, - @ViewBuilder component: @escaping () -> Component, - feedbackState: FormFieldFeedbackState = .default, - attributedTitle: AttributedString? = nil, - attributedDescription: AttributedString? = nil, - isTitleRequired: Bool = false - ) { - let viewModel = FormFieldViewModel( - theme: theme, - feedbackState: feedbackState, - title: attributedTitle, - description: attributedDescription, - isTitleRequired: isTitleRequired - ) - - self.viewModel = viewModel - self._spacing = ScaledMetric(wrappedValue: viewModel.spacing) - self.component = component() - } - - public var body: some View { - VStack(alignment: .leading, spacing: self.spacing) { - - if let title = self.viewModel.title { - Text(title) - .font(self.viewModel.titleFont.font) - .foregroundStyle(self.viewModel.titleColor.color) - .accessibilityIdentifier(FormFieldAccessibilityIdentifier.formFieldLabel) - } - self.component - - if let description = self.viewModel.description { - Text(description) - .font(self.viewModel.descriptionFont.font) - .foregroundStyle(self.viewModel.descriptionColor.color) - .accessibilityIdentifier(FormFieldAccessibilityIdentifier.formFieldHelperMessage) - } - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier(FormFieldAccessibilityIdentifier.formField) - } -} diff --git a/core/Sources/Components/FormField/View/SwiftUI/FormFieldViewSnapshotTests.swift b/core/Sources/Components/FormField/View/SwiftUI/FormFieldViewSnapshotTests.swift deleted file mode 100644 index 8d09a9676..000000000 --- a/core/Sources/Components/FormField/View/SwiftUI/FormFieldViewSnapshotTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// FormFieldViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 14.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -@testable import SparkCore - -final class FormfieldViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = FormfieldScenarioSnapshotTests.allCases - - var _isOn: Bool = true - lazy var isOn: Binding = { - return Binding( - get: { return _isOn }, - set: { newValue in - _isOn = newValue - } - ) - }() - - for scenario in scenarios { - let configurations = scenario.configuration() - - for configuration in configurations { - - let component = HStack { - Toggle("", isOn: isOn) - .labelsHidden() - Spacer() - } - - let view = FormFieldView( - theme: self.theme, - component: { - component - }, - feedbackState: configuration.feedbackState, - title: configuration.label, - description: configuration.helperMessage, - isTitleRequired: configuration.isRequired - ) - .frame(width: UIScreen.main.bounds.size.width) - .fixedSize(horizontal: false, vertical: true) - .disabled(!configuration.isEnabled) - .background(.systemBackground) - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - record: true, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/FormField/View/UIKit/FormFieldUIView.swift b/core/Sources/Components/FormField/View/UIKit/FormFieldUIView.swift deleted file mode 100644 index 32ceb415c..000000000 --- a/core/Sources/Components/FormField/View/UIKit/FormFieldUIView.swift +++ /dev/null @@ -1,308 +0,0 @@ -// -// FormFieldUIView.swift -// SparkCore -// -// Created by alican.aycil on 30.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -/// The `FormFieldUIView`renders a component with title and subtitle using UIKit. -public final class FormFieldUIView: UIControl { - - // MARK: - Private Properties. - - private let titleLabel: UILabel = { - let label = UILabel() - label.backgroundColor = .clear - label.numberOfLines = 0 - label.adjustsFontForContentSizeCategory = true - label.accessibilityIdentifier = FormFieldAccessibilityIdentifier.formFieldLabel - label.isAccessibilityElement = true - return label - }() - - private let descriptionLabel: UILabel = { - let label = UILabel() - label.backgroundColor = .clear - label.numberOfLines = 0 - label.adjustsFontForContentSizeCategory = true - label.accessibilityIdentifier = FormFieldAccessibilityIdentifier.formFieldHelperMessage - label.isAccessibilityElement = true - return label - }() - - private lazy var stackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [self.titleLabel, self.descriptionLabel]) - stackView.axis = .vertical - stackView.spacing = self.spacing - stackView.alignment = .fill - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - private var cancellables = Set() - @ScaledUIMetric private var spacing: CGFloat - - // MARK: - Public properties. - - /// The title of formfield. - public var title: String? { - get { - return self.titleLabel.text - } - set { - self.viewModel.setTitle(newValue.map(NSAttributedString.init)) - } - } - - /// The attributedTitle of formfield. - public var attributedTitle: NSAttributedString? { - get { - return self.titleLabel.attributedText - } - set { - self.viewModel.setTitle(newValue) - } - } - - public var isTitleRequired: Bool { - get { - return self.viewModel.isTitleRequired - } - set { - self.viewModel.isTitleRequired = newValue - } - } - - /// The description of formfield. - public var descriptionString: String? { - get { - return self.descriptionLabel.text - } - set { - self.viewModel.description = newValue.map(NSAttributedString.init) - } - } - - /// The attributedDescription of formfield. - public var attributedDescription: NSAttributedString? { - get { - return self.descriptionLabel.attributedText - } - set { - self.viewModel.description = newValue - } - } - - /// Returns the theme of the formfield. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// Returns the theme of the formfield. - public var feedbackState: FormFieldFeedbackState { - get { - return self.viewModel.feedbackState - } - set { - self.viewModel.feedbackState = newValue - } - } - - /// The current state of the component. - public override var isEnabled: Bool { - get { - return self.component.isEnabled - } - set { - self.component.isEnabled = newValue - } - } - - public override var isHighlighted: Bool { - get { - return self.component.isHighlighted - } - set { - self.component.isHighlighted = newValue - } - } - - /// The current selection state of the component. - public override var isSelected: Bool { - get { - return self.component.isSelected - } - set { - self.component.isSelected = newValue - } - } - - /// The component of formfield. - public var component: Component { - didSet { - oldValue.removeFromSuperview() - self.setComponent() - } - } - - var viewModel: FormFieldViewModel - - // MARK: - Initialization - - /// Not implemented. Please use another init. - /// - Parameter coder: the coder. - public required init?(coder: NSCoder) { - fatalError("not implemented") - } - - /// Initialize a new checkbox UIKit-view. - /// - Parameters: - /// - theme: The current Spark-Theme. - /// - component: The component is covered by formfield. - /// - feedbackState: The formfield feedback state. 'Default' or 'Error'. - /// - title: The formfield title. - /// - description: The formfield helper message. - /// - isTitleRequired: The asterisk symbol at the end of title. - /// - isEnabled: The formfield's component isEnabled value. - /// - isSelected: The formfield's component isSelected state. - public convenience init( - theme: Theme, - component: Component, - feedbackState: FormFieldFeedbackState = .default, - title: String? = nil, - description: String? = nil, - isTitleRequired: Bool = false, - isEnabled: Bool = true, - isSelected: Bool = false - ) { - let attributedTitle: NSAttributedString? = title.map(NSAttributedString.init) - let attributedDescription: NSAttributedString? = description.map(NSAttributedString.init) - self.init( - theme: theme, - component: component, - feedbackState: feedbackState, - attributedTitle: attributedTitle, - attributedDescription: attributedDescription, - isTitleRequired: isTitleRequired, - isEnabled: isEnabled, - isSelected: isSelected - ) - } - - /// Initialize a new checkbox UIKit-view. - /// - Parameters: - /// - theme: The current Spark-Theme. - /// - component: The component is covered by formfield. - /// - feedbackState: The formfield feedback state. 'Default' or 'Error'. - /// - attributedTitle: The formfield attributedTitle. - /// - attributedDescription: The formfield attributed helper message. - /// - isTitleRequired: The asterisk symbol at the end of title. - /// - isEnabled: The formfield's component isEnabled value. - /// - isSelected: The formfield's component isSelected state. - public init( - theme: Theme, - component: Component, - feedbackState: FormFieldFeedbackState = .default, - attributedTitle: NSAttributedString? = nil, - attributedDescription: NSAttributedString? = nil, - isTitleRequired: Bool = false, - isEnabled: Bool = true, - isSelected: Bool = false - ) { - let viewModel = FormFieldViewModel( - theme: theme, - feedbackState: feedbackState, - title: attributedTitle, - description: attributedDescription, - isTitleRequired: isTitleRequired - ) - - self.viewModel = viewModel - self.spacing = viewModel.spacing - self.component = component - - super.init(frame: .zero) - - self.isEnabled = isEnabled - self.isSelected = isSelected - self.commonInit() - } - - private func commonInit() { - self.setupViews() - self.setComponent() - self.subscribe() - self.updateAccessibility() - } - - private func updateAccessibility() { - self.accessibilityIdentifier = FormFieldAccessibilityIdentifier.formField - self.isAccessibilityElement = false - self.accessibilityContainerType = .semanticGroup - } - - private func setComponent() { - self.stackView.insertArrangedSubview(self.component, at: 1) - } - - private func setupViews() { - self.addSubview(self.stackView) - NSLayoutConstraint.stickEdges(from: self.stackView, to: self) - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { return } - - self._spacing.update(traitCollection: traitCollection) - self.stackView.spacing = self.spacing - } - - private func subscribe() { - - Publishers.CombineLatest3( - self.viewModel.$title, - self.viewModel.$titleFont, - self.viewModel.$titleColor - ) - .subscribe(in: &self.cancellables) { [weak self] title, font, color in - guard let self else { return } - let labelHidden: Bool = (title?.string ?? "").isEmpty - self.titleLabel.isHidden = labelHidden - self.titleLabel.font = font.uiFont - self.titleLabel.textColor = color.uiColor - self.titleLabel.attributedText = title - } - - Publishers.CombineLatest3( - self.viewModel.$description, - self.viewModel.$descriptionFont, - self.viewModel.$descriptionColor - ) - .subscribe(in: &self.cancellables) { [weak self] title, font, color in - guard let self else { return } - let labelHidden: Bool = (title?.string ?? "").isEmpty - self.descriptionLabel.isHidden = labelHidden - self.descriptionLabel.font = font.uiFont - self.descriptionLabel.textColor = color.uiColor - self.descriptionLabel.attributedText = title - } - - self.viewModel.$spacing.subscribe(in: &self.cancellables) { [weak self] spacing in - guard let self = self else { return } - self._spacing.wrappedValue = spacing - self.stackView.spacing = self.spacing - } - } -} diff --git a/core/Sources/Components/FormField/View/UIKit/FormFieldUIViewSnapshotTests.swift b/core/Sources/Components/FormField/View/UIKit/FormFieldUIViewSnapshotTests.swift deleted file mode 100644 index 99fda55fc..000000000 --- a/core/Sources/Components/FormField/View/UIKit/FormFieldUIViewSnapshotTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// FormFieldUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by alican.aycil on 14.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -final class FormFieldUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = FormfieldScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration() - - for configuration in configurations { - - let component = UISwitch() - component.setOn(true, animated: false) - - let view = FormFieldUIView( - theme: self.theme, - component: component, - feedbackState: configuration.feedbackState, - title: configuration.label, - description: configuration.helperMessage, - isTitleRequired: configuration.isRequired, - isEnabled: configuration.isEnabled - ) - view.backgroundColor = UIColor.systemBackground - view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - view.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width) - ]) - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } - - static func makeSingleCheckbox() -> UIControl { - return CheckboxUIView( - theme: SparkTheme.shared, - text: "Hello World", - checkedImage: UIImage.mock, - selectionState: .unselected, - alignment: .left - ) - } - - static func makeVerticalCheckbox() -> UIControl { - let view = CheckboxGroupUIView( - checkedImage: UIImage.mock, - items: [ - CheckboxGroupItemDefault(title: "Checkbox 1", id: "1", selectionState: .unselected, isEnabled: true), - CheckboxGroupItemDefault(title: "Checkbox 2", id: "2", selectionState: .selected, isEnabled: true), - ], - theme: SparkTheme.shared, - intent: .success, - accessibilityIdentifierPrefix: "checkbox" - ) - view.layout = .vertical - return view - } - - static func makeSingleRadioButton() -> UIControl { - return RadioButtonUIView( - theme: SparkTheme.shared, - intent: .info, - id: "radiobutton", - label: NSAttributedString(string: "Hello World"), - isSelected: true - ) - } - - static func makeVerticalRadioButton() -> UIControl { - return RadioButtonUIGroupView( - theme: SparkTheme.shared, - intent: .danger, - selectedID: "radiobutton", - items: [ - RadioButtonUIItem(id: "1", label: "Radio Button 1"), - RadioButtonUIItem(id: "2", label: "Radio Button 2"), - ], - groupLayout: .vertical - ) - } -} - -private extension UIImage { - static let mock: UIImage = UIImage(systemName: "checkmark")?.withRenderingMode(.alwaysTemplate) ?? UIImage() -} diff --git a/core/Sources/Components/Icon/AccessibilityIdentifier/IconAccessibilityIdentifier.swift b/core/Sources/Components/Icon/AccessibilityIdentifier/IconAccessibilityIdentifier.swift deleted file mode 100644 index f6f043ec4..000000000 --- a/core/Sources/Components/Icon/AccessibilityIdentifier/IconAccessibilityIdentifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// IconAccessibilityIdentifier.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 20.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum IconAccessibilityIdentifier { - - // MARK: - Properties - - public static let view = "spark-icon-image" -} diff --git a/core/Sources/Components/Icon/Enum/IconIntent.swift b/core/Sources/Components/Icon/Enum/IconIntent.swift deleted file mode 100644 index 8b89a61e8..000000000 --- a/core/Sources/Components/Icon/Enum/IconIntent.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// IconIntent.swift -// Spark -// -// Created by Jacklyn Situmorang on 10.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Intents of the icon. -public enum IconIntent: CaseIterable { - case accent - case basic - case alert - case error - case neutral - case main - case support - case success -} diff --git a/core/Sources/Components/Icon/Enum/IconSize.swift b/core/Sources/Components/Icon/Enum/IconSize.swift deleted file mode 100644 index 2d5f65db6..000000000 --- a/core/Sources/Components/Icon/Enum/IconSize.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// IconSize.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 11.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Different sizes of icon. -public enum IconSize: CaseIterable { - /// Small icon with size of 16x16px - case small - - /// Small icon with size of 24x24px - case medium - - /// Small icon with size of 32x32px - case large - - /// Small icon with size of 40x40px - case extraLarge -} - -// MARK: - Extension -extension IconSize { - public var value: CGFloat { - switch self { - case .small: - return Constants.valueSmall - case .medium: - return Constants.valueMedium - case .large: - return Constants.valueLarge - case .extraLarge: - return Constants.valueExtraLarge - } - } -} - -// MARK: - Constants -private enum Constants { - static var valueSmall: CGFloat = 16 - static var valueMedium: CGFloat = 24 - static var valueLarge: CGFloat = 32 - static var valueExtraLarge: CGFloat = 40 -} diff --git a/core/Sources/Components/Icon/UseCase/IconGetColorUseCase.swift b/core/Sources/Components/Icon/UseCase/IconGetColorUseCase.swift deleted file mode 100644 index 226145a9b..000000000 --- a/core/Sources/Components/Icon/UseCase/IconGetColorUseCase.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// IconGetColorUseCase.swift -// Spark -// -// Created by Jacklyn Situmorang on 10.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol IconGetColorUseCaseable { - func execute(for intent: IconIntent, colors: Colors) -> any ColorToken -} - -struct IconGetColorUseCase: IconGetColorUseCaseable { - - // MARK: - Methods - - func execute(for intent: IconIntent, colors: Colors) -> any ColorToken { - switch intent { - case .accent: - return colors.accent.accent - case .basic: - return colors.basic.basic - case .alert: - return colors.feedback.alert - case .error: - return colors.feedback.error - case .neutral: - return colors.feedback.neutral - case .main: - return colors.main.main - case .support: - return colors.support.support - case .success: - return colors.feedback.success - } - } -} diff --git a/core/Sources/Components/Icon/UseCase/IconGetColorUseCaseTests.swift b/core/Sources/Components/Icon/UseCase/IconGetColorUseCaseTests.swift deleted file mode 100644 index ad1940c86..000000000 --- a/core/Sources/Components/Icon/UseCase/IconGetColorUseCaseTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// IconGetColorUseCaseTests.swift -// Spark -// -// Created by Jacklyn Situmorang on 10.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class IconGetColorUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let colorsMock = ColorsGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_when_icon_is_alert_case() { - testExecute(givenIntent: .alert, expectedColorToken: self.colorsMock.feedback.alert) - } - - func test_execute_when_icon_is_error_case() { - testExecute(givenIntent: .error, expectedColorToken: self.colorsMock.feedback.error) - } - - func test_execute_when_icon_is_neutral_case() { - testExecute(givenIntent: .neutral, expectedColorToken: self.colorsMock.feedback.neutral) - } - - func test_execute_when_icon_is_main_case() { - testExecute(givenIntent: .main, expectedColorToken: self.colorsMock.main.main) - } - - func test_execute_when_icon_is_support_case() { - testExecute(givenIntent: .support, expectedColorToken: self.colorsMock.support.support) - } - - func test_execute_when_icon_is_success_case() { - testExecute(givenIntent: .success, expectedColorToken: self.colorsMock.feedback.success) - } - - func test_execute_when_icon_is_accent_case() { - testExecute(givenIntent: .accent, expectedColorToken: self.colorsMock.accent.accent) - } - - func test_execute_when_icon_is_basic_case() { - testExecute(givenIntent: .basic, expectedColorToken: self.colorsMock.basic.basic) - } -} - -// MARK: - Extension - -private extension IconGetColorUseCaseTests { - func testExecute( - givenIntent: IconIntent, - expectedColorToken: any ColorToken - ) { - // GIVEN - let useCase = IconGetColorUseCase() - - // WHEN - let colorToken = useCase.execute( - for: givenIntent, - colors: colorsMock - ) - - // THEN - XCTAssertIdentical( - colorToken as? ColorTokenGeneratedMock, - expectedColorToken as? ColorTokenGeneratedMock, - "Wrong color for .\(givenIntent) case" - ) - } -} diff --git a/core/Sources/Components/Icon/View/Model/IconViewModel.swift b/core/Sources/Components/Icon/View/Model/IconViewModel.swift deleted file mode 100644 index f2dd70de1..000000000 --- a/core/Sources/Components/Icon/View/Model/IconViewModel.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// IconViewModel.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 11.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -final class IconViewModel: ObservableObject { - - // MARK: - Properties - - private(set) var theme: Theme - private(set) var intent: IconIntent - private let getColorUseCase: IconGetColorUseCaseable - - // MARK: - Published properties - - @Published var color: any ColorToken - @Published var size: IconSize - - // MARK: - Initializers - - init( - theme: Theme, - intent: IconIntent, - size: IconSize, - getColorUseCase: IconGetColorUseCaseable = IconGetColorUseCase() - ) { - self.theme = theme - self.intent = intent - self.size = size - self.getColorUseCase = getColorUseCase - self.color = getColorUseCase.execute(for: intent, colors: theme.colors) - } - - // MARK: - Setters - - func set(theme: Theme) { - self.theme = theme - self.updateColor() - } - - func set(intent: IconIntent) { - if self.intent != intent { - self.intent = intent - self.updateColor() - } - } - - func set(size: IconSize) { - if self.size != size { - self.size = size - } - } - - // MARK: - Private funcs - - private func updateColor() { - self.color = self.getColorUseCase.execute(for: self.intent, colors: self.theme.colors) - } -} diff --git a/core/Sources/Components/Icon/View/Model/IconViewModelTests.swift b/core/Sources/Components/Icon/View/Model/IconViewModelTests.swift deleted file mode 100644 index 6c7148b12..000000000 --- a/core/Sources/Components/Icon/View/Model/IconViewModelTests.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// IconViewModelTests.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 25.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class IconViewModelTests: XCTestCase { - - // MARK: - Properties - - var theme: ThemeGeneratedMock! - var getColorUseCase: IconGetColorUseCaseableGeneratedMock! - var colorToken: ColorTokenGeneratedMock! - var cancellables: Set! - var sut: IconViewModel! - - // MARK: - Setup - - override func setUpWithError() throws { - try super.setUpWithError() - - self.theme = ThemeGeneratedMock.mocked() - self.getColorUseCase = IconGetColorUseCaseableGeneratedMock() - self.colorToken = ColorTokenGeneratedMock.random() - self.cancellables = .init() - - self.getColorUseCase.executeWithIntentAndColorsReturnValue = self.colorToken - - self.sut = IconViewModel( - theme: self.theme, - intent: .alert, - size: .small, - getColorUseCase: self.getColorUseCase - ) - } - - // MARK: - Tests - - func test_init() throws { - for iconIntent in IconIntent.allCases { - for iconSize in IconSize.allCases { - // GIVEN - self.getColorUseCase.executeWithIntentAndColorsCallsCount = 0 - - self.sut = IconViewModel( - theme: self.theme, - intent: iconIntent, - size: iconSize, - getColorUseCase: self.getColorUseCase - ) - - // THEN - XCTAssertIdentical( - self.sut.theme as? ThemeGeneratedMock, - self.theme, - "Icon theme doesn't match expected theme" - ) - - XCTAssertIdentical( - self.sut.color as? ColorTokenGeneratedMock, - self.colorToken, - "Icon color doesn't match the expected color" - ) - - XCTAssertTrue( - self.sut.size.value == iconSize.value, - "Icon size doesn't match the given size" - ) - - self.testGetColorUseCaseExecute( - givenIntent: iconIntent, - expectedCallsCount: 1 - ) - } - } - } - - func test_set_theme() throws { - // GIVEN - self.getColorUseCase.executeWithIntentAndColorsCallsCount = 0 - let newTheme = ThemeGeneratedMock.mocked() - - // WHEN - self.sut.set(theme: newTheme) - self.theme = newTheme - - // THEN - XCTAssertIdentical( - self.sut.theme as? ThemeGeneratedMock, - newTheme, - "Theme is not updated" - ) - - self.testGetColorUseCaseExecute(givenIntent: .alert, expectedCallsCount: 1) - } - - func test_set_intent() throws { - for iconIntent in IconIntent.allCases { - // GIVEN - self.sut = IconViewModel( - theme: self.theme, - intent: iconIntent, - size: .medium, - getColorUseCase: self.getColorUseCase - ) - - self.getColorUseCase.executeWithIntentAndColorsCallsCount = 0 - - // THEN - - XCTAssertEqual(self.sut.intent, iconIntent, "Icon intent doesn't match the given intent") - self.testGetColorUseCaseExecute( - expectedCallsCount: 0 - ) - - let newIntent = self.randomizeIntentAndRemoveCurrent(iconIntent) - self.sut.set(intent: newIntent) - - XCTAssertEqual(self.sut.intent, newIntent, "Icon intent should not match the initial given intent") - self.testGetColorUseCaseExecute( - givenIntent: newIntent, - expectedCallsCount: 1 - ) - } - } - - func test_set_size() { - for iconSize in IconSize.allCases { - // GIVEN - self.sut = IconViewModel( - theme: self.theme, - intent: .neutral, - size: iconSize, - getColorUseCase: self.getColorUseCase - ) - - // THEN - XCTAssertEqual(self.sut.size, iconSize, "Icon size doesn't match the given size") - - self.sut.set(size: self.randomizeSizeAndRemoveCurrent(iconSize)) - - XCTAssertNotEqual(self.sut.size, iconSize, "Icon size should not match the initial given size") - } - } - - func test_color_subscription_on_intent_change() throws { - // GIVEN - let expectation = expectation(description: "Color updated on intent change") - expectation.expectedFulfillmentCount = 2 - self.sut = IconViewModel( - theme: self.theme, - intent: .alert, - size: .medium, - getColorUseCase: self.getColorUseCase - ) - - self.sut.$color.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // WHEN - self.sut.set(intent: .support) - - // THEN - wait(for: [expectation], timeout: 0.1) - } - - func test_size_subscription_on_size_change() throws { - // GIVEN - let expectation = expectation(description: "Size changed") - expectation.expectedFulfillmentCount = 2 - self.sut = IconViewModel( - theme: self.theme, - intent: .alert, - size: .medium, - getColorUseCase: self.getColorUseCase - ) - - self.sut.$size.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // WHEN - self.sut.set(size: .extraLarge) - - // THEN - wait(for: [expectation], timeout: 0.1) - } - - private func testGetColorUseCaseExecute( - givenIntent: IconIntent? = nil, - expectedCallsCount: Int - ) { - XCTAssertEqual( - self.getColorUseCase.executeWithIntentAndColorsCallsCount, - expectedCallsCount, - "Wrong call number on execute on getColorUseCase" - ) - - if expectedCallsCount > 0 { - let args = self.getColorUseCase.executeWithIntentAndColorsReceivedArguments - - XCTAssertEqual( - args?.intent, - givenIntent, - "Wrong intent parameter on execute on getColorUseCase" - ) - - XCTAssertIdentical( - args?.colors as? ColorsGeneratedMock, - self.theme.colors as? ColorsGeneratedMock, - "Wrong colors parameter on execute on getColorUseCase" - ) - } - } - - private func randomizeIntentAndRemoveCurrent(_ currentIntent: IconIntent) -> IconIntent { - let filteredIntents = IconIntent.allCases.filter { $0 != currentIntent } - let randomIndex = Int.random(in: 0...filteredIntents.count - 1) - - return filteredIntents[randomIndex] - } - - private func randomizeSizeAndRemoveCurrent(_ currentSize: IconSize) -> IconSize { - let filteredSizes = IconSize.allCases.filter { $0 != currentSize } - let randomIndex = Int.random(in: 0...filteredSizes.count - 1) - - return filteredSizes[randomIndex] - } -} diff --git a/core/Sources/Components/Icon/View/SwiftUI/IconView.swift b/core/Sources/Components/Icon/View/SwiftUI/IconView.swift deleted file mode 100644 index a567b324f..000000000 --- a/core/Sources/Components/Icon/View/SwiftUI/IconView.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// IconView.swift -// Spark -// -// Created by Jacklyn Situmorang on 24.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct IconView: View { - - // MARK: - Private properties - - @ObservedObject private var viewModel: IconViewModel - @ScaledMetric private var sizeValue: CGFloat - private var iconImage: Image - - // MARK: - Initialization - - /// A SwiftUI Icon component - /// - Parameters: - /// - theme: The Spark theme - /// - intent: Intent of icon - /// - size: Size of icon - public init( - theme: Theme, - intent: IconIntent, - size: IconSize, - iconImage: Image - ) { - self.viewModel = IconViewModel( - theme: theme, - intent: intent, - size: size - ) - self._sizeValue = ScaledMetric(wrappedValue: size.value) - self.iconImage = iconImage - } - - public var body: some View { - iconImage - .resizable() - .frame(width: sizeValue, height: sizeValue) - .foregroundColor(self.viewModel.color.color) - .accessibilityIdentifier(IconAccessibilityIdentifier.view) - } -} diff --git a/core/Sources/Components/Icon/View/SwiftUI/IconViewSnapshotTests.swift b/core/Sources/Components/Icon/View/SwiftUI/IconViewSnapshotTests.swift deleted file mode 100644 index 48aa58120..000000000 --- a/core/Sources/Components/Icon/View/SwiftUI/IconViewSnapshotTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// IconViewSnapshotTests.swift -// SparkCoreTests -// -// Created by Jacklyn Situmorang on 24.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import SnapshotTesting - -@testable import SparkCore - -final class IconViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - private let theme: Theme = SparkTheme.shared - private var iconImage = Image(systemName: "lock.circle") - - // MARK: - Tests - func test_swiftUI_icon_for_all_intents() throws { - for intent in IconIntent.allCases { - let iconView = IconView( - theme: self.theme, - intent: intent, - size: .medium, - iconImage: iconImage - ) - self.assertSnapshotInDarkAndLight(matching: iconView, testName: "\(#function)-\(intent)") - } - } - - func test_swiftUI_icon_for_all_sizes() throws { - for size in IconSize.allCases { - let iconView = IconView( - theme: self.theme, - intent: .success, - size: size, - iconImage: iconImage - ) - self.assertSnapshotInDarkAndLight(matching: iconView, testName: "\(#function)-\(size)") - } - } -} diff --git a/core/Sources/Components/Icon/View/UIKit/IconUIView.swift b/core/Sources/Components/Icon/View/UIKit/IconUIView.swift deleted file mode 100644 index ad6647edb..000000000 --- a/core/Sources/Components/Icon/View/UIKit/IconUIView.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// IconUIView.swift -// Spark -// -// Created by Jacklyn Situmorang on 11.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -public final class IconUIView: UIView { - - // MARK: - Public properties - - /// UIImage of the icon. - public var icon: UIImage? { - get { - return self.imageView.image - } - set { - self.imageView.image = newValue - } - } - - /// Used theme for the icon. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.set(theme: newValue) - } - } - - /// Intent of icon. - public var intent: IconIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.set(intent: newValue) - } - } - - /// Size of icon. - public var size: IconSize { - get { - return self.viewModel.size - } - set { - self.viewModel.set(size: newValue) - } - } - - public override var intrinsicContentSize: CGSize { - return CGSize(width: self.width, height: self.height) - } - - // MARK: - Private properties - - private var cancellables = Set() - private var heightConstraint: NSLayoutConstraint? - private var widthConstraint: NSLayoutConstraint? - - @ScaledUIMetric private var height: CGFloat = .zero - @ScaledUIMetric private var width: CGFloat = .zero - - private let viewModel: IconViewModel - - private let imageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleToFill - imageView.isAccessibilityElement = false - imageView.setContentCompressionResistancePriority(.required, for: .horizontal) - imageView.setContentCompressionResistancePriority(.required, for: .vertical) - - return imageView - }() - - // MARK: - Initializers - - public init( - iconImage: UIImage?, - theme: Theme, - intent: IconIntent, - size: IconSize - ) { - self.viewModel = IconViewModel(theme: theme, intent: intent, size: size) - - super.init(frame: .zero) - - self.icon = iconImage - self.setupView() - self.setupSubscriptions() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Private funcs - - private func setupView() { - self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = .clear - self.setContentHuggingPriority(.required, for: .horizontal) - self.setContentHuggingPriority(.required, for: .vertical) - self.accessibilityIdentifier = IconAccessibilityIdentifier.view - - self.addSubview(imageView) - self.imageView.tintColor = self.viewModel.color.uiColor - self.imageView.layoutMargins = UIEdgeInsets(vertical: .zero, horizontal: .zero) - - self.heightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.height) - self.widthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: self.width) - self.heightConstraint?.isActive = true - self.widthConstraint?.isActive = true - - let anchorConstraint = [ - self.imageView.topAnchor.constraint(equalTo: topAnchor), - self.imageView.leftAnchor.constraint(equalTo: leftAnchor), - self.imageView.rightAnchor.constraint(equalTo: rightAnchor), - self.imageView.bottomAnchor.constraint(equalTo: bottomAnchor) - ] - - NSLayoutConstraint.activate(anchorConstraint) - } - - private func setupSubscriptions() { - self.viewModel.$color.subscribe(in: &self.cancellables) { [weak self] color in - self?.updateIconColor(color) - } - - self.viewModel.$size.subscribe(in: &self.cancellables) { [weak self] size in - self?.height = size.value - self?._height.update(traitCollection: self?.traitCollection) - - self?.width = size.value - self?._width.update(traitCollection: self?.traitCollection) - - self?.updateIconSize() - self?.invalidateIntrinsicContentSize() - } - } - - private func updateIconColor(_ color: any ColorToken) { - self.imageView.tintColor = color.uiColor - } - - private func updateIconSize() { - if self.heightConstraint?.constant != self.height { - self.heightConstraint?.constant = self.height - self.updateConstraintsIfNeeded() - } - - if self.widthConstraint?.constant != self.width { - self.widthConstraint?.constant = self.width - self.updateConstraintsIfNeeded() - } - } - - // MARK: - Trait collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self.invalidateIntrinsicContentSize() - self._height.update(traitCollection: traitCollection) - self._width.update(traitCollection: traitCollection) - self.updateIconSize() - } - -} diff --git a/core/Sources/Components/Icon/View/UIKit/IconUIViewSnapshotTests.swift b/core/Sources/Components/Icon/View/UIKit/IconUIViewSnapshotTests.swift deleted file mode 100644 index e06d68625..000000000 --- a/core/Sources/Components/Icon/View/UIKit/IconUIViewSnapshotTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// IconUIViewSnapshotTests.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 17.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class IconUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: Tests - func test_icon_intent() { - for intent in IconIntent.allCases { - let iconView = IconUIView( - iconImage: UIImage(systemName: "lock.circle"), - theme: SparkTheme.shared, - intent: intent, - size: .medium - ) - assertSnapshotInDarkAndLight(matching: iconView, testName: "\(#function)-\(intent)") - } - } - - func test_icon_size() { - for size in IconSize.allCases { - let iconView = IconUIView( - iconImage: UIImage(systemName: "lock.circle"), - theme: SparkTheme.shared, - intent: .neutral, - size: size - ) - assertSnapshotInDarkAndLight(matching: iconView, testName: "\(#function)-\(size)") - } - } -} diff --git a/core/Sources/Components/ProgressBar/AccessibilityIdentifier/ProgressBarAccessibilityIdentifier.swift b/core/Sources/Components/ProgressBar/AccessibilityIdentifier/ProgressBarAccessibilityIdentifier.swift deleted file mode 100644 index 273827727..000000000 --- a/core/Sources/Components/ProgressBar/AccessibilityIdentifier/ProgressBarAccessibilityIdentifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ProgressBarAccessibilityIdentifier.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The accessibility identifiers for the progressBar. -public enum ProgressBarAccessibilityIdentifier { - - // MARK: - Properties - - /// The content view accessibility identifier. - public static let contentView = "spark-progressBar-contentView" - /// The progress bar accessibility identifier. - public static let progressBar = "spark-progress-bar" - /// The indicator view accessibility identifier. - public static let indicatorView = "spark-progressBar-indicatorView" - /// The bottom indicator view accessibility identifier. Only used with progress bar double - public static let bottomIndicatorView = "spark-progressBar-bottomIndicatorView" -} diff --git a/core/Sources/Components/ProgressBar/Constants/ProgressBarConstants.swift b/core/Sources/Components/ProgressBar/Constants/ProgressBarConstants.swift deleted file mode 100644 index 2e7f277dc..000000000 --- a/core/Sources/Components/ProgressBar/Constants/ProgressBarConstants.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ProgressBarConstants.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum ProgressBarConstants { - enum Animation { - /// Animation duration is 400ms - static let duration = 0.4 - /// Animation max width ratio is 0.5 (component half the width) - static let maxWidthRatio = 0.5 - } - - /// The height of the track & indicators - static let height: CGFloat = 4 -} diff --git a/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationType.swift b/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationType.swift deleted file mode 100644 index 3d0cad521..000000000 --- a/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationType.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ProgressBarIndeterminateAnimationType.swift -// SparkCore -// -// Created by robin.lemaire on 28/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -enum ProgressBarIndeterminateAnimationType: Equatable { - case easeIn - case easeOut - case reset - - // MARK: - Properties - - /// Get the next animation type - mutating func next() { - switch self { - case .easeIn: - self = .easeOut - case .easeOut: - self = .reset - case .reset: - self = .easeIn - } - } -} diff --git a/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationTypeTests.swift b/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationTypeTests.swift deleted file mode 100644 index bd42f515c..000000000 --- a/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateAnimationTypeTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ProgressBarIndeterminateAnimationTypeTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ProgressBarIndeterminateAnimationTypeTests: XCTestCase { - - // MARK: - Tests - - func test_next() throws { - // GIVEN - let items: [( - givenType: ProgressBarIndeterminateAnimationType, - expectedNextType: ProgressBarIndeterminateAnimationType - )] = [ - ( - givenType: .easeIn, - expectedNextType: .easeOut - ), - ( - givenType: .easeOut, - expectedNextType: .reset - ), - ( - givenType: .reset, - expectedNextType: .easeIn - ) - ] - - for item in items { - // WHEN - var type = item.givenType - type.next() - - // THEN - XCTAssertEqual( - type, - item.expectedNextType, - "Wrong next type when type is \(item.givenType)" - ) - } - } -} diff --git a/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateStatus.swift b/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateStatus.swift deleted file mode 100644 index 8f6e72648..000000000 --- a/core/Sources/Components/ProgressBar/Enum/Indeterminate/ProgressBarIndeterminateStatus.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ProgressBarIndeterminateStatus.swift -// SparkCore -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -enum ProgressBarIndeterminateStatus: Equatable { - case start - case stop -} diff --git a/core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarDoubleIntent.swift b/core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarDoubleIntent.swift deleted file mode 100644 index a030c86e6..000000000 --- a/core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarDoubleIntent.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ProgressBarDoubleIntent.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The intent of the progress bar double. -public enum ProgressBarDoubleIntent: CaseIterable { - case accent - case alert - case basic - case danger - case main - case success -} diff --git a/core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarIntent.swift b/core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarIntent.swift deleted file mode 100644 index 1959fc524..000000000 --- a/core/Sources/Components/ProgressBar/Enum/Intent/ProgressBarIntent.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ProgressBarIntent.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The intent of the progress bar and progress bar indeterminate. -public enum ProgressBarIntent: CaseIterable { - case accent - case alert - case basic - case danger - case info - case main - case neutral - case success - case support -} diff --git a/core/Sources/Components/ProgressBar/Enum/ProgressBarShape.swift b/core/Sources/Components/ProgressBar/Enum/ProgressBarShape.swift deleted file mode 100644 index e94b64bf1..000000000 --- a/core/Sources/Components/ProgressBar/Enum/ProgressBarShape.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProgressBarShape.swift -// SparkCore -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// All ProgressBar variants can have different shapes. -public enum ProgressBarShape: CaseIterable, Equatable { - /// ProgressBar with rounded corners. - case rounded - /// Square button with no rounded corners. - case square -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData+ExtensionTests.swift b/core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData+ExtensionTests.swift deleted file mode 100644 index 6f678c0fe..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ProgressBarAnimatedData+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import Foundation - -extension ProgressBarAnimatedData { - - // MARK: - Properties - - static func mocked( - leadingSpaceWidth: CGFloat = 1, - indicatorWidth: CGFloat = 2 - ) -> Self { - return .init( - leadingSpaceWidth: leadingSpaceWidth, - indicatorWidth: indicatorWidth - ) - } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData.swift b/core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData.swift deleted file mode 100644 index 413392568..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/AnimatedData/ProgressBarAnimatedData.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ProgressBarAnimatedData.swift -// SparkCore -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct ProgressBarAnimatedData: Equatable { - - // MARK: - Properties - - let leadingSpaceWidth: CGFloat - let indicatorWidth: CGFloat -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors+ExtensionTests.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors+ExtensionTests.swift deleted file mode 100644 index 52f19f48e..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors+ExtensionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ProgressBarDoubleColors+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ProgressBarDoubleColors { - - // MARK: - Properties - - static func mocked( - trackBackgroundColorToken: any ColorToken = ColorTokenGeneratedMock.random(), - indicatorBackgroundColorToken: any ColorToken = ColorTokenGeneratedMock.random(), - bottomIndicatorBackgroundColorToken: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - trackBackgroundColorToken: trackBackgroundColorToken, - indicatorBackgroundColorToken: indicatorBackgroundColorToken, - bottomIndicatorBackgroundColorToken: bottomIndicatorBackgroundColorToken - ) - } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors.swift deleted file mode 100644 index a9ee8c5c4..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColors.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ProgressBarDoubleColors.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct ProgressBarDoubleColors: ProgressBarMainColors { - - // MARK: - Properties - - let trackBackgroundColorToken: any ColorToken - let indicatorBackgroundColorToken: any ColorToken - let bottomIndicatorBackgroundColorToken: any ColorToken -} - -// MARK: Hashable & Equatable - -extension ProgressBarDoubleColors { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.trackBackgroundColorToken) - hasher.combine(self.indicatorBackgroundColorToken) - hasher.combine(self.bottomIndicatorBackgroundColorToken) - } - - static func == (lhs: ProgressBarDoubleColors, rhs: ProgressBarDoubleColors) -> Bool { - return lhs.trackBackgroundColorToken.equals(rhs.trackBackgroundColorToken) && - lhs.indicatorBackgroundColorToken.equals(rhs.indicatorBackgroundColorToken) && - lhs.bottomIndicatorBackgroundColorToken.equals(rhs.bottomIndicatorBackgroundColorToken) - } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColorsTests.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColorsTests.swift deleted file mode 100644 index aa4bb1366..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Double/ProgressBarDoubleColorsTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ProgressBarDoubleColorsTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class ProgressBarDoubleColorsTests: XCTestCase { - - // MARK: - Tests - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = ProgressBarDoubleColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.main, - bottomIndicatorBackgroundColorToken: colors.feedback.alert - ) - - let colors2 = ProgressBarDoubleColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.main, - bottomIndicatorBackgroundColorToken: colors.feedback.alert - ) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = ProgressBarDoubleColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.main, - bottomIndicatorBackgroundColorToken: colors.feedback.alert - ) - - let colors2 = ProgressBarDoubleColors( - trackBackgroundColorToken: colors.base.onBackground, - indicatorBackgroundColorToken: colors.main.onMain, - bottomIndicatorBackgroundColorToken: colors.feedback.onAlert - ) - - XCTAssertNotEqual(colors1, colors2) - } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/ProgressBarMainColors.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/ProgressBarMainColors.swift deleted file mode 100644 index 4bc657a0d..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/ProgressBarMainColors.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ProgressBarMainColors.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -protocol ProgressBarMainColors: Hashable, Equatable { - var trackBackgroundColorToken: any ColorToken { get } - var indicatorBackgroundColorToken: any ColorToken { get } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors+ExtensionTests.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors+ExtensionTests.swift deleted file mode 100644 index cb4475763..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors+ExtensionTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ProgressBarColors+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ProgressBarColors { - - // MARK: - Properties - - static func mocked( - trackBackgroundColorToken: any ColorToken = ColorTokenGeneratedMock.random(), - indicatorBackgroundColorToken: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - trackBackgroundColorToken: trackBackgroundColorToken, - indicatorBackgroundColorToken: indicatorBackgroundColorToken - ) - } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors.swift deleted file mode 100644 index 3fa9e9bc7..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColors.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ProgressBarColors.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct ProgressBarColors: ProgressBarMainColors { - - // MARK: - Properties - - let trackBackgroundColorToken: any ColorToken - let indicatorBackgroundColorToken: any ColorToken -} - -// MARK: Hashable & Equatable - -extension ProgressBarColors { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.trackBackgroundColorToken) - hasher.combine(self.indicatorBackgroundColorToken) - } - - static func == (lhs: ProgressBarColors, rhs: ProgressBarColors) -> Bool { - return lhs.trackBackgroundColorToken.equals(rhs.trackBackgroundColorToken) && - lhs.indicatorBackgroundColorToken.equals(rhs.indicatorBackgroundColorToken) - } -} diff --git a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColorsTests.swift b/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColorsTests.swift deleted file mode 100644 index 52e82db9f..000000000 --- a/core/Sources/Components/ProgressBar/Model/Internal/Colors/Single/ProgressBarColorsTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ProgressBarColorsTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class ProgressBarColorsTests: XCTestCase { - - // MARK: - Tests - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = ProgressBarColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.main - ) - - let colors2 = ProgressBarColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.main - ) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = ProgressBarColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.main - ) - - let colors2 = ProgressBarColors( - trackBackgroundColorToken: colors.base.background, - indicatorBackgroundColorToken: colors.main.onMain - ) - - XCTAssertNotEqual(colors1, colors2) - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCase.swift b/core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCase.swift deleted file mode 100644 index 9951c4e9f..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCase.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ProgressBarGetAnimatedDataUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable, AutoMockTest -protocol ProgressBarGetAnimatedDataUseCaseable { - func execute( - type: ProgressBarIndeterminateAnimationType?, - trackWidth: CGFloat - ) -> ProgressBarAnimatedData -} - -struct ProgressBarGetAnimatedDataUseCase: ProgressBarGetAnimatedDataUseCaseable { - - // MARK: - Type alias - - private typealias Constants = ProgressBarConstants - - // MARK: - Methods - - func execute( - type: ProgressBarIndeterminateAnimationType?, - trackWidth: CGFloat - ) -> ProgressBarAnimatedData { - switch type { - case .easeIn: - let indicatorMaxWidth = trackWidth * Constants.Animation.maxWidthRatio - return .init( - leadingSpaceWidth: (trackWidth - indicatorMaxWidth) / 2, - indicatorWidth: indicatorMaxWidth - ) - - case .easeOut: - return .init( - leadingSpaceWidth: trackWidth, - indicatorWidth: 0 - ) - - case .reset, .none: - return .init( - leadingSpaceWidth: 0, - indicatorWidth: 0 - ) - } - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCaseTests.swift b/core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCaseTests.swift deleted file mode 100644 index 1a5f826f0..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetAnimatedData/ProgressBarGetAnimatedDataUseCaseTests.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ProgressBarGetAnimatedDataUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 29/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ProgressBarGetAnimatedDataUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let trackWidth: CGFloat = 100 - - // MARK: - Tests - - func test_execute_when_type_is_easeIn() { - self.testExecute( - givenType: .easeIn, - expectedAnimatedData: .init( - leadingSpaceWidth: ((self.trackWidth - self.trackWidth * 0.5) / 2), - indicatorWidth: self.trackWidth * 0.5 - ) - ) - } - - func test_execute_when_type_is_easeOut() { - self.testExecute( - givenType: .easeOut, - expectedAnimatedData: .init( - leadingSpaceWidth: self.trackWidth, - indicatorWidth: 0 - ) - ) - } - - func test_execute_when_type_is_reset() { - self.testExecute( - givenType: .reset, - expectedAnimatedData: .init( - leadingSpaceWidth: 0, - indicatorWidth: 0 - ) - ) - } - - func test_execute_when_type_is_none() { - self.testExecute( - givenType: .none, - expectedAnimatedData: .init( - leadingSpaceWidth: 0, - indicatorWidth: 0 - ) - ) - } -} - -// MARK: - Execute Testing - -private extension ProgressBarGetAnimatedDataUseCaseTests { - - func testExecute( - givenType: ProgressBarIndeterminateAnimationType?, - expectedAnimatedData: ProgressBarAnimatedData - ) { - // GIVEN - let useCase = ProgressBarGetAnimatedDataUseCase() - - // WHEN - let animatedData = useCase.execute( - type: givenType, - trackWidth: self.trackWidth - ) - - // THEN - XCTAssertEqual( - animatedData, - expectedAnimatedData - ) - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCase.swift b/core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCase.swift deleted file mode 100644 index e4f89b265..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCase.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ProgressBarGetColorUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct ProgressBarDoubleGetColorsUseCase: ProgressBarMainGetColorsUseCaseable { - - // MARK: - Methods - - func execute( - intent: ProgressBarDoubleIntent, - colors: Colors, - dims: Dims - ) -> ProgressBarDoubleColors { - let indicatorBackgroundColorToken: any ColorToken - switch intent { - case .accent: - indicatorBackgroundColorToken = colors.accent.accent - case .alert: - indicatorBackgroundColorToken = colors.feedback.alert - case .basic: - indicatorBackgroundColorToken = colors.basic.basic - case .danger: - indicatorBackgroundColorToken = colors.feedback.error - case .main: - indicatorBackgroundColorToken = colors.main.main - case .success: - indicatorBackgroundColorToken = colors.feedback.success - } - - return .init( - trackBackgroundColorToken: colors.base.onBackground.opacity(dims.dim4), - indicatorBackgroundColorToken: indicatorBackgroundColorToken, - bottomIndicatorBackgroundColorToken: indicatorBackgroundColorToken.opacity(dims.dim3) - ) - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCaseTests.swift b/core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCaseTests.swift deleted file mode 100644 index 3e9622d43..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetColors/Double/ProgressBarDoubleGetColorsUseCaseTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// ProgressBarDoubleGetColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ProgressBarDoubleGetColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let colorsMock = ColorsGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_when_intent_is_accent_case() throws { - try self.testExecute( - givenIntent: .accent, - expectedIndicatorBackgroundColorToken: self.colorsMock.accent.accent - ) - } - - func test_execute_when_intent_is_alert_case() throws { - try self.testExecute( - givenIntent: .alert, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.alert - ) - } - - func test_execute_when_intent_is_basic_case() throws { - try self.testExecute( - givenIntent: .basic, - expectedIndicatorBackgroundColorToken: self.colorsMock.basic.basic - ) - } - - func test_execute_when_intent_is_danger_case() throws { - try self.testExecute( - givenIntent: .danger, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.error - ) - } - - func test_execute_when_intent_is_main_case() throws { - try self.testExecute( - givenIntent: .main, - expectedIndicatorBackgroundColorToken: self.colorsMock.main.main - ) - } - - func test_execute_when_intent_is_success_case() throws { - try self.testExecute( - givenIntent: .success, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.success - ) - } -} - -// MARK: - Execute Testing - -private extension ProgressBarDoubleGetColorsUseCaseTests { - - func testExecute( - givenIntent: ProgressBarDoubleIntent, - expectedIndicatorBackgroundColorToken: any ColorToken - ) throws { - // GIVEN - let dimsMock = DimsGeneratedMock.mocked() - - let expectedTrackBackgroundColorToken = self.colorsMock.base.onBackground.opacity(dimsMock.dim4) - let bottomIndicatorBackgroundColorToken = expectedIndicatorBackgroundColorToken.opacity(dimsMock.dim3) - - let useCase = ProgressBarDoubleGetColorsUseCase() - - // WHEN - let colors = useCase.execute( - intent: givenIntent, - colors: self.colorsMock, - dims: dimsMock - ) - - // THEN - XCTAssertEqual( - colors.trackBackgroundColorToken.hashValue, - expectedTrackBackgroundColorToken.hashValue, - "Wrong trackBackgroundColorToken for .\(givenIntent) case" - ) - XCTAssertIdentical( - colors.indicatorBackgroundColorToken as? ColorTokenGeneratedMock, - expectedIndicatorBackgroundColorToken as? ColorTokenGeneratedMock, - "Wrong indicatorBackgroundColorToken for .\(givenIntent) case" - ) - XCTAssertEqual( - colors.bottomIndicatorBackgroundColorToken.hashValue, - bottomIndicatorBackgroundColorToken.hashValue, - "Wrong bottomIndicatorBackgroundColorToken for .\(givenIntent) case" - ) - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetColors/Protocol/ProgressBarMainGetColorsUseCaseable.swift b/core/Sources/Components/ProgressBar/UseCase/GetColors/Protocol/ProgressBarMainGetColorsUseCaseable.swift deleted file mode 100644 index e02238920..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetColors/Protocol/ProgressBarMainGetColorsUseCaseable.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ProgressBarMainGetColorsUseCaseable.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable, AutoMockTest -protocol ProgressBarMainGetColorsUseCaseable { - associatedtype Intent: Equatable - associatedtype Return: Equatable - - // sourcery: colors = "Identical", dims = "Identical" - func execute(intent: Intent, - colors: Colors, - dims: Dims) -> Return -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCase.swift b/core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCase.swift deleted file mode 100644 index 0448a74b9..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCase.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ProgressBarGetColorUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct ProgressBarGetColorsUseCase: ProgressBarMainGetColorsUseCaseable { - - // MARK: - Methods - - func execute( - intent: ProgressBarIntent, - colors: Colors, - dims: Dims - ) -> ProgressBarColors { - let indicatorBackgroundColorToken: any ColorToken - switch intent { - case .accent: - indicatorBackgroundColorToken = colors.accent.accent - case .alert: - indicatorBackgroundColorToken = colors.feedback.alert - case .basic: - indicatorBackgroundColorToken = colors.basic.basic - case .danger: - indicatorBackgroundColorToken = colors.feedback.error - case .info: - indicatorBackgroundColorToken = colors.feedback.info - case .main: - indicatorBackgroundColorToken = colors.main.main - case .neutral: - indicatorBackgroundColorToken = colors.feedback.neutral - case .success: - indicatorBackgroundColorToken = colors.feedback.success - case .support: - indicatorBackgroundColorToken = colors.support.support - } - - return .init( - trackBackgroundColorToken: colors.base.onBackground.opacity(dims.dim4), - indicatorBackgroundColorToken: indicatorBackgroundColorToken - ) - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCaseTests.swift b/core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCaseTests.swift deleted file mode 100644 index 92435c9e3..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetColors/Single/ProgressBarGetColorsUseCaseTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// ProgressBarGetColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ProgressBarGetColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let colorsMock = ColorsGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_when_intent_is_accent_case() throws { - try self.testExecute( - givenIntent: .accent, - expectedIndicatorBackgroundColorToken: self.colorsMock.accent.accent - ) - } - - func test_execute_when_intent_is_alert_case() throws { - try self.testExecute( - givenIntent: .alert, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.alert - ) - } - - func test_execute_when_intent_is_basic_case() throws { - try self.testExecute( - givenIntent: .basic, - expectedIndicatorBackgroundColorToken: self.colorsMock.basic.basic - ) - } - - func test_execute_when_intent_is_danger_case() throws { - try self.testExecute( - givenIntent: .danger, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.error - ) - } - - func test_execute_when_intent_is_info_case() throws { - try self.testExecute( - givenIntent: .info, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.info - ) - } - - func test_execute_when_intent_is_main_case() throws { - try self.testExecute( - givenIntent: .main, - expectedIndicatorBackgroundColorToken: self.colorsMock.main.main - ) - } - - func test_execute_when_intent_is_neutral_case() throws { - try self.testExecute( - givenIntent: .neutral, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.neutral - ) - } - - func test_execute_when_intent_is_support_case() throws { - try self.testExecute( - givenIntent: .support, - expectedIndicatorBackgroundColorToken: self.colorsMock.support.support - ) - } - - func test_execute_when_intent_is_success_case() throws { - try self.testExecute( - givenIntent: .success, - expectedIndicatorBackgroundColorToken: self.colorsMock.feedback.success - ) - } -} - -// MARK: - Execute Testing - -private extension ProgressBarGetColorsUseCaseTests { - - func testExecute( - givenIntent: ProgressBarIntent, - expectedIndicatorBackgroundColorToken: any ColorToken - ) throws { - // GIVEN - let dimsMock = DimsGeneratedMock.mocked() - - let expectedTrackBackgroundColorToken = self.colorsMock.base.onBackground.opacity(dimsMock.dim4) - - let useCase = ProgressBarGetColorsUseCase() - - // WHEN - let colors = useCase.execute( - intent: givenIntent, - colors: self.colorsMock, - dims: dimsMock - ) - - // THEN - XCTAssertEqual( - colors.trackBackgroundColorToken.hashValue, - expectedTrackBackgroundColorToken.hashValue, - "Wrong trackBackgroundColorToken for .\(givenIntent) case" - ) - XCTAssertIdentical( - colors.indicatorBackgroundColorToken as? ColorTokenGeneratedMock, - expectedIndicatorBackgroundColorToken as? ColorTokenGeneratedMock, - "Wrong indicatorBackgroundColorToken for .\(givenIntent) case" - ) - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCase.swift b/core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCase.swift deleted file mode 100644 index 3482762b2..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCase.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ProgressBarGetCornerRadiusUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable, AutoMockTest -protocol ProgressBarGetCornerRadiusUseCaseable { - - // sourcery: border = "Identical" - func execute( - shape: ProgressBarShape, - border: Border - ) -> CGFloat -} - -struct ProgressBarGetCornerRadiusUseCase: ProgressBarGetCornerRadiusUseCaseable { - - // MARK: - Type alias - - private typealias Constants = ProgressBarConstants - - // MARK: - Methods - - func execute( - shape: ProgressBarShape, - border: Border - ) -> CGFloat { - switch shape { - case .rounded: - return border.radius.full - case .square: - return .zero - } - } -} diff --git a/core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCaseTests.swift b/core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCaseTests.swift deleted file mode 100644 index 83a4ee9dd..000000000 --- a/core/Sources/Components/ProgressBar/UseCase/GetCornerRadius/ProgressBarGetCornerRadiusUseCaseTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// ProgressBarGetCornerRadiusUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class ProgressBarGetCornerRadiusUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let borderMock = BorderGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_cornerRadius_when_shape_is_rounded_case() { - self.testExecute( - givenShape: .rounded, - expectedRadius: self.borderMock.radius.full - ) - } - - func test_execute_cornerRadius_when_shape_is_square_case() { - self.testExecute( - givenShape: .square, - expectedRadius: 0 - ) - } -} - -// MARK: - Execute Testing - -private extension ProgressBarGetCornerRadiusUseCaseTests { - - func testExecute( - givenShape: ProgressBarShape, - expectedRadius: CGFloat - ) { - // GIVEN - let useCase = ProgressBarGetCornerRadiusUseCase() - - // WHEN - let cornerRadius = useCase.execute( - shape: givenShape, - border: self.borderMock - ) - - // THEN - XCTAssertEqual( - cornerRadius, - expectedRadius, - "Wrong corner radius for .\(givenShape) shape case" - ) - } -} diff --git a/core/Sources/Components/ProgressBar/View/Common/ProgressBarConfigurationSnapshotTests.swift b/core/Sources/Components/ProgressBar/View/Common/ProgressBarConfigurationSnapshotTests.swift deleted file mode 100644 index bf3d66bd7..000000000 --- a/core/Sources/Components/ProgressBar/View/Common/ProgressBarConfigurationSnapshotTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ProgressBarConfigurationSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -struct ProgressBarConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: ProgressBarScenarioSnapshotTests - - let intent: Intent - let shape: ProgressBarShape - let value: CGFloat - var bottomValue: CGFloat { return self.value + 0.1 } - let width: CGFloat = 100 - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.shape)" + "Shape", - "\(self.value)" + "Value" - ].joined(separator: "-") - } -} diff --git a/core/Sources/Components/ProgressBar/View/Common/ProgressBarScenarioSnapshotTests.swift b/core/Sources/Components/ProgressBar/View/Common/ProgressBarScenarioSnapshotTests.swift deleted file mode 100644 index 010a181df..000000000 --- a/core/Sources/Components/ProgressBar/View/Common/ProgressBarScenarioSnapshotTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// ProgressBarScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 18/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI - -enum ProgressBarScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration() throws -> [ProgressBarConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1() - case .test2: - return try self.test2() - case .test3: - return try self.test3() - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - intents: all - /// - value : 0.5 - /// - shape: default - /// - mode : all - /// - size : default - private func test1() -> [ProgressBarConfigurationSnapshotTests] { - let intentPossibilities = Intent.allCases - - return intentPossibilities.map { intent -> ProgressBarConfigurationSnapshotTests in - .init( - scenario: self, - intent: intent, - shape: .square, - value: 0.5, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test all shapes for all a11y sizes - /// - /// Content: - /// - intent: main - /// - value : 0.5 - /// - shapes: all - /// - mode : default - /// - sizes : all - private func test2() throws -> [ProgressBarConfigurationSnapshotTests] { - let shapesPossibilities = ProgressBarShape.allCases - - return try shapesPossibilities.map { shape -> ProgressBarConfigurationSnapshotTests in - .init( - scenario: self, - intent: try Intent.firstCase, - shape: shape, - value: 0.5, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - ) - } - } - - /// Test 3 - /// - /// Description: To test some values for all a11y sizes - /// - /// Content: - /// - intent: basic - /// - value : 0 + 0.3 + 0.75 + 1 - /// - shape: default - /// - mode : default - /// - sizes : all - private func test3() throws -> [ProgressBarConfigurationSnapshotTests] { - let valuesPossibilities = [0, 0.3, 0.75, 1] - - return try valuesPossibilities.map { value -> ProgressBarConfigurationSnapshotTests in - .init( - scenario: self, - intent: try Intent.firstCase, - shape: .square, - value: value, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - ) - } - } -} - -// MARK: - Extension - -private extension CaseIterable { - - static var firstCase: Self { - get throws { - guard let firstCase = Self.allCases.first else { - throw ProgressBarScenarioError.noIntent - } - - return firstCase - } - } -} - -// MARK: - Error - -private enum ProgressBarScenarioError: Error { - case noIntent -} diff --git a/core/Sources/Components/ProgressBar/View/SwiftUI/Internal/ProgressBarContentView.swift b/core/Sources/Components/ProgressBar/View/SwiftUI/Internal/ProgressBarContentView.swift deleted file mode 100644 index 35de63944..000000000 --- a/core/Sources/Components/ProgressBar/View/SwiftUI/Internal/ProgressBarContentView.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// ProgressBarContentView.swift -// SparkCore -// -// Created by robin.lemaire on 27/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -struct ProgressBarContentView: View { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = ProgressBarAccessibilityIdentifier - private typealias Constants = ProgressBarConstants - - // MARK: - Components - - private var indicatorView: () -> IndicatorView - - // MARK: - Properties - - private let trackCornerRadius: CGFloat? - private let trackBackgroundColor: (any ColorToken)? - - @ScaledMetric private var height: CGFloat = Constants.height - - // MARK: - Initialization - - init( - trackCornerRadius: CGFloat?, - trackBackgroundColor: (any ColorToken)?, - @ViewBuilder indicatorView: @escaping () -> IndicatorView - ) { - self.trackCornerRadius = trackCornerRadius - self.trackBackgroundColor = trackBackgroundColor - self.indicatorView = indicatorView - } - - // MARK: - View - - var body: some View { - ZStack() { - // Track - RoundedRectangle(cornerRadius: self.trackCornerRadius ?? 0) - .fill(self.trackBackgroundColor) - - // Indicator view integration - self.indicatorView() - } - .frame(height: self.height) - .accessibilityIdentifier(AccessibilityIdentifier.progressBar) - } -} diff --git a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleView.swift b/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleView.swift deleted file mode 100644 index b78179c6c..000000000 --- a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleView.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// ProgressBarDoubleView.swift -// SparkCore -// -// Created by robin.lemaire on 28/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct ProgressBarDoubleView: View { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = ProgressBarAccessibilityIdentifier - private typealias Constants = ProgressBarConstants - - // MARK: - Properties - - @ObservedObject var viewModel: ProgressBarDoubleViewModel - - private let topValue: CGFloat - private let bottomValue: CGFloat - - // MARK: - Initialization - - /// Initialize a new progress bar double view - /// - Parameters: - /// - theme: The spark theme of the progress bar double. - /// - intent: The intent of the progress bar double. - /// - shape: The shape of the progress bar double. - /// - topValue: The top indicator value of the progress bar double.. Value **MUST** be into 0 (for 0 %) and 1 (for 100%) - /// - bottomValue: The bottom indicator value of the progress bar double.. Value **MUST** be into 0 (for 0 %) and 1 (for 100%) - public init( - theme: any Theme, - intent: ProgressBarDoubleIntent, - shape: ProgressBarShape, - topValue: CGFloat, - bottomValue: CGFloat - ) { - self.viewModel = .init( - for: .swiftUI, - theme: theme, - intent: intent, - shape: shape - ) - self.topValue = topValue - self.bottomValue = bottomValue - } - - // MARK: - View - - public var body: some View { - ProgressBarContentView( - trackCornerRadius: self.viewModel.cornerRadius, - trackBackgroundColor: self.viewModel.colors?.trackBackgroundColorToken, - indicatorView: { - // Bottom Indicator - self.bottomIndicator() - - // Top Indicator - self.topIndicator() - } - ) - } - - @ViewBuilder - private func bottomIndicator() -> some View { - if self.viewModel.isValidIndicatorValue(self.bottomValue) { - self.bottomRectangle().proportionalWidth(from: self.bottomValue) - } else { - self.bottomRectangle() - } - } - - private func bottomRectangle() -> some View { - RoundedRectangle(cornerRadius: self.viewModel.cornerRadius ?? 0) - .fill(self.viewModel.colors?.bottomIndicatorBackgroundColorToken) - .accessibilityIdentifier(AccessibilityIdentifier.bottomIndicatorView) - } - - @ViewBuilder - private func topIndicator() -> some View { - if self.viewModel.isValidIndicatorValue(self.topValue) { - self.topRectangle().proportionalWidth(from: self.topValue) - } else { - self.topRectangle() - } - } - - private func topRectangle() -> some View { - RoundedRectangle(cornerRadius: self.viewModel.cornerRadius ?? 0) - .fill(self.viewModel.colors?.indicatorBackgroundColorToken) - .accessibilityIdentifier(AccessibilityIdentifier.indicatorView) - - } - -} diff --git a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleViewSnapshotTests.swift b/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleViewSnapshotTests.swift deleted file mode 100644 index c2bac0635..000000000 --- a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarDoubleViewSnapshotTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ProgressBarDoubleViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore -import SwiftUI - -final class ProgressBarDoubleViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = ProgressBarScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [ProgressBarConfigurationSnapshotTests] = try scenario.configuration() - for configuration in configurations { - let view = ProgressBarDoubleView( - theme: self.theme, - intent: configuration.intent, - shape: configuration.shape, - topValue: configuration.value, - bottomValue: configuration.bottomValue - ) - .frame(width: configuration.width) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarIndeterminateView.swift b/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarIndeterminateView.swift deleted file mode 100644 index 1e696dd9f..000000000 --- a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarIndeterminateView.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// ProgressBarIndeterminateView.swift -// SparkCore -// -// Created by robin.lemaire on 27/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import Combine - -public struct ProgressBarIndeterminateView: View { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = ProgressBarAccessibilityIdentifier - private typealias Constants = ProgressBarConstants - - // MARK: - Properties - - @ObservedObject var viewModel: ProgressBarIndeterminateViewModel - @State private var animationStepTimer = Self.createTimer() - @State private var width: CGFloat = .zero - - // MARK: - Initialization - - /// Initialize a new progress bar indeterminate view - /// - Parameters: - /// - theme: The spark theme of the progress bar indeterminate. - /// - intent: The intent of the progress bar indeterminate. - /// - shape: The shape of the progress bar indeterminate. - /// - isAnimating: Animate or not the progress bar indeterminate. - public init( - theme: any Theme, - intent: ProgressBarIntent, - shape: ProgressBarShape, - isAnimating: Bool - ) { - self.viewModel = .init( - for: .swiftUI, - theme: theme, - intent: intent, - shape: shape, - isAnimating: isAnimating - ) - } - - // MARK: - View - - public var body: some View { - ProgressBarContentView( - trackCornerRadius: self.viewModel.cornerRadius, - trackBackgroundColor: self.viewModel.colors?.trackBackgroundColorToken, - indicatorView: { - GeometryReader { geometryReader in - RoundedRectangle(cornerRadius: self.viewModel.cornerRadius ?? 0) - .fill(self.viewModel.colors?.indicatorBackgroundColorToken) - .frame(width: self.viewModel.animatedData?.indicatorWidth ?? 0) - .offset(x: self.viewModel.animatedData?.leadingSpaceWidth ?? 0) - .opacity(self.viewModel.indicatorOpacity ?? 0) - .onAppear { - self.width = geometryReader.size.width - } - .onChange(of: geometryReader.size) { newSize in - self.width = newSize.width - } - } - } - ) - .onReceive(self.viewModel.$animationType) { type in - let animation: Animation? - switch type { - case .easeIn: - animation = .easeIn(duration: Constants.Animation.duration) - case .easeOut: - animation = .easeOut(duration: Constants.Animation.duration) - default: - animation = .none - } - - withAnimation(animation) { - self.viewModel.updateAnimatedData( - from: self.width - ) - } - } - .onReceive(self.animationStepTimer) { time in - self.viewModel.animationStepIsDone() - } - .onReceive(self.viewModel.$animationStatus) { status in - switch status { - case .start: - self.animationStepTimer = Self.createTimer() - case .stop: - self.animationStepTimer.upstream.connect().cancel() - case .none: - break - } - } - } - - // MARK: - Timer - - private static func createTimer() -> Publishers.Autoconnect { - return Timer.publish( - every: Constants.Animation.duration, - on: .main, - in: .common - ) - .autoconnect() - } -} diff --git a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarView.swift b/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarView.swift deleted file mode 100644 index fc853b6cb..000000000 --- a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarView.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// ProgressBarView.swift -// SparkCore -// -// Created by robin.lemaire on 27/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct ProgressBarView: View { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = ProgressBarAccessibilityIdentifier - private typealias Constants = ProgressBarConstants - - // MARK: - Properties - - @ObservedObject var viewModel: ProgressBarViewModel - - private let value: CGFloat - - // MARK: - Initialization - - /// Initialize a new progress bar view - /// - Parameters: - /// - theme: The spark theme of the progress bar. - /// - intent: The intent of the progress bar. - /// - shape: The shape of the progress bar. - /// - value: The indicator value of the progress bar. Value **MUST** be into 0 (for 0 %) and 1 (for 100%) - public init( - theme: any Theme, - intent: ProgressBarIntent, - shape: ProgressBarShape, - value: CGFloat - ) { - self.viewModel = .init( - for: .swiftUI, - theme: theme, - intent: intent, - shape: shape - ) - self.value = value - } - - // MARK: - View - - public var body: some View { - ProgressBarContentView( - trackCornerRadius: self.viewModel.cornerRadius, - trackBackgroundColor: self.viewModel.colors?.trackBackgroundColorToken, - indicatorView: { - self.indicatorView() - } - ) - .accessibilityValue("\(Int(round(self.value * 100)))%") - } - - @ViewBuilder - private func indicatorView() -> some View { - if self.viewModel.isValidIndicatorValue(self.value) { - self.indicatorRectangle() - .proportionalWidth(from: self.value) - } else { - self.indicatorRectangle() - } - } - - @ViewBuilder - private func indicatorRectangle() -> some View { - RoundedRectangle(cornerRadius: self.viewModel.cornerRadius ?? 0) - .fill(self.viewModel.colors?.indicatorBackgroundColorToken) - .accessibilityIdentifier(AccessibilityIdentifier.indicatorView) - } -} diff --git a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarViewSnapshotTests.swift b/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarViewSnapshotTests.swift deleted file mode 100644 index aeda6d97a..000000000 --- a/core/Sources/Components/ProgressBar/View/SwiftUI/Public/ProgressBarViewSnapshotTests.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ProgressBarViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore -import SwiftUI - -final class ProgressBarViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = ProgressBarScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [ProgressBarConfigurationSnapshotTests] = try scenario.configuration() - for configuration in configurations { - let view = ProgressBarView( - theme: self.theme, - intent: configuration.intent, - shape: configuration.shape, - value: configuration.value - ) - .frame(width: configuration.width) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIView.swift b/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIView.swift deleted file mode 100644 index 96cc0d034..000000000 --- a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIView.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// ProgressBarDoubleUIView.swift -// SparkCore -// -// Created by robin.lemaire on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine -import SwiftUI - -/// The UIKit version for the progress bar double. -public final class ProgressBarDoubleUIView: ProgressBarMainUIView { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = ProgressBarAccessibilityIdentifier - - // MARK: - Components - - private let bottomIndicatorView = UIView() - - // MARK: - Public Properties - - /// The spark theme of the progress bar double. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.set(theme: newValue) - } - } - - /// The intent of the progress bar double. - public var intent: ProgressBarDoubleIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.set(intent: newValue) - } - } - - /// The shape of the progress bar double. - public var shape: ProgressBarShape { - get { - return self.viewModel.shape - } - set { - self.viewModel.set(shape: newValue) - } - } - - /// The top indicator value of the progress bar double. - /// note: Value **MUST** be into 0 (for 0 %) and 1 (for 100%) - public var topValue: CGFloat = 0 { - didSet { - self.updateWidthConstraints( - &self.indicatorWidthConstraint, - multiplier: self.topValue, - view: self.indicatorView - ) - } - } - - /// The bottom indicator value of the progress bar double. - /// note: Value **MUST** be into 0 (for 0 %) and 1 (for 100%) - public var bottomValue: CGFloat = 0 { - didSet { - self.updateWidthConstraints( - &self.bottomIndicatorWidthConstraint, - multiplier: self.bottomValue, - view: self.bottomIndicatorView - ) - } - } - - // MARK: - Private Properties - - private let viewModel: ProgressBarDoubleViewModel - - private var bottomIndicatorWidthConstraint: NSLayoutConstraint? - - private var subscriptions = Set() - - // MARK: - Initialization - - /// Initialize a new progress bar double view - /// - Parameters: - /// - theme: The spark theme of the progress bar double. - /// - intent: The intent of the progress bar double. - /// - shape: The shape of the progress bar double. - public init( - theme: Theme, - intent: ProgressBarDoubleIntent, - shape: ProgressBarShape - ) { - self.viewModel = .init( - for: .uiKit, - theme: theme, - intent: intent, - shape: shape - ) - - super.init() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - override func setupView() { - super.setupView() - - // Add subview - self.insertSubview( - self.bottomIndicatorView, - belowSubview: self.indicatorView - ) - - // Identifiers - self.bottomIndicatorView.accessibilityIdentifier = AccessibilityIdentifier.bottomIndicatorView - - // Setup constraints - self.setupBottomIndicatorConstraints() - - // Setup subscriptions - self.setupSubscriptions() - - // Load view model - self.viewModel.load() - } - - // MARK: - Subscribe - - private func setupSubscriptions() { - // Colors - self.viewModel.$colors.subscribe(in: &self.subscriptions) { [weak self] colors in - guard let self, let colors else { return } - - self.updateColors(colors) - self.bottomIndicatorView.backgroundColor = colors.bottomIndicatorBackgroundColorToken.uiColor - } - - // Corner Radius - self.viewModel.$cornerRadius.subscribe(in: &self.subscriptions) { [weak self] cornerRadius in - guard let cornerRadius else { return } - self?.cornerRadius = cornerRadius - } - } - - // MARK: - Constraints - - internal func setupBottomIndicatorConstraints() { - self.setupSpecificIndicatorConstraints( - &self.bottomIndicatorWidthConstraint, - on: self.bottomIndicatorView - ) - } - - // MARK: - Update UI - - override func updateCornerRadius() { - super.updateCornerRadius() - - self.bottomIndicatorView.setCornerRadius(self.cornerRadius) - } - - // MARK: - Update Constraints - - override func updateWidthConstraints( - _ constraint: inout NSLayoutConstraint?, - multiplier: CGFloat, - view: UIView - ) { - // Update constraints only if value is valid - guard self.viewModel.isValidIndicatorValue(multiplier) else { - return - } - - super.updateWidthConstraints(&constraint, multiplier: multiplier, view: view) - } -} diff --git a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIViewSnapshotTests.swift b/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIViewSnapshotTests.swift deleted file mode 100644 index 01bb12a16..000000000 --- a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarDoubleUIViewSnapshotTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ProgressBarDoubleUIViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore - -final class ProgressBarDoubleUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = ProgressBarScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [ProgressBarConfigurationSnapshotTests] = try scenario.configuration() - for configuration in configurations { - let view: ProgressBarDoubleUIView = .init( - theme: self.theme, - intent: configuration.intent, - shape: configuration.shape - ) - view.topValue = configuration.value - view.bottomValue = configuration.bottomValue - - view.translatesAutoresizingMaskIntoConstraints = false - view.widthAnchor.constraint(equalToConstant: configuration.width).isActive = true - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarIndeterminateUIView.swift b/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarIndeterminateUIView.swift deleted file mode 100644 index 4dddf19a8..000000000 --- a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarIndeterminateUIView.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// ProgressBarIndeterminateUIView.swift -// SparkCore -// -// Created by robin.lemaire on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine - -/// The UIKit version for the progress bar indeterminate. -public final class ProgressBarIndeterminateUIView: ProgressBarMainUIView { - - // MARK: - Type alias - - private typealias Constants = ProgressBarConstants - - // MARK: - Public Properties - - /// The spark theme of the progress bar indeterminate. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.set(theme: newValue) - } - } - - /// The shape of the progress bar indeterminate. - public var shape: ProgressBarShape { - get { - return self.viewModel.shape - } - set { - self.viewModel.set(shape: newValue) - } - } - - /// The intent of the progress bar indeterminate. - public var intent: ProgressBarIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.set(intent: newValue) - } - } - - // MARK: - Private Properties - - private let viewModel: ProgressBarIndeterminateViewModel - - var indicatorLeadingConstraint: NSLayoutConstraint? - - var firstAnimator: UIViewPropertyAnimator? - var lastAnimator: UIViewPropertyAnimator? - - private var subscriptions = Set() - - // MARK: - Initialization - - /// Initialize a new progress bar indeterminate view. - /// By default, the animation is not started. - /// - Parameters: - /// - theme: The spark theme of the progress bar indeterminate. - /// - intent: The intent of the progress bar indeterminate. - /// - shape: The shape of the progress bar indeterminate. - public init( - theme: Theme, - intent: ProgressBarIntent, - shape: ProgressBarShape - ) { - self.viewModel = .init( - for: .uiKit, - theme: theme, - intent: intent, - shape: shape, - isAnimating: false - ) - - super.init() - - // Setup - self.setupSubscriptions() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - override func setupView() { - super.setupView() - - // Setup subscriptions - self.setupSubscriptions() - - // Load view model - self.viewModel.load() - } - - // MARK: - Constraints - - override func setupIndicatorConstraints() { - self.indicatorView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - self.indicatorView.topAnchor.constraint(equalTo: self.topAnchor), - self.indicatorView.bottomAnchor.constraint(equalTo: self.bottomAnchor) - ]) - - self.indicatorLeadingConstraint = self.indicatorView.leadingAnchor.constraint( - equalTo: self.trackView.leadingAnchor - ) - self.indicatorLeadingConstraint?.isActive = true - - self.indicatorWidthConstraint = self.indicatorView.widthAnchor.constraint(equalToConstant: 0) - self.indicatorWidthConstraint?.isActive = true - } - - // MARK: - Animation - - /// Start the infinite animation - public func startAnimating() { - if !self.viewModel.isAnimating { - self.resetIndicatorConstraints() - self.reloadAnimation(isFirstAnimation: true) - - self.viewModel.isAnimating = true - } - } - - /// Stop the infinite animation - public func stopAnimating() { - if self.viewModel.isAnimating { - // Stop animation - self.firstAnimator?.stopAnimation(true) - self.lastAnimator?.stopAnimation(true) - - self.resetIndicatorConstraints() - - self.viewModel.isAnimating = false - } - } - - private func reloadAnimation(isFirstAnimation: Bool) { - let animationDuration = Constants.Animation.duration - // Frames - // ** - // First Animator - let easeInAnimation = self.viewModel.easeInAnimatedData( - trackWidth: self.trackView.frame.width - ) - self.firstAnimator = UIViewPropertyAnimator( - duration: animationDuration, - curve: .easeIn - ) - self.firstAnimator?.addAnimations { [weak self] in - guard let self else { return } - - self.indicatorLeadingConstraint?.constant = easeInAnimation.leadingSpaceWidth - self.indicatorWidthConstraint?.constant = easeInAnimation.indicatorWidth - - self.layoutIfNeeded() - } - // ** - - // ** - // Last Animator - let easeOutAnimation = self.viewModel.easeOutAnimatedData( - trackWidth: self.trackView.frame.width - ) - self.lastAnimator = UIViewPropertyAnimator( - duration: animationDuration, - curve: .easeOut - ) - self.lastAnimator?.addAnimations { [weak self] in - guard let self else { return } - - self.indicatorLeadingConstraint?.constant = easeOutAnimation.leadingSpaceWidth - self.indicatorWidthConstraint?.constant = easeOutAnimation.indicatorWidth - - self.layoutIfNeeded() - } - self.lastAnimator?.addCompletion { [weak self] position in - guard let self else { return } - - if position == .end { - self.resetIndicatorConstraints() - - self.reloadAnimation( - isFirstAnimation: false - ) - } - } - // ** - - // Start animations - self.firstAnimator?.startAnimation( - afterDelay: isFirstAnimation ? 0 : animationDuration - ) - self.lastAnimator?.startAnimation( - afterDelay: isFirstAnimation ? animationDuration : animationDuration * 2 - ) - } - - // MARK: - Subscribe - - private func setupSubscriptions() { - // Colors - self.viewModel.$colors.subscribe(in: &self.subscriptions) { [weak self] colors in - guard let self, let colors else { return } - - self.updateColors(colors) - } - - // Corner Radius - self.viewModel.$cornerRadius.subscribe(in: &self.subscriptions) { [weak self] cornerRadius in - guard let cornerRadius else { return } - self?.cornerRadius = cornerRadius - } - } - - // MARK: - Update Constraints - - private func resetIndicatorConstraints() { - let animation = self.viewModel.resetAnimatedData( - trackWidth: self.trackView.frame.width - ) - self.indicatorLeadingConstraint?.constant = animation.leadingSpaceWidth - self.indicatorWidthConstraint?.constant = animation.indicatorWidth - - self.layoutIfNeeded() - } -} diff --git a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIView.swift b/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIView.swift deleted file mode 100644 index 454f773b0..000000000 --- a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIView.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// ProgressBarUIView.swift -// SparkCore -// -// Created by robin.lemaire on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine -import SwiftUI - -/// The UIKit version for the progress bar. -public final class ProgressBarUIView: ProgressBarMainUIView { - - // MARK: - Public Properties - - /// The spark theme of the progress bar. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.set(theme: newValue) - } - } - - /// The intent of the progress bar. - public var intent: ProgressBarIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.set(intent: newValue) - } - } - - /// The shape of the progress bar. - public var shape: ProgressBarShape { - get { - return self.viewModel.shape - } - set { - self.viewModel.set(shape: newValue) - } - } - - /// The indicator value of the progress bar. - /// note: Value **MUST** be into 0 (for 0 %) and 1 (for 100%) - public var value: CGFloat = 0 { - didSet { - guard self.value != oldValue else { return } - self.didUpdateValue() - } - } - - // MARK: - Private Properties - - private let viewModel: ProgressBarViewModel - - private var subscriptions = Set() - - // MARK: - Initialization - - /// Initialize a new progress bar view - /// - Parameters: - /// - theme: The spark theme of the progress bar. - /// - intent: The intent of the progress bar. - /// - shape: The shape of the progress bar. - public init( - theme: Theme, - intent: ProgressBarIntent, - shape: ProgressBarShape - ) { - self.viewModel = .init( - for: .uiKit, - theme: theme, - intent: intent, - shape: shape - ) - - super.init() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func didUpdateValue() { - self.accessibilityValue = "\(Int(self.value * 100.0))%" - self.updateWidthConstraints( - &self.indicatorWidthConstraint, - multiplier: self.value, - view: self.indicatorView - ) - } - - // MARK: - View setup - - override func setupView() { - super.setupView() - - // Setup subscriptions - self.setupSubscriptions() - - // Load view model - self.viewModel.load() - - self.didUpdateValue() - } - - // MARK: - Subscribe - - private func setupSubscriptions() { - // Colors - self.viewModel.$colors.subscribe(in: &self.subscriptions) { [weak self] colors in - guard let self, let colors else { return } - - self.updateColors(colors) - } - - // Corner Radius - self.viewModel.$cornerRadius.subscribe(in: &self.subscriptions) { [weak self] cornerRadius in - guard let cornerRadius else { return } - self?.cornerRadius = cornerRadius - } - } - - // MARK: - Update Constraints - - override func updateWidthConstraints( - _ constraint: inout NSLayoutConstraint?, - multiplier: CGFloat, - view: UIView - ) { - // Update constraints only if value is valid - guard self.viewModel.isValidIndicatorValue(multiplier) else { - return - } - - super.updateWidthConstraints(&constraint, multiplier: multiplier, view: view) - } -} diff --git a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIViewSnapshotTests.swift b/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIViewSnapshotTests.swift deleted file mode 100644 index c91c1b085..000000000 --- a/core/Sources/Components/ProgressBar/View/UIKit/ProgressBarUIViewSnapshotTests.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ProgressBarUIViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore - -final class ProgressBarUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() throws { - let scenarios = ProgressBarScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [ProgressBarConfigurationSnapshotTests] = try scenario.configuration() - for configuration in configurations { - let view: ProgressBarUIView = .init( - theme: self.theme, - intent: configuration.intent, - shape: configuration.shape - ) - view.value = configuration.value - - view.translatesAutoresizingMaskIntoConstraints = false - view.widthAnchor.constraint(equalToConstant: configuration.width).isActive = true - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/ProgressBar/View/UIKit/ProgressMainBarUIView.swift b/core/Sources/Components/ProgressBar/View/UIKit/ProgressMainBarUIView.swift deleted file mode 100644 index cf580488e..000000000 --- a/core/Sources/Components/ProgressBar/View/UIKit/ProgressMainBarUIView.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// ProgressBarMainUIView.swift -// SparkCore -// -// Created by robin.lemaire on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// This ProgressMainBar view contains all communs subviews (track & indicator), styles, constraints, ... for all progress bars. -/// This view doesn't have a public init. -public class ProgressBarMainUIView: UIView { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = ProgressBarAccessibilityIdentifier - private typealias Constants = ProgressBarConstants - - // MARK: - Components - - internal let trackView = UIView() - internal let indicatorView = UIView() - - // MARK: - Private Properties - - private var heightConstraint: NSLayoutConstraint? - internal var indicatorWidthConstraint: NSLayoutConstraint? - - @ScaledUIMetric private var height: CGFloat = Constants.height - var cornerRadius: CGFloat = 0 { - didSet { - self.updateCornerRadius() - } - } - - // MARK: - Initialization - - internal init() { - super.init(frame: .zero) - - // Setup - self.setupView() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - /// Setup the view: subviews, identifiers, constraints, UI. - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupView() { - // Add subviews - self.addSubview(self.trackView) - self.addSubview(self.indicatorView) - - // Accessibility - self.isAccessibilityElement = true - self.accessibilityIdentifier = AccessibilityIdentifier.progressBar - - // View properties - self.backgroundColor = .clear - - // Setup constraints - self.setupConstraints() - - // Updates - self.updateHeight() - self.updateCornerRadius() - } - - // MARK: - Layout - - public override func layoutSubviews() { - super.layoutSubviews() - - self.layoutIfNeeded() - self.updateCornerRadius() - } - - // MARK: - Constraints - - private func setupConstraints() { - // Global - self.setupViewConstraints() - - // Subviews - self.setupTrackConstraints() - self.setupIndicatorConstraints() - } - - private func setupViewConstraints() { - self.translatesAutoresizingMaskIntoConstraints = false - - self.heightConstraint = self.heightAnchor.constraint(equalToConstant: .zero) - self.heightConstraint?.isActive = true - } - - private func setupTrackConstraints() { - self.trackView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.stickEdges( - from: self.trackView, - to: self - ) - } - - /// Setup the indicator view constraints on the view (top, leading, trailing, trailing) and width (a percent of the view width). - /// This method is internal because it can be overriden by the view that inherits from this class. - internal func setupIndicatorConstraints() { - self.setupSpecificIndicatorConstraints( - &self.indicatorWidthConstraint, - on: self.indicatorView - ) - } - - /// Setup the some indicator view constraints on the view (top, leading, trailing, trailing) and width (a percent of the view width). - /// - Parameters: - /// - widthConstraint: width layoutConstraint to set from the width of the view - /// - view: view to constrain - /// This method is internal because it can be overriden by the view that inherits from this class - internal final func setupSpecificIndicatorConstraints( - _ widthConstraint: inout NSLayoutConstraint?, - on view: UIView - ) { - view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - view.topAnchor.constraint(equalTo: self.topAnchor), - view.leadingAnchor.constraint(equalTo: self.leadingAnchor), - view.bottomAnchor.constraint(equalTo: self.bottomAnchor), - view.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor) - ]) - - widthConstraint = view.widthAnchor.constraint( - equalTo: self.trackView.widthAnchor - ) - widthConstraint?.isActive = true - } - - /// Update the width constraints with a multiplier for a view. - /// - Parameters: - /// - widthConstraint: width layoutConstraint to set from the width of the view - /// - multiplier: multiplier of the constraint - /// - view: view to constrain - /// This method is internal because it can be overridenn by the view that inherits from this class - internal func updateWidthConstraints( - _ constraint: inout NSLayoutConstraint?, - multiplier: CGFloat, - view: UIView - ) { - NSLayoutConstraint.updateMultiplier( - on: &constraint, - multiplier: multiplier, - layout: view.widthAnchor, - equalTo: self.trackView.widthAnchor - ) - view.updateConstraintsIfNeeded() - } - - // MARK: - Update UI - - /// Update the corner radius of the track and the indicator view - /// This method is internal because it can be overridenn by the view that inherits from this class - internal func updateCornerRadius() { - self.trackView.setCornerRadius(self.cornerRadius) - self.indicatorView.setCornerRadius(self.cornerRadius) - } - - private func updateHeight() { - // Reload size only if value changed - if self.height > 0 && self.height != self.heightConstraint?.constant { - self.heightConstraint?.constant = self.height - self.updateConstraintsIfNeeded() - } - } - - /// Update the background color of the track and the indicator view - /// This method is internal because it can be overriden by the view that inherits from this class - internal func updateColors(_ colors: any ProgressBarMainColors) { - self.trackView.backgroundColor = colors.trackBackgroundColorToken.uiColor - self.indicatorView.backgroundColor = colors.indicatorBackgroundColorToken.uiColor - } - - // MARK: - Trait Collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self._height.update(traitCollection: self.traitCollection) - self.updateHeight() - } - - // MARK: - Intrinsic Content Size - - public override var intrinsicContentSize: CGSize { - return CGSize( - width: self.frame.width, - height: self.height - ) - } -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Double/ProgressBarDoubleViewModel.swift b/core/Sources/Components/ProgressBar/ViewModel/Double/ProgressBarDoubleViewModel.swift deleted file mode 100644 index 98b8d51cc..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Double/ProgressBarDoubleViewModel.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ProgressBarDoubleViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -final class ProgressBarDoubleViewModel: ProgressBarMainViewModel { - - // MARK: - Initialization - - convenience init( - for frameworkType: FrameworkType, - theme: Theme, - intent: ProgressBarDoubleIntent, - shape: ProgressBarShape - ) { - self.init( - for: frameworkType, - theme: theme, - intent: intent, - shape: shape, - getColorsUseCase: ProgressBarDoubleGetColorsUseCase() - ) - } -} - -// MARK: - Extension - -extension ProgressBarDoubleViewModel: ProgressBarValueViewModel { -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModel.swift b/core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModel.swift deleted file mode 100644 index 14e9346a3..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModel.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// ProgressBarIndeterminateViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -// sourcery: AutoPublisherTest, AutoViewModelStub -/// Indeterminate ViewModel use the IndeterminateStyle ViewModel because the style are the same -final class ProgressBarIndeterminateViewModel: ProgressBarMainViewModel { - - // MARK: - Properties - - @Published var isAnimating: Bool { - didSet { - self.isAnimatedDidUpdate() - } - } - @Published private(set) var animatedData: ProgressBarAnimatedData? - @Published private(set) var animationType: ProgressBarIndeterminateAnimationType? - @Published private(set) var animationStatus: ProgressBarIndeterminateStatus? - @Published private(set) var indicatorOpacity: Double? - - private let getAnimatedDataUseCase: ProgressBarGetAnimatedDataUseCaseable - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - theme: Theme, - intent: ProgressBarIntent, - shape: ProgressBarShape, - isAnimating: Bool, - getColorsUseCase: ProgressBarGetColorsUseCase = ProgressBarGetColorsUseCase(), - getAnimatedDataUseCase: ProgressBarGetAnimatedDataUseCaseable = ProgressBarGetAnimatedDataUseCase() - ) { - self.isAnimating = isAnimating - - self.getAnimatedDataUseCase = getAnimatedDataUseCase - - super.init( - for: frameworkType, - theme: theme, - intent: intent, - shape: shape, - getColorsUseCase: getColorsUseCase - ) - } - - // MARK: - Update - - override func updateAll() { - super.updateAll() - - self.isAnimatedDidUpdate() - } - - private func isAnimatedDidUpdate() { - self.animationStatus = self.isAnimating ? .start : .stop - self.animationType = self.isAnimating ? .easeIn : .none - self.indicatorOpacity = (self.animationType == .none) ? 0 : 1 - } - - // MARK: - Animation - - func updateAnimatedData(from trackWidth: CGFloat) { - self.animatedData = self.getAnimatedDataUseCase.execute( - type: self.animationType, - trackWidth: trackWidth - ) - } - - func animationStepIsDone() { - if self.isAnimating { - self.animationType?.next() - } else { - self.animationStatus = .stop - } - } - - func easeInAnimatedData(trackWidth: CGFloat) -> ProgressBarAnimatedData { - return self.getAnimatedDataUseCase.execute( - type: .easeIn, - trackWidth: trackWidth - ) - } - - func easeOutAnimatedData(trackWidth: CGFloat) -> ProgressBarAnimatedData { - return self.getAnimatedDataUseCase.execute( - type: .easeOut, - trackWidth: trackWidth - ) - } - - func resetAnimatedData(trackWidth: CGFloat) -> ProgressBarAnimatedData { - return self.getAnimatedDataUseCase.execute( - type: .reset, - trackWidth: trackWidth - ) - } -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModelTests.swift b/core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModelTests.swift deleted file mode 100644 index a00586fae..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Indeterminate/ProgressBarIndeterminateViewModelTests.swift +++ /dev/null @@ -1,341 +0,0 @@ -// -// ProgressBarIndeterminateViewModelTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 21/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import XCTest -@testable import SparkCore -import Combine - -final class ProgressBarIndeterminateViewModelTests: XCTestCase { - - // MARK: - Properties - - private var subscriptions = Set() - - // MARK: - Setup - - override func tearDown() { - super.tearDown() - - // Clear publishers - self.subscriptions.removeAll() - } - - // MARK: - Init Tests - - func test_properties_on_init_when_frameworkType_is_UIKit_and_isAnimating_is_false() { - self.testPropertiesOnInit( - givenFrameworkType: .uiKit - ) - } - - func test_properties_on_init_when_frameworkType_is_SwiftUI_and_isAnimating_is_false() { - self.testPropertiesOnInit( - givenFrameworkType: .swiftUI - ) - } - - private func testPropertiesOnInit( - givenFrameworkType: FrameworkType - ) { - // GIVEN / WHEN - let givenIsAnimating = false - - let stub = Stub( - frameworkType: givenFrameworkType, - isAnimating: givenIsAnimating - ) - let isSwiftUI = givenFrameworkType == .swiftUI - - stub.subscribePublishers(on: &self.subscriptions) - - // THEN - - // ** - // Published properties - - // Is Animating - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - isAnimating: stub.isAnimatingPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: givenIsAnimating - ) - - // Animation data - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animatedData: stub.animatedDataPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: nil - ) - - // Animation type - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animationType: stub.animationTypePublisherMock, - expectedNumberOfSinks: 1, - expectedValue: nil - ) - - // Animation status - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animationStatus: stub.animationStatusPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: isSwiftUI ? .stop : nil - ) - - // Indicator opacity - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - indicatorOpacity: stub.indicatorOpacityPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: isSwiftUI ? 0 : nil - ) - // ** - - // Use Cases - ProgressBarGetAnimatedDataUseCaseableMockTest.XCTAssert( - stub.getAnimatedDataUseCaseMock, - expectedNumberOfCalls: 0, - expectedReturnValue: stub.animatedData - ) - } - - func test_set_isAnimating_to_true() { - // GIVEN - let givenIsAnimating = true - - let stub = Stub( - isAnimating: false - ) - - stub.subscribePublishers(on: &self.subscriptions) - stub.resetMockedData() - - // WHEN - stub.viewModel.isAnimating = givenIsAnimating - - // THEN - - // ** - // Published properties - - // Is Animating - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - isAnimating: stub.isAnimatingPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: givenIsAnimating - ) - - // Animation data - ProgressBarIndeterminateViewModelPublisherTest.XCTSinksCount( - animatedData: stub.animatedDataPublisherMock, - expectedNumberOfSinks: 0 - ) - - // Animation type - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animationType: stub.animationTypePublisherMock, - expectedNumberOfSinks: 1, - expectedValue: .easeIn - ) - - // Animation status - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animationStatus: stub.animationStatusPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: .start - ) - - // Indicator opacity - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - indicatorOpacity: stub.indicatorOpacityPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: 1 - ) - // ** - - // Use Cases - ProgressBarGetAnimatedDataUseCaseableMockTest.XCTAssert( - stub.getAnimatedDataUseCaseMock, - expectedNumberOfCalls: 0, - expectedReturnValue: stub.animatedData - ) - } - - // MARK: - Animation Tests - - func test_updateAnimatedData() { - // GIVEN - let givenTrackWidth: CGFloat = 200 - - let stub = Stub() - - stub.subscribePublishers(on: &self.subscriptions) - stub.resetMockedData() - - // WHEN - stub.viewModel.updateAnimatedData( - from: givenTrackWidth - ) - - // THEN - // Published properties - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animatedData: stub.animatedDataPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: stub.animatedData - ) - - // Use Case - ProgressBarGetAnimatedDataUseCaseableMockTest.XCTAssert( - stub.getAnimatedDataUseCaseMock, - expectedNumberOfCalls: 1, - givenType: stub.viewModel.animationType, - givenTrackWidth: givenTrackWidth, - expectedReturnValue: stub.animatedData - ) - } - - func test_animationStepIsDone_when_isAnimating_is_true() { - self.testAnimationStepIsDone( - givenIsAnimating: true - ) - } - - func test_animationStepIsDone_when_isAnimating_is_false() { - self.testAnimationStepIsDone( - givenIsAnimating: false - ) - } - - func testAnimationStepIsDone( - givenIsAnimating: Bool - ) { - // GIVEN - let stub = Stub( - isAnimating: givenIsAnimating - ) - - var animationType = stub.viewModel.animationType - animationType?.next() - - stub.subscribePublishers(on: &self.subscriptions) - stub.resetMockedData() - - // WHEN - stub.viewModel.animationStepIsDone() - - // THEN - // ** - // Published properties - - // Animation type - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animationType: stub.animationTypePublisherMock, - expectedNumberOfSinks: givenIsAnimating ? 1 : 0, - expectedValue: animationType - ) - - // Animation status - ProgressBarIndeterminateViewModelPublisherTest.XCTAssert( - animationStatus: stub.animationStatusPublisherMock, - expectedNumberOfSinks: givenIsAnimating ? 0 : 1, - expectedValue: .stop - ) - // ** - } - - func test_easeInAnimatedData() { - self.testAnimatedData( - givenType: .easeIn - ) - } - - func test_easeOutAnimatedData() { - self.testAnimatedData( - givenType: .easeOut - ) - } - - func test_resetAnimatedData() { - self.testAnimatedData( - givenType: .reset - ) - } - - private func testAnimatedData( - givenType: ProgressBarIndeterminateAnimationType - ) { - // GIVEN - let givenTrackWidth: CGFloat = 200 - - let stub = Stub() - - // WHEN - let animatedData: ProgressBarAnimatedData - switch givenType { - case .easeIn: - animatedData = stub.viewModel.easeInAnimatedData( - trackWidth: givenTrackWidth - ) - case .easeOut: - animatedData = stub.viewModel.easeOutAnimatedData( - trackWidth: givenTrackWidth - ) - case .reset: - animatedData = stub.viewModel.resetAnimatedData( - trackWidth: givenTrackWidth - ) - } - - // THEN - ProgressBarGetAnimatedDataUseCaseableMockTest.XCTAssert( - stub.getAnimatedDataUseCaseMock, - expectedNumberOfCalls: 1, - givenType: givenType, - givenTrackWidth: givenTrackWidth, - expectedReturnValue: animatedData - ) - } -} - -private final class Stub: ProgressBarIndeterminateViewModelStub { - - // MARK: - Properties - - let animatedData = ProgressBarAnimatedData.mocked() - - // MARK: - Initialization - - init( - frameworkType: FrameworkType = .uiKit, - isAnimating: Bool = false - ) { - // ** - // Use Cases - let getAnimatedDataUseCaseMock = ProgressBarGetAnimatedDataUseCaseableGeneratedMock() - getAnimatedDataUseCaseMock.executeWithTypeAndTrackWidthReturnValue = self.animatedData - // ** - - // ** - // View Model - let viewModel = ProgressBarIndeterminateViewModel( - for: frameworkType, - theme: ThemeGeneratedMock.mocked(), - intent: .main, - shape: .rounded, - isAnimating: isAnimating, - getColorsUseCase: .init(), - getAnimatedDataUseCase: getAnimatedDataUseCaseMock - ) - // ** - - super.init( - viewModel: viewModel, - getAnimatedDataUseCaseMock: getAnimatedDataUseCaseMock - ) - } -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModel.swift b/core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModel.swift deleted file mode 100644 index 91c0c0b97..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModel.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// ProgressBarMainViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -// sourcery: AutoPublisherTest, AutoViewModelStub -// sourcery: = "ProgressBarMainGetColorsUseCaseableGeneratedMock" -class ProgressBarMainViewModel< - GetColorsUseCase: ProgressBarMainGetColorsUseCaseable ->: ObservableObject { - - // MARK: - Properties - - private let frameworkType: FrameworkType - private(set) var theme: Theme - private(set) var intent: GetColorsUseCase.Intent - private(set) var shape: ProgressBarShape - - // MARK: - Published Properties - - @Published private (set) var colors: GetColorsUseCase.Return? - @Published private (set) var cornerRadius: CGFloat? - - // MARK: - Private Properties - - private let getColorsUseCase: GetColorsUseCase - private let getCornerRadiusUseCase: ProgressBarGetCornerRadiusUseCaseable - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - theme: Theme, - intent: GetColorsUseCase.Intent, - shape: ProgressBarShape, - getColorsUseCase: GetColorsUseCase, - getCornerRadiusUseCase: ProgressBarGetCornerRadiusUseCaseable = ProgressBarGetCornerRadiusUseCase() - ) { - self.frameworkType = frameworkType - - self.theme = theme - self.intent = intent - self.shape = shape - - self.getColorsUseCase = getColorsUseCase - self.getCornerRadiusUseCase = getCornerRadiusUseCase - - // Load the values directly on init just for SwiftUI - if frameworkType == .swiftUI { - self.updateAll() - } - } - - // MARK: - Load - - func load() { - // Update all values when UIKit view is ready to receive published values - if self.frameworkType == .uiKit { - self.updateAll() - } - } - - // MARK: - Setter - - func set(theme: Theme) { - self.theme = theme - - self.updateAll() - } - - func set(intent: GetColorsUseCase.Intent) { - if self.intent != intent { - self.intent = intent - - self.colorsDidUpdate() - } - } - - func set(shape: ProgressBarShape) { - if self.shape != shape { - self.shape = shape - - self.shapeDidUpdate() - } - } - - // MARK: - Private Update - - internal func updateAll() { - self.colorsDidUpdate() - self.shapeDidUpdate() - } - - private func colorsDidUpdate() { - self.colors = self.getColorsUseCase.execute( - intent: self.intent, - colors: self.theme.colors, - dims: self.theme.dims - ) - } - - private func shapeDidUpdate() { - self.cornerRadius = self.getCornerRadiusUseCase.execute( - shape: self.shape, - border: self.theme.border - ) - } -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModelTests.swift b/core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModelTests.swift deleted file mode 100644 index 1d8d20ff3..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Main/ProgressBarMainViewModelTests.swift +++ /dev/null @@ -1,428 +0,0 @@ -// -// ProgressBarMainViewModelTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore -import Combine - -final class ProgressBarMainViewModelTests: XCTestCase { - - // MARK: - Properties - - private var subscriptions = Set() - - // MARK: - Setup - - override func tearDown() { - super.tearDown() - - // Clear publishers - self.subscriptions.removeAll() - } - - // MARK: - Init Tests - - func test_properties_on_init_when_frameworkType_is_UIKit() throws { - try self.testPropertiesOnInit( - givenFrameworkType: .uiKit - ) - } - - func test_properties_on_init_when_frameworkType_is_SwiftUI() throws { - try self.testPropertiesOnInit( - givenFrameworkType: .swiftUI - ) - } - - private func testPropertiesOnInit( - givenFrameworkType: FrameworkType - ) throws { - // GIVEN - let intentMock: Stub.Intent = .init() - let shapeMock: ProgressBarShape = .square - - let isUIKit = givenFrameworkType == .uiKit - - // WHEN - let stub = Stub( - frameworkType: givenFrameworkType, - intent: intentMock, - shape: shapeMock - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // THEN - XCTAssertIdentical( - viewModel.theme as? ThemeGeneratedMock, - stub.themeMock, - "Wrong theme value" - ) - XCTAssertEqual( - viewModel.intent, - intentMock, - "Wrong intent value" - ) - XCTAssertEqual( - viewModel.shape, - shapeMock, - "Wrong shape value" - ) - - // ** - // Published properties - ProgressBarMainViewModelPublisherTest.XCTAssert( - colors: stub.colorsPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: !isUIKit ? stub.colorsMock : nil - ) - ProgressBarMainViewModelPublisherTest.XCTAssert( - cornerRadius: stub.cornerRadiusPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: !isUIKit ? stub.cornerRadiusMock : nil - ) - // ** - - // Use Cases - ProgressBarMainGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: (isUIKit ? 0 : 1), - givenIntent: intentMock, - givenColors: stub.themeMock.colors as? ColorsGeneratedMock, - givenDims: stub.themeMock.dims as? DimsGeneratedMock, - expectedReturnValue: stub.colorsMock - ) - ProgressBarGetCornerRadiusUseCaseableMockTest.XCTAssert( - stub.getCornerRadiusUseCaseMock, - expectedNumberOfCalls: (isUIKit ? 0 : 1), - givenShape: shapeMock, - givenBorder: stub.themeMock.border as? BorderGeneratedMock, - expectedReturnValue: stub.cornerRadiusMock - ) - } - - // MARK: - Load Tests - - func test_published_properties_on_load_when_frameworkType_is_UIKit() throws { - try self.testPublishedPropertiesOnLoad( - givenFrameworkType: .uiKit - ) - } - - func test_published_properties_on_load_when_frameworkType_is_SwiftUI() throws { - try self.testPublishedPropertiesOnLoad( - givenFrameworkType: .swiftUI - ) - } - - func testPublishedPropertiesOnLoad( - givenFrameworkType: FrameworkType - ) throws { - // GIVEN - let intentMock: Stub.Intent = .init() - let shapeMock: ProgressBarShape = .square - - let isUIKit = givenFrameworkType == .uiKit - - // WHEN - let stub = Stub( - frameworkType: givenFrameworkType, - intent: intentMock, - shape: shapeMock - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - viewModel.load() - - // THEN - // ** - // Published properties - ProgressBarMainViewModelPublisherTest.XCTAssert( - colors: stub.colorsPublisherMock, - expectedNumberOfSinks: isUIKit ? 1 : 0, - expectedValue: isUIKit ? stub.colorsMock : nil - ) - ProgressBarMainViewModelPublisherTest.XCTAssert( - cornerRadius: stub.cornerRadiusPublisherMock, - expectedNumberOfSinks: isUIKit ? 1 : 0, - expectedValue: isUIKit ? stub.cornerRadiusMock : nil - ) - // ** - - // Use Cases - ProgressBarMainGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: (isUIKit ? 1 : 0), - givenIntent: intentMock, - givenColors: stub.themeMock.colors as? ColorsGeneratedMock, - givenDims: stub.themeMock.dims as? DimsGeneratedMock, - expectedReturnValue: stub.colorsMock - ) - ProgressBarGetCornerRadiusUseCaseableMockTest.XCTAssert( - stub.getCornerRadiusUseCaseMock, - expectedNumberOfCalls: (isUIKit ? 1 : 0), - givenShape: shapeMock, - givenBorder: stub.themeMock.border as? BorderGeneratedMock, - expectedReturnValue: stub.cornerRadiusMock - ) - } - - // MARK: - Setter Tests - - func test_set_theme() { - // GIVEN - let newTheme = ThemeGeneratedMock.mocked() - - let intentMock: Stub.Intent = .init() - let shapeMock: ProgressBarShape = .square - - let stub = Stub( - intent: intentMock, - shape: shapeMock - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(theme: newTheme) - - // THEN - XCTAssertIdentical(viewModel.theme as? ThemeGeneratedMock, - newTheme, - "Wrong theme value") - - // ** - // Published properties - ProgressBarMainViewModelPublisherTest.XCTAssert( - colors: stub.colorsPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: stub.colorsMock - ) - ProgressBarMainViewModelPublisherTest.XCTAssert( - cornerRadius: stub.cornerRadiusPublisherMock, - expectedNumberOfSinks: 1, - expectedValue: stub.cornerRadiusMock - ) - // ** - - // Use Cases - ProgressBarMainGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: 1, - givenIntent: intentMock, - givenColors: newTheme.colors as? ColorsGeneratedMock, - givenDims: newTheme.dims as? DimsGeneratedMock, - expectedReturnValue: stub.colorsMock - ) - ProgressBarGetCornerRadiusUseCaseableMockTest.XCTAssert( - stub.getCornerRadiusUseCaseMock, - expectedNumberOfCalls: 1, - givenShape: shapeMock, - givenBorder: newTheme.border as? BorderGeneratedMock, - expectedReturnValue: stub.cornerRadiusMock - ) - } - - func test_set_intent_with_different_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: true - ) - } - - func test_set_intent_with_same_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: false - ) - } - - private func testSetIntent( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: Stub.Intent = .init() - let newValue: Stub.Intent = givenIsDifferentNewValue ? .init() : defaultValue - - let stub = Stub( - intent: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(intent: newValue) - - // THEN - XCTAssertEqual(viewModel.intent, - newValue, - "Wrong intent value") - - // ** - // Published properties - ProgressBarMainViewModelPublisherTest.XCTAssert( - colors: stub.colorsPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: givenIsDifferentNewValue ? stub.colorsMock : nil - ) - - ProgressBarMainViewModelPublisherTest.XCTSinksCount( - cornerRadius: stub.cornerRadiusPublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // Use Cases - ProgressBarMainGetColorsUseCaseableMockTest.XCTAssert( - stub.getColorsUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenIntent: newValue, - givenColors: stub.themeMock.colors as? ColorsGeneratedMock, - givenDims: stub.themeMock.dims as? DimsGeneratedMock, - expectedReturnValue: stub.colorsMock - ) - } - - func test_set_shape_with_different_new_value() { - self.testSetShape( - givenIsDifferentNewValue: true - ) - } - - func test_set_shape_with_same_new_value() { - self.testSetShape( - givenIsDifferentNewValue: false - ) - } - - private func testSetShape( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: ProgressBarShape = .square - let newValue: ProgressBarShape = givenIsDifferentNewValue ? .rounded : defaultValue - - let stub = Stub( - shape: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(shape: newValue) - - // THEN - XCTAssertEqual(viewModel.shape, - newValue, - "Wrong shape value") - - // ** - // Published properties - ProgressBarMainViewModelPublisherTest.XCTSinksCount( - colors: stub.colorsPublisherMock, - expectedNumberOfSinks: 0 - ) - - ProgressBarMainViewModelPublisherTest.XCTAssert( - cornerRadius: stub.cornerRadiusPublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0, - expectedValue: givenIsDifferentNewValue ? stub.cornerRadiusMock : nil - ) - // ** - - // Use Cases - ProgressBarGetCornerRadiusUseCaseableMockTest.XCTAssert( - stub.getCornerRadiusUseCaseMock, - expectedNumberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenShape: newValue, - givenBorder: stub.themeMock.border as? BorderGeneratedMock, - expectedReturnValue: stub.cornerRadiusMock - ) - } -} - -// MARK: - Stub - -private final class Stub: ProgressBarMainViewModelStub { - - // MARK: - Associated Type - - typealias Intent = Stub.GetColorsUseCase.Intent - - // MARK: - Data Properties - - let frameworkType: FrameworkType - - let themeMock = ThemeGeneratedMock.mocked() - - let colorsMock = Stub.GetColorsUseCase.ReturnMock() - let cornerRadiusMock = 2.0 - - // MARK: - Initialization - - init( - frameworkType: FrameworkType = .uiKit, - intent: Intent = .init(), - shape: ProgressBarShape = .square, - isEnabled: Bool = true, - isImages: Bool = false, - text: String? = nil, - attributedText: NSAttributedString? = nil, - userInteractionEnabled: Bool = true - ) { - // Data properties - self.frameworkType = frameworkType - - // ** - // Use Cases - let getColorsUseCaseMock = Stub.GetColorsUseCase() - getColorsUseCaseMock.executeWithIntentAndColorsAndDimsReturnValue = self.colorsMock - - let getCornerRadiusUseCaseMock = ProgressBarGetCornerRadiusUseCaseableGeneratedMock() - getCornerRadiusUseCaseMock.executeWithShapeAndBorderReturnValue = self.cornerRadiusMock - // ** - - // View Model - let viewModel = ProgressBarMainViewModel( - for: frameworkType, - theme: self.themeMock, - intent: intent, - shape: shape, - getColorsUseCase: getColorsUseCaseMock, - getCornerRadiusUseCase: getCornerRadiusUseCaseMock - ) - - super.init( - viewModel: viewModel, - getColorsUseCaseMock: getColorsUseCaseMock, - getCornerRadiusUseCaseMock: getCornerRadiusUseCaseMock - ) - } -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Single/ProgressBarViewModel.swift b/core/Sources/Components/ProgressBar/ViewModel/Single/ProgressBarViewModel.swift deleted file mode 100644 index 006716ae0..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Single/ProgressBarViewModel.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ProgressBarViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -final class ProgressBarViewModel: ProgressBarMainViewModel { - - // MARK: - Initialization - - convenience init( - for frameworkType: FrameworkType, - theme: Theme, - intent: ProgressBarIntent, - shape: ProgressBarShape - ) { - self.init( - for: frameworkType, - theme: theme, - intent: intent, - shape: shape, - getColorsUseCase: ProgressBarGetColorsUseCase() - ) - } -} - -// MARK: - Extension - -extension ProgressBarViewModel: ProgressBarValueViewModel { -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModel.swift b/core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModel.swift deleted file mode 100644 index a007bd34f..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModel.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ProgressBarValueViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol ProgressBarValueViewModel { - /// Function used to check if the value is valid or not - /// - Parameter value: should be between 0...1 - /// - Returns: true if the value is valid otherwise false - func isValidIndicatorValue(_ value: CGFloat) -> Bool -} - -extension ProgressBarValueViewModel { - - func isValidIndicatorValue( - _ value: CGFloat - ) -> Bool { - return (0...1).contains(value) - } -} diff --git a/core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModelTests.swift b/core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModelTests.swift deleted file mode 100644 index 655c3fd1d..000000000 --- a/core/Sources/Components/ProgressBar/ViewModel/Value/ProgressBarValueViewModelTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// ProgressBarValueViewModelTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 20/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import XCTest -@testable import SparkCore - -final class ProgressBarValueViewModelTests: XCTestCase { - - // MARK: - Tests - - func test_isValidIndicatorValue_when_value_parameter_is_valid() throws { - // GIVEN - let viewModel = ProgressBarValueViewModelMock() - - let values = [ - 0, - 0.3, - 0.5, - 0.8, - 1.0 - ] - - // WHEN - for value in values { - let isValid = viewModel.isValidIndicatorValue(value) - - // THEN - XCTAssertTrue( - isValid, - "Wrong isValid when value is \(value)" - ) - } - } - - func test_isValidIndicatorValue_when_value_parameter_is_invalid() throws { - // GIVEN - let viewModel = ProgressBarValueViewModelMock() - - let values = [ - -0.1 as CGFloat, - -2, - 1.1, - 2 - ] - - // WHEN - for value in values { - let value = viewModel.isValidIndicatorValue(value) - - // THEN - XCTAssertFalse( - value, - "Wrong isValid when value is \(value)" - ) - } - } -} - -// MARK: - Mock - -private struct ProgressBarValueViewModelMock: ProgressBarValueViewModel { -} diff --git a/core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifier.swift b/core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifier.swift deleted file mode 100644 index ae8551ddd..000000000 --- a/core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifier.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ProgressTrackerAccessibilityIdentifier.swift -// SparkCore -// -// Created by Michael Zimmermann on 09.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the Progress Tracker -public enum ProgressTrackerAccessibilityIdentifier { - - /// The general identifier for the control - public static let identifier = "spark-progress-tracker" - - public static let indicator = "\(Self.identifier)-indicator" - - public static let label = "\(Self.identifier)-label" - - public static func indicator(forIndex index: Int) -> String { - return "\(Self.indicator)-\(index)" - } - - public static func label(forIndex index: Int) -> String { - return "\(Self.label)-\(index)" - } -} diff --git a/core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifierTests.swift b/core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifierTests.swift deleted file mode 100644 index 99a2fd591..000000000 --- a/core/Sources/Components/ProgressTracker/AccessibilityIdentifier/ProgressTrackerAccessibilityIdentifierTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ProgressTrackerAccessibilityIdentifierTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 09.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SparkCore -import XCTest - -final class ProgressTrackerAccessibilityIdentifierTests: XCTestCase { - - func test_indicator_identifier() { - XCTAssertEqual(ProgressTrackerAccessibilityIdentifier.indicator(forIndex: 99), "spark-progress-tracker-indicator-99") - } - - func test_label_identifier() { - XCTAssertEqual(ProgressTrackerAccessibilityIdentifier.label(forIndex: 99), "spark-progress-tracker-label-99") - } -} diff --git a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerIntent.swift b/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerIntent.swift deleted file mode 100644 index d1c927674..000000000 --- a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerIntent.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ProgressTrackerIntent.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// All possible intents of the progress tracker -public enum ProgressTrackerIntent: CaseIterable { - case accent - case alert - case basic - case danger - case info - case main - case neutral - case success - case support -} diff --git a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerInteractionState.swift b/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerInteractionState.swift deleted file mode 100644 index 1b37cebac..000000000 --- a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerInteractionState.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ProgressTrackerInteractionState.swift -// SparkCore -// -// Created by Michael Zimmermann on 29.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The possible interaction states of the progress tracker -public enum ProgressTrackerInteractionState: CaseIterable { - case none - case discrete - case continuous - case independent -} diff --git a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerOrientation.swift b/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerOrientation.swift deleted file mode 100644 index 6d37b4ddb..000000000 --- a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerOrientation.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProgressTrackerOrientation.swift -// SparkCore -// -// Created by Michael Zimmermann on 22.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The orientation of the progress tracker -public enum ProgressTrackerOrientation: CaseIterable { - case horizontal - case vertical -} diff --git a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerSize.swift b/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerSize.swift deleted file mode 100644 index bf6f12b5d..000000000 --- a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerSize.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ProgressTrackerSize.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The size of the progress tracker -public enum ProgressTrackerSize: CGFloat, CaseIterable { - case small = 16 - case medium = 24 - case large = 32 -} diff --git a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerVariant.swift b/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerVariant.swift deleted file mode 100644 index 433296284..000000000 --- a/core/Sources/Components/ProgressTracker/Enum/ProgressTrackerVariant.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProgressTrackerVariant.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The progress tracker variant -public enum ProgressTrackerVariant: CaseIterable { - case outlined - case tinted -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerColors.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerColors.swift deleted file mode 100644 index 8338c4901..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerColors.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ProgressTrackerColors.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// A model cotaining the colors of the progress tracker indicator -struct ProgressTrackerColors: Equatable { - let background: any ColorToken - let outline: any ColorToken - let content: any ColorToken - - static func == (lhs: ProgressTrackerColors, rhs: ProgressTrackerColors) -> Bool { - return lhs.background.equals(rhs.background) && - lhs.outline.equals(rhs.outline) && - lhs.content.equals(rhs.content) - } -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerConstants.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerConstants.swift deleted file mode 100644 index 20d927149..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerConstants.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ProgressTrackerConstants.swift -// SparkCore -// -// Created by Michael Zimmermann on 26.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// Constant definitions of the model tracker -enum ProgressTrackerConstants { - static let borderWidth: CGFloat = 1.0 - static let trackSize: CGFloat = 1.0 - static let iconHeight: CGFloat = 16.0 -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerContent.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerContent.swift deleted file mode 100644 index f1d663df9..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerContent.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// ProgressTrackerContent.swift -// SparkCore -// -// Created by Michael Zimmermann on 22.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import UIKit -import SwiftUI - -/// A model representing the content of a single Progress Tracker Indicator -protocol ProgressTrackerContentIndicating { - associatedtype ImageType: Equatable - associatedtype TextType: Equatable - var indicatorImage: ImageType? { get set } - var label: String? { get set } - - init() -} - -/// The content model of the UIKit progress tracker indicator -struct ProgressTrackerUIIndicatorContent: ProgressTrackerContentIndicating, Equatable { - typealias TextType = NSAttributedString - - var indicatorImage: UIImage? - var label: String? -} - -/// The content model of tje SwiftUI progress tracker indicator -struct ProgressTrackerIndicatorContent: ProgressTrackerContentIndicating, Equatable { - typealias TextType = AttributedString - - var indicatorImage: Image? - var label: String? -} - -/// A model representing the content of a progress tracker. -struct ProgressTrackerContent: Equatable where ComponentContent: Equatable { - - var numberOfPages: Int { - didSet { - self.currentPageIndex = min(self.currentPageIndex, self.numberOfPages - 1) - } - } - var showDefaultPageNumber: Bool - var currentPageIndex: Int { - didSet { - self.currentPageIndex = min(max(self.currentPageIndex, 0), self.numberOfPages - 1) - } - } - var preferredIndicatorImage: ComponentContent.ImageType? - var preferredCurrentPageIndicatorImage: ComponentContent.ImageType? - var completedPageIndicatorImage: ComponentContent.ImageType? - private var content = [Int: ComponentContent]() - private var currentPageIndicator = [Int: ComponentContent.ImageType]() - var labels = [Int: ComponentContent.TextType]() - - /// Returns true, if the content contains a label - var hasLabel: Bool { - return self.numberOfLabels > 0 - } - - /// The number of labels - var numberOfLabels: Int { - return labels.values.reduce(0) { partialResult, value in - return partialResult + 1 - } - } - - // MARK: - Initialization - init( - numberOfPages: Int, - currentPageIndex: Int = 0, - showDefaultPageNumber: Bool = true, - preferredIndicatorImage: ComponentContent.ImageType? = nil, - preferredCurrentPageIndicatorImage: ComponentContent.ImageType? = nil, - completedPageIndicatorImage: ComponentContent.ImageType? = nil) - { - self.numberOfPages = numberOfPages - self.currentPageIndex = currentPageIndex - self.showDefaultPageNumber = showDefaultPageNumber - self.preferredIndicatorImage = preferredIndicatorImage - self.completedPageIndicatorImage = completedPageIndicatorImage - self.preferredCurrentPageIndicatorImage = preferredCurrentPageIndicatorImage - } - - /// A function which determines, that so much content has changed, that the view needs to be setup again. - /// The view needs to be setup from fresh, when the number of pages have changed, or the number of labels. - func needsUpdateOfLayout(otherComponent content: Self) -> Bool { - if content.numberOfPages != self.numberOfPages { - return true - } - if content.numberOfLabels != self.numberOfLabels { - return true - } - - return false - } - - /// Return the content for the indicator at that given index. - func pageContent(atIndex index: Int) -> ComponentContent { - var content: ComponentContent - - if let pageContent = self.content[index] { - content = pageContent - } else { - content = .init() - } - - if content.label == nil && self.showDefaultPageNumber { - content.label = "\(index + 1)" - } - - if index == self.currentPageIndex { - if let currentPageImage = self.currentPageIndicator[index] { - content.indicatorImage = currentPageImage - } else if let currentPageImage = self.preferredCurrentPageIndicatorImage { - content.indicatorImage = currentPageImage - } else if content.indicatorImage == nil { - content.indicatorImage = self.preferredIndicatorImage - } - } else if index < self.currentPageIndex, let completedImage = self.completedPageIndicatorImage { - content.indicatorImage = completedImage - } else if content.indicatorImage == nil { - content.indicatorImage = self.preferredIndicatorImage - } - - return content - } - - /// Set the indicator image at the specified index - mutating func setIndicatorImage(_ image: ComponentContent.ImageType?, atIndex index: Int) { - var content: ComponentContent - - if let pageContent = self.content[index] { - content = pageContent - } else { - content = .init() - } - content.indicatorImage = image - self.content[index] = content - } - - /// Set the current page indicator image at the specified index - mutating func setCurrentPageIndicatorImage(_ image: ComponentContent.ImageType?, atIndex index: Int) { - - self.currentPageIndicator[index] = image - } - - /// Set an attribute label at the given index - mutating func setAttributedLabel(_ attributedLabel: ComponentContent.TextType?, atIndex index: Int) { - self.labels[index] = attributedLabel - } - - /// Return the attributed label at the given index - func getAttributedLabel(atIndex index: Int) -> ComponentContent.TextType? { - return self.labels[index].flatMap{ $0 } - } - - /// Set the indicator label at the given index - mutating func setIndicatorLabel(_ label: String?, atIndex index: Int) { - var content: ComponentContent - - var indicatorLabel: String? - - if let label { - indicatorLabel = String(label.prefix(2)) - } - - if let pageContent = self.content[index] { - content = pageContent - } else { - content = .init() - } - content.label = indicatorLabel - self.content[index] = content - } - - /// Return the indicator label at the given index - func getIndicatorLabel(atIndex index: Int) -> String? { - return self.content[index]?.label - } - - func getIndicatorAccessibilityLabel(atIndex index: Int) -> String { - return self.getIndicatorLabel(atIndex: index) ?? "\(index + 1)" - } -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerContentTests.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerContentTests.swift deleted file mode 100644 index a998cf690..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerContentTests.swift +++ /dev/null @@ -1,241 +0,0 @@ -// -// ProgressTrackerContentTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 25.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -// swiftlint:disable force_unwrapping -final class ProgressTrackerContentTests: XCTestCase { - - // MARK: - Tests - func test_uses_default_label() { - // GIVEN - let sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: true) - - // THEN - XCTAssertEqual(sut.pageContent(atIndex: 0).label, "1", "Expected label to be 1") - XCTAssertEqual(sut.pageContent(atIndex: 1).label, "2", "Expected label to be 1") - } - - func test_uses_no_label() { - // GIVEN - let sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: false) - - // THEN - XCTAssertNil(sut.pageContent(atIndex: 0).label, "Expected label 1 to be nil") - XCTAssertNil(sut.pageContent(atIndex: 1).label, "Expected label 2 to be nil") - } - - func test_uses_set_indicator_label() { - // GIVEN - var sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: false) - - // WHEN - sut.setIndicatorLabel("X", atIndex: 0) - XCTAssertEqual(sut.getIndicatorLabel(atIndex: 0), "X", "Expected indicator label to be X") - - sut.setIndicatorLabel("A", atIndex: 0) - sut.setIndicatorLabel("B", atIndex: 1) - - // THEN - XCTAssertFalse(sut.hasLabel, "Expected not to labels") - XCTAssertEqual(sut.pageContent(atIndex: 0).label, "A", "Expected label 1 to be A") - XCTAssertEqual(sut.pageContent(atIndex: 1).label, "B", "Expected label 2 to be B") - } - - func test_indicator_label_max_length() { - // GIVEN - var sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: false) - - // WHEN - sut.setIndicatorLabel("XXX", atIndex: 0) - - // THEN - XCTAssertEqual(sut.getIndicatorLabel(atIndex: 0), "XX", "Expected indicator label to be XX") - - } - - func test_uses_set_label() { - // GIVEN - var sut = ProgressTrackerContent(numberOfPages: 4, currentPageIndex: 1, showDefaultPageNumber: false) - - // WHEN - sut.setAttributedLabel(NSAttributedString(string: "A"), atIndex: 0) - sut.setAttributedLabel(NSAttributedString(string: "B"), atIndex: 1) - sut.setAttributedLabel(nil, atIndex: 3) - - // THEN - XCTAssertTrue(sut.hasLabel, "Expected hasLabel to be true") - XCTAssertEqual(sut.numberOfLabels, 2, "Expected number of labels to be 2") - XCTAssertEqual(sut.labels[0], NSAttributedString(string: "A"), "Expected label to be A") - XCTAssertEqual(sut.labels[1], NSAttributedString(string: "B"), "Expected label 2 to be B") - } - - func test_uses_preferred_image() { - // GIVEN - let preferredImage = UIImage(systemName: "pencil")! - let sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: false, preferredIndicatorImage: preferredImage) - - // THEN - XCTAssertEqual(sut.pageContent(atIndex: 0).indicatorImage, preferredImage, "Expected image 1 to be preferred") - XCTAssertEqual(sut.pageContent(atIndex: 1).indicatorImage, preferredImage, "Expected image 2 to be preferred") - } - - func test_uses_preferred_current_page_image() { - // GIVEN - let preferredImage = UIImage(systemName: "pencil")! - let currentPagePreferredImage = UIImage(systemName: "lock.circle") - let sut = ProgressTrackerContent( - numberOfPages: 2, - currentPageIndex: 1, - showDefaultPageNumber: false, - preferredIndicatorImage: preferredImage, - preferredCurrentPageIndicatorImage: currentPagePreferredImage - ) - - // THEN - XCTAssertEqual(sut.pageContent(atIndex: 0).indicatorImage, preferredImage, "Expected image 1 to be preferred") - XCTAssertEqual(sut.pageContent(atIndex: 1).indicatorImage, currentPagePreferredImage, "Expected image 2 to be currentPagePreferredImage") - } - - func test_uses_image_set() { - // GIVEN - let preferredImage = UIImage(systemName: "pencil")! - let currentPagePreferredImage = UIImage(systemName: "lock.circle") - let contentImage = UIImage(systemName: "pencil.circle")! - - var sut = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2, - showDefaultPageNumber: false, - preferredIndicatorImage: preferredImage, - preferredCurrentPageIndicatorImage: currentPagePreferredImage - ) - - /// WHEN - sut.setIndicatorImage(preferredImage, atIndex: 1) - sut.setIndicatorImage(contentImage, atIndex: 1) - sut.setIndicatorImage(contentImage, atIndex: 2) - - // THEN - XCTAssertEqual(sut.pageContent(atIndex: 0).indicatorImage, preferredImage, "Expected image 1 to be preferredImage") - XCTAssertEqual(sut.pageContent(atIndex: 1).indicatorImage, contentImage, "Expected image 2 to be currentImage") - XCTAssertEqual(sut.pageContent(atIndex: 2).indicatorImage, currentPagePreferredImage, "Expected image 3 to be contentPagePreferredImage") - } - - func test_uses_current_page_image_set() { - // GIVEN - let preferredImage = UIImage(systemName: "pencil")! - let currentPagePreferredImage = UIImage(systemName: "lock.circle") - let visitedImage = UIImage(systemName: "pencil.circle")! - let currentContentImage = UIImage(systemName: "trash")! - - var sut = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 1, - showDefaultPageNumber: false, - preferredIndicatorImage: preferredImage, - preferredCurrentPageIndicatorImage: currentPagePreferredImage, - completedPageIndicatorImage: visitedImage - ) - - sut.setCurrentPageIndicatorImage(currentContentImage, atIndex: 1) - - // THEN - XCTAssertEqual(sut.pageContent(atIndex: 0).indicatorImage, visitedImage, "Expected image 1 to be currentPagePreferredImage") - XCTAssertEqual(sut.pageContent(atIndex: 1).indicatorImage, currentContentImage, "Expected image 2 to be contentImage") - XCTAssertEqual(sut.pageContent(atIndex: 2).indicatorImage, preferredImage, "Expected image 3 to be preferred") - } - - func test_attributed_label() { - // GIVEN - var sut = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2 - ) - - // WHEN - sut.setAttributedLabel(NSAttributedString(string: "hello"), atIndex: 1) - - // THEN - XCTAssertEqual(sut.getAttributedLabel(atIndex: 1)?.string, "hello") - } - - func test_needs_update_of_layout_when_pagecount_differs() { - // GIVEN - let sut = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2 - ) - - let other = ProgressTrackerContent( - numberOfPages: 4, - currentPageIndex: 2 - ) - - // THEN - XCTAssertTrue(sut.needsUpdateOfLayout(otherComponent: other)) - } - - func test_needs_update_of_label_counts_differ() { - // GIVEN - let sut = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2 - ) - - var other = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2 - ) - - other.setAttributedLabel(NSAttributedString(string: "A"), atIndex: 0) - - // THEN - XCTAssertTrue(sut.needsUpdateOfLayout(otherComponent: other)) - } - - func test_no_need_for_layout_updated() { - // GIVEN - let sut = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2 - ) - - let other = ProgressTrackerContent( - numberOfPages: 3, - currentPageIndex: 2 - ) - - // THEN - XCTAssertFalse(sut.needsUpdateOfLayout(otherComponent: other)) - } - - func test_accessibility_label_with_no_content() { - // GIVEN - let sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: false) - - // THEN - XCTAssertEqual(sut.getIndicatorAccessibilityLabel(atIndex: 0), "1", "Expected label 0 to be 1") - XCTAssertEqual(sut.getIndicatorAccessibilityLabel(atIndex: 1), "2", "Expected label 1 to be 2") - } - - func test_accessibility_label_with_content() { - // GIVEN - var sut = ProgressTrackerContent(numberOfPages: 2, currentPageIndex: 1, showDefaultPageNumber: false) - - // WHEN - sut.setIndicatorLabel("A", atIndex: 0) - sut.setIndicatorLabel("B", atIndex: 1) - - // THEN - XCTAssertEqual(sut.getIndicatorAccessibilityLabel(atIndex: 0), "A", "Expected accessibility label to be A") - XCTAssertEqual(sut.getIndicatorAccessibilityLabel(atIndex: 1), "B", "Expected accessibility label to be B") - } -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerSizePreferences.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerSizePreferences.swift deleted file mode 100644 index 847cf5e69..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerSizePreferences.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ProgressTrackerSizePreferences.swift -// SparkCore -// -// Created by Michael Zimmermann on 20.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// Preference keys used by the SwiftUI layout of the progress tracker. -struct ProgressTrackerSizePreferences: PreferenceKey { - typealias Value = [Int: CGRect] - - static var defaultValue = [Int: CGRect]() - - static func reduce( - value: inout [Int: CGRect], - nextValue: () -> [Int: CGRect] - ) { - value.merge(nextValue()) { $1 } - } -} - diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerSpacing.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerSpacing.swift deleted file mode 100644 index f1bde81c1..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerSpacing.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProgressTrackerSpacing.swift -// SparkCore -// -// Created by Michael Zimmermann on 24.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// Spacings defined in the progress tracker -struct ProgressTrackerSpacing: Updateable, Equatable { - var trackIndicatorSpacing: CGFloat - var minLabelSpacing: CGFloat -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerState.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerState.swift deleted file mode 100644 index 37fe5ad7a..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerState.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ProgressTrackerState.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The possible states of the progress tracker -struct ProgressTrackerState: Updateable, Equatable { - var isEnabled: Bool - var isPressed: Bool - var isSelected: Bool - - static var normal = ProgressTrackerState(isEnabled: true, isPressed: false, isSelected: false) - - static var disabled = ProgressTrackerState(isEnabled: false, isPressed: false, isSelected: false) - - static var selected = ProgressTrackerState(isEnabled: true, isPressed: false, isSelected: true) - - static var pressed = ProgressTrackerState(isEnabled: true, isPressed: true, isSelected: false) - - var isDisabled: Bool { - return !self.isEnabled - } -} diff --git a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerTintedColors.swift b/core/Sources/Components/ProgressTracker/Model/ProgressTrackerTintedColors.swift deleted file mode 100644 index ad3165663..000000000 --- a/core/Sources/Components/ProgressTracker/Model/ProgressTrackerTintedColors.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProgressTrackerIntentColors.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// Colors defined by a tinted progress tracker indicator -struct ProgressTrackerTintedColors { - let background: any ColorToken - let content: any ColorToken -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCase.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCase.swift deleted file mode 100644 index f75121388..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCase.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ProgressTrackerGetColorsUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol ProgressTrackerGetColorsUseCaseable { - func execute(colors: Colors, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - state: ProgressTrackerState) -> ProgressTrackerColors -} - -/// A use case that returns the color of the progress tracker indicator. -struct ProgressTrackerGetColorsUseCase: ProgressTrackerGetColorsUseCaseable { - - // MARK: - Properties - let getTintedColorsUseCase: any ProgressTrackerGetVariantColorsUseCaseable - let getOutlinedColorsUseCase: any ProgressTrackerGetVariantColorsUseCaseable - - // MARK: - Initialization - init( - getTintedColorsUseCase: some ProgressTrackerGetVariantColorsUseCaseable = ProgressTrackerGetTintedColorsUseCase(), - getOutlinedColorsUseCase: some ProgressTrackerGetVariantColorsUseCaseable = ProgressTrackerGetOutlinedColorsUseCase()) { - self.getTintedColorsUseCase = getTintedColorsUseCase - self.getOutlinedColorsUseCase = getOutlinedColorsUseCase - } - - // MARK: Execute - /// Returns the colors of the progress tracker indicator - func execute( - colors: Colors, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - state: ProgressTrackerState) -> ProgressTrackerColors { - switch variant { - case .outlined: return self.getOutlinedColorsUseCase.execute(colors: colors, intent: intent, state: state) - case .tinted: return self.getTintedColorsUseCase.execute(colors: colors, intent: intent, state: state) - } - } -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCaseTests.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCaseTests.swift deleted file mode 100644 index 9ec952c10..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetColorsUseCaseTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// ProgressTrackerGetColorsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 18.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import XCTest - -@testable import SparkCore - -final class ProgressTrackerGetColorsUseCaseTests: XCTestCase { - - var sut: ProgressTrackerGetColorsUseCase! - var theme: ThemeGeneratedMock! - var outlinedUseCase: ProgressTrackerGetVariantColorsUseCaseableGeneratedMock! - var tintedUseCase: ProgressTrackerGetVariantColorsUseCaseableGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - self.outlinedUseCase = ProgressTrackerGetVariantColorsUseCaseableGeneratedMock() - self.tintedUseCase = ProgressTrackerGetVariantColorsUseCaseableGeneratedMock() - let sut = ProgressTrackerGetColorsUseCase( - getTintedColorsUseCase: self.tintedUseCase, - getOutlinedColorsUseCase: self.outlinedUseCase - ) - - self.sut = sut - } - - // MARK: - Tests - func test_tinted_colors() { - // GIVEN - let colors = self.theme.colors - let expectedColors = ProgressTrackerColors( - background: colors.main.mainContainer, - outline: colors.main.mainContainer, - content: colors.main.onMainContainer) - - self.tintedUseCase.executeWithColorsAndIntentAndStateReturnValue = expectedColors - - // WHEN - let tabColors = self.sut.execute( - colors: colors, - intent: .basic, - variant: .tinted, - state: .normal) - - // THEN - XCTAssertEqual(tabColors, expectedColors) - } - - func test_outlined_colors() { - // GIVEN - let colors = self.theme.colors - let expectedColors = ProgressTrackerColors( - background: colors.main.mainContainer, - outline: colors.main.main, - content: colors.main.main) - - self.outlinedUseCase.executeWithColorsAndIntentAndStateReturnValue = expectedColors - - // WHEN - let tabColors = self.sut.execute( - colors: colors, - intent: .basic, - variant: .outlined, - state: .normal) - - // THEN - XCTAssertEqual(tabColors, expectedColors) - } -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCase.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCase.swift deleted file mode 100644 index eb2f54ff4..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCase.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// ProgressTrackerGetOutlinedColorsUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// A use case to calculate the outlined colors of the progress tracker indicator -struct ProgressTrackerGetOutlinedColorsUseCase: ProgressTrackerGetVariantColorsUseCaseable { - - /// Return the colors of the progress tracker indicator - func execute(colors: Colors, - intent: ProgressTrackerIntent, - state: ProgressTrackerState - ) -> ProgressTrackerColors { - let intentColors: ProgressTrackerColors = { - if state.isSelected { - return self.selectedColors(colors: colors, intent: intent) - } else if state.isPressed { - return self.pressedColors(colors: colors, intent: intent) - } else { - return self.enabledColors(colors: colors, intent: intent) - } - }() - - return ProgressTrackerColors( - background: intentColors.background, - outline: intentColors.outline, - content: intentColors.content) - } - - private func pressedColors(colors: Colors, intent: ProgressTrackerIntent) -> ProgressTrackerColors { - switch intent { - case .accent: - return .init( - background: colors.states.accentContainerPressed, - outline: colors.accent.accent, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.states.alertContainerPressed, - outline: colors.feedback.alert, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.states.basicContainerPressed, - outline: colors.basic.basic, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.states.errorContainerPressed, - outline: colors.feedback.error, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.states.infoContainerPressed, - outline: colors.feedback.info, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.states.mainContainerPressed, - outline: colors.main.main, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.states.neutralContainerPressed, - outline: colors.feedback.neutral, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.states.successContainerPressed, - outline: colors.feedback.success, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.states.supportContainerPressed, - outline: colors.support.support, - content: colors.support.onSupportContainer) - } - } - - private func selectedColors(colors: Colors, intent: ProgressTrackerIntent) -> ProgressTrackerColors { - - switch intent { - case .accent: - return .init( - background: colors.accent.accentContainer, - outline: colors.accent.accent, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.feedback.alertContainer, - outline: colors.feedback.alert, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.basic.basicContainer, - outline: colors.basic.basic, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.feedback.errorContainer, - outline: colors.feedback.error, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.feedback.infoContainer, - outline: colors.feedback.info, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.main.mainContainer, - outline: colors.main.main, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.feedback.neutralContainer, - outline: colors.feedback.neutral, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.feedback.successContainer, - outline: colors.feedback.success, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.support.supportContainer, - outline: colors.support.support, - content: colors.support.onSupportContainer) - } - } - - private func enabledColors(colors: Colors, intent: ProgressTrackerIntent) -> ProgressTrackerColors { - switch intent { - case .accent: - return .init( - background: ColorTokenDefault.clear, - outline: colors.accent.accent, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.alert, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: ColorTokenDefault.clear, - outline: colors.basic.basic, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.error, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.info, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: ColorTokenDefault.clear, - outline: colors.main.main, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.neutral, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.success, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: ColorTokenDefault.clear, - outline: colors.support.support, - content: colors.support.onSupportContainer) - } - } -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCaseTests.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCaseTests.swift deleted file mode 100644 index 6e5418d27..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetOutlinedColorsUseCaseTests.swift +++ /dev/null @@ -1,234 +0,0 @@ -// -// ProgressTrackerGetOutlinedColorsUseCaseTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 18.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ProgressTrackerGetOutlinedColorsUseCaseTests: XCTestCase { - - var sut: ProgressTrackerGetOutlinedColorsUseCase! - var colors: ColorsGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.colors = ColorsGeneratedMock.mocked() - self.sut = ProgressTrackerGetOutlinedColorsUseCase() - } - - // MARK: - Tests - func test_selected_colors() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .selected) - - // THEN - XCTAssertEqual(colors, intent.selectedColors(self.colors), "Selected colors for intent \(intent) not as expected") - } - } - - func test_enabled_colors() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .normal) - - // THEN - XCTAssertEqual(colors, intent.enabledColors(self.colors), "Enabled colors for intent \(intent) not as expected") - } - } - - func test_pressed_colors() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .pressed) - - // THEN - XCTAssertEqual(colors, intent.pressedColors(self.colors), "Pressed colors for intent \(intent) not as expected") - } - } - - func test_colors_disabled() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .disabled) - - let expectedColors = intent.enabledColors(self.colors) - - // THEN - XCTAssertEqual(colors, expectedColors, "Disabled colors for intent \(intent) not as expected") - } - } -} - -// MARK: Private helpers -private extension ProgressTrackerIntent { - - func selectedColors(_ colors: Colors) -> ProgressTrackerColors { - switch self { - case .accent: - return .init( - background: colors.accent.accentContainer, - outline: colors.accent.accent, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.feedback.alertContainer, - outline: colors.feedback.alert, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.basic.basicContainer, - outline: colors.basic.basic, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.feedback.errorContainer, - outline: colors.feedback.error, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.feedback.infoContainer, - outline: colors.feedback.info, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.main.mainContainer, - outline: colors.main.main, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.feedback.neutralContainer, - outline: colors.feedback.neutral, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.feedback.successContainer, - outline: colors.feedback.success, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.support.supportContainer, - outline: colors.support.support, - content: colors.support.onSupportContainer) - } - } - - func enabledColors(_ colors: Colors) -> ProgressTrackerColors { - switch self { - case .accent: - return .init( - background: ColorTokenDefault.clear, - outline: colors.accent.accent, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.alert, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: ColorTokenDefault.clear, - outline: colors.basic.basic, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.error, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.info, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: ColorTokenDefault.clear, - outline: colors.main.main, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.neutral, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: ColorTokenDefault.clear, - outline: colors.feedback.success, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: ColorTokenDefault.clear, - outline: colors.support.support, - content: colors.support.onSupportContainer) - } - } - - func pressedColors(_ colors: Colors) -> ProgressTrackerColors { - switch self { - case .accent: - return .init( - background: colors.states.accentContainerPressed, - outline: colors.accent.accent, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.states.alertContainerPressed, - outline: colors.feedback.alert, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.states.basicContainerPressed, - outline: colors.basic.basic, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.states.errorContainerPressed, - outline: colors.feedback.error, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.states.infoContainerPressed, - outline: colors.feedback.info, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.states.mainContainerPressed, - outline: colors.main.main, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.states.neutralContainerPressed, - outline: colors.feedback.neutral, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.states.successContainerPressed, - outline: colors.feedback.success, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.states.supportContainerPressed, - outline: colors.support.support, - content: colors.support.onSupportContainer) - } - } -} - diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCase.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCase.swift deleted file mode 100644 index 16fcdb47b..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCase.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ProgressTrackerGetSpacingsUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 24.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol ProgressTrackerGetSpacingsUseCaseable { - - func execute(spacing: LayoutSpacing, orientation: ProgressTrackerOrientation) -> ProgressTrackerSpacing -} - -/// A use case returning the spacings between the progress tracker indicators -struct ProgressTrackerGetSpacingsUseCase: ProgressTrackerGetSpacingsUseCaseable { - - func execute(spacing: LayoutSpacing, orientation: ProgressTrackerOrientation) -> ProgressTrackerSpacing { - - switch orientation { - case .horizontal: return ProgressTrackerSpacing( - trackIndicatorSpacing: spacing.small, - minLabelSpacing: spacing.medium) - case .vertical: return ProgressTrackerSpacing( - trackIndicatorSpacing: spacing.small, - minLabelSpacing: spacing.medium) - } - - } - -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCaseTests.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCaseTests.swift deleted file mode 100644 index dd0d0103c..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetSpacingsUseCaseTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ProgressTrackerGetSpacingsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 24.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ProgressTrackerGetSpacingsUseCaseTests: XCTestCase { - - // MARK: Properties - var sut: ProgressTrackerGetSpacingsUseCase! - var spacing: LayoutSpacing! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.spacing = LayoutSpacingGeneratedMock.mocked() - self.sut = ProgressTrackerGetSpacingsUseCase() - } - - // MARK: - Tests - func test_horizontal_spacing() { - let spacing = self.sut.execute(spacing: self.spacing, orientation: .horizontal) - - XCTAssertEqual(spacing, .init(trackIndicatorSpacing: 3.0, minLabelSpacing: 5.0)) - } - - func test_vertical_spacing() { - let spacing = self.sut.execute(spacing: self.spacing, orientation: .vertical) - - XCTAssertEqual(spacing, .init(trackIndicatorSpacing: 3.0, minLabelSpacing: 5.0)) - } -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCase.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCase.swift deleted file mode 100644 index 18ff63c1f..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCase.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// ProgressTrackerGetTintedColorsUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// A use case to calculate the tinted colors of the progress tracker -struct ProgressTrackerGetTintedColorsUseCase: ProgressTrackerGetVariantColorsUseCaseable { - - func execute(colors: Colors, - intent: ProgressTrackerIntent, - state: ProgressTrackerState - ) -> ProgressTrackerColors { - let intentColors: ProgressTrackerTintedColors = { - if state.isSelected { - return self.selectedColors(colors: colors, intent: intent) - } else if state.isPressed { - return self.pressedColors(colors: colors, intent: intent) - } else { - return self.enabledColors(colors: colors, intent: intent) - } - }() - - return ProgressTrackerColors( - background: intentColors.background, - outline: intentColors.background, - content: intentColors.content) - } - - // MARK: - Private functions - private func pressedColors(colors: Colors, intent: ProgressTrackerIntent) -> ProgressTrackerTintedColors { - switch intent { - case .accent: - return .init( - background: colors.states.accentContainerPressed, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.states.alertContainerPressed, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.states.basicContainerPressed, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.states.errorContainerPressed, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.states.infoContainerPressed, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.states.mainContainerPressed, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.states.neutralContainerPressed, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.states.successContainerPressed, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.states.supportContainerPressed, - content: colors.support.onSupportContainer) - } - } - - private func selectedColors(colors: Colors, intent: ProgressTrackerIntent) -> ProgressTrackerTintedColors { - - switch intent { - case .accent: - return .init( - background: colors.accent.accent, - content: colors.accent.onAccent) - case .alert: - return .init( - background: colors.feedback.alert, - content: colors.feedback.onAlert) - case .basic: - return .init( - background: colors.basic.basic, - content: colors.basic.onBasic) - case .danger: - return .init( - background: colors.feedback.error, - content: colors.feedback.onError) - case .info: - return .init( - background: colors.feedback.info, - content: colors.feedback.onInfo) - case .main: - return .init( - background: colors.main.main, - content: colors.main.onMain) - case .neutral: - return .init( - background: colors.feedback.neutral, - content: colors.feedback.onNeutral) - case .success: - return .init( - background: colors.feedback.success, - content: colors.feedback.onSuccess) - case .support: - return .init( - background: colors.support.support, - content: colors.support.onSupport) - } - } - - private func enabledColors(colors: Colors, intent: ProgressTrackerIntent) -> ProgressTrackerTintedColors { - switch intent { - case .accent: - return .init( - background: colors.accent.accentContainer, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.feedback.alertContainer, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.basic.basicContainer, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.feedback.errorContainer, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.feedback.infoContainer, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.main.mainContainer, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.feedback.neutralContainer, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.feedback.successContainer, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.support.supportContainer, - content: colors.support.onSupportContainer) - } - } -} - diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCaseTests.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCaseTests.swift deleted file mode 100644 index c332221fb..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTintedColorsUseCaseTests.swift +++ /dev/null @@ -1,229 +0,0 @@ -// -// ProgressTrackerGetTintedColorsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 18.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ProgressTrackerGetTintedColorsUseCaseTests: XCTestCase { - - // MARK: Properties - var sut: ProgressTrackerGetTintedColorsUseCase! - var colors: ColorsGeneratedMock! - - // MARK: Setup - override func setUp() { - super.setUp() - - self.sut = ProgressTrackerGetTintedColorsUseCase() - self.colors = ColorsGeneratedMock.mocked() - } - - // MARK: - Tests - func test_selected_colors() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .selected) - - // THEN - XCTAssertEqual(colors, intent.selectedColors(self.colors), "Selected colors for intent \(intent) not as expected") - } - } - - func test_enabled_colors() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .normal) - - // THEN - XCTAssertEqual(colors, intent.enabledColors(self.colors), "Enabled colors for intent \(intent) not as expected") - } - } - - func test_pressed_colors() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .pressed) - - // THEN - XCTAssertEqual(colors, intent.pressedColors(self.colors), "Pressed colors for intent \(intent) not as expected") - } - } - - func test_colors_disabled() { - // GIVEN - - for intent in ProgressTrackerIntent.allCases { - // WHEN - - let colors = self.sut.execute(colors: self.colors, intent: intent, state: .disabled) - - let expectedColors = intent.enabledColors(self.colors) - - // THEN - XCTAssertEqual(colors, expectedColors, "Disabled colors for intent \(intent) not as expected") - } - } -} - -// MARK: - Private helpers -private extension ProgressTrackerIntent { - - func selectedColors(_ colors: Colors) -> ProgressTrackerColors { - let tintedColors: ProgressTrackerTintedColors = { - switch self { - case .accent: - return .init( - background: colors.accent.accent, - content: colors.accent.onAccent) - case .alert: - return .init( - background: colors.feedback.alert, - content: colors.feedback.onAlert) - case .basic: - return .init( - background: colors.basic.basic, - content: colors.basic.onBasic) - case .danger: - return .init( - background: colors.feedback.error, - content: colors.feedback.onError) - case .info: - return .init( - background: colors.feedback.info, - content: colors.feedback.onInfo) - case .main: - return .init( - background: colors.main.main, - content: colors.main.onMain) - case .neutral: - return .init( - background: colors.feedback.neutral, - content: colors.feedback.onNeutral) - case .success: - return .init( - background: colors.feedback.success, - content: colors.feedback.onSuccess) - case .support: - return .init( - background: colors.support.support, - content: colors.support.onSupport) - } - }() - - return ProgressTrackerColors( - background: tintedColors.background, - outline: tintedColors.background, - content: tintedColors.content) - } - - func enabledColors(_ colors: Colors) -> ProgressTrackerColors { - let tintedColors: ProgressTrackerTintedColors = { - switch self { - case .accent: - return .init( - background: colors.accent.accentContainer, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.feedback.alertContainer, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.basic.basicContainer, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.feedback.errorContainer, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.feedback.infoContainer, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.main.mainContainer, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.feedback.neutralContainer, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.feedback.successContainer, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.support.supportContainer, - content: colors.support.onSupportContainer) - } - }() - - return ProgressTrackerColors( - background: tintedColors.background, - outline: tintedColors.background, - content: tintedColors.content) - } - - func pressedColors(_ colors: Colors) -> ProgressTrackerColors { - let tintedColors: ProgressTrackerTintedColors = { - switch self { - case .accent: - return .init( - background: colors.states.accentContainerPressed, - content: colors.accent.onAccentContainer) - case .alert: - return .init( - background: colors.states.alertContainerPressed, - content: colors.feedback.onAlertContainer) - case .basic: - return .init( - background: colors.states.basicContainerPressed, - content: colors.basic.onBasicContainer) - case .danger: - return .init( - background: colors.states.errorContainerPressed, - content: colors.feedback.onErrorContainer) - case .info: - return .init( - background: colors.states.infoContainerPressed, - content: colors.feedback.onInfoContainer) - case .main: - return .init( - background: colors.states.mainContainerPressed, - content: colors.main.onMainContainer) - case .neutral: - return .init( - background: colors.states.neutralContainerPressed, - content: colors.feedback.onNeutralContainer) - case .success: - return .init( - background: colors.states.successContainerPressed, - content: colors.feedback.onSuccessContainer) - case .support: - return .init( - background: colors.states.supportContainerPressed, - content: colors.support.onSupportContainer) - } - }() - - return ProgressTrackerColors( - background: tintedColors.background, - outline: tintedColors.background, - content: tintedColors.content) - } -} - diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCase.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCase.swift deleted file mode 100644 index a6740a310..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCase.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ProgressTrackerGetTrackColorUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 24.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol ProgressTrackerGetTrackColorUseCaseable { - func execute(colors: Colors, - intent: ProgressTrackerIntent) -> any ColorToken -} - -/// A use cate returning the color of the `track` between the progress tracker indicators. -struct ProgressTrackerGetTrackColorUseCase: ProgressTrackerGetTrackColorUseCaseable { - - func execute(colors: Colors, - intent: ProgressTrackerIntent) -> any ColorToken { - switch intent { - case .basic: return colors.basic.basic - case .accent: return colors.accent.accent - case .alert: return colors.feedback.alert - case .danger: return colors.feedback.error - case .info: return colors.feedback.info - case .main: return colors.main.main - case .neutral: return colors.feedback.neutral - case .success: return colors.feedback.success - case .support: return colors.support.support - } - } -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCaseTests.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCaseTests.swift deleted file mode 100644 index e743cc873..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetTrackColorUseCaseTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ProgressTrackerGetTrackColorUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 24.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ProgressTrackerGetTrackColorUseCaseTests: XCTestCase { - - // MARK: - Properties - var sut: ProgressTrackerGetTrackColorUseCase! - var colors: ColorsGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.colors = ColorsGeneratedMock.mocked() - self.sut = ProgressTrackerGetTrackColorUseCase() - } - - // MARK: - Tests - func test_all_intents() { - let expectedColors: [ProgressTrackerIntent: any ColorToken] = - [ - .accent: self.colors.accent.accent, - .alert: self.colors.feedback.alert, - .basic: self.colors.basic.basic, - .danger: self.colors.feedback.error, - .info: self.colors.feedback.info, - .main: self.colors.main.main, - .neutral: self.colors.feedback.neutral, - .success: self.colors.feedback.success, - .support: self.colors.support.support - ] - - for intent in ProgressTrackerIntent.allCases { - let expectedColor = expectedColors[intent] - let givenColor = self.sut.execute(colors: self.colors, intent: intent) - XCTAssertTrue(expectedColor?.equals(givenColor) == true, "Enabled color for \(intent) is not as expected") - } - } -} diff --git a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetVariantColorsUseCaseable.swift b/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetVariantColorsUseCaseable.swift deleted file mode 100644 index 7b89bc4a4..000000000 --- a/core/Sources/Components/ProgressTracker/UseCase/ProgressTrackerGetVariantColorsUseCaseable.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ProgressTrackerGetVariantColorsUseCaseable.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol ProgressTrackerGetVariantColorsUseCaseable { - func execute(colors: Colors, - intent: ProgressTrackerIntent, - state: ProgressTrackerState - ) -> ProgressTrackerColors -} diff --git a/core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerConfigurationSnapshotTests.swift b/core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerConfigurationSnapshotTests.swift deleted file mode 100644 index a8f336f33..000000000 --- a/core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerConfigurationSnapshotTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// ProgressTrackerConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -@testable import SparkCore - -struct ProgressTrackerConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: ProgressTrackerScenarioSnapshotTests - - let intent: ProgressTrackerIntent - let variant: ProgressTrackerVariant - let state: ProgressTrackerState - let contentType: ProgressTrackerContentType - let size: ProgressTrackerSize - let orientation: ProgressTrackerOrientation - - let labels: [String] - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - let frame: CGRect? - - init(scenario: ProgressTrackerScenarioSnapshotTests, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - state: ProgressTrackerState, - contentType: ProgressTrackerContentType, - size: ProgressTrackerSize, - orientation: ProgressTrackerOrientation, - labels: [String], - modes: [ComponentSnapshotTestMode], - sizes: [UIContentSizeCategory], - frame: CGRect? = nil) { - self.scenario = scenario - self.intent = intent - self.variant = variant - self.state = state - self.contentType = contentType - self.size = size - self.orientation = orientation - self.labels = labels - self.modes = modes - self.sizes = sizes - self.frame = frame - } - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.variant)", - "\(self.state)", - "\(self.contentType)", - "\(self.size)", - "\(self.orientation)", - labels.isEmpty ? "noLabels" : "labels", - self.state.isDisabled ? "disabled" : "enabled", - self.state.isSelected ? "selected" : "notSelected" - ].joined(separator: "-") - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerScenarioSnapshotTests.swift b/core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerScenarioSnapshotTests.swift deleted file mode 100644 index e2092a613..000000000 --- a/core/Sources/Components/ProgressTracker/View/CommonTests/ProgressTrackerScenarioSnapshotTests.swift +++ /dev/null @@ -1,289 +0,0 @@ -// -// ProgressTrackerScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI -import UIKit - -@testable import SparkCore - -enum ProgressTrackerContentType { - case icon - case text - case empty -} - -enum ProgressTrackerScenarioSnapshotTests: String, CaseIterable { - case test1 // All intents - case test2 // All variants and states - case test3 // Test content resilience - case test4 // Test component sizes - case test5 // Test all a11y sizes - case test6 // Test orientation with different label sizes - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1(isSwiftUIComponent: isSwiftUIComponent) - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3(isSwiftUIComponent: isSwiftUIComponent) - case .test4: - return self.test4(isSwiftUIComponent: isSwiftUIComponent) - case .test5: - return self.test5(isSwiftUIComponent: isSwiftUIComponent) - case .test6: - return self.test6(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - intents: all - /// - variant: outlined - /// - state: enabled - /// - content: icon - /// - size: medium - /// - orientation: horizontal - /// - label: none - /// - mode: all - /// - a11y size: medium - private func test1(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - let intents = ProgressTrackerIntent.allCases - - return intents.map { - .init( - scenario: self, - intent: $0, - variant: .outlined, - state: .normal, - contentType: .icon, - size: .medium, - orientation: .horizontal, - labels: [], - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test all variants & states - /// - /// Content: - /// - intents: basic - /// - variant: all - /// - state: all // except selected, since this will also be tested with normal - /// - content: icon - /// - size: medium - /// - orientation: horizontal - /// - label: none - /// - mode: light - /// - a11y size: medium - private func test2(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - let variants = ProgressTrackerVariant.allCases - let states: [ProgressTrackerState] = [.normal, .pressed, .disabled] - - let allCases: [(variant: ProgressTrackerVariant, state: ProgressTrackerState)] = variants.flatMap{ variant in - return states.map{ state in - return (variant, state) - } - } - - return allCases.map { - .init( - scenario: self, - intent: .basic, - variant: $0.variant, - state: $0.state, - contentType: .icon, - size: .medium, - orientation: .horizontal, - labels: [], - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 3 - /// - /// Description: To test content resilience - /// - /// Content: - /// - intents: basic - /// - variant: outlined - /// - state: enabled - /// - content: char/none - /// - size: medium - /// - orientation: horizontal - /// - label: none - /// - mode: light - /// - a11y size: medium - private func test3(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - - let allContents: [ProgressTrackerContentType] = [.empty, .text] - - return allContents.map { - .init( - scenario: self, - intent: .basic, - variant: .outlined, - state: .normal, - contentType: $0, - size: .medium, - orientation: .horizontal, - labels: [], - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 4 - /// - /// Description: To test component sizes - /// - /// Content: - /// - intents: basic - /// - variant: outlined - /// - state: enabled - /// - content: text - /// - size: all - /// - orientation: horizontal - /// - label: none - /// - mode: light - /// - a11y size: medium - private func test4(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - - return ProgressTrackerSize.allCases.map { - .init( - scenario: self, - intent: .basic, - variant: .outlined, - state: .normal, - contentType: .text, - size: $0, - orientation: .horizontal, - labels: [], - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 5 - /// - /// Description: To test all a11y sizes - /// - /// Content: - /// - intents: basic - /// - variant: outlined - /// - state: enabled - /// - content: text - /// - size: medium - /// - orientation: horizontal - /// - label: none - /// - mode: light - /// - a11y size: all - private func test5(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - - return [ - .init( - scenario: self, - intent: .basic, - variant: .outlined, - state: .normal, - contentType: .text, - size: .medium, - orientation: .horizontal, - labels: [], - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - ) - ] - } - - /// Test 6 - /// - /// Description: test orientations with different labels sizes - /// - /// Content: - /// - intents: basic - /// - variant: outlined - /// - state: enabled - /// - content: text - /// - size: medium - /// - orientation: all - /// - label: with long label / none - /// - mode: light - /// - a11y size: medium - private func test6(isSwiftUIComponent: Bool) -> [ProgressTrackerConfigurationSnapshotTests] { - - let longLabels = [ - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - "Lorem ipsum dolor sit amet", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.", - "Ut enim ad minim veniam", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit"] - - return [ - .init( - scenario: self, - intent: .basic, - variant: .outlined, - state: .normal, - contentType: .text, - size: .medium, - orientation: .vertical, - labels: [], - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ), - .init( - scenario: self, - intent: .basic, - variant: .outlined, - state: .normal, - contentType: .text, - size: .medium, - orientation: .horizontal, - labels: longLabels, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default, - frame: CGRect(x: 0, y: 0, width: 600, height: 300) - ), - .init( - scenario: self, - intent: .basic, - variant: .outlined, - state: .normal, - contentType: .text, - size: .medium, - orientation: .vertical, - labels: longLabels, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default, - frame: CGRect(x: 0, y: 0, width: 300, height: 300) - ) - ] - } -} diff --git a/core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModel.swift b/core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModel.swift deleted file mode 100644 index 0fd96b133..000000000 --- a/core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModel.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// ProgressTrackerIndicatorViewModel.swift -// SparkCore -// -// Created by Michael Zimmermann on 22.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -/// A view model for a single Progress Tracker Indicator -final class ProgressTrackerIndicatorViewModel: ObservableObject { - - var theme: Theme { - didSet { - self.updateColors() - self.font = theme.typography.body2Highlight - } - } - - var intent: ProgressTrackerIntent { - didSet { - guard self.intent != oldValue else { return } - self.updateColors() - } - } - - var variant: ProgressTrackerVariant { - didSet { - guard self.variant != oldValue else { return } - self.updateColors() - } - } - - var state: ProgressTrackerState { - didSet { - guard self.state != oldValue else { return } - self.updateColors() - self.updateOpacity() - } - } - - // MARK: - Private properties - private let colorsUseCase: ProgressTrackerGetColorsUseCaseable - - // MARK: - Published properties - @Published var size: ProgressTrackerSize - @Published var content: ComponentContent - @Published var colors: ProgressTrackerColors - @Published var font: TypographyFontToken - @Published var opacity: CGFloat = 1.0 - - // MARK: Initialization - init(theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - content: ComponentContent, - state: ProgressTrackerState = .normal, - colorsUseCase: ProgressTrackerGetColorsUseCaseable = ProgressTrackerGetColorsUseCase() - ) { - self.theme = theme - self.intent = intent - self.variant = variant - self.size = size - self.content = content - self.colorsUseCase = colorsUseCase - self.state = state - - self.colors = colorsUseCase.execute(colors: theme.colors, intent: intent, variant: variant, state: state) - - self.font = theme.typography.body2Highlight - self.updateOpacity() - } - - private func updateColors() { - self.colors = self.colorsUseCase.execute(colors: theme.colors, intent: intent, variant: variant, state: self.state) - } - - private func updateOpacity() { - self.opacity = self.state.isEnabled ? 1.0 : self.theme.dims.dim3 - } - - func set(enabled: Bool) { - self.state.isEnabled = enabled - } - - func set(highlighted: Bool) { - self.state.isPressed = highlighted - } - - func set(selected: Bool) { - self.state.isSelected = selected - } -} diff --git a/core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModelTests.swift b/core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModelTests.swift deleted file mode 100644 index 1bbb284df..000000000 --- a/core/Sources/Components/ProgressTracker/View/ProgressTrackerIndicatorViewModelTests.swift +++ /dev/null @@ -1,246 +0,0 @@ -// -// ProgressTrackerIndicatorViewModelTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 25.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class ProgressTrackerIndicatorViewModelTests: XCTestCase { - - var cancellables = Set() - var theme: ThemeGeneratedMock! - var colorsUseCase: ProgressTrackerGetColorsUseCaseableGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - self.colorsUseCase = ProgressTrackerGetColorsUseCaseableGeneratedMock() - - self.colorsUseCase - .executeWithColorsAndIntentAndVariantAndStateReturnValue = .init( - background: self.theme.colors.basic.basicContainer, - outline: self.theme.colors.basic.onBasic, - content: self.theme.colors.basic.basic - ) - } - - // MARK: - Tests - func test_initialization() { - // Given - let sut = self.sut(intent: .alert, variant: .tinted, size: .small, state: .selected) - - // Then - XCTAssertEqual(sut.state, .selected, "Expected state to be selected") - XCTAssertEqual(sut.intent, .alert, "Expected intent to be alert") - XCTAssertEqual(sut.variant, .tinted, "Expected variant to be tinted") - XCTAssertEqual(sut.size, .small, "Expected size to be small") - XCTAssertIdentical(sut.theme as? ThemeGeneratedMock, self.theme, "Expected theme to be identical") - - XCTAssertEqual(self.colorsUseCase.executeWithColorsAndIntentAndVariantAndStateCallsCount, 1) - } - - func test_update_theme() { - // Given - let sut = self.sut() - let expectation = expectation(description: "Expect colors to have been triggered") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.theme = self.theme - - // Then - wait(for: [expectation]) - } - - func test_intent_change() { - // Given - let sut = self.sut(intent: .basic) - let expectation = expectation(description: "Expect colors to have been triggered twice") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.intent = .danger - - // Then - wait(for: [expectation]) - } - - func test_intent_no_change() { - // Given - let sut = self.sut(intent: .basic) - let expectation = expectation(description: "Expect colors to have been triggered once") - expectation.expectedFulfillmentCount = 1 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.intent = .basic - - // Then - wait(for: [expectation]) - } - - func test_variant_change() { - // Given - let sut = self.sut(variant: .outlined) - let expectation = expectation(description: "Expect colors to have been triggered twice") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.variant = .tinted - - // Then - wait(for: [expectation]) - } - - func test_variant_no_change() { - // Given - let sut = self.sut(variant: .outlined) - let expectation = expectation(description: "Expect colors to have been triggered once") - expectation.expectedFulfillmentCount = 1 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.variant = .outlined - - // Then - wait(for: [expectation]) - } - - func test_state_change() { - // Given - let sut = self.sut(state: .normal) - let expectation = expectation(description: "Expect colors to have been triggered twice") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.state = .pressed - - // Then - wait(for: [expectation]) - } - - func test_state_no_change() { - // Given - let sut = self.sut(state: .pressed) - let expectation = expectation(description: "Expect colors to have been triggered once") - expectation.expectedFulfillmentCount = 1 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.state = .pressed - - // Then - wait(for: [expectation]) - } - - func test_selected_changed() { - // Given - let sut = self.sut(state: .normal) - let expectation = expectation(description: "Expect colors to have been triggered twice") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.set(selected: true) - - // Then - wait(for: [expectation]) - } - - func test_enabled_changed() { - // Given - let sut = self.sut(state: .normal) - let expectation = expectation(description: "Expect colors to have been triggered twice") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.set(enabled: false) - - // Then - wait(for: [expectation]) - } - - func test_highlighted_changed() { - // Given - let sut = self.sut(state: .normal) - let expectation = expectation(description: "Expect colors to have been triggered twice") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.set(highlighted: true) - - // Then - wait(for: [expectation]) - } - - // MARK: Private functions - private func sut( - intent: ProgressTrackerIntent = .basic, - variant: ProgressTrackerVariant = .outlined, - size: ProgressTrackerSize = .large, - state: ProgressTrackerState = .normal) -> ProgressTrackerIndicatorViewModel { - return .init( - theme: self.theme, - intent: intent, - variant: variant, - size: size, - content: ProgressTrackerUIIndicatorContent(), - state: state, - colorsUseCase: self.colorsUseCase - ) - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModel.swift b/core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModel.swift deleted file mode 100644 index 8a67ff7c5..000000000 --- a/core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModel.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// ProgressTrackerTrackViewModel.swift -// SparkCore -// -// Created by Michael Zimmermann on 30.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// A view model for the Progress Tracker Track -final class ProgressTrackerTrackViewModel: ObservableObject { - - var theme: Theme { - didSet { - self.updateLineColor() - } - } - - var intent: ProgressTrackerIntent { - didSet { - guard self.intent != oldValue else { return } - self.updateLineColor() - } - } - - var isEnabled: Bool { - didSet { - guard self.isEnabled != oldValue else { return } - self.updateOpacity() - } - } - - private var useCase: ProgressTrackerGetTrackColorUseCaseable - @Published var lineColor: any ColorToken - @Published var opacity: CGFloat = 1.0 - - // MARK: - Initialization - init(theme: Theme, - intent: ProgressTrackerIntent, - isEnabled: Bool = true, - useCase: ProgressTrackerGetTrackColorUseCaseable = ProgressTrackerGetTrackColorUseCase() - ) { - self.theme = theme - self.intent = intent - self.useCase = useCase - self.isEnabled = isEnabled - self.lineColor = useCase.execute(colors: theme.colors, intent: intent) - self.updateOpacity() - } - - private func updateLineColor() { - let newLineColor = useCase.execute(colors: theme.colors, intent: intent) - if !newLineColor.equals(self.lineColor) { - self.lineColor = newLineColor - } - } - - private func updateOpacity() { - self.opacity = self.isEnabled ? 1.0 : self.theme.dims.dim3 - } -} diff --git a/core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModelTests.swift b/core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModelTests.swift deleted file mode 100644 index be58f4f4b..000000000 --- a/core/Sources/Components/ProgressTracker/View/ProgressTrackerTrackViewModelTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// ProgressTrackerTrackViewModelTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 06.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class ProgressTrackerTrackViewModelTests: XCTestCase { - - var colors: ColorsGeneratedMock! - var theme: ThemeGeneratedMock! - var useCase: ProgressTrackerGetTrackColorUseCaseableGeneratedMock! - var cancellables = Set() - - override func setUp() { - super.setUp() - - self.colors = ColorsGeneratedMock.mocked() - self.theme = ThemeGeneratedMock.mocked() - self.useCase = ProgressTrackerGetTrackColorUseCaseableGeneratedMock() - - self.useCase.executeWithColorsAndIntentReturnValue = ColorTokenGeneratedMock.random() - } - - func test_theme_updates_line_color_updates() { - // GIVEN - let sut = self.sut() - let expect = expectation(description: "Expect line color to have been published") - - // WHEN - sut.theme = ThemeGeneratedMock.mocked() - - sut.$lineColor.subscribe(in: &self.cancellables) { color in - XCTAssertEqual(self.useCase.executeWithColorsAndIntentReturnValue.uiColor, color.uiColor, "Expected color to have only been set once") - expect.fulfill() - } - - // THEN - wait(for: [expect], timeout: 1) - XCTAssertEqual(self.useCase.executeWithColorsAndIntentCallsCount, 2, "Expected use case to have been executed twice") - - let arguments = self.useCase.executeWithColorsAndIntentReceivedArguments - - XCTAssertNotIdentical(arguments?.colors as? ColorsGeneratedMock, self.colors, "Expected theme to have changed") - } - - func test_intent_updates_line_color_updates() { - // GIVEN - let sut = self.sut(intent: .basic) - - // WHEN - sut.intent = .main - - // THEN - XCTAssertEqual(self.useCase.executeWithColorsAndIntentCallsCount, 2, "Expected use case to have been called twice") - - let arguments = self.useCase.executeWithColorsAndIntentReceivedArguments - - XCTAssertEqual(arguments?.intent, .main, "Expected argument of use case to be main") - } - - func test_is_enabled_updates_opacity_updates() { - // GIVEN - let sut = self.sut(isEnabled: true) - - let expect = expectation(description: "Expected opacity to have changed") - expect.expectedFulfillmentCount = 2 - - var opacities = [CGFloat]() - sut.$opacity.subscribe(in: &self.cancellables) { opacity in - opacities.append(opacity) - expect.fulfill() - } - - // WHEN - sut.isEnabled = false - - wait(for: [expect], timeout: 0.01) - - // THEN - XCTAssertEqual(opacities, [1.0, self.theme.dims.dim3]) - } - - func test_intent_the_same_no_color_updates() { - // GIVEN - let sut = self.sut(intent: .basic) - - // WHEN - sut.intent = .basic - - // THEN - XCTAssertEqual(self.useCase.executeWithColorsAndIntentCallsCount, 1, "Expected use case to only have been called once") - - let arguments = self.useCase.executeWithColorsAndIntentReceivedArguments - - XCTAssertEqual(arguments?.intent, .basic, "Expected argument of use case to be basic") - } - - func test_is_enabled_not_updated_no_line_color_updates() { - // GIVEN - let sut = self.sut(isEnabled: true) - - let expect = expectation(description: "Expected opacity to have changed") - expect.isInverted = true - - sut.$opacity.dropFirst().subscribe(in: &self.cancellables) { opacity in - expect.fulfill() - } - - // WHEN - sut.isEnabled = true - - wait(for: [expect], timeout: 0.01) - } - - func test_theme_produces_different_line_color() { - - // GIVEN - let colors = [UIColor.red, .blue] - self.useCase.executeWithColorsAndIntentReturnValue = ColorTokenGeneratedMock.init(uiColor: colors[0]) - let sut = self.sut() - - let expect = expectation(description: "Expect line color to have been published") - expect.expectedFulfillmentCount = 2 - - // WHEN - var calls = 0 - sut.$lineColor.subscribe(in: &self.cancellables) { color in - XCTAssertEqual(color.uiColor, colors[calls], "Expected color to be \(calls == 0 ? "red" : "blue")") - expect.fulfill() - calls += 1 - } - - self.useCase.executeWithColorsAndIntentReturnValue = ColorTokenGeneratedMock.init(uiColor: colors[1]) - sut.theme = ThemeGeneratedMock.mocked() - - // THEN - wait(for: [expect], timeout: 1) - } - - private func sut( - intent: ProgressTrackerIntent = .basic, - isEnabled: Bool = true) -> ProgressTrackerTrackViewModel { - return .init( - theme: self.theme, - intent: intent, - isEnabled: isEnabled, - useCase: self.useCase - ) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/ProgressTrackerViewModel.swift b/core/Sources/Components/ProgressTracker/View/ProgressTrackerViewModel.swift deleted file mode 100644 index 056c28aab..000000000 --- a/core/Sources/Components/ProgressTracker/View/ProgressTrackerViewModel.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// ProgressTrackerViewModel.swift -// SparkCore -// -// Created by Michael Zimmermann on 24.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -/// A view model for a Progress Tracker. -final class ProgressTrackerViewModel: ObservableObject where ComponentContent: Equatable { - - var theme: Theme { - didSet { - self.themeDidUpdate() - } - } - - var showDefaultPageNumber: Bool { - get { - return self.content.showDefaultPageNumber - } - set { - guard self.content.showDefaultPageNumber != newValue else { return } - self.content.showDefaultPageNumber = newValue - } - } - - private (set) var isEnabled: Bool = true { - didSet { - guard self.isEnabled != oldValue else { return } - self.updateEnabledIndices() - } - } - - var numberOfPages: Int { - set { - self.content.numberOfPages = newValue - } - get { - return self.content.numberOfPages - } - } - - var currentPageIndex: Int { - set { - self.content.currentPageIndex = min(max(0, newValue), self.content.numberOfPages - 1) - } - get { - return self.content.currentPageIndex - } - } - - // MARK: Published properties - @Published var orientation: ProgressTrackerOrientation { - didSet { - guard self.orientation != oldValue else { return } - self.updateSpacings() - } - } - - @Published var useFullWidth = false - @Published var content: ProgressTrackerContent - @Published var disabledIndices = Set() - - @Published var spacings: ProgressTrackerSpacing - @Published var font: TypographyFontToken - @Published var labelColor: any ColorToken - @Published var interactionState: ProgressTrackerInteractionState = .none - - @Published var currentPressedIndicator: Int? - - // MARK: Private properties - private var spacingUseCase: ProgressTrackerGetSpacingsUseCaseable - - // MARK: - Initialization - init(theme: Theme, - orientation: ProgressTrackerOrientation, - content: ProgressTrackerContent, - spacingUseCase: ProgressTrackerGetSpacingsUseCaseable = ProgressTrackerGetSpacingsUseCase() - ) { - self.orientation = orientation - self.theme = theme - self.content = content - self.spacingUseCase = spacingUseCase - - self.spacings = spacingUseCase.execute(spacing: theme.layout.spacing, orientation: orientation) - - self.font = theme.typography.body2Highlight - self.labelColor = theme.colors.base.onSurface - } - - private func themeDidUpdate() { - self.updateSpacings() - self.updateFont() - self.updateLabelColor() - } - - private func updateSpacings() { - self.spacings = spacingUseCase.execute(spacing: self.theme.layout.spacing, orientation: self.orientation) - } - - private func updateFont() { - self.font = self.theme.typography.body2Highlight - } - - private func updateLabelColor() { - self.labelColor = self.theme.colors.base.onSurface - } - - func labelOpacity(forIndex index: Int) -> CGFloat { - return self.labelOpacity(isDisabled: self.disabledIndices.contains(index)) - } - - func labelOpacity(isDisabled: Bool) -> CGFloat { - return isDisabled ? self.theme.dims.dim1 : 1.0 - } - - @discardableResult - func setIsEnabled(_ isEnabled: Bool) -> Self { - self.isEnabled = isEnabled - return self - } - - func setIsEnabled(isEnabled: Bool, forIndex index: Int) { - if isEnabled { - self.disabledIndices.remove(index) - } else { - self.disabledIndices.insert(index) - } - } - - func isEnabled(at index: Int) -> Bool { - return !self.disabledIndices.contains(index) - } - - func isSelected(at index: Int) -> Bool { - return self.content.currentPageIndex == index - } - - func isHighlighted(at index: Int) -> Bool { - return self.currentPressedIndicator == index - } - - private func updateEnabledIndices() { - if !self.isEnabled { - for i in (0..() - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.theme = ThemeGeneratedMock.mocked() - self.spacingsUseCase = ProgressTrackerGetSpacingsUseCaseableGeneratedMock() - self.spacingsUseCase.executeWithSpacingAndOrientationReturnValue = .stub() - } - - // MARK: - Tests - func test_vertical_setup() { - // GIVEN - let _ = self.sut(orientation: .vertical) - - // THEN - XCTAssertEqual(self.spacingsUseCase.executeWithSpacingAndOrientationCallsCount, 1, "Use case expected to have been called") - - let spacingsParameters = self.spacingsUseCase.executeWithSpacingAndOrientationReceivedArguments - - XCTAssertEqual(spacingsParameters?.orientation, .vertical, "Orientation expected to have been vertical") - } - - func test_horizontal_setup() { - // GIVEN - let _ = self.sut(orientation: .horizontal) - - // THEN - XCTAssertEqual(self.spacingsUseCase.executeWithSpacingAndOrientationCallsCount, 1, "Spacings use case expected to have been called") - - let spacingsParameters = self.spacingsUseCase.executeWithSpacingAndOrientationReceivedArguments - - XCTAssertEqual(spacingsParameters?.orientation, .horizontal, "Orientation expected to be have been horizontal") - } - - func test_theme_changed() { - // GIVEN - let sut = self.sut(orientation: .horizontal) - let expectation = expectation(description: "Wait for spacing & font change") - expectation.expectedFulfillmentCount = 2 - - Publishers.Zip(sut.$spacings, sut.$font) - .sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // WHEN - sut.theme = ThemeGeneratedMock.mocked() - - // THEN - wait(for: [expectation], timeout: 1) - } - - func test_orientation_is_changed_spacings_updated() { - // GIVEN - let sut = self.sut(orientation: . vertical) - let expectation = expectation(description: "Wait for spacings to be change") - expectation.expectedFulfillmentCount = 2 - - sut.$spacings.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // WHEN - sut.orientation = .horizontal - - // THEN - wait(for: [expectation], timeout: 1) - let arguments = self.spacingsUseCase.executeWithSpacingAndOrientationReceivedArguments - XCTAssertIdentical(arguments?.spacing as? LayoutSpacingGeneratedMock, self.theme.layout.spacing as? LayoutSpacingGeneratedMock) - XCTAssertEqual(arguments?.orientation, .horizontal) - XCTAssertEqual(self.spacingsUseCase.executeWithSpacingAndOrientationCallsCount, 2) - } - - func test_orientation_is_not_changed_spacings_is_not_updated() { - // GIVEN - let sut = self.sut(orientation: . vertical) - - // WHEN - sut.orientation = .vertical - - // THEN - XCTAssertEqual(self.spacingsUseCase.executeWithSpacingAndOrientationCallsCount, 1) - - } - - func test_change_show_default_page_number() { - // GIVEN - let sut = sut(orientation: .vertical, showDefaultPageNumber: false) - - XCTAssertFalse(sut.content.showDefaultPageNumber, "Expected show default page number of content to be false") - XCTAssertFalse(sut.showDefaultPageNumber, "Expected show default page number to be false") - - // WHEN - sut.showDefaultPageNumber = true - - // THEN - XCTAssertTrue(sut.content.showDefaultPageNumber, "Expected show default page number of content to be true") - XCTAssertTrue(sut.showDefaultPageNumber, "Expected show default page number to be true") - } - - func test_change_number_of_pages() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 2) - - XCTAssertEqual(sut.content.numberOfPages, 2, "Expected number of pages of content to be 2") - XCTAssertEqual(sut.numberOfPages, 2, "Expected number of pages to be 2") - - // WHEN - sut.numberOfPages = 6 - - // THEN - XCTAssertEqual(sut.content.numberOfPages, 6, "Expected number of pages of content to be 6") - XCTAssertEqual(sut.numberOfPages, 6, "Expected number of pages to be 6") - } - - func test_change_current_page_index() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - let allGivenExpected: [(given: Int, expected: Int)] = [(-1, 0), (0, 0), (3, 3), (4, 3) ] - - for givenExpected in allGivenExpected { - // WHEN - sut.currentPageIndex = givenExpected.given - - // THEN - XCTAssertEqual(sut.currentPageIndex, givenExpected.expected, "Expected current page index when set to \(givenExpected.given) to be \(givenExpected.expected)") - } - } - - func test_set_disabled() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - - for i in 0..<4 { - XCTAssertEqual(sut.labelOpacity(forIndex: i), 1.0, "Expected opacity of label at \(i) to be 1.0") - } - - // WHEN - sut.setIsEnabled(false) - - // THEN - XCTAssertEqual(sut.disabledIndices.count, 4, "Expected all indices to be disabled") - - for i in 0..<4 { - XCTAssertEqual(sut.labelOpacity(forIndex: i), self.theme.dims.dim1, "Expected opacity of label at \(i) to be dim3") - } - } - - func test_set_enabled_single_item() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - - // WHEN - sut.setIsEnabled(false) - sut.setIsEnabled(isEnabled: true, forIndex: 0) - - // THEN - XCTAssertEqual(sut.disabledIndices, Set(arrayLiteral: 1, 2, 3)) - } - - func test_set_disabled_single_item() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - - // WHEN - sut.setIsEnabled(isEnabled: false, forIndex: 0) - - // THEN - XCTAssertEqual(sut.disabledIndices, Set(arrayLiteral: 0)) - } - - func test_set_enabled_after_disabled() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - - // WHEN - sut.setIsEnabled(false) - sut.setIsEnabled(true) - - // THEN - XCTAssertEqual(sut.disabledIndices.count, 0) - } - - func test_enabled_color() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - - // THEN - XCTAssertEqual(sut.disabledIndices.count, 0) - } - - func test_disabled_opacity_published() { - // GIVEN - let sut = sut(orientation: .vertical, numberOfPages: 4) - - let expect = expectation(description: "disabled index should be published") - expect.expectedFulfillmentCount = 2 - - var publishedCount = 0 - - sut.$disabledIndices.subscribe(in: &self.cancellables) { disabledIndices in - if publishedCount == 1 { - XCTAssertEqual(disabledIndices, Set([0])) - } - publishedCount += 1 - expect.fulfill() - } - - // WHEN - sut.setIsEnabled(isEnabled: false, forIndex: 0) - - // THEN - wait(for: [expect], timeout: 0.01) - } - - // MARK: - Private helper functions - private func sut( - orientation: ProgressTrackerOrientation, - numberOfPages: Int = 4, - showDefaultPageNumber: Bool = true - ) -> ProgressTrackerViewModel { - let content = ProgressTrackerContent( - numberOfPages: numberOfPages, - currentPageIndex: 0, - showDefaultPageNumber: showDefaultPageNumber) - return ProgressTrackerViewModel( - theme: self.theme, - orientation: orientation, - content: content, - spacingUseCase: self.spacingsUseCase - ) - } -} - -private extension ProgressTrackerSpacing { - static func stub() -> ProgressTrackerSpacing { - return ProgressTrackerSpacing(trackIndicatorSpacing: 1.0, minLabelSpacing: 2.0) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerAccessibilityTraitsViewModifier.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerAccessibilityTraitsViewModifier.swift deleted file mode 100644 index 12bf069f0..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerAccessibilityTraitsViewModifier.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ProgressTrackerAccessibilityTraitsViewModifier.swift -// SparkCore -// -// Created by Michael Zimmermann on 11.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -struct ProgressTrackerAccessibilityTraitsViewModifier: ViewModifier { - typealias AccessibilityIdentifier = ProgressTrackerAccessibilityIdentifier - - private let viewModel: ProgressTrackerViewModel - private let index: Int - - init(viewModel: ProgressTrackerViewModel, index: Int) { - self.viewModel = viewModel - self.index = index - } - - func body(content: Content) -> some View { - return content - .accessibilityElement(children: .combine) - .accessibilityAddTraits(self.getAccessibilityTraits(index: self.index)) - .accessibilityIdentifier(AccessibilityIdentifier.indicator(forIndex: self.index)) - .accessibilityValue("\(self.index)") - } - - private func getAccessibilityTraits(index: Int) -> AccessibilityTraits { - - var accessibilityTraits: AccessibilityTraits = AccessibilityTraits() - - if self.viewModel.interactionState != .none { - _ = accessibilityTraits.insert(.isButton) - } - if index == self.viewModel.currentPageIndex { - _ = accessibilityTraits.insert(.isSelected) - } - - return accessibilityTraits - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerContinuousGestureHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerContinuousGestureHandlerTests.swift deleted file mode 100644 index 7e540a8d3..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerContinuousGestureHandlerTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// ProgressTrackerContinuousGestureHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 27.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class ProgressTrackerContinuousGestureHandlerTests: XCTestCase { - - var _currentPageIndex: Int = 0 - - lazy var currentPageIndex = Binding( - get: { return self._currentPageIndex }, - set: { self._currentPageIndex = $0 } - ) - - private var _currentTouchedPageIndex: Int? - - lazy var currentTouchedPageIndex = Binding( - get: { return self._currentTouchedPageIndex }, - set: { self._currentTouchedPageIndex = $0 } - ) - - private var indicators = (0...3).map{ CGRect(x: $0 * 40, y: 0, width: 40, height: 40)} - - private var disabledIndices = Set() - - // MARK: - Tests - func test_index_0_is_current_1_may_be_selected() { - // Given - self._currentPageIndex = 0 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 1, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 1, "Current page is not updated") - } - - func test_drag_along_view() { - // Given - self._currentPageIndex = 0 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 1, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onChanged(location: CGPoint(x: 121, y: 0)) - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 2, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 1, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 159, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 2, "Current page is not updated") - } - - func test_index_2_is_current_0_may_be_selected() { - // Given - self._currentPageIndex = 2 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 0, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 1, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 2, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 1, "Current page is not updated") - } - - func test_cant_skip_disabled() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(1) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - func test_touch_outside_frame_ignored() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(1) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 161, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Current touched page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 161, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - // MARK: Private helpers - private func sut() -> ProgressTrackerContinuousGestureHandler { - return .init( - currentPageIndex: self.currentPageIndex, - currentTouchedPageIndex: self.currentTouchedPageIndex, - indicators: self.indicators, - frame: CGRect(x: 0, y: 0, width: self.indicators.count * 40, height: 40), - disabledIndices: self.disabledIndices) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerDiscreteGestureHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerDiscreteGestureHandlerTests.swift deleted file mode 100644 index 9e9a22480..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerDiscreteGestureHandlerTests.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// ProgressTrackerDiscreteGestureHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 27.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class ProgressTrackerDiscreteGestureHandlerTests: XCTestCase { - - var _currentPageIndex: Int = 0 - - lazy var currentPageIndex = Binding( - get: { return self._currentPageIndex }, - set: { self._currentPageIndex = $0 } - ) - - private var _currentTouchedPageIndex: Int? - - lazy var currentTouchedPageIndex = Binding( - get: { return self._currentTouchedPageIndex }, - set: { self._currentTouchedPageIndex = $0 } - ) - - private var indicators = (0...3).map{ CGRect(x: $0 * 40, y: 0, width: 40, height: 40)} - - private var disabledIndices = Set() - - // MARK: - Tests - func test_index_0_is_current_1_may_be_selected() { - // Given - self._currentPageIndex = 0 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 1, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 1, "Current page is not updated") - } - - func test_index_2_is_current_0_may_be_selected() { - // Given - self._currentPageIndex = 2 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 0, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 1, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 2, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 1, "Current page is not updated") - } - - func test_cant_skip_disabled() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(1) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - func test_touch_outside_frame_ignored() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(1) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 161, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Current touched page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 161, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - // MARK: Private helpers - private func sut() -> ProgressTrackerDiscreteGestureHandler { - return .init( - currentPageIndex: self.currentPageIndex, - currentTouchedPageIndex: self.currentTouchedPageIndex, - indicators: self.indicators, - frame: CGRect(x: 0, y: 0, width: self.indicators.count * 40, height: 40), - disabledIndices: self.disabledIndices) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerGestureHandler.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerGestureHandler.swift deleted file mode 100644 index fd8b664b3..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerGestureHandler.swift +++ /dev/null @@ -1,199 +0,0 @@ -// -// ProgressTrackerGestureHandler.swift -// SparkCore -// -// Created by Michael Zimmermann on 21.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -// MARK: - Protocol -/// Touch handlers of the swiftui progress tracker -protocol ProgressTrackerGestureHandling { - func onChanged(location: CGPoint) - func onEnded(location: CGPoint) - func onCancelled() -} - -/// A gesture handler that has no actions. -final class ProgressTrackerNoneGestureHandler: ProgressTrackerGestureHandling { - func onChanged(location: CGPoint) {} - func onEnded(location: CGPoint) {} - func onCancelled() {} -} - -/// An `abstract` gesture handler. -class ProgressTrackerGestureHandler: ProgressTrackerGestureHandling { - - @Binding var currentPageIndex: Int - @Binding var currentTouchedPageIndex: Int? - let indicators: [CGRect] - let frame: CGRect - let disabledIndeces: Set - - init(currentPageIndex: Binding, - currentTouchedPageIndex: Binding, - indicators: [CGRect], - frame: CGRect, - disabledIndices: Set - ) { - self._currentPageIndex = currentPageIndex - self._currentTouchedPageIndex = currentTouchedPageIndex - self.indicators = indicators - self.frame = frame - self.disabledIndeces = disabledIndices - } - - func onChanged(location: CGPoint) {} - func onEnded(location: CGPoint) {} - func onCancelled() {} -} - -/// A gesture handler, that let's the user access any page of the page tracker -final class ProgressTrackerIndependentGestureHandler: ProgressTrackerGestureHandler { - - override func onChanged(location: CGPoint) { - guard self.frame.contains(location) else { - self.currentTouchedPageIndex = nil - return - } - - let index = self.indicators.index(closestTo: location) - if let index, - self.currentTouchedPageIndex == nil, - !self.disabledIndeces.contains(index) - { - self.currentTouchedPageIndex = index - } - } - - override func onEnded(location: CGPoint) { - guard self.frame.contains(location) else { - self.currentTouchedPageIndex = nil - return - } - - guard let currentTouchedPageIndex else { - return - } - self.currentPageIndex = currentTouchedPageIndex - self.currentTouchedPageIndex = nil - } - - override func onCancelled() { - self.currentTouchedPageIndex = nil - } -} - -/// A gesture handler that only allows access to the direct neighboring pages and one one page change is allowed during one touch handling. -final class ProgressTrackerDiscreteGestureHandler: ProgressTrackerGestureHandler { - - override func onChanged(location: CGPoint) { - guard self.frame.contains(location) else { - self.currentTouchedPageIndex = nil - return - } - - guard let index = self.indicators.index(closestTo: location) else { return } - - let currentPressedPageIndex: Int? - - if index > self.currentPageIndex { - currentPressedPageIndex = self.currentPageIndex + 1 - } else if index < self.currentPageIndex { - currentPressedPageIndex = self.currentPageIndex - 1 - } else { - currentPressedPageIndex = nil - } - - if let nextIndex = currentPressedPageIndex, self.disabledIndeces.contains(nextIndex) { - return - } - - if self.currentTouchedPageIndex != currentPressedPageIndex { - self.currentTouchedPageIndex = currentPressedPageIndex - } - } - - override func onEnded(location: CGPoint) { - guard self.frame.contains(location) else { - self.currentTouchedPageIndex = nil - return - } - - guard let currentTouchedPageIndex else { return } - - self.currentPageIndex = currentTouchedPageIndex - self.currentTouchedPageIndex = nil - } - - override func onCancelled() { - self.currentTouchedPageIndex = nil - } - -} - -/// A gesture handler, that allows the user to swipe across all indicators of the progress tracker and switch from one page to the next in one `drag` gesture. -final class ProgressTrackerContinuousGestureHandler: ProgressTrackerGestureHandler { - - override func onChanged(location: CGPoint) { - guard self.frame.contains(location) else { - self.currentTouchedPageIndex = nil - return - } - - guard let index = self.indicators.index(closestTo: location) else { return } - - if let currentTouchedPageIndex { - let nextPageIndex: Int? - if index > currentTouchedPageIndex { - nextPageIndex = currentTouchedPageIndex + 1 - } else if index < currentTouchedPageIndex { - nextPageIndex = currentTouchedPageIndex - 1 - } else { - nextPageIndex = nil - } - - if let nextPageIndex, self.disabledIndeces.contains(nextPageIndex) { - return - } else if let nextPageIndex { - self.currentPageIndex = currentTouchedPageIndex - self.currentTouchedPageIndex = nextPageIndex - } - - } else { - let currentPressedPageIndex: Int? - - if index > self.currentPageIndex { - currentPressedPageIndex = self.currentPageIndex + 1 - } else if index < self.currentPageIndex { - currentPressedPageIndex = self.currentPageIndex - 1 - } else { - currentPressedPageIndex = nil - } - - if let currentPressedPageIndex, self.disabledIndeces.contains(currentPressedPageIndex) { - return - } else if self.currentTouchedPageIndex != currentPressedPageIndex { - self.currentTouchedPageIndex = currentPressedPageIndex - } - } - } - - override func onEnded(location: CGPoint) { - guard self.frame.contains(location) else { - self.currentTouchedPageIndex = nil - return - } - - guard let currentTouchedPageIndex else { return } - - self.currentPageIndex = currentTouchedPageIndex - self.currentTouchedPageIndex = nil - } - - override func onCancelled() { - self.currentTouchedPageIndex = nil - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerHorizontalView.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerHorizontalView.swift deleted file mode 100644 index ac3e3086e..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerHorizontalView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// ProgressTrackerHorizontalView.swift -// SparkCore -// -// Created by Michael Zimmermann on 16.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A progress tracker with a horizontal layout -struct ProgressTrackerHorizontalView: View { - typealias Content = ProgressTrackerContent - typealias AccessibilityIdentifier = ProgressTrackerAccessibilityIdentifier - - @ObservedObject private var viewModel: ProgressTrackerViewModel - private let intent: ProgressTrackerIntent - private let variant: ProgressTrackerVariant - private let size: ProgressTrackerSize - @ScaledMetric private var scaleFactor = 1.0 - @Binding var currentPageIndex: Int - - private var spacing: CGFloat { - return self.viewModel.spacings.minLabelSpacing * self.scaleFactor - } - - private var trackSize: CGFloat { - return 1.0 * scaleFactor - } - - private var trackIndicatorSpacing: CGFloat { - return self.viewModel.spacings.trackIndicatorSpacing * self.scaleFactor - } - - // MARK: - Initialization - init( - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - currentPageIndex: Binding, - viewModel: ProgressTrackerViewModel - ) { - self.viewModel = viewModel - self.variant = variant - self.size = size - self.intent = intent - self._currentPageIndex = currentPageIndex - } - - // MARK: - Body - var body: some View { - ZStack(alignment: .topLeading) { - self.horizontalLayout() - .coordinateSpace(name: AccessibilityIdentifier.identifier) - } - .overlayPreferenceValue(ProgressTrackerSizePreferences.self) { preferences in - self.horizontalTracks(preferences: preferences) - } - } - - // MARK: - Private functions - @ViewBuilder - private func horizontalLayout() -> some View { - HStack(alignment: .top, spacing: self.spacing) { - ForEach((0.. some View { - let trackSpacing = self.trackIndicatorSpacing - GeometryReader { geometry in - ForEach((1.. ProgressTrackerTrackView { - ProgressTrackerTrackView( - theme: self.viewModel.theme, - intent: self.intent, - orientation: self.viewModel.orientation) - } - - @ViewBuilder - private func content(at index: Int) -> some View { - VStack(alignment: .center) { - if let label = self.viewModel.content.getAttributedLabel(atIndex: index) { - self.indicator(at: index) - self.label(label, at: index) - } else { - self.indicator(at: index) - .accessibilityLabel(self.viewModel.content.getIndicatorAccessibilityLabel(atIndex: index)) - } - } - } - - @ViewBuilder - private func label(_ label: AttributedString, at index: Int) -> some View { - Text(label) - .multilineTextAlignment(.center) - .font(self.viewModel.font.font) - .foregroundStyle(self.viewModel.labelColor.color) - .opacity(self.viewModel.labelOpacity(forIndex: index)) - } - - @ViewBuilder - private func indicator(at index: Int) -> some View { - ProgressTrackerIndicatorView( - theme: self.viewModel.theme, - intent: self.intent, - variant: self.variant, - size: self.size, - content: self.viewModel.content.pageContent(atIndex: index)) - .selected(self.viewModel.isSelected(at: index)) - .highlighted(self.viewModel.isHighlighted(at: index)) - .disabled(!self.viewModel.isEnabled(at: index)) - .overlay { - GeometryReader { geo in - Color.clear.anchorPreference(key: ProgressTrackerSizePreferences.self, value: .bounds) { anchor in - [index: geo.frame(in: .named(AccessibilityIdentifier.identifier)).normalized] - } - } - } - } -} - -private extension View { - func accessibilityAttributes(viewModel: ProgressTrackerViewModel, index: Int) -> some View { - return modifier(ProgressTrackerAccessibilityTraitsViewModifier(viewModel: viewModel, index: index)) - } -} - -/// Horizontal distance from one point to the other including an offset. -private extension CGRect { - func xDistance(to other: CGRect, offset: CGFloat = 0) -> CGFloat { - return max((other.minX - self.maxX) - (offset * 2.0), 0) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndependentGestureHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndependentGestureHandlerTests.swift deleted file mode 100644 index 132743798..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndependentGestureHandlerTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// ProgressTrackerIndependentGestureHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 27.03.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class ProgressTrackerIndependentGestureHandlerTests: XCTestCase { - - var _currentPageIndex: Int = 0 - - lazy var currentPageIndex = Binding( - get: { return self._currentPageIndex }, - set: { self._currentPageIndex = $0 } - ) - - private var _currentTouchedPageIndex: Int? - - lazy var currentTouchedPageIndex = Binding( - get: { return self._currentTouchedPageIndex }, - set: { self._currentTouchedPageIndex = $0 } - ) - - private var indicators = (0...3).map{ CGRect(x: $0 * 40, y: 0, width: 40, height: 40)} - - private var disabledIndices = Set() - - // MARK: - Tests - func test_index_0_is_current_3_may_be_selected() { - // Given - self._currentPageIndex = 0 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 140, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 3, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 140, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 3, "Current page is not updated") - } - - func test_index_3_is_current_0_may_be_selected() { - // Given - self._currentPageIndex = 3 - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 0, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 0, "Next select page is 1") - XCTAssertEqual(self._currentPageIndex, 3, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 0, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - func test_can_skip_disabled() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(1) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertEqual(self._currentTouchedPageIndex, 2, "Current touched page is 2") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 2, "Current page is not updated") - } - - func test_touch_outside_frame_ignored() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(1) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 161, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Current touched page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 161, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - func test_disabled_cant_be_selected() { - // Given - self._currentPageIndex = 0 - self.disabledIndices.insert(2) - let sut = self.sut() - - // When - sut.onChanged(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Current touched page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - - // When - sut.onEnded(location: CGPoint(x: 81, y: 0)) - - // Then - XCTAssertNil(self._currentTouchedPageIndex, "Next select page is nil") - XCTAssertEqual(self._currentPageIndex, 0, "Current page is not updated") - } - - // MARK: Private helpers - private func sut() -> ProgressTrackerIndependentGestureHandler { - return .init( - currentPageIndex: self.currentPageIndex, - currentTouchedPageIndex: self.currentTouchedPageIndex, - indicators: self.indicators, - frame: CGRect(x: 0, y: 0, width: self.indicators.count * 40, height: 40), - disabledIndices: self.disabledIndices) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndicatorView.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndicatorView.swift deleted file mode 100644 index 68465a628..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerIndicatorView.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// ProgressTrackerIndicatorView.swift -// SparkCore -// -// Created by Michael Zimmermann on 13.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// The indicator view is the round indicator which may contain text (max 2 characters) or an image. -struct ProgressTrackerIndicatorView: View { - @ObservedObject private var viewModel: ProgressTrackerIndicatorViewModel - - @ScaledMetric var scaleFactor: CGFloat = 1.0 - - private var imageHeight: CGFloat { - return self.scaleFactor * ProgressTrackerConstants.iconHeight - } - - private var borderWidth: CGFloat { - return self.scaleFactor * ProgressTrackerConstants.borderWidth - } - - // MARK: - Initialization - init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - content: ProgressTrackerIndicatorContent) { - let viewModel = ProgressTrackerIndicatorViewModel( - - theme: theme, - intent: intent, - variant: variant, - size: size, - content: content, - state: .normal - ) - - self.viewModel = viewModel - } - - // MARK: - Body - var body: some View { - ZStack(alignment: .center) { - - Circle() - .fill(self.viewModel.colors.background.color) - - if self.viewModel.size != .small { - if let image = self.viewModel.content.indicatorImage { - image.resizable() - .renderingMode(.template) - .foregroundStyle(self.viewModel.colors.content.color) - .scaledToFit() - .frame( - width: self.imageHeight, - height: self.imageHeight - ) - } else if let label = self.viewModel.content.label { - Text(String(label)) - .font(self.viewModel.font.font) - .foregroundStyle(self.viewModel.colors.content.color) - } - } - - Circle() - .strokeBorder( - self.viewModel.colors.outline.color, - lineWidth: self.borderWidth - ) - } - .frame(width: self.viewModel.size.rawValue * self.scaleFactor, height: self.viewModel.size.rawValue * self.scaleFactor) - .compositingGroup() - .opacity(self.viewModel.opacity) - } - - // MARK: Modifiers - func highlighted(_ isHighlighted: Bool) -> Self { - self.viewModel.set(highlighted: isHighlighted) - return self - } - - func selected(_ isSelected: Bool) -> Self { - self.viewModel.set(selected: isSelected) - return self - } - - func disabled(_ isDisabled: Bool) -> some View { - self.viewModel.set(enabled: !isDisabled) - return self - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerTrackView.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerTrackView.swift deleted file mode 100644 index d25013f50..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerTrackView.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// ProgressTrackerTrackView.swift -// SparkCore -// -// Created by Michael Zimmermann on 15.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// The track view is the small divider line between the indicators -struct ProgressTrackerTrackView: View { - @ObservedObject private var viewModel: ProgressTrackerTrackViewModel - private let orientation: ProgressTrackerOrientation - - @ScaledMetric private var scaleFactor = 1.0 - - private var trackSize: CGFloat { - return self.scaleFactor * ProgressTrackerConstants.trackSize - } - - // MARK: - Initialization - init(theme: Theme, - intent: ProgressTrackerIntent, - orientation: ProgressTrackerOrientation) { - self.orientation = orientation - self.viewModel = ProgressTrackerTrackViewModel(theme: theme, intent: intent) - } - - // MARK: - Body - var body: some View { - if self.orientation == .horizontal { - self.line() - .frame(height: self.trackSize) - .frame(minWidth: self.trackSize) - } else { - self.line() - .frame(width: self.trackSize) - .frame(minHeight: self.trackSize) - } - } - - @ViewBuilder - private func line() -> some View { - Rectangle() - .fill(self.viewModel.lineColor.color) - .opacity(self.viewModel.opacity) - } - - // MARK: - View Modifiers - func disabled(_ isDisabled: Bool) -> some View { - self.viewModel.isEnabled = !isDisabled - return self - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerVerticalView.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerVerticalView.swift deleted file mode 100644 index 456f34211..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerVerticalView.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// ProgressTrackerVerticalView.swift -// SparkCore -// -// Created by Michael Zimmermann on 16.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A progress tracker with a horizontal layout -struct ProgressTrackerVerticalView: View { - typealias Content = ProgressTrackerContent - typealias AccessibilityIdentifier = ProgressTrackerAccessibilityIdentifier - - @ObservedObject private var viewModel: ProgressTrackerViewModel - private let intent: ProgressTrackerIntent - private let variant: ProgressTrackerVariant - private let size: ProgressTrackerSize - @ScaledMetric private var scaleFactor = 1.0 - @Binding var currentPageIndex: Int - - private var spacing: CGFloat { - return self.viewModel.spacings.minLabelSpacing * self.scaleFactor - } - - private var trackSize: CGFloat { - return 1.0 * scaleFactor - } - - private var verticalStackSpacing: CGFloat { - return (self.trackIndicatorSpacing * 2.0) + self.trackSize - } - - private var trackIndicatorSpacing: CGFloat { - return self.viewModel.spacings.trackIndicatorSpacing * self.scaleFactor - } - - // MARK: - Initialization - init( - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - currentPageIndex: Binding, - viewModel: ProgressTrackerViewModel - ) { - self.viewModel = viewModel - self.variant = variant - self.size = size - self.intent = intent - self._currentPageIndex = currentPageIndex - } - - // MARK: - Body - var body: some View { - ZStack(alignment: .topLeading) { - self.verticalLayout() - .coordinateSpace(name: AccessibilityIdentifier.identifier) - } - .overlayPreferenceValue(ProgressTrackerSizePreferences.self) { preferences in - self.verticalTracks(preferences: preferences) - } - } - - // MARK: - Private functions - @ViewBuilder - private func verticalLayout() -> some View { - VStack(alignment: .leading, spacing: self.verticalStackSpacing) { - ForEach((0.. some View { - HStack(alignment: .xAlignment) { - self.indicator(at: index) - .alignmentGuide(.xAlignment) { $0.height / 2 } - - if let label = self.viewModel.content.getAttributedLabel(atIndex: index) { - self.label(label, at: index) - .fixedSize(horizontal: false, vertical: true) - .alignmentGuide(.xAlignment) { ($0.height - ($0[.lastTextBaseline] - $0[.firstTextBaseline])) / 2 } - } - } - } - - @ViewBuilder - private func verticalTracks(preferences: [Int: CGRect]) -> some View { - let trackSpacing = self.trackIndicatorSpacing - GeometryReader { geometry in - ForEach((1.. ProgressTrackerTrackView { - ProgressTrackerTrackView( - theme: self.viewModel.theme, - intent: self.intent, - orientation: self.viewModel.orientation) - } - - @ViewBuilder - private func content(at index: Int) -> some View { - self.indicator(at: index) - - if let label = self.viewModel.content.getAttributedLabel(atIndex: index) { - self.label(label, at: index) - } - } - - @ViewBuilder - private func label(_ label: AttributedString, at index: Int) -> some View { - Text(label) - .font(self.viewModel.font.font) - .foregroundStyle(self.viewModel.labelColor.color) - .opacity(self.viewModel.labelOpacity(forIndex: index)) - } - - @ViewBuilder - private func indicator(at index: Int) -> some View { - ProgressTrackerIndicatorView( - theme: self.viewModel.theme, - intent: self.intent, - variant: self.variant, - size: self.size, - content: self.viewModel.content.pageContent(atIndex: index)) - .selected(self.viewModel.isSelected(at: index)) - .highlighted(self.viewModel.isHighlighted(at: index)) - .disabled(!self.viewModel.isEnabled(at: index)) - .overlay { - GeometryReader { geo in - Color.clear.anchorPreference(key: ProgressTrackerSizePreferences.self, value: .bounds) { anchor in - [index: geo.frame(in: .named(AccessibilityIdentifier.identifier)).normalized] - } - } - } - } -} - -private extension View { - func accessibilityAttributes(viewModel: ProgressTrackerViewModel, index: Int) -> some View { - return modifier(ProgressTrackerAccessibilityTraitsViewModifier(viewModel: viewModel, index: index)) - } -} - -/// Alignment guide for the label and the indicator. The first line of the label is to be aligned centrally with the indicator. -private extension VerticalAlignment { - private enum XAlignment: AlignmentID { - static func defaultValue(in dimension: ViewDimensions) -> CGFloat { - return dimension[VerticalAlignment.top] - } - } - static let xAlignment = VerticalAlignment(XAlignment.self) -} - -/// Vertical distance from one point to the next including an offset. -private extension CGRect { - func yDistance(to other: CGRect, offset: CGFloat = 0) -> CGFloat { - return max((other.minY - self.maxY) - (offset * 2.0), 0) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerView.swift b/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerView.swift deleted file mode 100644 index 6f510b45d..000000000 --- a/core/Sources/Components/ProgressTracker/View/SwiftUI/ProgressTrackerView.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// ProgressTrackerView.swift -// SparkCore -// -// Created by Michael Zimmermann on 15.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A progress tracker, similar to the UIPageControl -public struct ProgressTrackerView: View { - typealias Content = ProgressTrackerContent - typealias AccessibilityIdentifier = ProgressTrackerAccessibilityIdentifier - - // MARK: - Private properties - @ObservedObject private var viewModel: ProgressTrackerViewModel - private let intent: ProgressTrackerIntent - private let variant: ProgressTrackerVariant - private let size: ProgressTrackerSize - @Binding var currentPageIndex: Int - @Environment(\.isEnabled) private var isEnabled: Bool - - // MARK: - Initialization - /// Initializer - /// - Parameters: - /// - theme: the general theme - /// - intent: The intent defining the colors - /// - variant: Tinted or outlined - /// - size: The default is `medium` - /// - labels: The labels under each indicator - /// - orienation: The default is `horizontal` - /// - currentPageIndex: A binding representing the current page - public init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - labels: [String], - orientation: ProgressTrackerOrientation = .horizontal, - currentPageIndex: Binding - ) { - var content = Content(numberOfPages: labels.count) - content.currentPageIndex = currentPageIndex.wrappedValue - for (index, label) in labels.enumerated() { - content.setAttributedLabel(AttributedString(stringLiteral: label), atIndex: index) - } - - self.init(theme: theme, intent: intent, variant: variant, size: size, orientation: orientation, currentPageIndex: currentPageIndex, content: content) - } - - /// Initializer - /// - Parameters: - /// - theme: the general theme - /// - intent: The intent defining the colors - /// - variant: Tinted or outlined - /// - size: The default is `medium` - /// - numberOfPages: The number of track indicators (pages) - /// - orienation: The default is `horizontal` - /// - currentPageIndex: A binding representing the current page - public init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - numberOfPages: Int, - orientation: ProgressTrackerOrientation = .horizontal, - currentPageIndex: Binding - ) { - var content = Content(numberOfPages: numberOfPages) - content.currentPageIndex = currentPageIndex.wrappedValue - - self.init(theme: theme, intent: intent, variant: variant, size: size, orientation: orientation, currentPageIndex: currentPageIndex, content: content) - } - - init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - orientation: ProgressTrackerOrientation = .horizontal, - currentPageIndex: Binding, - content: Content - ) { - let viewModel = ProgressTrackerViewModel( - theme: theme, - orientation: orientation, - content: content) - - self.viewModel = viewModel - self.variant = variant - self.size = size - self.intent = intent - self._currentPageIndex = currentPageIndex - } - - // MARK: - Body - public var body: some View { - self.progressTrackerView - .accessibilityElement(children: .contain) - .accessibilityIdentifier(AccessibilityIdentifier.identifier) - .accessibilityValue("\(self.currentPageIndex)") - .overlayPreferenceValue(ProgressTrackerSizePreferences.self) { preferences in - if self.viewModel.interactionState != .none { - GeometryReader { geometry in - Color.black.opacity(0.000001) - .gesture(self.dragGesture(bounds: geometry.frame(in: .local), preferences: preferences)) - } - } - } - } - - // MARK: - Private functions - private func dragGesture(bounds: CGRect?, preferences: [Int: CGRect]) -> some Gesture { - - let indicators = preferences.sorted { $0.key < $1.key }.map(\.value) - let frame = bounds ?? .zero - - let gestureHandler = self.gestureHandler(frame: frame, indicators: indicators) - - return DragGesture(minimumDistance: .zero) - .onChanged { value in - gestureHandler.onChanged(location: value.location) - } - .onEnded { value in - gestureHandler.onEnded(location: value.location) - } - } - - @ViewBuilder - private var progressTrackerView: some View { - let viewModel = self.viewModel.setIsEnabled(self.isEnabled) - if viewModel.orientation == .horizontal { - ProgressTrackerHorizontalView(intent: self.intent, variant: self.variant, size: self.size, currentPageIndex: self.$currentPageIndex, viewModel: viewModel) - } else { - ProgressTrackerVerticalView(intent: self.intent, variant: self.variant, size: self.size, currentPageIndex: self.$currentPageIndex, viewModel: viewModel) - } - } - - private func gestureHandler(frame: CGRect, indicators: [CGRect]) -> any ProgressTrackerGestureHandling { - - switch self.viewModel.interactionState { - case .none: - return ProgressTrackerNoneGestureHandler() - case .discrete: - return ProgressTrackerDiscreteGestureHandler( - currentPageIndex: self._currentPageIndex, - currentTouchedPageIndex: self.$viewModel.currentPressedIndicator, - indicators: indicators, - frame: frame, - disabledIndices: self.viewModel.disabledIndices - ) - case .continuous: - return ProgressTrackerContinuousGestureHandler( - currentPageIndex: self._currentPageIndex, - currentTouchedPageIndex: self.$viewModel.currentPressedIndicator, - indicators: indicators, - frame: frame, - disabledIndices: self.viewModel.disabledIndices - ) - case .independent: - return ProgressTrackerIndependentGestureHandler( - currentPageIndex: self._currentPageIndex, - currentTouchedPageIndex: self.$viewModel.currentPressedIndicator, - indicators: indicators, - frame: frame, - disabledIndices: self.viewModel.disabledIndices - ) - } - } - - // MARK: - Public modifiers - /// If use full width is set to true, the horizontal view will try and scale as wide as possible. If it is not true, it will only use as little space as required. - public func useFullWidth(_ fullWidth: Bool) -> Self { - self.viewModel.useFullWidth = fullWidth - return self - } - - /// Set the indicator image at the specified index - /// - Parameters: - /// - image: An optional image. Setting the image to nil will remove it. - /// - forIndex: The index to use the image - public func indicatorImage(_ image: Image?, forIndex index: Int) -> Self { - self.viewModel.content.setIndicatorImage(image, atIndex: index) - return self - } - - /// Set the current indicator image at the given index. This indicator image will be shown when the page is selected - /// - Parameters: - /// - image: An optional image. Setting the image to nil will remove it - /// - forIndex: The page index for the image - public func currentPageIndicatorImage(_ image: Image?, forIndex index: Int) -> Self { - self.viewModel.content.setCurrentPageIndicatorImage(image, atIndex: index) - return self - } - - /// Set an attributed label aligned to the corresponding indicator. This will be below the indicator in a horizontal alignment and to the right of it in a vertical alignment. Setting an attributed label and label are mutually exclusive. Setting a label at the position of an attributed label will overwrite the attributed label. - /// - Parameters: - /// - attributedLabel: An optional attributed label to set at the given index. Setting this value to nil will remove an existing attributedLabel or label at the index. - /// - forIndex: The index of the label - public func attributedLabel(_ attributedLabel: AttributedString?, forIndex index: Int) -> Self { - self.viewModel.content.setAttributedLabel(attributedLabel, atIndex: index) - return self - } - - /// Set a label at the corresponding index. This will overwrite an existing attributed label at the same position. - /// - Parameters: - /// - label: An optional label. Setting it to nil, will remove an existing label or attributed label. - /// - index: The page index - public func label(_ label: String?, forIndex index: Int) -> Self { - let attributedLabel = label.map(AttributedString.init) - self.viewModel.content.setAttributedLabel(attributedLabel, atIndex: index) - return self - } - - /// Set a character on the indicator for the given index. - /// - Parameters: - /// - label: An optional character for the indicator label - /// - forIndex: The index of the indicator - public func indicatorLabel(_ label: String?, forIndex index: Int) -> Self { - self.viewModel.content.setIndicatorLabel(label, atIndex: index) - return self - } - - /// Set the indicator image of the already visited pages - public func completedIndicatorImage(_ image: Image?) -> Self { - self.viewModel.content.completedPageIndicatorImage = image - return self - } - - /// Set the indicator at - public func disable(_ isDisabled: Bool, forIndex index: Int) -> Self { - guard index < self.viewModel.numberOfPages else { return self } - - self.viewModel.setIsEnabled(isEnabled: !isDisabled, forIndex: index) - return self - } - - /// Set the default preferred indicator image - public func preferredIndicatorImage(_ image: Image?) -> Self { - self.viewModel.content.preferredIndicatorImage = image - - return self - } - - /// Set the default image for the current page indicator - public func preferredCurrentPageIndicatorImage(_ image: Image?) -> Self { - self.viewModel.content.preferredCurrentPageIndicatorImage = image - return self - } - - /// Set if the default page number should be shown - public func showDefaultPageNumber(_ showPageNumber: Bool) -> Self { - self.viewModel.showDefaultPageNumber = showPageNumber - return self - } - - /// Set the current interaction state - public func interactionState(_ interactionState: ProgressTrackerInteractionState) -> Self { - self.viewModel.interactionState = interactionState - return self - } -} - -extension CGRect { - var normalized: CGRect { - return CGRect(x: max(self.origin.x, 0), y: max(self.origin.y, 0), width: self.width, height: self.height) - } -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerAccessibilityUIControl.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerAccessibilityUIControl.swift deleted file mode 100644 index 4eebf0040..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerAccessibilityUIControl.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ProgressTrackerAccessibilityUIControl.swift -// Spark -// -// Created by Michael Zimmermann on 15.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit - -final class ProgressTrackerAccessibilityUIControl: UIControl { - - override var isHighlighted: Bool { - didSet { - self.subviews - .compactMap{$0 as? UIControl} - .forEach { $0.isHighlighted = self.isHighlighted } - } - } -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerContinuousUITouchHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerContinuousUITouchHandlerTests.swift deleted file mode 100644 index 88dbe8164..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerContinuousUITouchHandlerTests.swift +++ /dev/null @@ -1,199 +0,0 @@ -// -// ProgressTrackerContinuousUITouchHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -import Combine -import XCTest - -@testable import SparkCore - -final class ProgressTrackerContinuousUITouchHandlerTests: XCTestCase { - - var controls: [UIControl]! - - var sut: ProgressTrackerContinuousUITouchHandler! - var cancellables = Set() - - // MARK: - Setup - override func setUp() { - super.setUp() - self.controls = (0...4) - .map { index in - return CGRect(x: index * 50, y: 0, width: 50, height: 50) - } - .map(UIControl.init(frame:)) - - let sut = ProgressTrackerInteractionState.continuous.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) as? ProgressTrackerContinuousUITouchHandler - - XCTAssertNotNil(sut) - - self.sut = sut - } - - func test_touch_on_current_page_nothing_happens() { - // WHEN - self.sut.beginTracking(location: .zero) - - // THEN - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_touch_on_first_step() { - // WHEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_on_second_step() { - // WHEN - self.sut.beginTracking(location: CGPoint(x: 101, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_left_of_current_index() { - // GIVEN - self.sut.currentPageIndex = 3 - - // WHEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 2) - } - - func test_touch_right_of_current_index() { - // GIVEN - self.sut.currentPageIndex = 3 - - // WHEN - self.sut.beginTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 4) - } - - func test_touch_and_move() { - // GIVEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 301, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 2) - } - - func test_touch_and_move_to_current_page() { - // GIVEN - self.sut.currentPageIndex = 2 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - - // THEN - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_touch_and_move_over_current_page() { - // GIVEN - self.sut.currentPageIndex = 2 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 1, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_value_published_on_end_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - XCTAssertEqual(currentPage, 1) - expect.fulfill() - } - // WHEN - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect]) - - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_value_not_published_on_end_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - expect.isInverted = true - self.sut.beginTracking(location: CGPoint(x: 50, y: 10)) - - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - XCTFail("Nothing should have been published") - expect.fulfill() - } - // WHEN - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect], timeout: 0.01) - - XCTAssertEqual(self.sut.currentPageIndex, 0) - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_value_published_on_continue_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - expect.expectedFulfillmentCount = 4 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - var pages = [Int]() - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - pages.append(currentPage) - expect.fulfill() - } - // WHEN - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - self.sut.continueTracking(location: CGPoint(x: 151, y: 10)) - self.sut.continueTracking(location: CGPoint(x: 201, y: 10)) - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect]) - - XCTAssertEqual(pages, [1, 2, 3, 4]) - } - - func test_highlighted_onmove_tracking() { - // GIVEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - XCTAssertEqual(self.sut.trackingPageIndex, 1) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 0, y: 10)) - - XCTAssertNil(self.sut.trackingPageIndex) - - self.sut.continueTracking(location: CGPoint(x: 51, y: 10)) - self.sut.continueTracking(location: CGPoint(x: 75, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerDiscreteUITouchHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerDiscreteUITouchHandlerTests.swift deleted file mode 100644 index 3f4896f4a..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerDiscreteUITouchHandlerTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// ProgressTrackerDiscreteUITouchHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class ProgressTrackerDiscreteUITouchHandlerTests: XCTestCase { - - var controls: [UIControl]! - - var sut: ProgressTrackerDiscreteUITouchHandler! - var cancellables = Set() - - // MARK: - Setup - override func setUp() { - super.setUp() - self.controls = (0...4) - .map { index in - return CGRect(x: index * 50, y: 0, width: 50, height: 50) - } - .map(UIControl.init(frame:)) - - let sut = ProgressTrackerInteractionState.discrete.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) as? ProgressTrackerDiscreteUITouchHandler - - XCTAssertNotNil(sut) - - self.sut = sut - } - - func test_touch_on_current_page_nothing_happens() { - // WHEN - self.sut.beginTracking(location: .zero) - - // THEN - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_touch_on_first_step() { - // WHEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_on_second_step() { - // WHEN - self.sut.beginTracking(location: CGPoint(x: 101, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_left_of_current_index() { - // GIVEN - self.sut.currentPageIndex = 3 - - // WHEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 2) - } - - func test_touch_right_of_current_index() { - // GIVEN - self.sut.currentPageIndex = 3 - - // WHEN - self.sut.beginTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 4) - } - - func test_touch_and_move() { - // GIVEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 301, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_and_move_to_current_page() { - // GIVEN - self.sut.currentPageIndex = 2 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - - // THEN - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_touch_and_move_over_current_page() { - // GIVEN - self.sut.currentPageIndex = 2 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 1, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_value_published_on_end_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - XCTAssertEqual(currentPage, 1) - expect.fulfill() - } - // WHEN - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect]) - - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_value_not_published_on_end_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - expect.isInverted = true - self.sut.beginTracking(location: CGPoint(x: 50, y: 10)) - - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - XCTFail("Nothing should have been published") - expect.fulfill() - } - // WHEN - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect], timeout: 0.01) - - XCTAssertEqual(self.sut.currentPageIndex, 0) - XCTAssertNil(self.sut.trackingPageIndex) - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndependentUITouchHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndependentUITouchHandlerTests.swift deleted file mode 100644 index dcaa43223..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndependentUITouchHandlerTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// ProgressTrackerIndependentUITouchHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class ProgressTrackerIndependentUITouchHandlerTests: XCTestCase { - - var controls: [UIControl]! - - var sut: ProgressTrackerIndependentUITouchHandler! - var cancellables = Set() - - // MARK: - Setup - override func setUp() { - super.setUp() - self.controls = (0...4) - .map { index in - return CGRect(x: index * 50, y: 0, width: 50, height: 50) - } - .map(UIControl.init(frame:)) - - let sut = ProgressTrackerInteractionState.independent.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) as? ProgressTrackerIndependentUITouchHandler - - XCTAssertNotNil(sut) - - self.sut = sut - } - - func test_touch_on_current_page_nothing_happens() { - // WHEN - self.sut.beginTracking(location: .zero) - - // THEN - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_touch_on_first_step() { - // WHEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_on_second_step() { - // WHEN - self.sut.beginTracking(location: CGPoint(x: 101, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 2) - } - - func test_touch_left_of_current_index() { - // GIVEN - self.sut.currentPageIndex = 3 - - // WHEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_right_of_current_index() { - // GIVEN - self.sut.currentPageIndex = 3 - - // WHEN - self.sut.beginTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 4) - } - - func test_touch_and_move() { - // GIVEN - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 301, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_and_move_to_current_page() { - // GIVEN - self.sut.currentPageIndex = 2 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_touch_and_move_over_current_page() { - // GIVEN - self.sut.currentPageIndex = 2 - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - self.sut.continueTracking(location: CGPoint(x: 101, y: 10)) - - // WHEN - self.sut.continueTracking(location: CGPoint(x: 1, y: 10)) - - // THEN - XCTAssertEqual(self.sut.trackingPageIndex, 1) - } - - func test_value_published_on_end_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - self.sut.beginTracking(location: CGPoint(x: 51, y: 10)) - - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - XCTAssertEqual(currentPage, 1) - expect.fulfill() - } - // WHEN - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect]) - - XCTAssertNil(self.sut.trackingPageIndex) - } - - func test_value_not_published_on_end_tracking() { - // GIVEN - let expect = expectation(description: "Expect current page to be published") - expect.isInverted = true - self.sut.beginTracking(location: CGPoint(x: 50, y: 10)) - - self.sut.currentPagePublisher.subscribe(in: &self.cancellables) { currentPage in - XCTFail("Nothing should have been published") - expect.fulfill() - } - // WHEN - self.sut.endTracking(location: CGPoint(x: 251, y: 10)) - - // THEN - wait(for: [expect], timeout: 0.01) - - XCTAssertEqual(self.sut.currentPageIndex, 0) - XCTAssertNil(self.sut.trackingPageIndex) - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndicatorUIControl.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndicatorUIControl.swift deleted file mode 100644 index 804a4a9f7..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerIndicatorUIControl.swift +++ /dev/null @@ -1,290 +0,0 @@ -// -// ProgressTrackerIndicatorUIControl.swift -// SparkCore -// -// Created by Michael Zimmermann on 26.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import Foundation -import UIKit - -/// The round small indicator on the progress tracker -final class ProgressTrackerIndicatorUIControl: UIControl { - - private let viewModel: ProgressTrackerIndicatorViewModel - - var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - var intent: ProgressTrackerIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - var content: ProgressTrackerUIIndicatorContent { - get { - return self.viewModel.content - } - set { - self.viewModel.content = newValue - } - } - - var size: ProgressTrackerSize { - get { - return self.viewModel.size - } - set { - self.viewModel.size = newValue - } - } - - var variant: ProgressTrackerVariant { - get { - return self.viewModel.variant - } - set { - self.viewModel.variant = newValue - } - } - - private lazy var indicatorView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.isUserInteractionEnabled = false - return view - }() - - lazy var label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.isUserInteractionEnabled = false - label.adjustsFontForContentSizeCategory = true - label.isHidden = true - label.numberOfLines = 1 - return label - }() - - private lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.isHidden = true - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.isAccessibilityElement = false - imageView.isUserInteractionEnabled = false - imageView.adjustsImageSizeForAccessibilityContentSizeCategory = true - - imageView.setContentCompressionResistancePriority(.required, for: .horizontal) - imageView.setContentCompressionResistancePriority(.required, for: .vertical) - - return imageView - }() - - private var cancellables = Set() - private var heightConstraint: NSLayoutConstraint? - private var imageHeightConstraint: NSLayoutConstraint? - - @ScaledUIMetric var scaleFactor: CGFloat = 1.0 - - private var borderWidth: CGFloat { - return self.scaleFactor * ProgressTrackerConstants.borderWidth - } - - private var imageHeight: CGFloat { - return self.scaleFactor * ProgressTrackerConstants.iconHeight - } - - override var isHighlighted: Bool { - didSet { - self.viewModel.set(highlighted: self.isHighlighted) - } - } - - override var isEnabled: Bool { - didSet { - self.viewModel.set(enabled: self.isEnabled) - if self.isEnabled { - self.accessibilityTraits.remove(.notEnabled) - } else { - self.accessibilityTraits.insert(.notEnabled) - } - } - } - - override var isSelected: Bool { - didSet { - self.viewModel.set(selected: self.isSelected) - if self.isSelected { - self.accessibilityTraits.insert(.selected) - } else { - self.accessibilityTraits.remove(.selected) - } - } - } - - // MARK: - Initialization - convenience init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize, - content: ProgressTrackerUIIndicatorContent) { - let viewModel = ProgressTrackerIndicatorViewModel( - - theme: theme, - intent: intent, - variant: variant, - size: size, - content: content, - state: .normal - ) - - self.init(viewModel: viewModel) - } - - init(viewModel: ProgressTrackerIndicatorViewModel) { - self.viewModel = viewModel - super.init(frame: .zero) - - self.setupView() - self.update(colors: self.viewModel.colors) - self.update(content: self.viewModel.content) - self.update(font: self.viewModel.font) - self.updateBorderWidth() - self.setupSubscriptions() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - self._scaleFactor.update(traitCollection: self.traitCollection) - - if self.traitCollection.hasDifferentSizeCategory(comparedTo: previousTraitCollection) { - self.sizesChanged() - } - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.updateBorderColor() - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Private functions - private func setupView() { - self.addSubviewSizedEqually(self.indicatorView) - self.indicatorView.addSubviewCentered(self.imageView) - self.indicatorView.addSubviewCentered(self.label) - - self.indicatorView.layer.cornerRadius = (self.viewModel.size.rawValue * self.scaleFactor) / 2 - self.alpha = self.viewModel.opacity - - let heightConstraint = self.indicatorView.heightAnchor.constraint(equalToConstant: self.viewModel.size.rawValue * self.scaleFactor) - - let imageHeightConstraint = - self.imageView.heightAnchor.constraint(equalToConstant: self.imageHeight) - - NSLayoutConstraint.activate([ - heightConstraint, - imageHeightConstraint, - self.imageView.widthAnchor - .constraint(equalTo: self.imageView.heightAnchor), - self.indicatorView.widthAnchor.constraint(equalTo: self.indicatorView.heightAnchor) - ]) - self.imageHeightConstraint = imageHeightConstraint - self.heightConstraint = heightConstraint - } - - private func setupSubscriptions() { - self.viewModel.$colors.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] colors in - self?.update(colors: colors) - } - self.viewModel.$size.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] size in - self?.update(size: size) - } - self.viewModel.$content.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] content in - self?.update(content: content) - } - self.viewModel.$font.removeDuplicates(by: { $0.uiFont == $1.uiFont }).subscribe(in: &self.cancellables) { [weak self] font in - self?.update(font: font) - } - self.viewModel.$opacity.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] opacity in - self?.alpha = opacity - } - } - - private func updateBorderWidth() { - self.indicatorView.setBorderWidth(self.borderWidth) - } - - private func update(font: TypographyFontToken) { - self.label.font = font.uiFont - } - - private func update(colors: ProgressTrackerColors) { - self.indicatorView.backgroundColor = colors.background.uiColor - self.imageView.tintColor = colors.content.uiColor - self.label.textColor = colors.content.uiColor - self.updateBorderColor(colors.outline) - } - - private func updateBorderColor() { - self.updateBorderColor(self.viewModel.colors.outline) - } - - private func updateBorderColor(_ color: any ColorToken) { - self.indicatorView.setBorderColor(from: color) - } - - private func update(content: ProgressTrackerUIIndicatorContent) { - self.update(content: content, andSize: self.viewModel.size) - } - - private func update(content: ProgressTrackerUIIndicatorContent, andSize size: ProgressTrackerSize) { - if size == .small { - self.imageView.isHidden = true - self.label.isHidden = true - } else if let image = content.indicatorImage { - self.imageView.image = image - self.imageView.isHidden = false - self.label.isHidden = true - } else if let text = content.label { - self.label.text = String(text) - self.label.isHidden = false - self.imageView.isHidden = true - } else { - self.imageView.isHidden = true - self.label.isHidden = true - } - } - - private func update(size: ProgressTrackerSize) { - self.update(content: self.viewModel.content, andSize: size) - self.heightConstraint?.constant = size.rawValue * self.scaleFactor - self.indicatorView.layer.cornerRadius = (size.rawValue * self.scaleFactor) / 2 - } - - private func sizesChanged() { - self.update(size: self.viewModel.size) - self.imageHeightConstraint?.constant = self.imageHeight - self.indicatorView.setBorderWidth(self.borderWidth) - } - - // MARK: Modifier - /// Change the size of the indicator - func set(size: ProgressTrackerSize) { - self.viewModel.size = size - } -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerTrackUIView.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerTrackUIView.swift deleted file mode 100644 index bc6ec5663..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerTrackUIView.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// ProgressTrackerTrackUIView.swift -// SparkCore -// -// Created by Michael Zimmermann on 30.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import Foundation -import UIKit - -// The small track between indicators in the Progress Tracker -final class ProgressTrackerTrackUIView: UIView { - - @ScaledUIMetric private var scaleFactor: CGFloat = 1.0 - - private lazy var lineView: UIView = { - let lineView = UIView() - lineView.translatesAutoresizingMaskIntoConstraints = false - return lineView - }() - - private var orientation: ProgressTrackerOrientation { - didSet { - guard self.orientation != oldValue else { return } - self.reorganizeView() - } - } - - var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - var intent: ProgressTrackerIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - var isEnabled: Bool { - get { - return self.viewModel.isEnabled - } - set { - guard newValue != self.viewModel.isEnabled else { return } - self.viewModel.isEnabled = newValue - } - } - - private let viewModel: ProgressTrackerTrackViewModel - private var cancellables = Set() - - private var sizeConstraints = [NSLayoutConstraint]() - - private var trackSize: CGFloat { - return self.scaleFactor * ProgressTrackerConstants.trackSize - } - - // MARK: Initialization - init(theme: Theme, - intent: ProgressTrackerIntent, - orientation: ProgressTrackerOrientation) { - self.orientation = orientation - self.viewModel = ProgressTrackerTrackViewModel(theme: theme, intent: intent) - super.init(frame: .zero) - - self.setupView() - self.setupSubscriptions() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if self.traitCollection.hasDifferentSizeCategory(comparedTo: previousTraitCollection) { - - self._scaleFactor.update(traitCollection: self.traitCollection) - - self.updateSizeConstraints() - } - } - - private func setupSubscriptions() { - self.viewModel.$lineColor.subscribe(in: &self.cancellables) { [weak self] newColor in - self?.lineView.backgroundColor = newColor.uiColor - } - - self.viewModel.$opacity.subscribe(in: &self.cancellables) { [weak self] opacity in - self?.lineView.alpha = opacity - } - } - - private func reorganizeView() { - self.lineView.removeFromSuperview() - self.sizeConstraints = [] - - self.setupView() - } - - private func setupView() { - self.addSubviewSizedEqually(self.lineView) - - if self.orientation == .horizontal { - self.sizeConstraints = [ - self.lineView.heightAnchor.constraint(equalToConstant: self.trackSize), - self.lineView.widthAnchor.constraint(greaterThanOrEqualToConstant: self.trackSize) - ] - } else { - self.sizeConstraints = [ - self.lineView.heightAnchor.constraint(greaterThanOrEqualToConstant: self.trackSize), - self.lineView.widthAnchor.constraint(equalToConstant: self.trackSize) - ] - } - - NSLayoutConstraint.activate(self.sizeConstraints) - self.lineView.backgroundColor = self.viewModel.lineColor.uiColor - self.alpha = self.viewModel.opacity - } - - private func updateSizeConstraints() { - for sizeConstraint in self.sizeConstraints { - sizeConstraint.constant = self.trackSize - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControl.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControl.swift deleted file mode 100644 index a6cbb5408..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControl.swift +++ /dev/null @@ -1,917 +0,0 @@ -// -// ProgressTrackerUIControl.swift -// SparkCore -// -// Created by Michael Zimmermann on 29.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import Foundation -import UIKit - -/// A progress tracker, similar to the UIPageControl -public final class ProgressTrackerUIControl: UIControl { - - typealias Content = ProgressTrackerContent - typealias AccessibilityIdentifier = ProgressTrackerAccessibilityIdentifier - - /// The general theme - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - self.didUpdate(theme: newValue) - } - } - - /// The intent defining the colors - public var intent: ProgressTrackerIntent { - didSet { - guard self.intent != oldValue else { return } - self.didUpdate(intent: self.intent) - } - } - - /// The orientation. There are two orientations, horizontal, which is the default, and vertical. - public var orientation: ProgressTrackerOrientation { - get { - return self.viewModel.orientation - } - set { - self.viewModel.orientation = newValue - } - } - - /// The coloring variant, tinted or outlined. - public var variant: ProgressTrackerVariant { - didSet { - self.didUpdate(variant: self.variant) - } - } - - /// The size of the indicator. Small indicator show now content. - public var size: ProgressTrackerSize { - didSet { - self.didUpdate(size: self.size) - } - } - - /// Boolean to enable/disable the control. - public override var isEnabled: Bool { - get { - return self.viewModel.isEnabled - } - set { - self.viewModel.setIsEnabled(newValue) - if newValue { - self.accessibilityTraits.remove(.notEnabled) - } else { - self.accessibilityTraits.insert(.notEnabled) - } - } - } - - /// A boolean determining if the page number should be shown on the indicator by default. - public var showDefaultPageNumber: Bool { - get { - return self.viewModel.showDefaultPageNumber - } - set { - self.viewModel.showDefaultPageNumber = newValue - } - } - - private var subject = PassthroughSubject() - public var publisher: some Publisher { - return self.subject - } - - /// The type of interaction enabled for the Progress Tracker - public var interactionState: ProgressTrackerInteractionState = .none { - didSet { - self.didUpdate(interactionState: self.interactionState) - } - } - - /// The number of pages shown in the Progress Tracker - public var numberOfPages: Int { - set { - self.viewModel.numberOfPages = newValue - } - get { - return self.viewModel.numberOfPages - } - } - - /// The current page. This value represents the index of the current page. - public var currentPageIndex: Int { - set { - self.viewModel.currentPageIndex = newValue - } - get { - return self.viewModel.currentPageIndex - } - } - - /// Enable continuous interaction on the progress tracker. - public var allowsContinuousInteraction: Bool { - set { - self.interactionState = newValue ? .continuous : .discrete - } - get { - return self.interactionState == .continuous && self.isUserInteractionEnabled - } - } - - // MARK: - Private variables - private let viewModel: ProgressTrackerViewModel - - lazy var indicatorViews = [ProgressTrackerIndicatorUIControl]() - private lazy var labels = [UILabel]() - private lazy var hiddenLabels = [UILabel]() - private lazy var trackViews = [ProgressTrackerTrackUIView]() - private lazy var indicatorContainerViews = [UIControl]() - - var viewCount = 0 - - @ScaledUIMetric private var scaleFactor: CGFloat = 1.0 - private var cancellables = Set() - - private var trackSpacingConstraints = [NSLayoutConstraint]() - private var labelSpacingConstraints = [NSLayoutConstraint]() - - private var trackSpacing: CGFloat { - return self.viewModel.spacings.trackIndicatorSpacing * self.scaleFactor - } - - private var labelSpacing: CGFloat { - return self.viewModel.spacings.minLabelSpacing * self.scaleFactor - } - - private var touchHandler: ProgressTrackerUITouchHandling? - - // MARK: Initialization - /// Initializer - /// - Parameters: - /// - theme: the general theme - /// - intent: The intent defining the colors - /// - variant: Tinted or outlined - /// - size: The default is `medium` - /// - labels: The labels under each indicator - /// - orienation: The default is `horizontal` - public convenience init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize = .medium, - labels: [String], - orientation: ProgressTrackerOrientation = .horizontal - ) { - var content = Content(numberOfPages: labels.count, currentPageIndex: 0) - for (index, label) in labels.enumerated() { - content.setAttributedLabel(NSAttributedString(string: label), atIndex: index) - } - - self.init( - theme: theme, - intent: intent, - variant: variant, - size: size, - content: content, - orientation: orientation - ) - } - - // MARK: Initialization - /// Initializer - /// - Parameters: - /// - theme: the general theme - /// - intent: The intent defining the colors - /// - variant: Tinted or outlined - /// - size: The default is `medium` - /// - numberOfPages: The number of track indicators (pages) - /// - orienation: The default is `horizontal` - public convenience init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize = .medium, - numberOfPages: Int, - orientation: ProgressTrackerOrientation = .horizontal - ) { - let content = Content(numberOfPages: numberOfPages, currentPageIndex: 0) - - self.init( - theme: theme, - intent: intent, - variant: variant, - size: size, - content: content, - orientation: orientation - ) - } - - // MARK: - Internal init - init( - theme: Theme, - intent: ProgressTrackerIntent, - variant: ProgressTrackerVariant, - size: ProgressTrackerSize = .medium, - content: Content, - orientation: ProgressTrackerOrientation = .horizontal - ) { - - let viewModel = ProgressTrackerViewModel( - theme: theme, - orientation: orientation, - content: content) - - self.viewModel = viewModel - self.variant = variant - self.size = size - self.intent = intent - - super.init(frame: .zero) - - self.setupView(content: content, orientation: orientation) - self.setupSubscriptions() - self.enableTouch() - self.addPanGestureToPreventCancelTracking() - self.isUserInteractionEnabled = false - self.accessibilityContainerType = .semanticGroup - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if self.traitCollection.hasDifferentSizeCategory(comparedTo: previousTraitCollection) { self._scaleFactor.update(traitCollection: self.traitCollection) - self.didUpdate(spacings: self.viewModel.spacings) - } - } - - // MARK: - Handle touch events - public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - let touchHandler = self.interactionState.touchHandler( - currentPageIndex: self.currentPageIndex, - indicatorViews: self.indicatorContainerViews) - - self.touchHandler = touchHandler - touchHandler.currentPagePublisher.subscribe(in: &self.cancellables) { [weak self] index in - self?.updateCurrentPageTrackingIndex(index) - } - - touchHandler.beginTracking(location: touch.location(in: self)) - - return true - } - - public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - - if !self.isHighlighted { - self.cancelHighlighted() - } else { - self.touchHandler?.continueTracking(location: touch.location(in: self)) - } - - return true - } - - public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { - self.cancelHighlighted() - - guard let location = touch?.location(in: self), self.isHighlighted else { - return - } - - self.touchHandler?.endTracking(location: location) - - self.touchHandler = nil - } - - // MARK: Touch handling actions - private func updateCurrentPageTrackingIndex(_ index: Int) { - self.cancelHighlighted() - - self.currentPageIndex = index - - self.subject.send(index) - self.sendActions(for: .valueChanged) - } - - // MARK: - Private functions - private func cancelHighlighted() { - guard let index = self.touchHandler?.trackingPageIndex else { return } - self.indicatorViews[safe: index]?.isHighlighted = false - } - - // MARK: Private functions - private func createContainerViews(numberOfPages: Int) -> [UIControl] { - guard numberOfPages > 0 else { return [] } - - return (0.. [ProgressTrackerIndicatorUIControl] { - guard content.numberOfPages > 0 else { return [] } - - return (0.. - [UILabel] { - guard content.hasLabel else { return [] } - return (0.., currentPageIndex: Int) { - - if self.viewModel.content.hasLabel { - let buttons = zip(self.indicatorViews, self.labels) - for (indicatorContainerView, button) in zip(indicatorContainerViews, buttons) { - indicatorContainerView.accessibilityElements = [button.0, button.1] - indicatorContainerView.shouldGroupAccessibilityChildren = true - indicatorContainerView.isAccessibilityElement = true - - indicatorContainerView.accessibilityLabel = button.1.accessibilityLabel ?? button.0.accessibilityLabel - } - } else { - for (indicatorContainerView, indicatorView) in zip(self.indicatorContainerViews, self.indicatorViews) { - indicatorContainerView.accessibilityElements = [indicatorView] - indicatorContainerView.shouldGroupAccessibilityChildren = true - indicatorContainerView.isAccessibilityElement = true - - indicatorContainerView.accessibilityLabel = indicatorView.accessibilityLabel - } - } - - for (index, indicator) in self.indicatorContainerViews.enumerated() { - self.setItemAccessibilityTraits(view: indicator, index: index, disabledIndices: disabledIndices, currentPageIndex: currentPageIndex) - } - } - - private func setItemAccessibilityTraits(view: UIView, index: Int, disabledIndices: Set, currentPageIndex: Int) { - - view.accessibilityIdentifier = AccessibilityIdentifier.indicator(forIndex: index) - view.accessibilityValue = "\(index)" - view.isAccessibilityElement = true - - if self.interactionState == .none { - view.accessibilityTraits.remove(.button) - view.accessibilityRespondsToUserInteraction = false - } else { - view.accessibilityTraits.insert(.button) - view.accessibilityRespondsToUserInteraction = true - } - - if disabledIndices.contains(index) { - view.accessibilityTraits.insert(.notEnabled) - } else { - view.accessibilityTraits.remove(.notEnabled) - } - if index == currentPageIndex { - view.accessibilityTraits.insert(.selected) - } else { - view.accessibilityTraits.remove(.selected) - } - } - - private func createHiddenLabels(content: Content) -> [UILabel] { - guard content.hasLabel else { return [] } - return (0.. [ProgressTrackerTrackUIView] { - guard numberOfPages > 1 else { return [] } - - return (0.. 0 else { return } - - self.setupIndicatorsAndLabels(content: content, orientation: orientation) - - if orientation == .horizontal { - self.setupHorizontalViewConstraints(content: content) - } else { - self.setupVerticalViewConstraints(content: content) - } - - self.accessibilityIdentifier = AccessibilityIdentifier.identifier - self.accessibilityValue = "\(self.currentPageIndex)" - if !self.viewModel.isEnabled { - self.accessibilityTraits.insert(.notEnabled) - } else { - self.accessibilityTraits.remove(.notEnabled) - } - if self.interactionState == .none { - self.accessibilityTraits.remove(.allowsDirectInteraction) - } else { - self.accessibilityTraits.insert(.allowsDirectInteraction) - } - - self.setItemsAccessibilityTraits(disabledIndices: self.viewModel.disabledIndices, currentPageIndex: self.viewModel.currentPageIndex) - - self.viewCount += 1 - } - - // MARK: Setup supscriptions - private func setupSubscriptions() { - self.viewModel.$content.dropFirst().removeDuplicates().subscribe(in: &self.cancellables) { [weak self] content in - self?.didUpdate(content: content) - } - - self.viewModel.$orientation.dropFirst().removeDuplicates().subscribe(in: &self.cancellables) { [weak self] orientation in - guard let self = self else { return } - self.setupView(content: self.viewModel.content, orientation: orientation) - } - - self.viewModel.$font.dropFirst().removeDuplicates(by: {$0.uiFont == $1.uiFont}).subscribe(in: &self.cancellables) { [weak self] font in - guard let self = self else { return } - for label in self.labels { - label.font = font.uiFont - } - } - - self.viewModel.$labelColor.dropFirst().removeDuplicates(by: {$0.uiColor == $1.uiColor}).subscribe(in: &self.cancellables) { [weak self] labelColor in - guard let self = self else { return } - for label in self.labels { - label.textColor = labelColor.uiColor - } - } - - self.viewModel.$spacings.dropFirst().removeDuplicates().subscribe(in: &self.cancellables) { [weak self] spacings in - self?.didUpdate(spacings: spacings) - } - - self.viewModel.$disabledIndices.dropFirst().removeDuplicates().subscribe(in: &self.cancellables) { disabledIndices in - self.didUpdateDisabledStatus(for: disabledIndices) - } - } - - private func setupHorizontalViewConstraints(content: Content) { - var precedingView = self.indicatorViews[0] - var constraints = [NSLayoutConstraint]() - - constraints.append(precedingView.topAnchor.constraint(equalTo: self.topAnchor)) - - let numberOfPages = self.indicatorViews.count - - if content.hasLabel { - for (indicator, indicatorContainerView) in zip(self.indicatorViews, self.indicatorContainerViews) { - constraints.append(contentsOf: [ - indicator.centerXAnchor.constraint(equalTo: indicatorContainerView.centerXAnchor), - indicator.topAnchor.constraint(equalTo: indicatorContainerView.topAnchor), - indicatorContainerView.widthAnchor.constraint(greaterThanOrEqualTo: indicator.widthAnchor) - ]) - } - for (label, indicatorContainerView) in zip(self.labels, self.indicatorContainerViews) { - constraints.append(contentsOf: [ - label.centerXAnchor.constraint(equalTo: indicatorContainerView.centerXAnchor), - label.bottomAnchor.constraint(equalTo: indicatorContainerView.bottomAnchor), - indicatorContainerView.widthAnchor.constraint(greaterThanOrEqualTo: label.widthAnchor) - ]) - } - } else { - for (indicator, indicatorContainer) in zip(self.indicatorViews, self.indicatorContainerViews) { - constraints.append(contentsOf: NSLayoutConstraint.edgeConstraints(from: indicator, to: indicatorContainer)) - } - } - - for i in 1.. 0 { - self.updateView(content: content) - } - } - - private func didUpdate(intent: ProgressTrackerIntent) { - for indicatorView in self.indicatorViews { - indicatorView.intent = intent - } - for trackView in self.trackViews { - trackView.intent = intent - } - } - - private func didUpdate(theme: Theme) { - for indicatorView in self.indicatorViews { - indicatorView.theme = theme - } - for trackView in self.trackViews { - trackView.theme = theme - } - } - - private func didUpdate(variant: ProgressTrackerVariant) { - for indicatorView in self.indicatorViews { - indicatorView.variant = variant - } - } - - private func didUpdate(size: ProgressTrackerSize) { - for indicatorView in self.indicatorViews { - indicatorView.size = size - } - } - - private func didUpdate(spacings: ProgressTrackerSpacing) { - self.trackSpacingConstraints.forEach { constraint in - constraint.constant = spacings.trackIndicatorSpacing * self.scaleFactor - } - - self.labelSpacingConstraints.forEach { constraint in - constraint.constant = spacings.minLabelSpacing * self.scaleFactor - } - - } - - private func didUpdate(interactionState: ProgressTrackerInteractionState) { - self.isUserInteractionEnabled = interactionState != .none - if self.interactionState == .none { - self.accessibilityTraits.remove(.allowsDirectInteraction) - } else { - self.accessibilityTraits.insert(.allowsDirectInteraction) - } - - self.setItemsAccessibilityTraits(disabledIndices: self.viewModel.disabledIndices, currentPageIndex: self.viewModel.currentPageIndex) - } - - private func didUpdateDisabledStatus(for disabledIndices: Set) { - for (index, view) in self.labels.enumerated() { - let isDisabled = disabledIndices.contains(index) - view.alpha = self.viewModel.labelOpacity(isDisabled: isDisabled) - } - for (index, view) in self.indicatorViews.enumerated() { - view.isEnabled = !disabledIndices.contains(index) - } - for (index, view) in self.trackViews.enumerated() { - view.isEnabled = !disabledIndices.contains(index + 1) - } - - self.setItemsAccessibilityTraits(disabledIndices: disabledIndices, currentPageIndex: self.viewModel.currentPageIndex) - } - - // MARK: - Public modifiers - /// Set the indicator image at the specified index - /// - Parameters: - /// - image: An optional image. Setting the image to nil will remove it. - /// - forIndex: The index to use the image - public func setIndicatorImage(_ image: UIImage?, forIndex index: Int) { - self.viewModel.content.setIndicatorImage(image, atIndex: index) - } - - /// Set the current indicator image at the given index. This indicator image will be shown when the page is selected - /// - Parameters: - /// - image: An optional image. Setting the image to nil will remove it - /// - forIndex: The page index for the image - public func setCurrentPageIndicatorImage(_ image: UIImage?, forIndex index: Int) { - self.viewModel.content.setCurrentPageIndicatorImage(image, atIndex: index) - } - - /// Set an attributed label aligned to the corresponding indicator. This will be below the indicator in a horizontal alignment and to the right of it in a vertical alignment. Setting an attributed label and label are mutually exclusive. Setting a label at the position of an attributed label will overwrite the attributed label. - /// - Parameters: - /// - attributedLabel: An optional attributed label to set at the given index. Setting this value to nil will remove an existing attributedLabel or label at the index. - /// - forIndex: The index of the label - public func setAttributedLabel(_ attributedLabel: NSAttributedString?, forIndex index: Int) { - self.viewModel.content.setAttributedLabel(attributedLabel, atIndex: index) - } - - /// Returns the attributed label at the given index. - public func getAttributedLabel(ofIndex index: Int) -> NSAttributedString? { - return self.viewModel.content.getAttributedLabel(atIndex: index) - } - - /// Set a label at the corresponding index. This will overwrite an existing attributed label at the same position. - /// - Parameters: - /// - label: An optional label. Setting it to nil, will remove an existing label or attributed label. - /// - index: The page index - public func setLabel(_ label: String?, forIndex index: Int) { - let attributedLabel = label.map(NSAttributedString.init) - self.viewModel.content.setAttributedLabel(attributedLabel, atIndex: index) - } - - /// Returns the label aligned to the indicator at the given index. - public func getLabel(forIndex index: Int) -> String? { - return self.viewModel.content.getAttributedLabel(atIndex: index)?.string - } - - /// Set a character on the indicator for the given index. - /// - Parameters: - /// - label: An optional character for the indicator label - /// - forIndex: The index of the indicator - public func setIndicatorLabel(_ label: String?, forIndex index: Int) { - self.viewModel.content.setIndicatorLabel(label, atIndex: index) - } - - /// Return the current indicator label at the given index. - public func getIndicatorLabel(forIndex index: Int) -> String? { - self.viewModel.content.getIndicatorLabel(atIndex: index) - } - - /// Set the indicator image of the already visited pages - public func setCompletedIndicatorImage(_ image: UIImage?) { - self.viewModel.content.completedPageIndicatorImage = image - } - - /// Return the indicator image of the pages already visited - public func getCompletedIndicatorImage() -> UIImage? { - return self.viewModel.content.completedPageIndicatorImage - } - - /// Set the indicator at - public func setIsEnabled(_ isEnabled: Bool, forIndex index: Int) { - guard index < self.indicatorViews.count else { return } - - self.viewModel.setIsEnabled(isEnabled: isEnabled, forIndex: index) - } - - /// Set the default preferred indicator image - public func setPreferredIndicatorImage(_ image: UIImage?) { - self.viewModel.content.preferredIndicatorImage = image - } - - /// Return the default preferred indicator image - public func getPreferredIndicatorImage() -> UIImage? { - return self.viewModel.content.preferredIndicatorImage - } - - /// Set the default image for the current page indicator - public func setPreferredCurrentPageIndicatorImage(_ image: UIImage?) { - self.viewModel.content.preferredCurrentPageIndicatorImage = image - } - - /// Return the default image for the current page indicator - public func getPreferredCurrentPageIndicatorImage() -> UIImage? { - return self.viewModel.content.preferredCurrentPageIndicatorImage - } -} - -// MARK: Private helper extensions -private extension Collection where Element: UIView { - func addToSuperView(_ superView: UIView) { - for view in self { - superView.addSubview(view) - } - } -} - -private extension UIView { - func addSubviews(_ views: any Collection) { - for view in views { - self.addSubview(view) - } - } -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControlSnapshotTests.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControlSnapshotTests.swift deleted file mode 100644 index 85badf692..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUIControlSnapshotTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// ProgressTrackerUIControlSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import SnapshotTesting - -@testable import SparkCore - -final class ProgressTrackerUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = ProgressTrackerScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: false) - for configuration in configurations { - let view: ProgressTrackerUIControl - - if configuration.labels.isEmpty { - view = ProgressTrackerUIControl( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - size: configuration.size, - numberOfPages: 5, - orientation: configuration.orientation - ) - } else { - view = ProgressTrackerUIControl( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - size: configuration.size, - labels: configuration.labels, - orientation: configuration.orientation - ) - } - - switch configuration.contentType { - case .icon: view.setPreferredIndicatorImage(UIImage(systemName: "lock.circle")) - case .text: - for i in 0..<5 { - view.setIndicatorLabel("A\(i + 1)", forIndex: i) - } - case .empty: - view.showDefaultPageNumber = false - } - - switch configuration.state { - case .disabled: view.isEnabled = false - case .selected: view.currentPageIndex = 1 - case .pressed: view.indicatorViews[1].isHighlighted = true - default: break - } - - view.backgroundColor = .systemBackground - view.translatesAutoresizingMaskIntoConstraints = false - - if let frame = configuration.frame { - let containerView = UIView(frame: frame) - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.widthAnchor.constraint(equalToConstant: frame.width).isActive = true - containerView.heightAnchor.constraint(equalToConstant: frame.height).isActive = true - containerView.addSubview(view) - - NSLayoutConstraint.activate([ - containerView.topAnchor.constraint(equalTo: view.topAnchor), - containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - containerView.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor), - containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - - self.assertSnapshot( - matching: containerView, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } else { - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - - } - } - } -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandler.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandler.swift deleted file mode 100644 index db469c467..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandler.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// ProgressTrackerUITouchHandler.swift -// SparkCore -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Combine -import Foundation -import UIKit - -/// Touch handling for the progress tracker. -/// There are four different typs of touch handler: -/// - ProgressTrackerNoneUITouchHandler. This ignores all touch events -/// - ProgressTrackerDiscreteUITouchHandler: This handles touches for discrete interaction. It is only possible to step from one page to the next with one interaction. -/// - ProgressTrackerContinuousUITouchHandler: This handles continuous (drag) interaction. It is possible to step from one page to the next, and then the following, etc. in one interaction. It is not possible to skip a step and all steps will be published. -/// - ProgressTrackerIndependentUITouchHandler: This handles touches quite similar to the discrete, but it is possible to skip steps. -protocol ProgressTrackerUITouchHandling { - /// The current page being tracked by the touch event - var trackingPageIndex: Int? { get } - /// The current page will be published to the current page publisher - var currentPagePublisher: any Publisher { get } - - /// Handle begin tracking - func beginTracking(location: CGPoint) - - /// Handle continue tracking - func continueTracking(location: CGPoint) - - /// Handle end tracking - func endTracking(location: CGPoint) -} - -/// A helper extention to return a touch handler matching the interaction state -extension ProgressTrackerInteractionState { - func touchHandler(currentPageIndex: Int, indicatorViews: [UIControl]) -> ProgressTrackerUITouchHandling { - switch self { - case .none: return ProgressTrackerNoneUITouchHandler() - case .continuous: return ProgressTrackerContinuousUITouchHandler(currentPageIndex: currentPageIndex, indicatorViews: indicatorViews) - case .discrete: return ProgressTrackerDiscreteUITouchHandler(currentPageIndex: currentPageIndex, indicatorViews: indicatorViews) - case .independent: return ProgressTrackerIndependentUITouchHandler(currentPageIndex: currentPageIndex, indicatorViews: indicatorViews) - } - } -} - -/// The root touch handler from which others inherit. -class ProgressTrackerUITouchHandler: ProgressTrackerUITouchHandling { - - /// The current page being tracked by the touch event - var trackingPageIndex: Int? { - didSet { - if let formerPageIndex = oldValue { - self.indicatorViews[formerPageIndex].isHighlighted = false - } - if let newIndex = self.trackingPageIndex { - self.indicatorViews[newIndex].isHighlighted = true - } - } - } - - /// The current page index - var currentPageIndex: Int { - get { - return self.currentPageSubject.value - } - set { - self.currentPageSubject.send(newValue) - } - } - - /// The number of pages - var numberOfPages: Int { - return indicatorViews.count - } - - /// The indicator views, each representing a page - var indicatorViews: [UIControl] = [] - - /// Changes to the current page are published to the publisher - private var currentPageSubject: CurrentValueSubject - var currentPagePublisher: any Publisher { - return self.currentPageSubject.dropFirst() - } - - // MARK: Initialization - fileprivate init(currentPageIndex: Int, - indicatorViews: [UIControl]) { - self.currentPageSubject = .init(currentPageIndex) - self.indicatorViews = indicatorViews - } - - func beginTracking(location: CGPoint) { - let index = self.trackingIndex(closestTo: location) - - guard let selectedIndex = index, self.indicatorViews[safe: selectedIndex]?.isEnabled == true else { return } - - self.trackingPageIndex = index - } - - /// Continue tracking is handled in each tracker seperately - func continueTracking(location: CGPoint) { } - - /// Tracking has finished. - func endTracking(location: CGPoint) { - if self.indicatorViews.index(closestTo: location) != self.currentPageIndex, - let index = self.trackingPageIndex, - self.indicatorViews[safe: index]?.isEnabled == true - { - self.updateCurrentPageTrackingIndex(index) - } - - self.trackingPageIndex = nil - } - - /// Set the new current page - fileprivate func updateCurrentPageTrackingIndex(_ index: Int) { - self.currentPageIndex = index - } - - /// Return the index of the indicator closest the current page - fileprivate func trackingIndex(closestTo location: CGPoint) -> Int? { - if let index = self.indicatorViews.index(closestTo: location), index != self.currentPageIndex { - return index < self.currentPageIndex ? max(0, self.currentPageIndex - 1) : min(self.numberOfPages - 1, self.currentPageIndex + 1) - } - return nil - } -} - -/// The `none` touch handler ignores all touch events -final class ProgressTrackerNoneUITouchHandler: ProgressTrackerUITouchHandling { - var trackingPageIndex: Int? - - private var voidSubject = PassthroughSubject() - var currentPagePublisher: any Publisher { - return self.voidSubject - } - - func beginTracking(location: CGPoint) {} - - func continueTracking(location: CGPoint) {} - - func endTracking(location: CGPoint) {} -} - -/// With the `independent` touch handler, steps in the progress tracker may be skipped. -final class ProgressTrackerIndependentUITouchHandler: ProgressTrackerUITouchHandler { - - override func beginTracking(location: CGPoint) { - let index = self.indicatorViews.index(closestTo: location) - guard let selectedIndex = index, self.indicatorViews[safe: selectedIndex]?.isEnabled == true else { return } - - if index != self.currentPageIndex { - self.trackingPageIndex = index - } - } - - override func continueTracking(location: CGPoint) { - if let index = self.trackingPageIndex { - self.indicatorViews[index].isHighlighted = true - } - } -} - -/// With the `discrete` touch handler, only those steps next to the current selected index may be selected. -final class ProgressTrackerDiscreteUITouchHandler: ProgressTrackerUITouchHandler { - - override func continueTracking(location: CGPoint) { - if self.indicatorViews.index(closestTo: location) == self.currentPageIndex { - self.trackingPageIndex = nil - } else if self.trackingPageIndex == nil, let index = self.trackingIndex(closestTo: location) { - self.trackingPageIndex = index - } else if let index = self.trackingPageIndex { - self.indicatorViews[index].isHighlighted = true - } - } -} - -/// With the `continuous` touch handler, multipe steps can be published by dragging across the view. Each single step will be published. -final class ProgressTrackerContinuousUITouchHandler: ProgressTrackerUITouchHandler { - - override func continueTracking(location: CGPoint) { - - guard let index = self.indicatorViews.index(closestTo: location) else { return } - - if index == self.currentPageIndex { - self.trackingPageIndex = nil - return - } - - if let trackingPageIndex = self.trackingPageIndex { - if let nextIndex = self.nextTrackingIndex(closestTo: location), self.isValidIndex(nextIndex) { - self.updateCurrentPageTrackingIndex(trackingPageIndex) - self.trackingPageIndex = nextIndex - } else { - self.indicatorViews[trackingPageIndex].isHighlighted = true - } - } else if let index = self.trackingIndex(closestTo: location), self.isValidIndex(index){ - self.trackingPageIndex = index - } - } - - private func isValidIndex(_ index: Int) -> Bool { - if index == self.currentPageIndex || self.indicatorViews[safe: index]?.isEnabled == false { - return false - } - return true - } - - private func nextTrackingIndex(closestTo location: CGPoint) -> Int? { - guard let trackingPageIndex = self.trackingPageIndex else { return nil } - - guard let index = self.indicatorViews.index(closestTo: location), index != self.currentPageIndex, - index != trackingPageIndex else - { return nil } - - return index < trackingPageIndex ? max(0, trackingPageIndex - 1) : min(self.numberOfPages - 1, trackingPageIndex + 1) - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerCreationTests.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerCreationTests.swift deleted file mode 100644 index 226e9a5b8..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerCreationTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ProgressTrackerUITouchHandlerCreationTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ProgressTrackerUITouchHandlerCreationTests: XCTestCase { - var controls: [UIControl]! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.controls = (0...4) - .map { index in - return CGRect(x: index * 50, y: 0, width: 50, height: 50) - } - .map(UIControl.init(frame:)) - } - - // MARK: - Tests - func test_setup_none() { - let sut = ProgressTrackerInteractionState.none.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) - - XCTAssertTrue(sut is ProgressTrackerNoneUITouchHandler) - } - - func test_setup_discrete() { - let sut = ProgressTrackerInteractionState.discrete.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) - - XCTAssertTrue(sut is ProgressTrackerDiscreteUITouchHandler) - } - - func test_setup_continuous() { - let sut = ProgressTrackerInteractionState.continuous.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) - - XCTAssertTrue(sut is ProgressTrackerContinuousUITouchHandler) - } - - func test_setup_independent() { - let sut = ProgressTrackerInteractionState.independent.touchHandler(currentPageIndex: 0, indicatorViews: self.controls) - - XCTAssertTrue(sut is ProgressTrackerIndependentUITouchHandler) - } - -} diff --git a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerTests.swift b/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerTests.swift deleted file mode 100644 index 4fbf6b1f1..000000000 --- a/core/Sources/Components/ProgressTracker/View/UIKit/ProgressTrackerUITouchHandlerTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ProgressTrackerUITouchHandlerTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 12.02.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class ProgressTrackerUITouchHandlerTests: XCTestCase { - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/core/Sources/Components/RadioButton/AccessibilityIdentifier/RadioButtonAccessibilityIdentifier.swift b/core/Sources/Components/RadioButton/AccessibilityIdentifier/RadioButtonAccessibilityIdentifier.swift deleted file mode 100644 index c27ad66c4..000000000 --- a/core/Sources/Components/RadioButton/AccessibilityIdentifier/RadioButtonAccessibilityIdentifier.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// RadioButtonAccessibilityIdentifier.swift -// SparkCore -// -// Created by michael.zimmermann on 14.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public enum RadioButtonAccessibilityIdentifier { - - // MARK: - Properties - - public static let radioButton = "spark-radio-button" - - /// The radio group title accessibility identifier. - public static let radioButtonGroupTitle = "spark-radio-button-group-title" - - /// The radio button text label accessibility identifier. - public static let radioButtonTextLabel = "spark-radio-button-text-label" - - public static func radioButtonIdentifier(id: ID) -> String { - return "\(radioButton)-\(id)" - } -} diff --git a/core/Sources/Components/RadioButton/Enum/RadioButtonGroupLayout.swift b/core/Sources/Components/RadioButton/Enum/RadioButtonGroupLayout.swift deleted file mode 100644 index a21f59d0b..000000000 --- a/core/Sources/Components/RadioButton/Enum/RadioButtonGroupLayout.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// RadioButtonGroupLayout.swift -// SparkCore -// -// Created by michael.zimmermann on 30.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Enum describing layout options for radio button groups. Currently horizontal and vertical layouts are supported. -@frozen -public enum RadioButtonGroupLayout { - /// Horizontal layout. - case horizontal - - /// Vertical layout. - case vertical -} diff --git a/core/Sources/Components/RadioButton/Enum/RadioButtonIntent.swift b/core/Sources/Components/RadioButton/Enum/RadioButtonIntent.swift deleted file mode 100644 index 9e305754c..000000000 --- a/core/Sources/Components/RadioButton/Enum/RadioButtonIntent.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// RadioButtonIntent.swift -// SparkCore -// -// Created by michael.zimmermann on 18.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum RadioButtonIntent: String, CaseIterable { - case basic - case support - case success - case alert - case danger - case info - case neutral - case accent - case main -} - diff --git a/core/Sources/Components/RadioButton/Enum/RadioButtonLabelPosition.swift b/core/Sources/Components/RadioButton/Enum/RadioButtonLabelPosition.swift deleted file mode 100644 index 112985c8b..000000000 --- a/core/Sources/Components/RadioButton/Enum/RadioButtonLabelPosition.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// RadioButtonPosition.swift -// SparkCore -// -// Created by michael.zimmermann on 30.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -/// The checkbox can be either on the leading or trailing edge of the view. -@frozen -@available(*, deprecated, renamed: "RadioButtonLabelAlignment", message: "Please use RadioButtonLabelAlignment instead") -public enum RadioButtonLabelPosition { - /// Radiobutton label on leading edge. - case left - - /// RadioButton label on trailing edge. - case right - - var alignment: RadioButtonLabelAlignment { - switch self { - case .left: return .leading - case .right: return .trailing - } - } -} - -@frozen -public enum RadioButtonLabelAlignment: String, CaseIterable { - /// Radiobutton label on leading edge. - case leading - - /// RadioButton label on trailing edge. - case trailing - - var position: RadioButtonLabelPosition { - switch self { - case .leading: return .left - case .trailing: return .right - } - } -} diff --git a/core/Sources/Components/RadioButton/Enum/RadioButtonState.swift b/core/Sources/Components/RadioButton/Enum/RadioButtonState.swift deleted file mode 100644 index f69b71911..000000000 --- a/core/Sources/Components/RadioButton/Enum/RadioButtonState.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// RadioButtonState.swift -// SparkCore -// -// Created by michael.zimmermann on 28.06.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -@frozen -@available(*, deprecated, message: "Use RadioButtonIntent and the attribute isEnabled instead. ") -public enum RadioButtonGroupState: Equatable, Hashable, CaseIterable { - case enabled - case disabled - case accent - case basic - - case success - case warning - case error -} - -extension RadioButtonGroupState { - var intent: RadioButtonIntent { - switch self { - case .enabled: return .basic - case .disabled: return .basic - case .accent: return .accent - case .basic: return .basic - case .success: return .success - case .warning: return .alert - case .error: return .danger - } - } -} diff --git a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonAttributes.swift b/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonAttributes.swift deleted file mode 100644 index 6661b8bb9..000000000 --- a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonAttributes.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// RadioButtonAttributes.swift -// SparkCore -// -// Created by michael.zimmermann on 18.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct RadioButtonAttributes: Equatable { - let colors: RadioButtonColors - let opacity: CGFloat - let spacing: CGFloat - let font: any TypographyFontToken - - static func == (lhs: RadioButtonAttributes, rhs: RadioButtonAttributes) -> Bool { - return lhs.colors == rhs.colors && - lhs.opacity == rhs.opacity && - lhs.spacing == rhs.spacing && - lhs.font.uiFont == rhs.font.uiFont - } -} diff --git a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonColors.swift b/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonColors.swift deleted file mode 100644 index eeff1098e..000000000 --- a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonColors.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// RadioButtonColors.swift -// SparkCore -// -// Created by michael.zimmermann on 11.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Colors available to the radio button: -/// - Button: this is the outline color of the unselected/selected radio button -/// - Halo: defines the color circumferencing the radio button, when it is pressed. -/// - Fill: defines the fill color of the radio button when it is selected. If the button is not selected, the fill color is `nil`. -/// - Label: The color of the adjoining label -/// - Sublabel: The color of the sub-label. This is only used for specific states of the radio button (`error`, `success` & `warning`) -struct RadioButtonColors: Equatable { - let button: any ColorToken - let label: any ColorToken - let halo: any ColorToken - let fill: any ColorToken - let surface: any ColorToken - - static func == (lhs: RadioButtonColors, rhs: RadioButtonColors) -> Bool { - return lhs.button.equals(rhs.button) - && lhs.label.equals(rhs.label) - && lhs.halo.equals(rhs.halo) - && lhs.fill.equals(rhs.fill) - && lhs.surface.equals(rhs.surface) - } - -} diff --git a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonConstants.swift b/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonConstants.swift deleted file mode 100644 index ba664a3b1..000000000 --- a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonConstants.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// RadioButtonConstants.swift -// SparkCore -// -// Created by michael.zimmermann on 13.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Constant definition for the extra padding used in a radio button, to ensure the touch area is large enough. -enum RadioButtonConstants { - - static let radioButtonPadding: CGFloat = 8 -} diff --git a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonGroupContent.swift b/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonGroupContent.swift deleted file mode 100644 index 45cb71462..000000000 --- a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonGroupContent.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// RadioButtonGroupContent.swift -// SparkCore -// -// Created by michael.zimmermann on 25.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct RadioButtonGroupContent: Updateable { - var title: String? - var supplementaryText: String? -} diff --git a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonStateAttribute.swift b/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonStateAttribute.swift deleted file mode 100644 index c96fc9a1e..000000000 --- a/core/Sources/Components/RadioButton/Properties/Internal/RadioButtonStateAttribute.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// RadioButtonStateAttribute.swift -// SparkCore -// -// Created by michael.zimmermann on 18.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct RadioButtonStateAttribute: Updateable { - var isSelected: Bool - var isEnabled: Bool -} diff --git a/core/Sources/Components/RadioButton/Properties/Public/RadioButtonItem.swift b/core/Sources/Components/RadioButton/Properties/Public/RadioButtonItem.swift deleted file mode 100644 index 981220b04..000000000 --- a/core/Sources/Components/RadioButton/Properties/Public/RadioButtonItem.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// RadioButtonItem.swift -// SparkCore -// -// Created by michael.zimmermann on 13.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// A simple struct for defining radio buttons using the ``RadioButtonGroupView``. -public struct RadioButtonItem { - - // MARK: - Properties - - public let id: ID - public let label: String - - // MARK: - Initialization - /// Parameters: - /// - id: A unique ID bound to a generic type which has the constraints that it need be ``Equatable`` & ``Hashable``. - /// - label: The label of the radio button - public init(id: ID, label: String) { - self.id = id - self.label = label - } -} diff --git a/core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItem.swift b/core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItem.swift deleted file mode 100644 index db65ab81a..000000000 --- a/core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItem.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// RadioButtonItem.swift -// SparkCore -// -// Created by michael.zimmermann on 13.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// A simple struct for defining radio buttons using the ``RadioButtonGroupView``. -public struct RadioButtonUIItem: Equatable & Hashable { - - // MARK: - Properties - - public let id: ID - public let label: NSAttributedString - - // MARK: - Initialization - /// Parameters: - /// - id: A unique ID bound to a generic type which has the constraints that it need be ``Equatable`` & ``Hashable``. - /// - label: The label of the radio button - public init(id: ID, label: String) { - self.id = id - self.label = NSAttributedString(string: label) - } - - /// Parameters: - /// - id: A unique ID bound to a generic type which has the constraints that it need be ``Equatable`` & ``Hashable``. - /// - label: The label of the radio button - public init(id: ID, label: NSAttributedString) { - self.id = id - self.label = label - } -} diff --git a/core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItemTests.swift b/core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItemTests.swift deleted file mode 100644 index 166ea31bc..000000000 --- a/core/Sources/Components/RadioButton/Properties/Public/RadioButtonUIItemTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// RadioButtonUIItemTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 31.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class RadioButtonUIItemTests: XCTestCase { - - // MARK: - Tests - func test_equal() { - let sut = RadioButtonUIItem(id: 1, label: "Label") - - XCTAssertEqual(sut, RadioButtonUIItem(id: 1, label: "Label")) - } - - func test_equal_attributed() { - let sut = RadioButtonUIItem(id: 1, label: NSAttributedString(string: "Label")) - - XCTAssertEqual(sut, RadioButtonUIItem(id: 1, label: "Label")) - } - - func test_not_equal_different_ids() { - let sut = RadioButtonUIItem(id: 1, label: "Label") - - XCTAssertNotEqual(sut, RadioButtonUIItem(id: 2, label: "Label")) - } - - func test_not_equal_different_labels() { - let sut = RadioButtonUIItem(id: 1, label: "Label") - - XCTAssertNotEqual(sut, RadioButtonUIItem(id: 2, label: "")) - } -} diff --git a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCase.swift b/core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCase.swift deleted file mode 100644 index 268dac094..000000000 --- a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCase.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// RadioButtonGetAttributesUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 18.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -protocol RadioButtonGetAttributesUseCaseable { - func execute(theme: Theme, - intent: RadioButtonIntent, - state: RadioButtonStateAttribute, - alignment: RadioButtonLabelAlignment - ) -> RadioButtonAttributes -} - -struct RadioButtonGetAttributesUseCase: RadioButtonGetAttributesUseCaseable { - let colorsUseCase: RadioButtonGetColorsUseCaseable - - init(colorsUseCase: RadioButtonGetColorsUseCaseable = RadioButtonGetColorsUseCase()) { - self.colorsUseCase = colorsUseCase - } - func execute(theme: Theme, - intent: RadioButtonIntent, - state: RadioButtonStateAttribute, - alignment: RadioButtonLabelAlignment - ) -> RadioButtonAttributes { - return RadioButtonAttributes( - colors: self.colorsUseCase.execute( - theme: theme, - intent: intent, - isSelected: state.isSelected), - opacity: theme.opacity(isEnabled: state.isEnabled), - spacing: theme.spacing(for: alignment), - font: theme.typography.body1) - } -} - -// MARK: - Private Helpers -private extension Theme { - func opacity(isEnabled: Bool) -> CGFloat { - return !isEnabled ? self.dims.dim3 : 1 - } - - func spacing(for alignment: RadioButtonLabelAlignment) -> CGFloat { - return alignment == .trailing ? self.layout.spacing.medium : self.layout.spacing.xxxLarge - } -} diff --git a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCaseTests.swift b/core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCaseTests.swift deleted file mode 100644 index d21f4c059..000000000 --- a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetAttributesUseCaseTests.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// RadioButtonGetAttributesUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by michael.zimmermann on 26.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class RadioButtonGetAttributesUseCaseTests: TestCase { - - var sut: RadioButtonGetAttributesUseCase! - var colorsUseCase: RadioButtonGetColorsUseCaseableGeneratedMock! - var theme: ThemeGeneratedMock! - var colors: RadioButtonColors! - - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - - self.colorsUseCase = RadioButtonGetColorsUseCaseableGeneratedMock() - self.sut = RadioButtonGetAttributesUseCase(colorsUseCase: self.colorsUseCase) - self.colors = RadioButtonColors( - button: ColorTokenGeneratedMock.random(), - label: ColorTokenGeneratedMock.random(), - halo: ColorTokenGeneratedMock.random(), - fill: ColorTokenGeneratedMock.random(), - surface: ColorTokenGeneratedMock.random() - ) - self.colorsUseCase.executeWithThemeAndIntentAndIsSelectedReturnValue = self.colors - - } - - func test_attributes_enabled_state_leading_alignment() { - // Given - let expectedAttributes = RadioButtonAttributes( - colors: self.colors, - opacity: 1, - spacing: self.theme.layout.spacing.xxxLarge, - font: self.theme.typography.body1) - - // When - let givenAttributes = self.sut.execute( - theme: self.theme, - intent: .basic, - state: .isEnabled, - alignment: .leading - ) - - XCTAssertEqual(givenAttributes, expectedAttributes) - } - - func test_attributes_enabled_state_trailing_alignment() { - // Given - let expectedAttributes = RadioButtonAttributes( - colors: self.colors, - opacity: 1, - spacing: self.theme.layout.spacing.medium, - font: self.theme.typography.body1) - - // When - let givenAttributes = self.sut.execute( - theme: self.theme, - intent: .basic, - state: .isEnabled, - alignment: .trailing - ) - - XCTAssertEqual(givenAttributes, expectedAttributes) - } - - func test_attributes_disabled_state_trailing_alignment() { - // Given - let expectedAttributes = RadioButtonAttributes( - colors: self.colors, - opacity: self.theme.dims.dim3, - spacing: self.theme.layout.spacing.medium, - font: self.theme.typography.body1) - - // When - let givenAttributes = self.sut.execute( - theme: self.theme, - intent: .basic, - state: .isDisabled, - alignment: .trailing - ) - - XCTAssertEqual(givenAttributes, expectedAttributes) - } -} - -private extension RadioButtonStateAttribute { - static var isEnabled = RadioButtonStateAttribute(isSelected: false, isEnabled: true) - static var isDisabled = RadioButtonStateAttribute(isSelected: false, isEnabled: false) -} diff --git a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCase.swift b/core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCase.swift deleted file mode 100644 index 651f345ab..000000000 --- a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCase.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// RadioButtonGetColorsUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 11.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol RadioButtonGetColorsUseCaseable { - func execute(theme: Theme, - intent: RadioButtonIntent, - isSelected: Bool) -> RadioButtonColors -} - -/// A use case to determine the colors of a radio button. -/// Properties: -/// - theming: Contains state and theme of the radio button ``RadioButtonTheming`` -/// -/// Functions: -/// - execute: takes a parameter if the radio button is selected or not, and returns a ``RadioButtonColors`` defining the various colors of the radion button. -struct RadioButtonGetColorsUseCase: RadioButtonGetColorsUseCaseable { - - // MARK: - Functions - /// - /// Calculate the colors of the radio button depending on it's state and whether it is selected or not. - /// - /// - Parameters: - /// - isSelected = true, when the radion button is selected, false otherwise. - /// - /// - Returns: ``RadioButtonColors`` which contains the various colors of the radio button. - func execute(theme: Theme, - intent: RadioButtonIntent, - isSelected: Bool) -> RadioButtonColors { - let buttonColor = theme.colors.buttonColor( - intent: intent, - isSelected: isSelected) - - return RadioButtonColors( - button: buttonColor, - label: theme.colors.base.onBackground, - halo: theme.colors.haloColor(intent: intent), - fill: isSelected ? buttonColor : ColorTokenDefault.clear, - surface: theme.colors.surfaceColor(intent: intent) - ) - } -} - -// MARK: - Private Extensions -private extension SparkCore.Colors { - func buttonColor( - intent: RadioButtonIntent, - isSelected: Bool) -> any ColorToken { - return isSelected ? self.selectedColor(intent: intent) : self.base.outline - } - - private func selectedColor(intent: RadioButtonIntent) -> any ColorToken { - switch intent { - case .basic: - return self.basic.basic - case .support: - return self.support.support - case .alert: - return self.feedback.alert - case .danger: - return self.feedback.error - case .info: - return self.feedback.info - case .neutral: - return self.feedback.neutral - case .accent: - return self.accent.accent - case .main: - return self.main.main - case .success: - return self.feedback.success - } - } - - func surfaceColor(intent: RadioButtonIntent) -> any ColorToken { - switch intent { - case .basic: - return self.basic.onBasic - case .support: - return self.support.onSupport - case .alert: - return self.feedback.onAlert - case .danger: - return self.feedback.onError - case .info: - return self.feedback.onInfo - case .neutral: - return self.feedback.onNeutral - case .accent: - return self.accent.onAccent - case .main: - return self.main.onMain - case .success: - return self.feedback.onSuccess - } - } - - func haloColor(intent: RadioButtonIntent) -> any ColorToken { - switch intent { - case .basic: - return self.basic.basicContainer - case .accent: - return self.accent.accentContainer - case .alert: - return self.feedback.alertContainer - case .info: - return self.feedback.infoContainer - case .support: - return self.support.supportContainer - case .danger: - return self.feedback.errorContainer - case .neutral: - return self.feedback.neutralContainer - case .main: - return self.main.mainContainer - case .success: - return self.feedback.successContainer - } - } -} diff --git a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCaseTests.swift b/core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCaseTests.swift deleted file mode 100644 index f017b9af6..000000000 --- a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetColorsUseCaseTests.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// RadioButtonGetColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 13.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import SwiftUI -import XCTest - -final class RadioButtonGetColorsUseCaseTests: XCTestCase { - - // MARK: - Properties - - var sut: RadioButtonGetColorsUseCase! - var theme: ThemeGeneratedMock! - - // MARK: - Setup - - override func setUp() { - super.setUp() - - // Given - let theme = ThemeGeneratedMock.mocked() - self.theme = theme - self.sut = RadioButtonGetColorsUseCase() - } - - // MARK: - Tests - func test_basic_colors_when_button_is_not_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.base.outline, - label: colors.base.onBackground, - halo: colors.basic.basicContainer, - fill: ColorTokenDefault.clear, - surface: colors.basic.onBasic - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .basic, - isSelected: false) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_danger_colors_when_button_is_not_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.base.outline, - label: colors.base.onBackground, - halo: colors.feedback.errorContainer, - fill: ColorTokenDefault.clear, - surface: colors.feedback.onError - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .danger, - isSelected: false) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_info_colors_when_button_is_not_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.base.outline, - label: colors.base.onBackground, - halo: colors.feedback.infoContainer, - fill: ColorTokenDefault.clear, - surface: colors.feedback.onInfo - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .info, - isSelected: false) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_alert_colors_when_button_is_not_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.base.outline, - label: colors.base.onBackground, - halo: colors.feedback.alertContainer, - fill: ColorTokenDefault.clear, - surface: colors.feedback.onAlert - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .alert, - isSelected: false) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_success_colors_when_button_is_not_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.base.outline, - label: colors.base.onBackground, - halo: colors.feedback.successContainer, - fill: ColorTokenDefault.clear, - surface: colors.feedback.onSuccess - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .success, - isSelected: false) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_accent_colors_when_button_is_not_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.base.outline, - label: colors.base.onBackground, - halo: colors.accent.accentContainer, - fill: ColorTokenDefault.clear, - surface: colors.accent.onAccent - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .accent, - isSelected: false) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_basic_colors_when_button_is_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.basic.basic, - label: colors.base.onBackground, - halo: colors.basic.basicContainer, - fill: colors.basic.basic, - surface: colors.basic.onBasic - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .basic, - isSelected: true) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_danger_colors_when_button_is_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.feedback.error, - label: colors.base.onBackground, - halo: colors.feedback.errorContainer, - fill: colors.feedback.error, - surface: colors.feedback.onError - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .danger, - isSelected: true) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_alert_colors_when_button_is_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.feedback.alert, - label: colors.base.onBackground, - halo: colors.feedback.alertContainer, - fill: colors.feedback.alert, - surface: colors.feedback.onAlert - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .alert, - isSelected: true) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_success_colors_when_button_is_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.feedback.success, - label: colors.base.onBackground, - halo: colors.feedback.successContainer, - fill: colors.feedback.success, - surface: colors.feedback.onSuccess - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .success, - isSelected: true) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - - func test_accent_colors_when_button_is_selected() throws { - // Given - let colors = self.theme.colors - let expectedColors = RadioButtonColors( - button: colors.accent.accent, - label: colors.base.onBackground, - halo: colors.accent.accentContainer, - fill: colors.accent.accent, - surface: colors.accent.onAccent - ) - - // When - let givenColors = self.sut.execute( - theme: self.theme, - intent: .accent, - isSelected: true) - - // Then - XCTAssertEqual(givenColors, expectedColors) - } - -} diff --git a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCase.swift b/core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCase.swift deleted file mode 100644 index adc62ec3f..000000000 --- a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCase.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// RadioButtonGetGroupColorUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 28.06.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol RadioButtonGetGroupColorUseCaseable { - func execute(colors: Colors, intent: RadioButtonIntent) -> any ColorToken -} - -/// GetRadioButtonGroupColorUseCase -/// Returns the color of the state of the radio button group -/// Functions: -/// - execute: takes a colors and states and returns a ``ColorToken`` defining the state. -struct RadioButtonGetGroupColorUseCase: RadioButtonGetGroupColorUseCaseable { - // MARK: - Functions - - /// Return the color token corresponding to the state - func execute(colors: Colors, intent: RadioButtonIntent) -> any ColorToken { - switch intent { - case .basic: - return colors.basic.basic - case .support: - return colors.support.support - case .alert: - return colors.feedback.alert - case .danger: - return colors.feedback.error - case .info: - return colors.feedback.info - case .neutral: - return colors.feedback.neutral - case .accent: - return colors.accent.accent - case .main: - return colors.main.main - case .success: - return colors.feedback.success - } - } -} diff --git a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCaseTests.swift b/core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCaseTests.swift deleted file mode 100644 index 536471dcb..000000000 --- a/core/Sources/Components/RadioButton/UseCases/RadioButtonGetGroupColorUseCaseTests.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// RadioButtonGetGroupColorUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 06.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class RadioButtonGetGroupColorUseCaseTests: XCTestCase { - - var sut: RadioButtonGetGroupColorUseCase! - var colors: ColorsGeneratedMock! - - override func setUp() { - super.setUp() - - self.sut = RadioButtonGetGroupColorUseCase() - self.colors = ColorsGeneratedMock.mocked() - } - // MARK: - Tests - - func test_alert() { - // When - let colorToken = sut.execute(colors: colors, intent: .alert) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.feedback.alert.uiColor) - XCTAssertEqual(colorToken.color, colors.feedback.alert.color) - } - - func test_danger() { - // When - let colorToken = sut.execute(colors: colors, intent: .danger) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.feedback.error.uiColor) - XCTAssertEqual(colorToken.color, colors.feedback.error.color) - } - - func test_success() { - // When - let colorToken = sut.execute(colors: colors, intent: .success) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.feedback.success.uiColor) - XCTAssertEqual(colorToken.color, colors.feedback.success.color) - } - - func test_basic() { - // When - let colorToken = sut.execute(colors: colors, intent: .basic) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.basic.basic.uiColor) - XCTAssertEqual(colorToken.color, colors.basic.basic.color) - } - - func test_accent() { - // When - let colorToken = sut.execute(colors: colors, intent: .accent) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.accent.accent.uiColor) - XCTAssertEqual(colorToken.color, colors.accent.accent.color) - } - - func test_main() { - // When - let colorToken = sut.execute(colors: colors, intent: .main) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.main.main.uiColor) - XCTAssertEqual(colorToken.color, colors.main.main.color) - } - - func test_support() { - // When - let colorToken = sut.execute(colors: colors, intent: .support) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.support.support.uiColor) - XCTAssertEqual(colorToken.color, colors.support.support.color) - } - - func test_info() { - // When - let colorToken = sut.execute(colors: colors, intent: .info) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.feedback.info.uiColor) - XCTAssertEqual(colorToken.color, colors.feedback.info.color) - } - - func test_neutral() { - // When - let colorToken = sut.execute(colors: colors, intent: .neutral) - - // Then - XCTAssertEqual(colorToken.uiColor, colors.feedback.neutral.uiColor) - XCTAssertEqual(colorToken.color, colors.feedback.neutral.color) - } - -} diff --git a/core/Sources/Components/RadioButton/View/RadioButtonGroupViewModel.swift b/core/Sources/Components/RadioButton/View/RadioButtonGroupViewModel.swift deleted file mode 100644 index 2737fd925..000000000 --- a/core/Sources/Components/RadioButton/View/RadioButtonGroupViewModel.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// RadioButtonGroupViewModel.swift -// SparkCore -// -// Created by michael.zimmermann on 28.06.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -/// The RadioButtonGroupViewModel is a view model used by the ``RadioButtonView`` to handle theming logic and state changes. -final class RadioButtonGroupViewModel: ObservableObject { - - // MARK: - Published Properties - @Published var sublabelFont: any TypographyFontToken - @Published var titleFont: any TypographyFontToken - @Published var titleColor: any ColorToken - @Published var sublabelColor: any ColorToken - @Published var spacing: CGFloat - @Published var labelSpacing: CGFloat - @Published var isDisabled: Bool - @Published var content: Content - - // MARK: - Internal Properties - var theme: any Theme { - didSet { - self.sublabelFont = self.theme.typography.caption - self.titleFont = self.theme.typography.subhead - self.titleColor = self.theme.colors.base.onSurface - self.sublabelColor = useCase.execute(colors: self.theme.colors, intent: self.intent) - self.spacing = self.theme.layout.spacing.large - self.labelSpacing = self.theme.layout.spacing.medium - } - } - - var intent: RadioButtonIntent { - didSet { - guard self.intent != oldValue else { return } - - self.sublabelColor = useCase.execute(colors: self.theme.colors, intent: self.intent) - } - } - - // MARK: Private Properties - private let useCase: any RadioButtonGetGroupColorUseCaseable - - // MARK: Initializers - convenience init( - theme: any Theme, - intent: RadioButtonIntent, - content: Content - ) { - self.init( - theme: theme, - intent: intent, - content: content, - useCase: RadioButtonGetGroupColorUseCase() - ) - } - - init(theme: any Theme, - intent: RadioButtonIntent, - content: Content, - useCase: any RadioButtonGetGroupColorUseCaseable) { - - self.theme = theme - self.intent = intent - self.useCase = useCase - self.isDisabled = false - self.content = content - - self.sublabelFont = self.theme.typography.caption - self.titleFont = self.theme.typography.subhead - self.titleColor = self.theme.colors.base.onSurface - self.sublabelColor = useCase.execute(colors: theme.colors, intent: intent) - self.spacing = self.theme.layout.spacing.large - self.labelSpacing = self.theme.layout.spacing.medium - } -} - diff --git a/core/Sources/Components/RadioButton/View/RadioButtonGroupViewModelTests.swift b/core/Sources/Components/RadioButton/View/RadioButtonGroupViewModelTests.swift deleted file mode 100644 index 516a0f012..000000000 --- a/core/Sources/Components/RadioButton/View/RadioButtonGroupViewModelTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// RadioButtonGroupViewModelTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 05.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import XCTest - -final class RadioButtonGroupViewModelTests: XCTestCase { - - var subscriptions = Set() - - // MARK: - Tests - func test_expect_all_values_published_on_setup() { - // Given - let sut = sut(intent: .basic) - let expectation = expectation(description: "Wait for subscriptions to be published") - expectation.expectedFulfillmentCount = 1 - - let publisher = Publishers.Zip( - Publishers.Zip4(sut.$sublabelFont, sut.$titleFont, sut.$titleColor, sut.$sublabelColor), - Publishers.Zip(sut.$spacing, sut.$labelSpacing) - ) - - publisher.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - wait(for: [expectation], timeout: 0.1) - } - - func test_theme_change() { - // Given - let sut = sut(intent: .basic) - let expectation = expectation(description: "Wait for subscriptions to be published") - expectation.expectedFulfillmentCount = 2 - - let publisher = Publishers.Zip( - Publishers.Zip4(sut.$sublabelFont, sut.$titleFont, sut.$titleColor, sut.$sublabelColor), - Publishers.Zip(sut.$spacing, sut.$labelSpacing) - ) - - publisher.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - sut.theme = ThemeGeneratedMock.mocked() - - wait(for: [expectation], timeout: 0.1) - } - - func test_intent_change() { - // Given - let sut = sut(intent: .basic) - let expectation = expectation(description: "Wait for sublabel color to be published") - expectation.expectedFulfillmentCount = 2 - - sut.$sublabelColor.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - sut.intent = .alert - - wait(for: [expectation], timeout: 0.1) - } - - // MARK: - Private helpers - private func sut(intent: RadioButtonIntent) -> RadioButtonGroupViewModel { - let useCase = RadioButtonGetGroupColorUseCaseableGeneratedMock() - useCase.executeWithColorsAndIntentReturnValue = ColorTokenGeneratedMock.random() - let theme = ThemeGeneratedMock.mocked() - - return RadioButtonGroupViewModel( - theme: theme, - intent: intent, - content: (), - useCase: useCase - ) - } -} diff --git a/core/Sources/Components/RadioButton/View/RadioButtonViewModel.swift b/core/Sources/Components/RadioButton/View/RadioButtonViewModel.swift deleted file mode 100644 index 7db0235d8..000000000 --- a/core/Sources/Components/RadioButton/View/RadioButtonViewModel.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// RadioButtonViewModel.swift -// SparkCore -// -// Created by michael.zimmermann on 11.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -/// The RadioButtonViewModel is a view model used by the ``RadioButtonView`` to handle theming logic and state changes. -final class RadioButtonViewModel: ObservableObject { - // MARK: - Injected Properties - - @Published var label: Either - let id: ID - - private var formerSelecteID: ID? - private let useCase: RadioButtonGetAttributesUseCaseable - - var theme: Theme - var intent: RadioButtonIntent - - private(set) var state: RadioButtonStateAttribute - - @Binding private (set) var selectedID: ID? - - // MARK: - Published Properties - - @Published var colors: RadioButtonColors - @Published var isDisabled: Bool - @Published var opacity: CGFloat - @Published var spacing: CGFloat - @Published var font: TypographyFontToken - @Published var alignment: RadioButtonLabelAlignment - - // MARK: - Initialization - convenience init(theme: Theme, - intent: RadioButtonIntent, - id: ID, - label: Either, - selectedID: Binding, - alignment: RadioButtonLabelAlignment = .trailing) { - - self.init(theme: theme, - intent: intent, - id: id, - label: label, - selectedID: selectedID, - alignment: alignment, - useCase: RadioButtonGetAttributesUseCase()) - } - - init(theme: Theme, - intent: RadioButtonIntent, - id: ID, - label: Either, - selectedID: Binding, - alignment: RadioButtonLabelAlignment, - useCase: RadioButtonGetAttributesUseCaseable) { - self.theme = theme - self.intent = intent - self.id = id - self.label = label - self._selectedID = selectedID - self.useCase = useCase - self.alignment = alignment - self.formerSelecteID = selectedID.wrappedValue - - let state = RadioButtonStateAttribute(isSelected: selectedID.wrappedValue == id, isEnabled: true) - let attributes = useCase.execute( - theme: theme, - intent: intent, - state: state, - alignment: alignment) - - self.state = state - self.colors = attributes.colors - self.opacity = attributes.opacity - self.spacing = attributes.spacing - self.font = attributes.font - self.isDisabled = !state.isEnabled - } - - // MARK: - Functions - func set(theme: Theme) { - self.theme = theme - self.themeDidUpdate() - self.alignmentDidUpdate() - } - - func set(enabled: Bool) { - guard enabled != self.state.isEnabled else { return } - - self.state = self.state.update(\.isEnabled, value: enabled) - self.updateViewAttributes() - self.isDisabled = !enabled - } - - func set(selected: Bool) { - if selected, self.id == self.selectedID { - return - } else if !selected, self.id != self.selectedID { - return - } - - if selected { - self.formerSelecteID = self.selectedID - self.selectedID = self.id - } else { - self.selectedID = self.formerSelecteID - } - self.updateViewAttributes() - } - - func set(intent: RadioButtonIntent) { - guard intent != self.intent else { return } - - self.intent = intent - self.intentDidUpdate() - } - - func set(alignment: RadioButtonLabelAlignment) { - guard self.alignment != alignment else { return } - - self.alignment = alignment - self.alignmentDidUpdate() - } - - func updateViewAttributes() { - self.state = self.state.update(\.isSelected, value: self.id == self.selectedID) - let attributes = self.useCase.execute( - theme: self.theme, - intent: self.intent, - state: self.state, - alignment: self.alignment) - - self.colors = attributes.colors - self.opacity = attributes.opacity - self.spacing = attributes.spacing - self.font = attributes.font - } - - // MARK: - Private Functions - - private func intentDidUpdate() { - self.updateViewAttributes() - } - - private func themeDidUpdate() { - self.updateViewAttributes() - } - - private func alignmentDidUpdate() { - self.updateViewAttributes() - } -} - diff --git a/core/Sources/Components/RadioButton/View/RadioButtonViewModelTests.swift b/core/Sources/Components/RadioButton/View/RadioButtonViewModelTests.swift deleted file mode 100644 index e07c46fcc..000000000 --- a/core/Sources/Components/RadioButton/View/RadioButtonViewModelTests.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// RadioButtonViewModelTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 13.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import SwiftUI -import XCTest - -final class RadioButtonViewModelTests: XCTestCase { - - // MARK: - Properties - var theme: ThemeGeneratedMock! - var bindingValue: Int? = 0 - var subscriptions: Set! - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.subscriptions = Set() - // Given - self.theme = ThemeGeneratedMock.mocked() - } - - // MARK: - Tests - func test_opacity_for_enabled() throws { - // Given - let sut = self.sut(intent: .basic) - - // When - sut.set(enabled: true) - let opacity = sut.opacity - - // Then - XCTAssertEqual(opacity, 1.00) - } - - func test_opacity_for_disabled() throws { - // Given - let sut = self.sut(intent: .basic) - - // When - sut.set(enabled: false) - let opacity = sut.opacity - - // Then - XCTAssertEqual(opacity, 0.40) - } - - func test_spacings() { - // When - let spacings = sutValues(for: \.spacing) - - // Then - XCTAssertEqual(spacings, Array(repeating: 5.0, count: 9)) - } - - func test_fonts() { - // When - let fonts = sutValues(for: \.font.font) - - // Then - XCTAssertEqual(fonts, Array(repeating: Font.body, count: 9)) - } - - func test_colors_reset_when_selected_value_set() { - // Given - let sut = self.sut(intent: .basic) - let expectation = XCTestExpectation(description: "Colors published when selection changes.") - expectation.expectedFulfillmentCount = 2 - - sut.$colors.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.set(selected: true) - - // Then - wait(for: [expectation], timeout: 0.5) - XCTAssertEqual(self.bindingValue, 1) - } - - func test_theme_update_publishes_changed_values() { - // Given - let sut = self.sut(intent: .basic) - let expectation = XCTestExpectation(description: "Changes to theme publishes value changes.") - expectation.expectedFulfillmentCount = 2 - - let publishers = Publishers.Zip4(sut.$opacity, sut.$spacing, sut.$font, sut.$colors) - - Publishers.Zip(publishers, sut.$colors) - .sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.set(theme: ThemeGeneratedMock.mocked()) - - // Then - wait(for: [expectation], timeout: 0.5) - } - - func test_state_update_publishes_changed_values() { - // Given - let sut = self.sut(intent: .basic) - let expectation = XCTestExpectation(description: "Changes to state publishes value changes.") - expectation.expectedFulfillmentCount = 2 - - Publishers.Zip(sut.$isDisabled, sut.$colors) - .sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.set(enabled: false) - - // Then - wait(for: [expectation], timeout: 0.5) - } - - func test_spacing_update_publishes_changed_values() { - // Given - let sut = self.sut(intent: .basic) - let expectation = XCTestExpectation(description: "Changes to label position publishes value changes.") - expectation.expectedFulfillmentCount = 2 - - var spacings = [CGFloat]() - - sut.$spacing.sink(receiveValue: { spacing in - spacings.append(spacing) - expectation.fulfill() - }) - .store(in: &self.subscriptions) - - // When - sut.set(alignment: .leading) - - // Then - wait(for: [expectation], timeout: 0.5) - - XCTAssertEqual(spacings, [self.theme.layout.spacing.medium, self.theme.layout.spacing.xxxLarge]) - } - - // MARK: - Private Helper Functions - - private func sutValues(for keyPath: KeyPath, T>) -> [T] { - return RadioButtonIntent.allCases - .map(self.sut(intent:)) - .map{ $0[keyPath: keyPath] } - } - - private func sut(intent: RadioButtonIntent) -> RadioButtonViewModel { - let seletedId = Binding( - get: { self.bindingValue }, - set: { self.bindingValue = $0 } - ) - - return RadioButtonViewModel( - theme: self.theme, - intent: intent, - id: 1, - label: .right("Test"), - selectedID: seletedId) - } -} - -private extension Theme where Self == ThemeGeneratedMock { - static func mocked() -> Self { - let theme = ThemeGeneratedMock() - let colors = ColorsGeneratedMock.mocked() - let layout = LayoutGeneratedMock.mocked() - let dims = DimsGeneratedMock.mocked() - let typography = TypographyGeneratedMock.mocked() - - theme.colors = colors - theme.layout = layout - theme.typography = typography - theme.dims = dims - - return theme - } -} diff --git a/core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonGroupView.swift b/core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonGroupView.swift deleted file mode 100644 index 8164a7b29..000000000 --- a/core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonGroupView.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// RadioButtonGroupView.swift -// SparkCore -// -// Created by michael.zimmermann on 12.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// RadioButtonGroupView is a radio button group control which renders a list of ``RadioButtonView``. -/// -/// The radio button group is created by providing: -/// - A theme -/// - An option title. If the title is empty, no title will be rendered. -/// - The selectedID, a binding value. -/// - A list of ``RadioButtonItem``. -/// -/// **Example** -/// ```swift -/// RadioButtonGroupView( -/// theme: self.theme, -/// title: "Radio Button Group", -/// selectedID: self.$selectedID, -/// items: [ -/// RadioButtonItem(label: "Label 1", id: 1), -/// RadioButtonItem(label: "Label 2", id: 2) -/// ] -/// } -/// ``` -public struct RadioButtonGroupView: View { - - // MARK: - Injected properties - - private var selectedID: Binding - private let items: [RadioButtonItem] - private let groupLayout: RadioButtonGroupLayout - private let labelAlignment: RadioButtonLabelAlignment - private let viewModel: RadioButtonGroupViewModel - - // MARK: - Local properties - - @ScaledMetric private var spacing: CGFloat - @ScaledMetric private var titleSpacing: CGFloat - - // MARK: - Initialization - - /// - Parameters - /// - theme: The theme defining colors and layout options. - /// - title: An option string. The title is rendered above the radio button items, if it is not empty. - /// - selectedID: a binding to the selected value. - /// - items: A list of ``RadioButtonItem`` - @available(*, deprecated, message: "Use init with intent instead.") - public init(theme: Theme, - title: String? = nil, - selectedID: Binding, - items: [RadioButtonItem], - radioButtonLabelPosition: RadioButtonLabelPosition = .right, - groupLayout: RadioButtonGroupLayout = .vertical, - state: RadioButtonGroupState = .enabled, - supplementaryLabel: String? = nil - ) { - self.init(theme: theme, - intent: state.intent, - selectedID: selectedID, - items: items, - labelAlignment: radioButtonLabelPosition.alignment, - groupLayout: groupLayout - ) - self.viewModel.content = RadioButtonGroupContent(title: title, supplementaryText: supplementaryLabel) - self.viewModel.isDisabled = state == .disabled - } - - /// - Parameters - /// - theme: The theme defining colors and layout options. - /// - title: An option string. The title is rendered above the radio button items, if it is not empty. - /// - selectedID: a binding to the selected value. - /// - items: A list of ``RadioButtonItem`` - public init(theme: Theme, - intent: RadioButtonIntent, - selectedID: Binding, - items: [RadioButtonItem], - labelAlignment: RadioButtonLabelAlignment = .trailing, - groupLayout: RadioButtonGroupLayout = .vertical - ) { - self.items = items - self.selectedID = selectedID - self.groupLayout = groupLayout - self.labelAlignment = labelAlignment - self.viewModel = RadioButtonGroupViewModel(theme: theme, intent: intent, content: .init()) - self._spacing = ScaledMetric(wrappedValue: self.viewModel.spacing) - self._titleSpacing = ScaledMetric(wrappedValue: self.viewModel.labelSpacing) - } - - // MARK: - Content - - public var body: some View { - - VStack(alignment: .leading, spacing: 0) { - if let title = self.viewModel.content.title { - radioButtonTitle(title) - .padding(.bottom, self.titleSpacing) - } - - if groupLayout == .vertical { - radioButtonItems - } else { - horizontalRadioButtons - } - - if let supplementaryLabel = self.viewModel.content.supplementaryText { - radioButtonSublabel(supplementaryLabel) - .padding(.top, self.titleSpacing) - } - } - } - - public func theme(_ theme: Theme) -> Self { - self.viewModel.theme = theme - return self - } - - @ViewBuilder - private func radioButtonTitle(_ title: String) -> some View { - Text(title) - .fixedSize(horizontal: false, vertical: true) - .font(self.viewModel.titleFont.font) - .foregroundColor(self.viewModel.titleColor.color) - .accessibilityIdentifier(RadioButtonAccessibilityIdentifier.radioButtonGroupTitle) - } - - @ViewBuilder - private func radioButtonSublabel(_ label: String) -> some View { - Text(label) - .font(self.viewModel.sublabelFont.font) - .foregroundColor(self.viewModel.sublabelColor.color) - } - - @ViewBuilder - private var horizontalRadioButtons: some View { - HStack(alignment: .top, spacing: self.spacing) { - radioButtonItems - } - } - - @ViewBuilder - private var radioButtonItems: some View { - ForEach(self.items, id: \.id) { item in - RadioButtonView( - theme: self.viewModel.theme, - intent: self.viewModel.intent, - id: item.id, - label: item.label, - selectedID: self.selectedID, - labelAlignment: self.labelAlignment - ) - .accessibilityIdentifier(RadioButtonAccessibilityIdentifier.radioButtonIdentifier(id: item.id)) - .padding(.bottom, self.bottomPadding(of: item)) - } - } - - private func bottomPadding(of item: RadioButtonItem) -> CGFloat { - if self.groupLayout == .horizontal || item.id == items.last?.id { - return 0 - } else { - return self.viewModel.spacing - } - } - - public func title(_ title: String) -> Self { - let content = self.viewModel.content.update(\.title, value: title) - self.viewModel.content = content - return self - } - - public func supplementaryText(_ supplementaryText: String) -> Self { - let content = self.viewModel.content.update(\.supplementaryText, value: supplementaryText) - self.viewModel.content = content - return self - } -} diff --git a/core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonView.swift b/core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonView.swift deleted file mode 100644 index fd4ca707e..000000000 --- a/core/Sources/Components/RadioButton/View/SwiftUI/RadioButtonView.swift +++ /dev/null @@ -1,211 +0,0 @@ -// -// RadioButtonView.swift -// SparkCore -// -// Created by michael.zimmermann on 05.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -private enum Constants { - static let pressedLineWidth: CGFloat = 4 - static let lineWidth: CGFloat = 2 - static let size: CGFloat = 24 - static let filledSize: CGFloat = 12 -} -/// RadioButtonView is a single radio button control. -/// Radio buttons are used for selecting a single value from a selection of values. -/// The values from which can be selected need to be ``Equatable`` & ``CustomStringConvertible``. -/// -/// The radio button is created by providing: -/// - A theme -/// - A unique ID -/// - A label representing the value to be selected -/// - The current selected ID: this is a binding and will change, when the button is selected. -/// - State: see ``RadioButtonGroupState``. The default state is ``.enabled`` -/// -/// **Example** -/// ```swift -/// @State var selectedID: Int = 1 -/// var body: any View { -/// VStack(alignment: .leading) { -/// RadioButtonView(theme: theme, id: 1, label: "label 1", selectedID: self.$selectedID) -/// RadioButtonView(theme: theme, id: 2, label: "label 2", selectedID: self.$selectedID) -/// ) -/// } -/// ``` -/// -/// An alternative to using ``RadioButtonViews`` is to use the ``RadioButtonGroupView``. -public struct RadioButtonView: View { - - // MARK: - Injected Properties - - @ObservedObject private var viewModel: RadioButtonViewModel - - // MARK: - Local Properties - - @Environment(\.isEnabled) private var isEnabled: Bool - @ScaledMetric private var pressedLineWidth: CGFloat = Constants.pressedLineWidth - @ScaledMetric private var lineWidth: CGFloat = Constants.lineWidth - @ScaledMetric private var size: CGFloat = Constants.size - @ScaledMetric private var filledSize: CGFloat = Constants.filledSize - @ScaledMetric private var spacing: CGFloat - - @State private var isPressed: Bool = false - - private var radioButtonSize: CGFloat { - return self.size + (self.pressedLineWidth * 2) - } - - // MARK: - Initialization - - /// - Parameters: - /// - theme: The theme used for designing colors and font of the radio button. - /// - id: A unique ID identifing the value of the item - /// - label: A text describing the value - /// - selectedID: A binding to which the id of the radio button will be assigned when selected. - /// - state: The current state, default value is `.enabled` - /// - labelPostion: The position of the label according to the radio button toggle. Default is `right` - public init(theme: Theme, - intent: RadioButtonIntent = .basic, - id: ID, - label: String, - selectedID: Binding, - labelAlignment: RadioButtonLabelAlignment = .trailing) { - let viewModel = RadioButtonViewModel( - theme: theme, - intent: intent, - id: id, - label: .right(label), - selectedID: selectedID, - alignment: labelAlignment) - self.init(viewModel: viewModel) - } - - @available(*, deprecated, message: "Use init with intent instead.") - public init(theme: Theme, - id: ID, - label: String, - selectedID: Binding, - groupState: RadioButtonGroupState = .enabled, - labelPosition: RadioButtonLabelPosition = .right) { - let viewModel = RadioButtonViewModel( - theme: theme, - intent: .basic, - id: id, - label: .right(label), - selectedID: selectedID, - alignment: labelPosition.alignment) - - viewModel.set(enabled: groupState != .disabled) - self.init(viewModel: viewModel) - } - - init(viewModel: RadioButtonViewModel) { - self.viewModel = viewModel - self._spacing = ScaledMetric(wrappedValue: viewModel.spacing) - } - - // MARK: - Content - - public var body: some View { - Button(action: { - self.viewModel.set(selected: true) - }, label: { - self.buttonAndLabel() - }) - .opacity(self.viewModel.opacity) - .buttonStyle(PressedButtonStyle(isPressed: self.$isPressed)) - .accessibilityLabel(self.viewModel.label.rightValue ?? RadioButtonAccessibilityIdentifier.radioButton) - .accessibilityValue(self.viewModel.id.description) - .isEnabledChanged { isEnabled in - self.viewModel.set(enabled: isEnabled) - } - } - - // MARK: - View modifier - @available(*, deprecated, message: "Use intent and disabled instead") - public func groupState(_ groupState: RadioButtonGroupState) -> Self { - self.viewModel.set(enabled: groupState != .disabled) - return self - } - - public func intent(_ intent: RadioButtonIntent) -> Self { - self.viewModel.set(intent: intent) - return self - } - - @available(*, deprecated, renamed: "alignment", message: "Please use func alignment() instead.") - public func labelPosition(_ labelPosition: RadioButtonLabelPosition) -> Self { - self.viewModel.set(alignment: labelPosition.alignment) - return self - } - - public func alignment(_ alignment: RadioButtonLabelAlignment) -> Self { - self.viewModel.set(alignment: alignment) - return self - } - - // MARK: - Private Functions - @ViewBuilder - private func buttonAndLabel() -> some View { - if self.viewModel.alignment == .trailing { - HStack(alignment: .top, spacing: self.spacing) { - self.radioButton() - self.label() - } - } else { - HStack(alignment: .top, spacing: 0) { - self.label() - - Spacer() - - self.radioButton() - .padding(.leading, viewModel.spacing) - } - } - } - - @ViewBuilder - private func label() -> some View { - if let text = self.viewModel.label.rightValue { - Text(text) - .font(self.viewModel.font.font) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(self.viewModel.colors.label.color) - } else { - EmptyView() - } - } - - @ViewBuilder - private func radioButton() -> some View { - let colors = self.viewModel.colors - - ZStack { - Circle() - .strokeBorder( - isPressed ? colors.halo.color : .clear, - lineWidth: self.pressedLineWidth - ) - - Circle() - .strokeBorder( - colors.button.color, - lineWidth: self.lineWidth - ) - .frame(width: self.size, height: self.size) - - Circle() - .fill() - .foregroundColor(colors.fill.color) - .frame(width: self.filledSize, height: self.filledSize) - } - .frame(width: self.radioButtonSize, - height: self.radioButtonSize) - .padding(-self.pressedLineWidth) - .animation(.easeIn(duration: 0.1), value: self.viewModel.selectedID) - } -} diff --git a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonToggleUIView.swift b/core/Sources/Components/RadioButton/View/UIKit/RadioButtonToggleUIView.swift deleted file mode 100644 index f6a986283..000000000 --- a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonToggleUIView.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// RadioButtonToggleUIView.swift -// SparkCore -// -// Created by michael.zimmermann on 21.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// Toggle view used for the radio button. When the toggle is pressed, a halo is rendered around the toggle. -final class RadioButtonToggleUIView: UIView { - - // MARK: Properties - private var haloColor: UIColor - private var buttonColor: UIColor - private var fillColor: UIColor - - private var pressedSublayer: CALayer? - - var isPressed: Bool = false { - didSet { - self.setNeedsDisplay() - } - } - - // MARK: Initialization - init() { - self.haloColor = .clear - self.buttonColor = .clear - self.fillColor = .clear - super.init(frame: .zero) - } - - /// Toggle view - /// This is the toggle used in the radio button to display whether an item is selected or not. - /// - /// Paramters: - /// - haloColor: The outermost color, usually set when the item is pressed - /// - buttonColor: The color of the inner circle - /// - fillColor: The filled dot in the middle. - init(haloColor: UIColor, buttonColor: UIColor, fillColor: UIColor) { - self.haloColor = haloColor - self.buttonColor = buttonColor - self.fillColor = fillColor - super.init(frame: .zero) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Methods - func setColors(_ colors: RadioButtonColors) { - self.haloColor = colors.halo.uiColor - self.buttonColor = colors.button.uiColor - self.fillColor = colors.fill.uiColor - self.setNeedsDisplay() - } - - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return false - } - - override func draw(_ rect: CGRect) { - super.draw(rect) - - guard - let ctx = UIGraphicsGetCurrentContext() - else { return } - - backgroundColor = .clear - let haloWidth = rect.width / 7 - let center = rect.width / 2 - let centerPoint = CGPoint(x: center, y: center) - - if self.isPressed { - let haloPath = UIBezierPath.circle(arcCenter: centerPoint, radius: (rect.width / 2 - haloWidth / 2)) - haloPath.lineWidth = haloWidth - ctx.setStrokeColor(haloColor.cgColor) - haloPath.stroke() - } - - let innerBorderWidth = (rect.width - haloWidth * 2) / 10 - let toggleWidth = rect.width - (haloWidth * 2) - let togglePath = UIBezierPath.circle(arcCenter: centerPoint, radius: (toggleWidth / 2 - innerBorderWidth / 2)) - togglePath.lineWidth = innerBorderWidth - ctx.setStrokeColor(buttonColor.cgColor) - togglePath.stroke() - - if fillColor != .clear { - let fillSize = (rect.width - haloWidth * 2) / 2 - let fillPath = UIBezierPath.circle(arcCenter: centerPoint, radius: fillSize / 2) - - ctx.setFillColor(fillColor.cgColor) - fillPath.fill() - } - } -} - -// MARK: Private helpers - -private extension UIBezierPath { - static func circle(arcCenter: CGPoint, - radius: CGFloat) -> UIBezierPath { - return UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true) - } -} diff --git a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupView.swift b/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupView.swift deleted file mode 100644 index e8a339bfb..000000000 --- a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupView.swift +++ /dev/null @@ -1,584 +0,0 @@ -// -// RadioButtonUIGroupView.swift -// SparkCore -// -// Created by michael.zimmermann on 24.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit -import SwiftUI - -/// RadioButtonGroupView embodies a radio button group and handles -public final class RadioButtonUIGroupView: UIControl { - - // MARK: - Private Properties - private var itemSpacingConstraints = [NSLayoutConstraint]() - private var itemLabelSpacingConstraints = [NSLayoutConstraint]() - private var allConstraints = [NSLayoutConstraint]() - private let valueSubject: PassthroughSubject - private let viewModel: RadioButtonGroupViewModel - - private var subscriptions = Set() - - @ScaledUIMetric private var spacing: CGFloat - @ScaledUIMetric private var labelSpacing: CGFloat - - private lazy var backingSelectedID: Binding = Binding( - get: { - return self.selectedID - }, - set: { newValue in - guard let newValue = newValue else { return } - self.selectedID = newValue - self.updateRadioButtonStates() - self.valueSubject.send(newValue) - self.delegate?.radioButtonGroup(self, didChangeSelection: newValue) - self.sendActions(for: .valueChanged) - } - ) - - public private(set) var radioButtonViews: [RadioButtonUIView] = [] - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - label.accessibilityIdentifier = RadioButtonAccessibilityIdentifier.radioButtonGroupTitle - label.setContentCompressionResistancePriority( - .required, - for: .horizontal) - return label - }() - - private lazy var supplementaryLabel: UILabel = { - let label = UILabel() - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - - label.setContentCompressionResistancePriority( - .required, - for: .horizontal) - - return label - }() - - // MARK: - Public Properties - /// All the items `RadioButtonUIItem` of the radio button group - public var items: [RadioButtonUIItem] { - set { - self.updateLayout(items: newValue) - } - get { - self.radioButtonViews.map{ - if let attributedText = $0.attributedText { - return .init(id: $0.id, label: attributedText) - } else { - return .init(id: $0.id, label: $0.text ?? "") - } - } - } - } - - public override var isEnabled: Bool { - didSet { - self.radioButtonViews.forEach{ $0.isEnabled = self.isEnabled } - } - } - - /// The number of radio button items in the radio button group - public var numberOfItems: Int { - return self.radioButtonViews.count - } - - /// An optional title of the radio button group - public var title: String? { - didSet { - self.titleDidUpdate() - } - } - - /// An optional supplementary text of the radio button group rendered at the bottom of the group. This is NOT well defined for the states `enabled` and disabled. - public var supplementaryText: String? { - didSet { - self.subtitleDidUpdate() - } - } - - /// The current selected ID. - public var selectedID: ID? { - didSet { - self.updateRadioButtonStates() - } - } - - /// The current theme - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - for radioButtonView in radioButtonViews { - radioButtonView.theme = newValue - } - self.viewModel.theme = newValue - } - } - - /// The label position `RadioButtonLabelPosition` according to the toggle, either `left` or `right`. The default value is `.left` - @available(*, deprecated, renamed: "alignment", message: "Please use alignment instead.") - public var radioButtonLabelPosition: RadioButtonLabelPosition { - get { - return self.labelAlignment.position - } - set { - self.labelAlignment = newValue.alignment - } - } - - /// The label position `RadioButtonLabelAlignment` according to the toggle, either `leading` or `trailing`. The default value is `.leading` - public var labelAlignment: RadioButtonLabelAlignment { - didSet { - guard self.labelAlignment != oldValue else { return } - - for radioButtonView in radioButtonViews { - radioButtonView.labelAlignment = labelAlignment - } - } - } - - /// The group layout `RadioButtonGroupLayout` of the radio buttons, either `horizontal` or `vertical`. The default is `vertical`. - public var groupLayout: RadioButtonGroupLayout { - didSet { - guard self.groupLayout != oldValue else { return } - - self.updateLayout(items: self.items) - } - } - - /// The intent `RadioButtonIntent` defining the colors of the radio button. - public var intent: RadioButtonIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - for radioButtonView in radioButtonViews { - radioButtonView.intent = newValue - } - } - } - - /// A delegate which can be set, to be notified of the changed selected item of the radio button. An alternative is to subscribe to the `publisher`. - public weak var delegate: (any RadioButtonUIGroupViewDelegate)? - - /// A change of the selected item will be published. This is an alternative method to the `delegate` of being notified of changes to the selected item. - public var publisher: some Publisher { - return self.valueSubject - } - - /// Set the accessibilityIdentifier. This identifier will be used as the accessibility identifier prefix of each radio button item, the suffix of that accessibility identifier being the index of the item within it's array. - public override var accessibilityIdentifier: String? { - didSet { - guard let identifier = accessibilityIdentifier else { return } - for (index, radioButtonView) in radioButtonViews.enumerated() { - radioButtonView.accessibilityIdentifier = "\(identifier)-\(index)" - } - } - } - - // MARK: Initializers - /// Initializer of the radio button ui group component. - /// Parameters: - /// - theme: The current theme. - /// - title: The title of the radio button group. This is optional, if it's not given, no title will be shown. - /// - selectedID: The current selected value of the radio button group. - /// - items: A list of `RadioButtonUIItem` which represent each item in the radio button group. - /// - radioButtonLabelPosition: The position of the label in each radio button item according to the toggle. The default value is, that the label is to the `right` of the toggle. - /// - groupLayout: The layout of the items within the group. These can be `horizontal` or `vertical`. The defalt is `vertical`. - /// - state: The state of the radiobutton group, see `RadioButtonGroupState` - @available(*, deprecated, message: "Use initializer with intent instead. Title and subtitle are also deprececated.") - public convenience init(theme: Theme, - title: String? = nil, - selectedID: ID, - items: [RadioButtonUIItem], - radioButtonLabelPosition: RadioButtonLabelPosition = .right, - groupLayout: RadioButtonGroupLayout = .vertical, - state: RadioButtonGroupState = .enabled, - supplementaryText: String? = nil) { - let viewModel = RadioButtonGroupViewModel( - theme: theme, - intent: state.intent, - content: () - ) - - self.init(viewModel: viewModel, - selectedID: selectedID, - items: items, - labelAlignment: radioButtonLabelPosition.alignment, - groupLayout: groupLayout) - - self.title = title - self.supplementaryText = supplementaryText - self.titleDidUpdate() - self.subtitleDidUpdate() - } - - /// Initializer of the radio button ui group component. - /// Parameters: - /// - theme: The current theme. - /// - intent: The default intent is `basic` - /// - selectedID: The current selected value of the radio button group. - /// - items: A list of `RadioButtonUIItem` which represent each item in the radio button group. - /// - radioButtonLabelPosition: The position of the label in each radio button item according to the toggle. The default value is, that the label is to the `right` of the toggle. - /// - groupLayout: The layout of the items within the group. These can be `horizontal` or `vertical`. The defalt is `vertical`. - public convenience init( - theme: Theme, - intent: RadioButtonIntent, - selectedID: ID?, - items: [RadioButtonUIItem], - labelAlignment: RadioButtonLabelAlignment = .trailing, - groupLayout: RadioButtonGroupLayout = .vertical) { - - let viewModel = RadioButtonGroupViewModel( - theme: theme, - intent: intent, - content: () - ) - - self.init( - viewModel: viewModel, - selectedID: selectedID, - items: items, - labelAlignment: labelAlignment, - groupLayout: groupLayout) - } - - init(viewModel: RadioButtonGroupViewModel, - selectedID: ID?, - items: [RadioButtonUIItem], - labelAlignment: RadioButtonLabelAlignment = .trailing, - groupLayout: RadioButtonGroupLayout = .vertical) { - self.viewModel = viewModel - self.selectedID = selectedID - self.labelAlignment = labelAlignment - self.groupLayout = groupLayout - self._spacing = ScaledUIMetric(wrappedValue: viewModel.spacing) - self._labelSpacing = ScaledUIMetric(wrappedValue: viewModel.labelSpacing) - self.valueSubject = PassthroughSubject() - - super.init(frame: .zero) - self.items = items - - self.setupView() - self.setupConstraints() - self.enableTouch() - self.setupSubscriptions() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - self._spacing.update(traitCollection: self.traitCollection) - self._labelSpacing.update(traitCollection: self.traitCollection) - - for constraint in self.itemSpacingConstraints { - constraint.constant = self.spacing - } - - for constraint in self.itemLabelSpacingConstraints { - constraint.constant = self.labelSpacing - } - } - - // MARK: Item Control - public func setTitle(_ title: String, forItemAt index: Int) { - guard index < self.radioButtonViews.count else { return } - - self.radioButtonViews[safe: index]?.text = title - } - - public func setTitle(_ title: NSAttributedString, forItemAt index: Int) { - guard index < self.radioButtonViews.count else { return } - - self.radioButtonViews[safe: index]?.attributedText = title - } - - public func addRadioButton(_ item: RadioButtonUIItem, atIndex index: Int = Int.max) { - - var items = self.items - if index < items.count { - items[index] = item - } else { - items.append(item) - } - - self.items = items - } - - public func removeRadioButton(at index: Int, animated: Bool = false) { - guard index < self.radioButtonViews.count else { return } - var items = self.items - items.remove(at: index) - - self.items = items - } - - // MARK: Private Methods - - private func setupView() { - - self.addSubview(self.titleLabel) - self.setTextOf(label: self.titleLabel, - title: self.title, - font: self.viewModel.titleFont.uiFont, - color: self.viewModel.titleColor.uiColor) - - for radioButtonView in self.radioButtonViews { - self.addSubview(radioButtonView) - } - - self.addSubview(self.supplementaryLabel) - self.setTextOf(label: self.supplementaryLabel, - title: self.supplementaryText, - font: self.viewModel.sublabelFont.uiFont, - color: self.viewModel.sublabelColor.uiColor) - } - - private func updateLayout(items: [RadioButtonUIItem]) { - NSLayoutConstraint.deactivate(self.allConstraints) - for view in self.radioButtonViews { - view.removeFromSuperview() - } - - self.radioButtonViews = self.createRadioButtonViews(items: items) - - for radioButtonView in radioButtonViews { - self.addSubview(radioButtonView) - } - - setupConstraints() - } - - private func createRadioButtonViews( - items: [RadioButtonUIItem]) -> [RadioButtonUIView] - { - return items.map { - let radioButtonView = RadioButtonUIView( - theme: self.theme, - intent: self.viewModel.intent, - id: $0.id, - label: $0.label, - selectedID: self.backingSelectedID, - labelAlignment: self.labelAlignment - ) - radioButtonView.accessibilityIdentifier = RadioButtonAccessibilityIdentifier.radioButtonIdentifier(id: $0.id) - - radioButtonView.translatesAutoresizingMaskIntoConstraints = false - - let action = UIAction { [weak self] _ in - self?.sendActions(for: .touchUpInside) - } - radioButtonView.addAction(action, for: .touchUpInside) - - return radioButtonView - } - } - - private func setupConstraints() { - NSLayoutConstraint.deactivate(self.allConstraints) - - if self.groupLayout == .vertical { - setupVerticalConstraints() - } else { - setupHorizontalConstraints() - } - } - - private func setupVerticalConstraints() { - var previousLayoutTopAnchor = self.topAnchor - var constraints = [NSLayoutConstraint]() - var spacing: CGFloat = 0 - - if self.title != nil { - constraints.append(self.titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor)) - constraints.append(self.titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)) - constraints.append(self.titleLabel.topAnchor.constraint(equalTo: previousLayoutTopAnchor)) - previousLayoutTopAnchor = self.titleLabel.bottomAnchor - spacing = self.labelSpacing - } - - for (index, radioButtonView) in radioButtonViews.enumerated() { - constraints.append(radioButtonView.leadingAnchor.constraint(equalTo: self.leadingAnchor)) - constraints.append(radioButtonView.trailingAnchor.constraint(equalTo: self.trailingAnchor)) - - let itemConstraint = radioButtonView.topAnchor.constraint(equalTo: previousLayoutTopAnchor, constant: spacing) - constraints.append(itemConstraint) - if spacing != 0 { - if index == 0 { - self.itemLabelSpacingConstraints.append(itemConstraint) - } else { - self.itemSpacingConstraints.append(itemConstraint) - } - } - spacing = self.spacing - - previousLayoutTopAnchor = radioButtonView.bottomAnchor - } - - if self.supplementaryText != nil { - constraints.append(self.supplementaryLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor)) - constraints.append(self.supplementaryLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)) - let topConstraint = self.supplementaryLabel.topAnchor.constraint(equalTo: previousLayoutTopAnchor, constant: self.labelSpacing) - constraints.append(topConstraint) - self.itemLabelSpacingConstraints.append(topConstraint) - previousLayoutTopAnchor = self.supplementaryLabel.bottomAnchor - } - - constraints.append(previousLayoutTopAnchor.constraint(equalTo: self.bottomAnchor)) - - self.allConstraints = constraints - NSLayoutConstraint.activate(constraints) - } - - private func setupHorizontalConstraints() { - var previousLayoutTopAnchor = self.topAnchor - var previousLayoutLeadingAnchor = self.leadingAnchor - var constraints = [NSLayoutConstraint]() - var spacing: CGFloat = 0 - - if self.title != nil { - constraints.append(self.titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor)) - constraints.append(self.titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)) - constraints.append(self.titleLabel.topAnchor.constraint(equalTo: previousLayoutTopAnchor)) - previousLayoutTopAnchor = self.titleLabel.bottomAnchor - spacing = self.labelSpacing - } - - var radioButtonAnchors = [NSLayoutYAxisAnchor]() - - for (index, radioButtonView) in radioButtonViews.enumerated() { - let topConstraint = radioButtonView.topAnchor.constraint(equalTo: previousLayoutTopAnchor, constant: spacing) - if spacing != 0 { - self.itemLabelSpacingConstraints.append(topConstraint) - } - constraints.append(topConstraint) - radioButtonAnchors.append(radioButtonView.bottomAnchor) - - if index == 0 { - constraints.append(radioButtonView.leadingAnchor.constraint(equalTo: previousLayoutLeadingAnchor)) - } else { - let itemConstraint = radioButtonView.leadingAnchor.constraint(equalTo: previousLayoutLeadingAnchor, constant: self.spacing) - self.itemSpacingConstraints.append(itemConstraint) - constraints.append(itemConstraint) - } - previousLayoutLeadingAnchor = radioButtonView.trailingAnchor - } - constraints.append(previousLayoutLeadingAnchor.constraint(equalTo: self.trailingAnchor)) - - var bottomAnchor: NSLayoutYAxisAnchor = self.bottomAnchor - var labelSpacing: CGFloat = 0 - - if self.supplementaryText != nil { - bottomAnchor = self.supplementaryLabel.topAnchor - labelSpacing = self.labelSpacing - constraints.append(self.supplementaryLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor)) - constraints.append(self.supplementaryLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)) - constraints.append(self.supplementaryLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)) - } - - for anchor in radioButtonAnchors { - let bottomConstraint = bottomAnchor.constraint(equalTo: anchor, constant: labelSpacing) - constraints.append(bottomConstraint) - if labelSpacing != 0 { - self.itemLabelSpacingConstraints.append(bottomConstraint) - } - } - - self.allConstraints = constraints - NSLayoutConstraint.activate(constraints) - } - - private func titleDidUpdate() { - if self.setTextOf(label: self.titleLabel, - title: self.title, - font: self.viewModel.titleFont.uiFont, - color: self.viewModel.titleColor.uiColor) { - self.setupConstraints() - } - } - - private func subtitleDidUpdate() { - if self.setTextOf( - label: self.supplementaryLabel, - title: self.supplementaryText, - font: self.viewModel.sublabelFont.uiFont, - color: self.viewModel.sublabelColor.uiColor) { - self.setupConstraints() - } - } - - private func setupSubscriptions() { - self.viewModel.$titleFont.subscribe(in: &self.subscriptions) { [weak self] font in - self?.titleLabel.font = font.uiFont - } - - self.viewModel.$titleColor.subscribe(in: &self.subscriptions) { [weak self] color in - self?.titleLabel.textColor = color.uiColor - } - - self.viewModel.$sublabelFont.subscribe(in: &self.subscriptions) { [weak self] font in - self?.supplementaryLabel.font = font.uiFont - } - - self.viewModel.$sublabelColor.subscribe(in: &self.subscriptions) { [weak self] color in - self?.supplementaryLabel.textColor = color.uiColor - } - - self.viewModel.$spacing.subscribe(in: &self.subscriptions) { [weak self] spacing in - guard let self = self else { return } - self._spacing = ScaledUIMetric(wrappedValue: spacing) - self.updateConstraints() - } - - self.viewModel.$labelSpacing.subscribe(in: &self.subscriptions) { [weak self] spacing in - guard let self = self else { return } - self._labelSpacing = ScaledUIMetric(wrappedValue: spacing) - self.updateConstraints() - } - } - - private func updateRadioButtonStates() { - for radioButtonView in self.radioButtonViews { - radioButtonView.toggleNeedsRedisplay() - } - } - - @discardableResult - private func setTextOf(label: UILabel, - title: String?, - font: UIFont, - color: UIColor) -> Bool { - label.font = font - label.textColor = color - - guard label.text != title else { return false } - - if label.text == nil { - label.isHidden = false - label.text = title - return true - } else if title == nil { - label.text = nil - label.isHidden = true - return true - } else { - label.text = title - return false - } - } -} diff --git a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupViewDelegate.swift b/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupViewDelegate.swift deleted file mode 100644 index e5312241b..000000000 --- a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIGroupViewDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// RadioButtonUIGroupViewDelegate.swift -// SparkCore -// -// Created by michael.zimmermann on 14.06.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Delegate that receives changes of radio button ui group view -public protocol RadioButtonUIGroupViewDelegate: AnyObject { - func radioButtonGroup(_ radioButtonGroup: some RadioButtonUIGroupView, didChangeSelection item: ID) -} diff --git a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIView.swift b/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIView.swift deleted file mode 100644 index 56951c503..000000000 --- a/core/Sources/Components/RadioButton/View/UIKit/RadioButtonUIView.swift +++ /dev/null @@ -1,512 +0,0 @@ -// -// RadioButtonUIView.swift -// Spark -// -// Created by michael.zimmermann on 18.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -// MARK: - Constants -private enum Constants { - static let toggleViewHeight: CGFloat = 32 - static let textLabelTopSpacing: CGFloat = 5 - static let haloWidth: CGFloat = 4 -} - -/// A radio button view composed of a toggle item, a label and a possible sublabel. -/// The color of the view is determined by the state. A possible sublabel is also part of the state. -/// The value of the radio button is represented by the generic type ID. -/// When the radio button is selected, it will change the binding value. -public final class RadioButtonUIView: UIControl { - - // MARK: - Injected Properties - private let viewModel: RadioButtonViewModel - - // MARK: - Public Properties - /// The general theme - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.set(theme: newValue) - } - } - - /// The current groupState - @available(*, deprecated, message: "Use intent and isEnabled instead.") - public var groupState: RadioButtonGroupState { - get { - if self.viewModel.state.isEnabled { - return .enabled - } else { - return .disabled - } - } - set { - self.viewModel.set(enabled: newValue != .disabled) - } - } - - public var intent: RadioButtonIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.set(intent: newValue) - } - } - - public override var isEnabled: Bool { - didSet { - self.viewModel.set(enabled: self.isEnabled) - } - } - - public override var isSelected: Bool { - get { - return self.viewModel.state.isSelected - } - set { - self.viewModel.set(selected: newValue) - } - } - - public override var isHighlighted: Bool { - get { - return self.toggleView.isPressed - } - set { - self.toggleView.isPressed = newValue - } - } - - /// The label of radio button - public var text: String? { - get { - return self.textLabel.text - } - set { - self.viewModel.label = .left(newValue.map(NSAttributedString.init)) - self.updateLabel() - } - } - - /// The ID of the radio button - public var id: ID { - get { - return self.viewModel.id - } - } - - /// The label of radio button - public var attributedText: NSAttributedString? { - get { - return self.textLabel.attributedText - } - set { - self.viewModel.label = .left(newValue) - self.updateLabel() - } - } - - /// The label of radio button - @available(*, deprecated, renamed: "attributedText") - public var label: NSAttributedString { - get { - return self.attributedText ?? NSAttributedString(string: "") - } - set { - self.attributedText = newValue - } - } - - /// The label position, right of left of the toggle - @available(*, deprecated, renamed: "labelAlignment", message: "Please use labelAlignment intead.") - public var labelPosition: RadioButtonLabelPosition { - get { - return self.viewModel.alignment.position - } - set { - self.viewModel.set(alignment: newValue.alignment) - } - } - - /// The label position according to the toggle - public var labelAlignment: RadioButtonLabelAlignment { - get { - return self.viewModel.alignment - } - set { - self.viewModel.set(alignment: newValue) - } - } - - /// Changes of the selection state is posted to the publisher. - public var publisher: some Publisher { - return self.subject - } - - // MARK: - Private Properties - @ScaledUIMetric private var toggleSize = Constants.toggleViewHeight - @ScaledUIMetric private var spacing: CGFloat - @ScaledUIMetric private var textLabelTopSpacing = Constants.textLabelTopSpacing - @ScaledUIMetric private var haloWidth = Constants.haloWidth - - private var subscriptions = Set() - private let subject = PassthroughSubject() - // Only needed as a binding for the single radio button - private var singleRadioButtonBinding: ValueBinding? - - // MARK: - View properties - private lazy var toggleView: RadioButtonToggleUIView = { - let toggleView = RadioButtonToggleUIView() - toggleView.isUserInteractionEnabled = false - toggleView.translatesAutoresizingMaskIntoConstraints = false - toggleView.backgroundColor = .clear - toggleView.sizeToFit() - toggleView.setContentCompressionResistancePriority( - .required, - for: .vertical) - toggleView.setContentCompressionResistancePriority( - .required, - for: .horizontal) - - return toggleView - }() - - private lazy var textLabel = UILabel.standard - - // MARK: - Constraint properties - private var toggleViewWidthConstraint: NSLayoutConstraint? - private var toggleViewHeightConstraint: NSLayoutConstraint? - private var toggleViewSpacingConstraint: NSLayoutConstraint? - private var labelViewTopConstraint: NSLayoutConstraint? - private var toggleViewTopConstraint: NSLayoutConstraint? - private var toggleViewLeadingConstraint: NSLayoutConstraint? - private var toggleViewTrailingConstraint: NSLayoutConstraint? - private var labelPositionConstraints: [NSLayoutConstraint] = [] - - // MARK: - Initialization - - /// The radio button component takes a theme, an id, a label and a binding - /// - /// Parameters: - /// - theme: The current theme - /// - id: The value of the radio button - /// - label: The text rendered to describe the value - /// - selectedID: A binding which is triggered when the radio button is selected - /// - groupState: the state of the radiobutton group - @available(*, deprecated, message: "Please use init with intent instead.") - public convenience init( - theme: Theme, - id: ID, - label: NSAttributedString, - selectedID: Binding, - groupState: RadioButtonGroupState = .enabled, - labelPosition: RadioButtonLabelPosition = .right - ) { - let viewModel = RadioButtonViewModel( - theme: theme, - intent: groupState.intent, - id: id, - label: .left(label), - selectedID: selectedID, - alignment: labelPosition.alignment) - - self.init(viewModel: viewModel) - } - - /// A radio button component which can be used as a standalone component. - /// This convenience init, avoids needing to use a binding. Changes to the selection state are published to the publisher. - /// - /// Parameters: - /// - theme: The current theme - /// - intent: The intent defining the color - /// - id: The value of the radio button - /// - label: The text rendered to describe the value - /// - isSelected: Bool, defining whether the radiobutton is selected or not. - /// - labelAlignment: the alignment of the label according to the toggle - public convenience init( - theme: Theme, - intent: RadioButtonIntent, - id: ID, - label: NSAttributedString, - isSelected: Bool, - labelAlignment: RadioButtonLabelAlignment = .trailing - ) { - let valueBinding = ValueBinding(selectedID: isSelected ? id : nil) - - let viewModel = RadioButtonViewModel( - theme: theme, - intent: intent, - id: id, - label: .left(label), - selectedID: valueBinding.binding, - alignment: labelAlignment) - - self.init(viewModel: viewModel) - self.singleRadioButtonBinding = valueBinding - } - - /// The radio button component takes a theme, an id, a label and a binding - /// - /// Parameters: - /// - theme: The current theme - /// - intent: The intent defining the color - /// - id: The value of the radio button - /// - label: The text rendered to describe the value - /// - selectedID: A binding which is triggered when the radio button is selected - /// - labelAlignment: the alignment of the label according to the toggle - public convenience init( - theme: Theme, - intent: RadioButtonIntent = .basic, - id: ID, - label: NSAttributedString, - selectedID: Binding, - labelAlignment: RadioButtonLabelAlignment = .trailing - ) { - let viewModel = RadioButtonViewModel( - theme: theme, - intent: intent, - id: id, - label: .left(label), - selectedID: selectedID, - alignment: labelAlignment) - - self.init(viewModel: viewModel) - } - - init(viewModel: RadioButtonViewModel) { - self.viewModel = viewModel - self._spacing = ScaledUIMetric(wrappedValue: viewModel.spacing) - - super.init(frame: CGRect.zero) - - self.arrangeViews() - self.setupButtonActions() - self.updateViewAttributes() - self.enableTouch() - self.setupSubscriptions() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public Functions - public func toggleNeedsRedisplay() { - self.viewModel.updateViewAttributes() - self.updateColors(self.viewModel.colors) - self.updateLabel() - self.toggleView.setNeedsDisplay() - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self._toggleSize.update(traitCollection: self.traitCollection) - self._spacing.update(traitCollection: self.traitCollection) - self._textLabelTopSpacing.update(traitCollection: self.traitCollection) - self._haloWidth.update(traitCollection: self.traitCollection) - - toggleViewSpacingConstraint?.constant = -self.spacing - toggleViewWidthConstraint?.constant = self.toggleSize - toggleViewHeightConstraint?.constant = self.toggleSize - - labelViewTopConstraint?.constant = self.textLabelTopSpacing - - toggleViewTopConstraint?.constant = -self.haloWidth - toggleViewLeadingConstraint?.constant = -self.haloWidth - toggleViewTrailingConstraint?.constant = self.haloWidth - } - - // MARK: - Private Functions - - private func setupSubscriptions() { - - self.viewModel.$opacity.subscribe(in: &self.subscriptions) { [weak self] opacity in - self?.alpha = opacity - } - - self.viewModel.$colors.subscribe(in: &self.subscriptions) { [weak self] colors in - guard let self else { return } - self.updateColors(colors) - self.updateLabel() - } - - self.viewModel.$font.subscribe(in: &self.subscriptions) { [weak self] font in - guard let self else { return } - self.textLabel.font = font.uiFont - self.updateLabel() - } - - self.viewModel.$alignment.subscribe(in: &self.subscriptions) { [weak self] _ in - self?.updatePositionConstraints() - } - - self.viewModel.$spacing.subscribe(in: &self.subscriptions) { [weak self] spacing in - guard let self else { return } - self._spacing = ScaledUIMetric(wrappedValue: spacing) - self.updatePositionConstraints() - } - } - - private func arrangeViews() { - self.translatesAutoresizingMaskIntoConstraints = false - - self.addSubview(self.toggleView) - self.addSubview(self.textLabel) - - self.setupConstraints() - self.updateLabel() - } - - private func updateViewAttributes() { - self.updateColors(self.viewModel.colors) - - self.updateLabel() - - self.alpha = self.viewModel.opacity - } - - private func updateLabel() { - self.textLabel.font = self.viewModel.font.uiFont - self.textLabel.textColor = self.viewModel.colors.label.uiColor - self.textLabel.attributedText = self.viewModel.label.leftValue - - self.textLabel.accessibilityIdentifier = RadioButtonAccessibilityIdentifier.radioButtonTextLabel - } - - private func updateColors(_ colors: RadioButtonColors) { - self.toggleView.setColors(colors) - self.textLabel.textColor = colors.label.uiColor - } - - private func setupButtonActions() { - self.addTarget(self, action: #selector(self.actionTapped(sender:)), for: .touchUpInside) - } - - private func setupConstraints() { - let toggleViewWidthConstraint = self.toggleView.widthAnchor.constraint(equalToConstant: self.toggleSize) - let toggleViewHeightConstraint = self.toggleView.heightAnchor.constraint(equalToConstant: self.toggleSize) - - let toggleViewSpacingConstraint = self.calculateToggleViewSpacingConstraint() - - let labelViewTopConstraint = self.textLabel.topAnchor.constraint( - equalTo: self.toggleView.topAnchor, constant: self.textLabelTopSpacing) - let toggleViewTopConstraint = self.toggleView.topAnchor.constraint( - equalTo: self.topAnchor, constant: -(self.haloWidth)) - let bottomViewConstraint = self.textLabel.bottomAnchor.constraint( - lessThanOrEqualTo: self.bottomAnchor, constant: 0) - - let labelPositionConstraints = calculatePositionConstraints() - - let constraints = [ - toggleViewWidthConstraint, - toggleViewHeightConstraint, - toggleViewSpacingConstraint, - toggleViewTopConstraint, - labelViewTopConstraint, - bottomViewConstraint - ] + labelPositionConstraints - - NSLayoutConstraint.activate(constraints) - - self.toggleViewWidthConstraint = toggleViewWidthConstraint - self.toggleViewHeightConstraint = toggleViewHeightConstraint - self.toggleViewSpacingConstraint = toggleViewSpacingConstraint - self.labelViewTopConstraint = labelViewTopConstraint - self.labelPositionConstraints = labelPositionConstraints - self.toggleViewTopConstraint = toggleViewTopConstraint - } - - private func calculatePositionConstraints() -> [NSLayoutConstraint] { - if self.viewModel.alignment == .trailing { - - let toggleViewLeadingConstraint = self.toggleView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -self.haloWidth) - self.toggleViewLeadingConstraint = toggleViewLeadingConstraint - - return [ - toggleViewLeadingConstraint, - self.textLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor) - ] - } else { - let toggleViewTrailingConstraint = self.toggleView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: self.haloWidth) - self.toggleViewTrailingConstraint = toggleViewTrailingConstraint - - return [ - toggleViewTrailingConstraint, - self.textLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), - ] - } - } - - private func calculateToggleViewSpacingConstraint() -> NSLayoutConstraint { - if self.viewModel.alignment == .trailing { - return self.toggleView.trailingAnchor.constraint( - equalTo: self.textLabel.leadingAnchor, constant: -self.spacing + self.haloWidth) - } else { - return self.textLabel.trailingAnchor.constraint( - lessThanOrEqualTo: self.toggleView.leadingAnchor, constant: -self.spacing + self.haloWidth) - } - } - - private func updatePositionConstraints() { - NSLayoutConstraint.deactivate([self.toggleViewSpacingConstraint].compactMap{ return $0 } + self.labelPositionConstraints) - - let toggleViewSpacingConstraint = calculateToggleViewSpacingConstraint() - let positionConstraints = self.calculatePositionConstraints() - - NSLayoutConstraint.activate([toggleViewSpacingConstraint] + positionConstraints) - - self.toggleViewSpacingConstraint = toggleViewSpacingConstraint - self.labelPositionConstraints = positionConstraints - } - - // MARK: - Control functions - @IBAction func actionTapped(sender: Any?) { - if !self.isSelected { - self.isSelected = true - self.subject.send(true) - self.sendActions(for: .valueChanged) - } - self.toggleView.isPressed = false - } -} - -// MARK: - Private Helpers - -private extension UILabel { - static var standard: UILabel { - let label = UILabel() - label.isUserInteractionEnabled = false - label.translatesAutoresizingMaskIntoConstraints = false - label.backgroundColor = .clear - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - label.adjustsFontForContentSizeCategory = true - label.setContentCompressionResistancePriority(.required, - for: .vertical) - return label - } -} - -// MARK: - Label Priorities -public extension RadioButtonUIView { - func setLabelContentCompressionResistancePriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentCompressionResistancePriority(priority, - for: axis) - } - - func setLabelContentHuggingPriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentHuggingPriority(priority, - for: axis) - } -} diff --git a/core/Sources/Components/Rating/AccessibilityIdentifier/RatingDisplayAccessibilityIdentifier.swift b/core/Sources/Components/Rating/AccessibilityIdentifier/RatingDisplayAccessibilityIdentifier.swift deleted file mode 100644 index 1b5abb3c7..000000000 --- a/core/Sources/Components/Rating/AccessibilityIdentifier/RatingDisplayAccessibilityIdentifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// RatingDisplayAccessibilityIdentifier.swift -// SparkCore -// -// Created by Michael Zimmermann on 21.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers of the rating display. -public enum RatingDisplayAccessibilityIdentifier { - - // MARK: - Properties - - /// The accessibility identifier. - public static let identifier = "spark-rating-display" -} diff --git a/core/Sources/Components/Rating/AccessibilityIdentifier/RatingInputAccessibilityIdentifier.swift b/core/Sources/Components/Rating/AccessibilityIdentifier/RatingInputAccessibilityIdentifier.swift deleted file mode 100644 index 26318cbb4..000000000 --- a/core/Sources/Components/Rating/AccessibilityIdentifier/RatingInputAccessibilityIdentifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// RatingInputAccessibilityIdentifier.swift -// SparkCore -// -// Created by Michael Zimmermann on 27.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers of the rating input. -public enum RatingInputAccessibilityIdentifier { - - // MARK: - Properties - - /// The accessibility identifier. - public static let identifier = "spark-rating-input" -} diff --git a/core/Sources/Components/Rating/Cache/CGLayerCache.swift b/core/Sources/Components/Rating/Cache/CGLayerCache.swift deleted file mode 100644 index 1b9af148a..000000000 --- a/core/Sources/Components/Rating/Cache/CGLayerCache.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CGLayerCache.swift -// SparkCore -// -// Created by michael.zimmermann on 08.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -// sourcery: AutoMockable -protocol CGLayerCaching { - func object(forKey: NSString) -> CGLayer? - func setObject(_ layer: CGLayer, forKey: NSString) -} - -/// A simple facade for a static NSCache -final class CGLayerCache: CGLayerCaching { - private static var cache = NSCache() - - func object(forKey key: NSString) -> CGLayer? { - return Self.cache.object(forKey: key) - } - - func setObject(_ layer: CGLayer, forKey key: NSString) { - Self.cache.setObject(layer, forKey: key) - } -} diff --git a/core/Sources/Components/Rating/Enum/RatingDisplaySize.swift b/core/Sources/Components/Rating/Enum/RatingDisplaySize.swift deleted file mode 100644 index 9de600cf8..000000000 --- a/core/Sources/Components/Rating/Enum/RatingDisplaySize.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// RatingDisplaySize.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum RatingDisplaySize: Int, CaseIterable { - case small = 12 - case medium = 16 - case large = 24 - case input = 40 -} diff --git a/core/Sources/Components/Rating/Enum/RatingIntent.swift b/core/Sources/Components/Rating/Enum/RatingIntent.swift deleted file mode 100644 index d341a7b7c..000000000 --- a/core/Sources/Components/Rating/Enum/RatingIntent.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// RatingIntent.swift -// SparkCore -// -// Created by michael.zimmermann on 09.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum RatingIntent: CaseIterable { - case main -} diff --git a/core/Sources/Components/Rating/Enum/RatingStarsCount.swift b/core/Sources/Components/Rating/Enum/RatingStarsCount.swift deleted file mode 100644 index 3e0666017..000000000 --- a/core/Sources/Components/Rating/Enum/RatingStarsCount.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// RatingStarsCount.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum RatingStarsCount: Int, CaseIterable { - case one = 1 - case five = 5 -} diff --git a/core/Sources/Components/Rating/Enum/StarDefaults.swift b/core/Sources/Components/Rating/Enum/StarDefaults.swift deleted file mode 100644 index fe120df1d..000000000 --- a/core/Sources/Components/Rating/Enum/StarDefaults.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// StarDefaults.swift -// SparkCore -// -// Created by Michael Zimmermann on 04.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit -import SwiftUI - -// MARK: - Default values -public enum StarDefaults { - public static let fillMode = StarFillMode.half - public static let rating = CGFloat(0.0) - public static let lineWidth = CGFloat(2.0) -} diff --git a/core/Sources/Components/Rating/Enum/StarFillMode.swift b/core/Sources/Components/Rating/Enum/StarFillMode.swift deleted file mode 100644 index c42ff80ac..000000000 --- a/core/Sources/Components/Rating/Enum/StarFillMode.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// StarFillMode.swift -// SparkCore -// -// Created by michael.zimmermann on 07.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The star fill mode determins how the star is to be filled -/// - full: the rating will be rounded to the next full number 0/1 and can be only empty or filled. -/// - half: the raing will be rounded to the next half number and can either be empty, half filled and filled. -/// - fraction: The fill rate will be the rating rounded to the neares fraction, e.g. half is fraction 2. -/// - exact: The star will be filled with the exact rating value -@frozen -public enum StarFillMode { - case full - case half - case fraction(_: CGFloat) - case exact - - // MARK: - Public functions - /// function rating - /// Calculate the rounded rating - public func rating(of givenValue: CGFloat) -> CGFloat { - if givenValue > 1 { - return 1.0 - } else if givenValue <= 0 { - return 0.0 - } - - switch self { - case .full: return givenValue.rounded() - case .half: return givenValue.halfRounded() - case let .fraction(fraction): return givenValue.rounded(by: fraction) - case .exact: return givenValue - } - } -} - -// MARK: - Private helpers -private extension CGFloat { - func rounded(by fraction: CGFloat) -> CGFloat { - return (self * fraction).rounded() / fraction - } - func halfRounded() -> CGFloat { - return self.rounded(by: 2.0) - } -} diff --git a/core/Sources/Components/Rating/Enum/StarFillModeUnitTests.swift b/core/Sources/Components/Rating/Enum/StarFillModeUnitTests.swift deleted file mode 100644 index e6581654d..000000000 --- a/core/Sources/Components/Rating/Enum/StarFillModeUnitTests.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// StarFillModeUnitTests.swift -// SparkCoreUnitTests -// -// Created by michael.zimmermann on 08.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class StarFillModeUnitTests: XCTestCase { - - func test_full_with_half_rating() { - XCTAssertEqual(StarFillMode.full.rating(of: 0.5), 1.0) - } - - func test_full_with_less_than_half_rating() { - XCTAssertEqual(StarFillMode.full.rating(of: 0.49), 0.0) - } - - func test_half_with_half_rating() { - XCTAssertEqual(StarFillMode.half.rating(of: 0.6), 0.5) - } - - func test_half_with_big_rating() { - XCTAssertEqual(StarFillMode.half.rating(of: 0.75), 1.0) - } - - func test_half_with_small_rating() { - XCTAssertEqual(StarFillMode.half.rating(of: 0.2), 0.0) - } - - func test_exact_rating() { - XCTAssertEqual(StarFillMode.exact.rating(of: 0.211), 0.211) - } - - func test_fraction_rating_round_down() { - XCTAssertEqual(StarFillMode.fraction(10).rating(of: 0.1111), 0.1) - } - - func test_fraction_rating_round_up() { - XCTAssertEqual(StarFillMode.fraction(10).rating(of: 0.15), 0.2) - } -} diff --git a/core/Sources/Components/Rating/Graphics/ShapeLayer.swift b/core/Sources/Components/Rating/Graphics/ShapeLayer.swift deleted file mode 100644 index 5fe3aabac..000000000 --- a/core/Sources/Components/Rating/Graphics/ShapeLayer.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// ShapeLayer.swift -// SparkCore -// -// Created by michael.zimmermann on 08.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -/// The Shape Layer draws a shape onto a layer and returns the CGLayer. -final class ShapeLayer { - // MARK: - Private variables - private let shape: CGPathShape - private let fillColor: CGColor - private let strokeColor: CGColor - private let fillPercentage: CGFloat - private let strokeWidth: CGFloat - - // MARK: - Initializer - init(shape: CGPathShape, - fillColor: CGColor, - strokeColor: CGColor, - fillPercentage: CGFloat, - strokeWidth: CGFloat) { - self.shape = shape - self.fillColor = fillColor - self.strokeColor = strokeColor - self.fillPercentage = fillPercentage - self.strokeWidth = strokeWidth - } - - /// Create a CGLayer and draw the shape on it. - func layer(graphicsContext: CGContext, size: CGSize) -> CGLayer { - guard let starLayer = CGLayer(graphicsContext, size: size, auxiliaryInfo: nil) else { - fatalError("Couldn't create layer") - } - - guard let context = starLayer.context else { - fatalError("Couldn't create layer") - } - - self.drawShape(graphicsContext: context, size: size) - return starLayer - } - - // MARK: - Private - private func drawShape(graphicsContext: CGContext, size: CGSize) { - let rect = CGRect(origin: .zero, size: size) - let path = self.shape.cgPath(rect: rect) - - graphicsContext.saveGState() - - graphicsContext.clip(to: rect) - graphicsContext.addPath(path) - graphicsContext.setLineWidth(self.strokeWidth) - graphicsContext.setStrokeColor(self.strokeColor) - graphicsContext.drawPath(using: .stroke) - - let insets = self.shape.insets.withHorizontalPadding(self.strokeWidth / 2.0) - let maskWidth = CGFloat((insets.right - insets.left) * fillPercentage) - - let maskHeight = rect.height - - let clipRect = CGRect( - x: insets.left, - y: 0, - width: maskWidth, - height: maskHeight - ) - graphicsContext.clip(to: clipRect) - graphicsContext.addPath(path) - graphicsContext.setFillColor(self.fillColor) - graphicsContext.drawPath(using: .fill) - - graphicsContext.addPath(path) - graphicsContext.setStrokeColor(self.fillColor) - graphicsContext.setLineWidth(self.strokeWidth) - graphicsContext.drawPath(using: .stroke) - - graphicsContext.restoreGState() - } -} - -// MARK: Private extensions -private extension UIEdgeInsets { - func withHorizontalPadding(_ padding: CGFloat) -> UIEdgeInsets { - return UIEdgeInsets(top: self.top, left: self.left - padding, bottom: self.bottom, right: self.right + padding) - } -} diff --git a/core/Sources/Components/Rating/Graphics/Star.swift b/core/Sources/Components/Rating/Graphics/Star.swift deleted file mode 100644 index 01300af43..000000000 --- a/core/Sources/Components/Rating/Graphics/Star.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// Star.swift -// SparkCore -// -// Created by michael.zimmermann on 08.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -protocol CGPathShape { - func cgPath(rect: CGRect) -> CGPath - var insets: UIEdgeInsets { get } -} - -/// A star shape to calculate the CGPath of a star. -final class Star: CGPathShape { - - // MARK: - Private variables - private let numberOfVertices: Int - private let vertexSize: CGFloat - private let cornerRadiusSize: CGFloat - - /// the outermost points of the star. - var insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - - // MARK: - Initializer - init(numberOfVertices: Int, - vertexSize: CGFloat, - cornerRadiusSize: CGFloat) { - self.numberOfVertices = numberOfVertices - self.vertexSize = vertexSize - self.cornerRadiusSize = cornerRadiusSize - } - - /// Return the CGPath of the star. - func cgPath(rect: CGRect) -> CGPath { - let path = CGMutablePath() - - self.insets = UIEdgeInsets(top: rect.minY, left: rect.minX, bottom: rect.maxY, right: rect.maxX) - - // the size of each angle step - let angleStep = ((.pi * 2) / CGFloat(self.numberOfVertices)) - - let initialOffset: CGFloat = -.pi / 2 - - // the start angle, the first star vertex is to start at the top, therefore the -90° rotation. - var angle: CGFloat = angleStep + initialOffset - - var centerPoint = CGPoint(x: rect.midX, y: rect.midY) - let fullRadius = (min(rect.width, rect.height) / 2) - let cornerRadius = self.cornerRadiusSize * fullRadius - let radius = fullRadius - cornerRadius - - // calculate offset to make star look centered vertically - let lowestVertex: Int = self.numberOfVertices / 2 - let lowestPoint = CGPoint( - x: centerPoint.x + radius * cos(angleStep * CGFloat(lowestVertex) + initialOffset), - y: centerPoint.y + radius * sin(angleStep * CGFloat(lowestVertex) + initialOffset) - ) - let highestPoint = CGPoint( - x: centerPoint.x + radius * cos(initialOffset), - y: centerPoint.y + radius * sin(initialOffset) - ) - - insets.top = highestPoint.y - insets.bottom = lowestPoint.y - insets.left = rect.maxX - insets.right = rect.minX - - let diff = (rect.maxY - highestPoint.y - lowestPoint.y) / 2 - - // modify centerpoint to make star look symmetric - centerPoint = CGPoint( - x: centerPoint.x, - y: centerPoint.y + diff) - - let arcAngle = cornerRadius / radius - - // draw the star - for i in 1...self.numberOfVertices { - let outerPoint1 = CGPoint( - x: centerPoint.x + (radius + cornerRadius) * cos(angle - arcAngle / 2), - y: centerPoint.y + (radius + cornerRadius) * sin(angle - arcAngle / 2) - ) - - let outerPoint2 = CGPoint( - x: centerPoint.x + (radius + cornerRadius) * cos(angle + arcAngle / 2), - y: centerPoint.y + (radius + cornerRadius) * sin(angle + arcAngle / 2) - ) - - let tangentPoint = CGPoint( - x: centerPoint.x + radius * cos(angle - arcAngle), - y: centerPoint.y + radius * sin(angle - arcAngle) - ) - - let nextTangentPoint = CGPoint( - x: centerPoint.x + radius * cos(angle + arcAngle), - y: centerPoint.y + radius * sin(angle + arcAngle) - ) - - let innerPoint = CGPoint( - x: centerPoint.x + (radius * self.vertexSize) * cos(angle + angleStep / 2), - y: centerPoint.y + (radius * self.vertexSize) * sin(angle + angleStep / 2) - ) - - if i == 1 { - path.move(to: tangentPoint) - } else { - path.addLine(to: tangentPoint) - } - - path.addCurve(to: nextTangentPoint, control1: outerPoint1, control2: outerPoint2) - - self.insets.left = [ - self.insets.left, - nextTangentPoint.x, - outerPoint1.x, - outerPoint2.x].reduce(rect.maxX, smallest) - self.insets.right = [ - self.insets.right, - nextTangentPoint.x, - outerPoint1.x, - outerPoint2.x].reduce(rect.minX, greatest) - - path.addLine(to: innerPoint) - - angle += angleStep - } - path.closeSubpath() - - return path - } -} - -// MARK: - Private functions -private func smallest(_ lh: CGFloat, _ rh: CGFloat) -> CGFloat { - return lh < rh ? lh : rh -} - -private func greatest(_ lh: CGFloat, _ rh: CGFloat) -> CGFloat { - return lh > rh ? lh : rh -} diff --git a/core/Sources/Components/Rating/Model/RatingColors.swift b/core/Sources/Components/Rating/Model/RatingColors.swift deleted file mode 100644 index d6eb00b00..000000000 --- a/core/Sources/Components/Rating/Model/RatingColors.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// RatingColors.swift -// SparkCore -// -// Created by michael.zimmermann on 09.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct RatingColors: Equatable { - var fillColor: any ColorToken - var strokeColor: any ColorToken - var opacity: CGFloat - - static func == (lhs: RatingColors, rhs: RatingColors) -> Bool { - return lhs.fillColor.equals(rhs.fillColor) && - lhs.strokeColor.equals(rhs.strokeColor) && - lhs.opacity == rhs.opacity - } -} diff --git a/core/Sources/Components/Rating/Model/RatingSizeAttributes.swift b/core/Sources/Components/Rating/Model/RatingSizeAttributes.swift deleted file mode 100644 index e1ec1f75b..000000000 --- a/core/Sources/Components/Rating/Model/RatingSizeAttributes.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// RatingSizeAttributes.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct RatingSizeAttributes: Equatable { - let borderWidth: CGFloat - let height: CGFloat - let spacing: CGFloat -} diff --git a/core/Sources/Components/Rating/Model/RatingState.swift b/core/Sources/Components/Rating/Model/RatingState.swift deleted file mode 100644 index 8e34006bc..000000000 --- a/core/Sources/Components/Rating/Model/RatingState.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// RatingState.swift -// SparkCore -// -// Created by michael.zimmermann on 09.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct RatingState: Updateable, Equatable { - var isEnabled: Bool - var isPressed: Bool - - static var standard = RatingState(isEnabled: true, isPressed: false) - static var disabled = RatingState(isEnabled: false, isPressed: false) - static var pressed = RatingState(isEnabled: true, isPressed: true) -} diff --git a/core/Sources/Components/Rating/Model/StarConfiguration.swift b/core/Sources/Components/Rating/Model/StarConfiguration.swift deleted file mode 100644 index 21b7aad33..000000000 --- a/core/Sources/Components/Rating/Model/StarConfiguration.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// StarConfiguration.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public struct StarConfiguration: Equatable, Sendable { - public var numberOfVertices: Int - public var vertexSize: CGFloat - public var cornerRadiusSize: CGFloat - - public var description: String { - return "\(self.numberOfVertices)-\(self.vertexSize)-\(self.cornerRadiusSize)" - } - - // MARK: - Default values - public enum Defaults { - public static let numberOfVertices = 5 - public static let vertexSize = CGFloat(0.65) - public static let cornerRadiusSize = CGFloat(0.15) - } - - public static let `default` = StarConfiguration( - numberOfVertices: Defaults.numberOfVertices, - vertexSize: Defaults.vertexSize, - cornerRadiusSize: Defaults.cornerRadiusSize) - - public init(numberOfVertices: Int, - vertexSize: CGFloat, - cornerRadiusSize: CGFloat) { - self.numberOfVertices = numberOfVertices - self.vertexSize = vertexSize - self.cornerRadiusSize = cornerRadiusSize - } - -} diff --git a/core/Sources/Components/Rating/TestHelpers/RatingDisplayConfigurationSnapshotTests.swift b/core/Sources/Components/Rating/TestHelpers/RatingDisplayConfigurationSnapshotTests.swift deleted file mode 100644 index a6431cf27..000000000 --- a/core/Sources/Components/Rating/TestHelpers/RatingDisplayConfigurationSnapshotTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// RatingDisplayConfigurationSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 20.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -@testable import SparkCore - -struct RatingDisplayConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: RatingDisplayScenarioSnapshotTests - - let rating: CGFloat - let size: RatingDisplaySize - let count: RatingStarsCount - let intent = RatingIntent.main - - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.size)", - "\(self.count)", - "\(self.rating)" - ].joined(separator: "-") - } -} diff --git a/core/Sources/Components/Rating/TestHelpers/RatingDisplayScenarioSnapshotTests.swift b/core/Sources/Components/Rating/TestHelpers/RatingDisplayScenarioSnapshotTests.swift deleted file mode 100644 index ea4273b3a..000000000 --- a/core/Sources/Components/Rating/TestHelpers/RatingDisplayScenarioSnapshotTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// RatingDisplayScenarioSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 20.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit -import SwiftUI - -@testable import SparkCore - -enum RatingDisplayScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) -> [RatingDisplayConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1(isSwiftUIComponent: isSwiftUIComponent) - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To various rating values - /// - /// Content: - /// - ratings: [1.0, 2.5, 5.5] - /// - size: medium - /// - count: five (number of stars) - /// - modes: all - /// - accessibility sizes: default - private func test1(isSwiftUIComponent: Bool) -> [RatingDisplayConfigurationSnapshotTests] { - let ratings: [CGFloat] = [1.0, 2.5, 5.5] - - return ratings.map { rating in - return .init( - scenario: self, - rating: rating, - size: .medium, - count: .five, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// - /// Description: To various accessibility sizes - /// - /// Content: - /// - ratings: [ 2.5] - /// - size: small - /// - count: five (number of stars) - /// - modes: default - /// - sizes: all - private func test2(isSwiftUIComponent: Bool) -> [RatingDisplayConfigurationSnapshotTests] { - return [.init( - scenario: self, - rating: 2.5, - size: .small, - count: .five, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - )] - } - - /// Test 3 - /// - /// Description: To various rating sizes - /// - /// Content: - /// - ratings: [2.5] - /// - size: [small, medium, large, input] - /// - count: five (number of stars) - /// - modes: default - /// - accessibility sizes: default - private func test3(isSwiftUIComponent: Bool) -> [RatingDisplayConfigurationSnapshotTests] { - - return RatingDisplaySize.allCases.map { size in - return .init( - scenario: self, - rating: 2.5, - size: size, - count: .five, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } -} diff --git a/core/Sources/Components/Rating/TestHelpers/RatingInputConfigurationSnapshotTests.swift b/core/Sources/Components/Rating/TestHelpers/RatingInputConfigurationSnapshotTests.swift deleted file mode 100644 index 6eee06813..000000000 --- a/core/Sources/Components/Rating/TestHelpers/RatingInputConfigurationSnapshotTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// RatingInputConfigurationSnapshotTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -@testable import SparkCore - -struct RatingInputConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: RatingInputScenarioSnapshotTests - - let rating: CGFloat - let intent = RatingIntent.main - - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - let state: RatingInputState - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.rating)", - "\(self.state)" - ].joined(separator: "-") - } -} - -enum RatingInputState: CaseIterable { - case enabled - case disabled - case pressed -} diff --git a/core/Sources/Components/Rating/TestHelpers/RatingInputScenarioSnapshotTests.swift b/core/Sources/Components/Rating/TestHelpers/RatingInputScenarioSnapshotTests.swift deleted file mode 100644 index 55a10b0f2..000000000 --- a/core/Sources/Components/Rating/TestHelpers/RatingInputScenarioSnapshotTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// RatingInputScenarioSnapshotTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit -import SwiftUI - -@testable import SparkCore - -enum RatingInputScenarioSnapshotTests: String, CaseIterable { - - case test1 - case test2 - case test3 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - func configuration(isSwiftUIComponent: Bool) -> [RatingInputConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1(isSwiftUIComponent: isSwiftUIComponent) - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To various rating values - /// - /// Content: - /// - ratings: 2.0 - /// - states: enabled - /// - modes: all - /// - accessibility sizes: default - private func test1(isSwiftUIComponent: Bool) -> [RatingInputConfigurationSnapshotTests] { - let ratings: [CGFloat] = [1.0, 5.0] - - return ratings.map { rating in - return .init( - scenario: self, - rating: rating, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default, - state: .enabled - ) - } - } - - /// Test 2 - /// - /// - /// Description: To various accessibility sizes - /// - /// Content: - /// - ratings: [1.0] - /// - modes: default - /// - accessibility sizes: all - /// - states: all - private func test2(isSwiftUIComponent: Bool) -> [RatingInputConfigurationSnapshotTests] { - return [.init( - scenario: self, - rating: 1.0, - modes: Constants.Modes.default, - sizes: Constants.Sizes.all, - state: .enabled - )] - } - - /// Test 3 - /// - /// Description: To various rating values - /// - /// Content: - /// - ratings: [1.0, 5.0] - /// - states: disabled, pressed - /// - modes: all - /// - accessibility sizes: default - private func test3(isSwiftUIComponent: Bool) -> [RatingInputConfigurationSnapshotTests] { - - return [RatingInputState.disabled, .pressed].map { state in - return .init( - scenario: self, - rating: 2.0, - modes: Constants.Modes.all, - sizes: Constants.Sizes.default, - state: state - ) - } - } - -} diff --git a/core/Sources/Components/Rating/UseCases/RatingGetColorsUseCase.swift b/core/Sources/Components/Rating/UseCases/RatingGetColorsUseCase.swift deleted file mode 100644 index 1e5d86a33..000000000 --- a/core/Sources/Components/Rating/UseCases/RatingGetColorsUseCase.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// RatingGetColorUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 09.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol RatingGetColorsUseCaseable { - func execute(theme: Theme, - intent: RatingIntent, - state: RatingState - ) -> RatingColors -} - -extension RatingGetColorsUseCaseable { - func execute(theme: Theme, - intent: RatingIntent - ) -> RatingColors { - return self.execute(theme: theme, intent: intent, state: .standard) - } -} - -/// Get the colors of the rating -struct RatingGetColorsUseCase: RatingGetColorsUseCaseable { - - /// Returns the rating colors. - /// - /// - Parameters: - /// - theme: the current theme - /// - intent: the intent defining the color in the theme - /// - state: the current state - func execute(theme: Theme, - intent: RatingIntent, - state: RatingState - ) -> RatingColors { - - var colors: RatingColors - let fillColor = state.isPressed ? theme.colors.states.mainVariantPressed : theme.colors.main.mainVariant - - switch intent { - case .main: colors = RatingColors( - fillColor: fillColor, - strokeColor: theme.colors.base.onSurface.opacity(theme.dims.dim3), - opacity: theme.dims.none - ) - } - - if !state.isEnabled { - colors.opacity = theme.dims.dim3 - } - return colors - } - -} diff --git a/core/Sources/Components/Rating/UseCases/RatingGetColorsUseCaseUnitTests.swift b/core/Sources/Components/Rating/UseCases/RatingGetColorsUseCaseUnitTests.swift deleted file mode 100644 index efaac7b82..000000000 --- a/core/Sources/Components/Rating/UseCases/RatingGetColorsUseCaseUnitTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// RatingGetColorsUseCaseUnitTests.swift -// SparkCore -// -// Created by michael.zimmermann on 09.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import XCTest - -@testable import SparkCore - -final class RatingGetColorsUseCaseUnitTests: XCTestCase { - - // MARK: - Variables - var sut: RatingGetColorsUseCase! - var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.theme = ThemeGeneratedMock.mocked() - self.sut = RatingGetColorsUseCase() - } - - // MARK: - Tests - func test_standard_display() { - // When - let colors = self.sut.execute( - theme: self.theme, - intent: .main, - state: .standard) - - // Then - let expectedColors = RatingColors( - fillColor: theme.colors.main.mainVariant, - strokeColor: theme.colors.base.onSurface.opacity(theme.dims.dim3), - opacity: theme.dims.none) - - XCTAssertEqual(colors, expectedColors) - } - - func test_disabled() { - // When - let colors = self.sut.execute( - theme: self.theme, - intent: .main, - state: .disabled) - - // Then - let expectedColors = RatingColors( - fillColor: theme.colors.main.mainVariant, - strokeColor: theme.colors.base.onSurface.opacity(theme.dims.dim3), - opacity: theme.dims.dim3) - - XCTAssertEqual(colors, expectedColors) - } - - func test_pressed() { - // When - let colors = self.sut.execute( - theme: self.theme, - intent: .main, - state: .pressed) - - // Then - let expectedColors = RatingColors( - fillColor: theme.colors.states.mainVariantPressed, - strokeColor: theme.colors.base.onSurface.opacity(theme.dims.dim3), - opacity: theme.dims.none) - - XCTAssertEqual(colors, expectedColors) - } -} diff --git a/core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCase.swift b/core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCase.swift deleted file mode 100644 index 0a126bca6..000000000 --- a/core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCase.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// RatingSizeAttributesUseCase.swift -// SparkCore -// -// Created by Michael Zimmermann on 20.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol RatingSizeAttributesUseCaseable { - func execute(spacing: LayoutSpacing, size: RatingDisplaySize) -> RatingSizeAttributes -} - -/// Calculates size attributes of the rating display according to the spacing and the size. -struct RatingSizeAttributesUseCase: RatingSizeAttributesUseCaseable { - - /// Returns: rating size attributes - /// - /// - Parameters: - /// - spacing: the spacing defined by the theme - /// - size: the size of the rating display - func execute(spacing: LayoutSpacing, size: RatingDisplaySize) -> RatingSizeAttributes { - switch size { - case .small: return size.sizeAttributes(spacing: spacing.small) - case .medium: return size.sizeAttributes(spacing: spacing.small) - case .large: return size.sizeAttributes(spacing: spacing.small) - case .input: return size.sizeAttributes(spacing: spacing.medium) - } - } -} - -// MARK: - Private helpers -private extension RatingDisplaySize { - func sizeAttributes(spacing: CGFloat) -> RatingSizeAttributes { - switch self { - case .small: return .init(borderWidth: 1.0, height: self.height, spacing: spacing) - case .medium: return .init(borderWidth: 1.33, height: self.height, spacing: spacing) - case .large: return .init(borderWidth: 2.0, height: self.height, spacing: spacing) - case .input: return .init(borderWidth: 3.33, height: self.height, spacing: spacing) - } - } - - var height: CGFloat { - return CGFloat(self.rawValue) - } -} diff --git a/core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCaseTests.swift b/core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCaseTests.swift deleted file mode 100644 index 2bfaf9a86..000000000 --- a/core/Sources/Components/Rating/UseCases/RatingSizeAttributesUseCaseTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// RatingSizeAttributesUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 20.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class RatingSizeAttributesUseCaseTests: XCTestCase { - - var spacing: LayoutSpacingGeneratedMock! - var sut: RatingSizeAttributesUseCase! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.spacing = LayoutSpacingGeneratedMock.mocked() - self.sut = RatingSizeAttributesUseCase() - } - - // MARK: - Tests - func test_small() { - // When - let sizes = sut.execute(spacing: self.spacing, size: .small) - - // Then - XCTAssertEqual(sizes, .init(borderWidth: 1.0, height: 12.0, spacing: 3.0)) - } - - func test_medium() { - // When - let sizes = sut.execute(spacing: self.spacing, size: .medium) - - // Then - XCTAssertEqual(sizes, .init(borderWidth: 1.33, height: 16.0, spacing: 3.0)) - } - - func test_large() { - // When - let sizes = sut.execute(spacing: self.spacing, size: .large) - - // Then - XCTAssertEqual(sizes, .init(borderWidth: 2.0, height: 24.0, spacing: 3.0)) - } - - func test_input() { - // When - let sizes = sut.execute(spacing: self.spacing, size: .input) - - // Then - XCTAssertEqual(sizes, .init(borderWidth: 3.33, height: 40.0, spacing: 5.0)) - } -} diff --git a/core/Sources/Components/Rating/View/RatingDisplayViewModel.swift b/core/Sources/Components/Rating/View/RatingDisplayViewModel.swift deleted file mode 100644 index 63884127c..000000000 --- a/core/Sources/Components/Rating/View/RatingDisplayViewModel.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// RatingDisplayViewModel.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation - -/// A view model for the rating display. -final class RatingDisplayViewModel: ObservableObject { - - /// The current theme of which colors and sizes are dependent. - var theme: Theme { - didSet { - self.colors = self.colorsUseCase.execute(theme: self.theme, intent: self.intent) - self.ratingSize = self.sizeUseCase.execute(spacing: theme.layout.spacing, size: size) - } - } - - /// The current intent which describes colors. - var intent: RatingIntent { - didSet { - guard self.intent != oldValue else { return } - self.colors = self.colorsUseCase.execute(theme: self.theme, intent: self.intent) - } - } - - /// The display size of the rating - var size: RatingDisplaySize { - didSet { - guard self.size != oldValue else { return } - self.ratingSize = self.sizeUseCase.execute(spacing: theme.layout.spacing, size: size) - } - } - - /// The number of stars in the rating display - var count: RatingStarsCount { - didSet { - guard self.count != oldValue else { return } - self.updateRatingValue() - } - } - - /// The current rating value - var rating: CGFloat { - didSet { - guard self.rating != oldValue else { return } - self.updateRatingValue() - } - } - - // MARK: - Published variables - /// The current selection / enabled state - @Published var ratingState: RatingState - /// The current defined colors - @Published var colors: RatingColors - /// Current size attributes - @Published var ratingSize: RatingSizeAttributes - /// The normalized rating value - @Published var ratingValue: CGFloat - - // MARK: - Private variables - private let colorsUseCase: RatingGetColorsUseCaseable - private let sizeUseCase: RatingSizeAttributesUseCaseable - - // MARK: Initializer - init(theme: Theme, - intent: RatingIntent, - size: RatingDisplaySize, - count: RatingStarsCount, - rating: CGFloat = 0.0, - ratingState: RatingState = .standard, - colorsUseCase: RatingGetColorsUseCaseable = RatingGetColorsUseCase(), - sizeUseCase: RatingSizeAttributesUseCaseable = RatingSizeAttributesUseCase() - ) { - self.theme = theme - self.intent = intent - self.size = size - self.colorsUseCase = colorsUseCase - self.colors = colorsUseCase.execute(theme: theme, intent: intent, state: ratingState) - self.sizeUseCase = sizeUseCase - self.ratingSize = sizeUseCase.execute(spacing: theme.layout.spacing, size: size) - self.ratingValue = count.ratingValue(rating) - self.rating = rating - self.count = count - self.ratingState = ratingState - } - - func updateState(isPressed: Bool) { - self.ratingState.isPressed = isPressed - self.colors = self.colorsUseCase.execute( - theme: self.theme, - intent: self.intent, - state: self.ratingState) - } - - @discardableResult - func updateState(isEnabled: Bool) -> Self { - guard self.ratingState.isEnabled != isEnabled else { return self } - - self.ratingState.isEnabled = isEnabled - self.colors = self.colorsUseCase.execute( - theme: self.theme, - intent: self.intent, - state: self.ratingState) - - return self - } - - // MARK: - Private functions - private func updateRatingValue() { - self.ratingValue = self.count.ratingValue(self.rating) - } -} - -// MARK: - Private helpers -private extension RatingStarsCount { - func ratingValue(_ rating: CGFloat) -> CGFloat { - switch self { - case .five: return rating - case .one: return rating / 5.0 - } - } -} diff --git a/core/Sources/Components/Rating/View/RatingDisplayViewModelTests.swift b/core/Sources/Components/Rating/View/RatingDisplayViewModelTests.swift deleted file mode 100644 index f5a55bb76..000000000 --- a/core/Sources/Components/Rating/View/RatingDisplayViewModelTests.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// RatingDisplayViewModelTests.swift -// SparkCoreUnitTests -// -// Created by Michael Zimmermann on 20.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// -import Combine -import XCTest - -@testable import SparkCore - -final class RatingDisplayViewModelTests: XCTestCase { - - // MARK: - Properties - var theme: ThemeGeneratedMock! - var colorsUseCase: RatingGetColorsUseCaseableGeneratedMock! - var sizeUseCase: RatingSizeAttributesUseCaseableGeneratedMock! - var sut: RatingDisplayViewModel! - var subscriptions: Set! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.colorsUseCase = RatingGetColorsUseCaseableGeneratedMock() - self.sizeUseCase = RatingSizeAttributesUseCaseableGeneratedMock() - self.theme = ThemeGeneratedMock.mocked() - self.subscriptions = .init() - - self.colorsUseCase.executeWithThemeAndIntentAndStateReturnValue = RatingColors( - fillColor: self.theme.colors.main.onMain, - strokeColor: self.theme.colors.base.background, - opacity: 1.0) - - self.sizeUseCase.executeWithSpacingAndSizeReturnValue = RatingSizeAttributes(borderWidth: 2.0, height: 20, spacing: 8) - - self.sut = RatingDisplayViewModel( - theme: self.theme, - intent: .main, - size: .small, - count: .five, - rating: 1.0, - colorsUseCase: self.colorsUseCase, - sizeUseCase: self.sizeUseCase - ) - } - - override func tearDown() { - super.tearDown() - self.subscriptions.removeAll() - } - - // MARK: - Tests - func test_setup() { - // Then - XCTAssertEqual(self.sut.colors.opacity, 1.0, "Expected opacity to 0.0") - XCTAssertEqual(self.sut.colors.fillColor.uiColor, self.theme.colors.main.onMain.uiColor, "Expected fill color to be onMain") - XCTAssertEqual(self.sut.colors.strokeColor.uiColor, self.theme.colors.base.background.uiColor, "Expected background color to be set") - XCTAssertEqual(self.sut.ratingValue, 1.0) - } - - func test_single_star() { - // Given - self.sut.rating = 2 - self.sut.count = .one - - XCTAssertEqual(self.sut.ratingValue, 0.4) - } - - func test_theme_updated() { - - // Given - let colorsPublisherMock: PublisherMock.Publisher> = .init(publisher: self.sut.$colors) - let ratingSizePublisherMock: PublisherMock.Publisher> = .init(publisher: self.sut.$ratingSize) - - colorsPublisherMock.loadTesting(on: &subscriptions) - ratingSizePublisherMock.loadTesting(on: &subscriptions) - - // When - self.sut.theme = self.theme - - // Then - XCTAssertPublisherSinkCountEqual( - on: colorsPublisherMock, - 2 - ) - - XCTAssertPublisherSinkCountEqual( - on: ratingSizePublisherMock, - 2 - ) - } - - func test_rating_size_did_update() { - // Given - let ratingSizePublisherMock: PublisherMock.Publisher> = .init(publisher: self.sut.$ratingSize) - - ratingSizePublisherMock.loadTesting(on: &subscriptions) - - // When - self.sut.size = .input - - // Then - XCTAssertPublisherSinkCountEqual( - on: ratingSizePublisherMock, - 2 - ) - } - - func test_rating_count_did_update() { - // Given - let ratingValuePublisherMock: PublisherMock.Publisher> = .init(publisher: self.sut.$ratingValue) - - ratingValuePublisherMock.loadTesting(on: &subscriptions) - - // When - self.sut.count = .one - - // Then - XCTAssertPublisherSinkCountEqual( - on: ratingValuePublisherMock, - 2 - ) - - XCTAssertEqual(self.sut.ratingValue, self.sut.rating / 5.0) - } - - func test_rating_did_update() { - // Given - let ratingValuePublisherMock: PublisherMock.Publisher> = .init(publisher: self.sut.$ratingValue) - - ratingValuePublisherMock.loadTesting(on: &subscriptions) - - // When - self.sut.count = .one - self.sut.rating = 4 - - // Then - XCTAssertPublisherSinkCountEqual( - on: ratingValuePublisherMock, - 3 - ) - - XCTAssertEqual(self.sut.ratingValue, self.sut.rating / 5.0) - } -} diff --git a/core/Sources/Components/Rating/View/SwiftUI/RatingDisplayView.swift b/core/Sources/Components/Rating/View/SwiftUI/RatingDisplayView.swift deleted file mode 100644 index 5bf80da9d..000000000 --- a/core/Sources/Components/Rating/View/SwiftUI/RatingDisplayView.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// RatingDisplayView.swift -// SparkCore -// -// Created by Michael Zimmermann on 04.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// RatingDisplayView is a view with which a 5 star rating can be shown. -/// The rating value is expected to be within the range [0...5]. Values outside of this range will be ignored. Anything less than zero will be shown as zero. Anything greater than 5 will be shown as five. -/// The rating display may be shown with 5 stars (the standard version) or just one star for a shortened version. For the shortened version, the expected value range is still [0...5] -public struct RatingDisplayView: View { - - // MARK: - Private properties - private let fillMode: StarFillMode - private let configuration: StarConfiguration - @ObservedObject private var viewModel: RatingDisplayViewModel - - // MARK: - Scaled metrics - @ScaledMetric private var scalingFactor: CGFloat = 1 - - // MARK: - Initialization - /// Create a rating display view with the following parameters - /// - Parameters: - /// - theme: The current theme - /// - intent: The intent to define the colors - /// - count: The number of stars in the rating view. The default is `five`. - /// - size: The size of the rating view. The default is `medium` - /// - rating: The rating value. This should be a value within the range 0...5 - /// - fillMode: Define incomplete stars are to be filled. The default is `.half` - /// - configuration: A configuration of the star. A default value is defined. - public init( - theme: Theme, - intent: RatingIntent, - count: RatingStarsCount = .five, - size: RatingDisplaySize = .medium, - rating: CGFloat = 0.0, - fillMode: StarFillMode = .half, - configuration: StarConfiguration = .default - ) { - let viewModel = RatingDisplayViewModel( - theme: theme, - intent: intent, - size: size, - count: count, - rating: rating - ) - self.fillMode = fillMode - self.viewModel = viewModel - self.configuration = configuration - } - - // MARK: - View - public var body: some View { - self.stars() - .accessibilityIdentifier(RatingDisplayAccessibilityIdentifier.identifier) - .accessibilityElement() - .accessibilityValue("\(self.viewModel.ratingValue.description) / \(self.viewModel.count.rawValue)") - } - - @ViewBuilder - private func stars() -> some View { - if self.viewModel.count == .one { - self.oneStar( - rating: self.viewModel.ratingValue, - index: 0 - ) - } else { - self.fiveStars() - } - } - - // MARK: - Private functions - @ViewBuilder - private func oneStar(rating: CGFloat, index: Int) -> some View { - let size = self.viewModel.ratingSize.height * self.scalingFactor - StarView( - rating: rating, - fillMode: self.fillMode, - lineWidth: self.viewModel.ratingSize.borderWidth * self.scalingFactor, - borderColor: self.viewModel.colors.strokeColor.color, - fillColor: self.viewModel.colors.fillColor.color, - configuration: self.configuration - ) - .frame( - width: size, - height: size - ) - .accessibilityIdentifier("\(RatingDisplayAccessibilityIdentifier.identifier)-\(index)") - } - - @ViewBuilder - private func fiveStars() -> some View { - HStack(spacing: self.viewModel.ratingSize.spacing * self.scalingFactor) { - ForEach((0...4), id: \.self) { index in - self.oneStar( - rating: self.viewModel.ratingValue - CGFloat(index), - index: index - ) - } - } - } -} diff --git a/core/Sources/Components/Rating/View/SwiftUI/RatingDisplayViewSnapshotTests.swift b/core/Sources/Components/Rating/View/SwiftUI/RatingDisplayViewSnapshotTests.swift deleted file mode 100644 index e5ff4f33b..000000000 --- a/core/Sources/Components/Rating/View/SwiftUI/RatingDisplayViewSnapshotTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// RatingDisplayViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 05.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -import UIKit - -@testable import SparkCore - -final class RatingDisplayViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = RatingDisplayScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: true) - for configuration in configurations { - let view = RatingDisplayView( - theme: self.theme, - intent: .main, - rating: configuration.rating - ) - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Rating/View/SwiftUI/RatingInputView.swift b/core/Sources/Components/Rating/View/SwiftUI/RatingInputView.swift deleted file mode 100644 index 52ef83362..000000000 --- a/core/Sources/Components/Rating/View/SwiftUI/RatingInputView.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// RatingInputView.swift -// SparkCore -// -// Created by Michael Zimmermann on 06.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A SwiftUI native rating input component. -public struct RatingInputView: View { - // MARK: - Private variables - @ObservedObject private var viewModel: RatingDisplayViewModel - @Environment(\.isEnabled) private var isEnabled: Bool - private var rating: Binding - private var configuration: StarConfiguration - private var intent: RatingIntent - - // MARK: - Initialization - /// Create a rating display view with the following parameters - /// - Parameters: - /// - theme: The current theme - /// - intent: The intent to define the colors - /// - rating: A binding containg the rating value. This should be a value within the range 0...5 - /// - configuration: A configuration of the star. A default value is defined. - public init( - theme: Theme, - intent: RatingIntent, - rating: Binding, - configuration: StarConfiguration = .default - ) { - self.rating = rating - self.intent = intent - self.configuration = configuration - self.viewModel = RatingDisplayViewModel( - theme: theme, - intent: intent, - size: .input, - count: .five) - } - - // MARK: - View - public var body: some View { - RatingInputInternalView(viewModel: viewModel.updateState(isEnabled: self.isEnabled), rating: self.rating, configuration: self.configuration) - } - - // MARK: - Internal functions - /// This function is just exposed for testing - internal func highlighted(_ isHiglighed: Bool) -> Self { - self.viewModel.updateState(isPressed: isHiglighed) - return self - } -} - -// MARK: - Internal Rating Input -struct RatingInputInternalView: View { - - // MARK: - Private variables - @ObservedObject private var viewModel: RatingDisplayViewModel - @State private var displayRating: CGFloat - @Binding private var rating: CGFloat - @ScaledMetric private var scaleFactor: CGFloat = 1.0 - private let configuration: StarConfiguration - - // MARK: - Initialization - /// Create a rating display view with the following parameters - /// - Parameters: - /// - viewModel: The view model of the view. - /// - rating: A binding containg the rating value. This should be a value within the range 0...5 - /// - configuration: A configuration of the star - init( - viewModel: RatingDisplayViewModel, - rating: Binding, - configuration: StarConfiguration - ) { - self._rating = rating - self._displayRating = State(initialValue: rating.wrappedValue) - self.configuration = configuration - self.viewModel = viewModel - } - - // MARK: - View - var body: some View { - let size = self.viewModel.ratingSize.height * self.scaleFactor - let lineWidth = self.viewModel.ratingSize.borderWidth * self.scaleFactor - let spacing = self.viewModel.ratingSize.spacing * self.scaleFactor - let width = size * CGFloat(self.viewModel.count.rawValue) + spacing * CGFloat(self.viewModel.count.maxIndex) - let viewRect = CGRect(x: 0, y: 0, width: width, height: size) - let colors = self.viewModel.colors - - HStack(spacing: spacing) { - ForEach((0...self.viewModel.count.maxIndex), id: \.self) { index in - StarView( - rating: self.displayRating - CGFloat(index), - fillMode: .full, - lineWidth: lineWidth, - borderColor: colors.strokeColor.color, - fillColor: colors.fillColor.color, - configuration: self.configuration - ) - .frame( - width: size, - height: size - ) - .accessibilityIdentifier("\(RatingInputAccessibilityIdentifier.identifier)-\(index)") - } - } - .compositingGroup() - .opacity(colors.opacity) - .gesture(self.dragGesture(viewRect: viewRect)) - .frame(width: width, height: size) - .accessibilityIdentifier(RatingInputAccessibilityIdentifier.identifier) - .accessibilityElement() - .accessibilityAdjustableAction { direction in - switch direction { - case .increment: - guard self.displayRating <= CGFloat(self.viewModel.count.maxIndex) else { break } - self.displayRating += 1 - case .decrement: - guard self.displayRating > 1 else { break } - self.displayRating -= 1 - @unknown default: - break - } - self.rating = self.displayRating - } - .accessibilityValue(self.displayRating.description) - } - - // MARK: - Private functions - private func dragGesture(viewRect: CGRect) -> some Gesture { - DragGesture(minimumDistance: 0.0) - .onChanged { value in - if let index = viewRect.pointIndex(of: value.location, horizontalSlices: self.viewModel.count.rawValue) { - self.displayRating = CGFloat(index + 1) - self.viewModel.updateState(isPressed: true) - } else { - self.displayRating = self._rating.wrappedValue - self.viewModel.updateState(isPressed: false) - } - } - .onEnded { value in - if let index = viewRect.pointIndex(of: value.location, horizontalSlices: self.viewModel.count.rawValue) { - self.rating = CGFloat(index + 1) - self.displayRating = CGFloat(index + 1) - } else { - self.displayRating = self._rating.wrappedValue - } - self.viewModel.updateState(isPressed: false) - } - } -} - -private extension RatingStarsCount { - var maxIndex: Int { - return rawValue - 1 - } -} diff --git a/core/Sources/Components/Rating/View/SwiftUI/RatingInputViewSnapshotTests.swift b/core/Sources/Components/Rating/View/SwiftUI/RatingInputViewSnapshotTests.swift deleted file mode 100644 index 1c23848c4..000000000 --- a/core/Sources/Components/Rating/View/SwiftUI/RatingInputViewSnapshotTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// RatingInputViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 08.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit - -@testable import SparkCore - -final class RatingInputViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - private var backingRating: CGFloat = 0 - - private lazy var rating: Binding = { - return Binding( - get: { return self.backingRating }, - set: { newValue, transaction in - self.backingRating = newValue - } - ) - }() - - func test() { - let scenarios = RatingInputScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: true) - for configuration in configurations { - self.backingRating = configuration.rating - let view = RatingInputView( - theme: self.theme, - intent: .main, - rating: self.rating) - .highlighted(configuration.state == .pressed) - .disabled(configuration.state == .disabled) - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Rating/View/SwiftUI/StarShape.swift b/core/Sources/Components/Rating/View/SwiftUI/StarShape.swift deleted file mode 100644 index f821d7bf8..000000000 --- a/core/Sources/Components/Rating/View/SwiftUI/StarShape.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// StarShape.swift -// SparkCore -// -// Created by Michael Zimmermann on 04.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A SwiftUI shape representing a star. -/// The paths of the stars are cached to avoid recalculating the same path multiple times. -struct StarShape: Shape { - // MARK: - Private variables - private static let cache: NSCache = .init() - private let configuration: StarConfiguration - - // MARK: - Initializer - init(configuration: StarConfiguration) { - self.configuration = configuration - } - - /// Returns the path of a star within the rect. - func path(in rect: CGRect) -> Path { - let cacheKey = self.cacheKey(rect: rect) - if let cgPath = Self.cache.object(forKey: cacheKey) { - return Path(cgPath) - } - - let cgPath = Star( - numberOfVertices: self.configuration.numberOfVertices, - vertexSize: self.configuration.vertexSize, - cornerRadiusSize: self.configuration.cornerRadiusSize) - .cgPath(rect: rect) - - Self.cache.setObject(cgPath, forKey: cacheKey) - - return Path(cgPath) - } - - // MARK: - Private functions - private func cacheKey(rect: CGRect) -> NSString { - return NSString(string: "\(self.configuration.description)_\(rect)") - } -} diff --git a/core/Sources/Components/Rating/View/SwiftUI/StarView.swift b/core/Sources/Components/Rating/View/SwiftUI/StarView.swift deleted file mode 100644 index 2dce4e14a..000000000 --- a/core/Sources/Components/Rating/View/SwiftUI/StarView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// StarView.swift -// SparkCore -// -// Created by Michael Zimmermann on 04.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// StarView is a single SwiftUI Star. -/// The star may have a rating value which should be in the range [0...1] -struct StarView: View { - // MARK: Private variables - private let rating: CGFloat - private let lineWidth: CGFloat - private let borderColor: Color - private let fillColor: Color - private let star: StarShape - - // MARK: - Initializer - /// Create a StarUIView with the following parameters - /// - /// - Parameters: - /// - rating: the value of the rating. This should be a number in the range [0...1] - /// - fillMode: the fill mode of the start. The star will be filled according to the rating and the fillMode. - /// - borderColor: The color of the border of the unfilled part of the star. - /// - fillColor: The color of the filled part of the star. - /// - configuration: StarConfiguration. The default = `default`. - init( - rating: CGFloat = StarDefaults.rating, - fillMode: StarFillMode = .half, - lineWidth: CGFloat = StarDefaults.lineWidth, - borderColor: Color, - fillColor: Color, - configuration: StarConfiguration = .default - ) { - self.rating = fillMode.rating(of: rating) - self.lineWidth = lineWidth - self.borderColor = borderColor - self.fillColor = fillColor - self.star = StarShape(configuration: configuration) - } - - // MARK: - View - var body: some View { - if self.rating <= 0 { - star.stroke(self.borderColor, lineWidth: self.lineWidth) - .accessibilityValue("0.0") - } else if self.rating >= 1 { - star.stroke(self.fillColor, lineWidth: self.lineWidth) - .overlay{ - star.fill(self.fillColor) - } - .accessibilityValue("1.0") - } else { - if #available(iOS 17.0, *) { - self.newVersion() - .accessibilityValue("\(self.rating)") - } else { - self.oldVersion() - .accessibilityValue("\(self.rating)") - } - } - } - - // MARK: - Private functions - @available(iOS 17.0, *) - @ViewBuilder - private func newVersion() -> some View { - self.star.stroke(self.borderColor, lineWidth: self.lineWidth) - .overlay { - self.background() - .mask { - self.star - .stroke(self.fillColor, lineWidth: self.lineWidth) - .fill(self.fillColor) - } - } - } - - @ViewBuilder - private func oldVersion() -> some View { - self.star.stroke(self.borderColor, lineWidth: self.lineWidth) - .overlay { - self.background() - .mask { - self.star.stroke(self.fillColor, lineWidth: self.lineWidth) - } - } - .overlay { - self.background() - .mask { - self.star.fill(self.fillColor) - } - } - } - - @ViewBuilder - func background() -> some View { - GeometryReader { geometry in - HStack(spacing: 0) { - Rectangle() - .fill(self.fillColor) - .frame(width: self.ratingPercent(width: geometry.size.width)) - Rectangle() - .fill(Color.clear) - } - } - } - - private func ratingPercent(width: CGFloat) -> CGFloat { - return width * self.rating - } -} - -struct StarView_Previews: PreviewProvider { - static var previews: some View { - StarView(rating: 0.4, - fillMode: .half, - lineWidth: 2.0, - borderColor: .gray, - fillColor: .purple, - configuration: .default) - .frame(width: 100, height: 100) - } -} diff --git a/core/Sources/Components/Rating/View/UIKit/RatingDisplayUIView.swift b/core/Sources/Components/Rating/View/UIKit/RatingDisplayUIView.swift deleted file mode 100644 index d3ad8cdb3..000000000 --- a/core/Sources/Components/Rating/View/UIKit/RatingDisplayUIView.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// RatingDisplayUIView.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// RatingDisplayUIView is a view with which a 5 star rating can be shown. -/// The rating value is expected to be within the range [0...5]. Values outside of this range will be ignored. Anything less than zero will be shown as zero. Anything greater than 5 will be shown as five. -/// The rating display may be shown with 5 stars (the standard version) or just one star for a shortened version. For the shortened version, the expected value range is still [0...5] -public class RatingDisplayUIView: UIView { - - // MARK: - Private variables - private lazy var stackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = self.spacing - stackView.distribution = .fillEqually - return stackView - }() - - private let viewModel: RatingDisplayViewModel - private let fillMode: StarFillMode - private var sizeConstraints = [NSLayoutConstraint]() - private var cancellable = Set() - - // MARK: - Public accessors - /// Count: the number of stars to show in the rating. - /// Only values five and one are allowed, five is the default. - public var count: RatingStarsCount { - get { - return self.viewModel.count - } - set { - guard self.viewModel.count != newValue else { return } - self.viewModel.count = newValue - self.updateView() - } - } - - /// The current rating. This should be a value in the range [0...5]. - /// Anything small than 0 will be treated as a 0, and anything greater than 5 will be treated as a five. - public var rating: CGFloat { - get { - return self.viewModel.rating - } - set { - self.viewModel.rating = newValue - } - } - - /// The current theme. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// The intent for defining the color. - public var intent: RatingIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - /// The size of the rating stars. - /// Possible sizes `small`, `medium` and `input`. - public var size: RatingDisplaySize { - get { - return self.viewModel.size - } - set { - self.viewModel.size = newValue - } - } - - // MARK: - Internal accessors - internal var isPressed: Bool { - get { - self.viewModel.ratingState.isPressed - } - set { - self.viewModel.updateState(isPressed: newValue) - } - } - - internal var isEnabled: Bool { - get { - self.viewModel.ratingState.isEnabled - } - set { - self.viewModel.updateState(isEnabled: newValue) - } - } - - internal var ratingStarViews: [StarUIView] { self.stackView.arrangedSubviews.compactMap { view in - return view as? StarUIView - } - } - - // MARK: - Scaled metrics - @ScaledUIMetric private var borderWidth: CGFloat - @ScaledUIMetric private var ratingSize: CGFloat - @ScaledUIMetric private var spacing: CGFloat - - // MARK: - Initialization - - /// Create a rating display view with the following parameters - /// - Parameters: - /// - theme: The current theme - /// - intent: The intent to define the colors - /// - count: The number of stars in the rating view. The default is `five`. - /// - size: The size of the rating view. The default is `medium` - /// - rating: The rating value. This should be a value within the range 0...5 - /// - fillMode: Define incomplete stars are to be filled. The default is `.half` - /// - configuration: A configuration of the star. A default value is defined. - public init( - theme: Theme, - intent: RatingIntent, - count: RatingStarsCount = .five, - size: RatingDisplaySize = .medium, - rating: CGFloat = 0.0, - fillMode: StarFillMode = .half, - configuration: StarConfiguration = .default - ) { - self.fillMode = fillMode - self.viewModel = RatingDisplayViewModel( - theme: theme, - intent: intent, - size: size, - count: count, - rating: rating - ) - self.spacing = self.viewModel.ratingSize.spacing - self.ratingSize = self.viewModel.ratingSize.height - self.borderWidth = self.viewModel.ratingSize.borderWidth - - super.init(frame: .zero) - self.setupView() - self.setupSubscriptions() - self.setUpAccessibility() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self._spacing.update(traitCollection: traitCollection) - self._borderWidth.update(traitCollection: traitCollection) - self._ratingSize.update(traitCollection: traitCollection) - - self.updateStarsViewsBorderWidths() - self.stackView.spacing = self.spacing - - self.sizeConstraints.forEach { constraint in - constraint.constant = self.ratingSize - } - } - - // MARK: - Private functions - private func setupView() { - self.accessibilityIdentifier = RatingDisplayAccessibilityIdentifier.identifier - var currentRating = self.viewModel.ratingValue - for i in 0.. { - return self.subject - } - - /// A delegate which is called on rating changes by user interaction - public weak var delegate: RatingInputUIViewDelegate? - - // MARK: - Private properties - private let ratingDisplay: RatingDisplayUIView - private var subject = PassthroughSubject() - private var lastSelectedIndex: Int? - - // MARK: - Initializer - /// Init - /// - Parameters - /// - theme: the current theme - /// - intent: the current intent defining the color - /// - rating: the current rating. This should be a value in the range between 0...5. The default value is 0 - /// - configuration: The star configuration, the default is `default` - public init( - theme: Theme, - intent: RatingIntent, - rating: CGFloat = 0.0, - configuration: StarConfiguration = .default - ) { - self.ratingDisplay = RatingDisplayUIView( - theme: theme, - intent: intent, - count: .five, - size: .input, - rating: rating, - fillMode: .full, - configuration: configuration - ) - - self.rating = rating - super.init(frame: .zero) - self.setupView() - self.enableTouch() - self.addPanGestureToPreventCancelTracking() - self.setupAccessibility() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Handle touch events - public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - return self.handleTouch(touch, with: event) - } - - public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - return self.handleTouch(touch, with: event) - } - - public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { - - guard let location = touch?.location(in: self) else { - self.ratingStarHighlightCancelled() - return - } - - if !self.bounds.contains(location) { - self.ratingStarHighlightCancelled() - } else if let index = self.ratingDisplay.ratingStarViews.index(closestTo: location) { - self.ratingStarSelected(index) - } else if let index = self.lastSelectedIndex { - self.ratingStarSelected(index) - } else { - self.ratingStarHighlightCancelled() - } - - self.lastSelectedIndex = nil - } - - // MARK: - Private functions - - // MARK: - View setup - private func setupView() { - self.ratingDisplay.isUserInteractionEnabled = false - self.addSubviewSizedEqually(self.ratingDisplay) - self.accessibilityIdentifier = RatingInputAccessibilityIdentifier.identifier - } - - // MARK: - Accessibility - private func setupAccessibility() { - self.isAccessibilityElement = true - self.accessibilityTraits = .adjustable - self.updateAccessibilityValue() - } - - public override func accessibilityIncrement() { - let incrementedRating = min(self.rating + 1, CGFloat(self.ratingDisplay.count.rawValue)) - self.ratingStarSelected(Int(incrementedRating) - 1) - } - - public override func accessibilityDecrement() { - let decrementedRating = max(self.rating - 1, 1) - self.ratingStarSelected(Int(decrementedRating) - 1) - } - - private func updateAccessibilityValue() { - self.accessibilityValue = "\(Int(self.rating))/\(self.ratingDisplay.count.rawValue)" - } - - // MARK: - Handling touch actions - private func handleTouch(_ touch: UITouch, with event: UIEvent?) -> Bool { - - let location = touch.location(in: self) - - if !self.frame.contains(location) { - if !self.isHighlighted { - self.ratingStarHighlightCancelled() - } - return true - } - - guard let index = self.ratingDisplay.ratingStarViews.index(closestTo: location) else { - if !self.isHighlighted { - self.ratingStarHighlightCancelled() - } - return true - } - - self.lastSelectedIndex = index - self.ratingStarHighlighted(index) - - return true - } - - private func ratingStarSelected(_ index: Int) { - let rating = CGFloat(index + 1) - - guard rating != self.rating else { return } - - self.rating = rating - - self.subject.send(rating) - self.sendActions(for: .valueChanged) - self.delegate?.rating(self, didChangeRating: rating) - self.updateAccessibilityValue() - } - - private func ratingStarHighlighted(_ index: Int) { - let rating = CGFloat(index + 1) - self.ratingDisplay.rating = rating - } - - private func ratingStarHighlightCancelled() { - self.ratingDisplay.rating = self.rating - } -} diff --git a/core/Sources/Components/Rating/View/UIKit/RatingInputUIViewDelegate.swift b/core/Sources/Components/Rating/View/UIKit/RatingInputUIViewDelegate.swift deleted file mode 100644 index a9ca82f7f..000000000 --- a/core/Sources/Components/Rating/View/UIKit/RatingInputUIViewDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// RatingInputUIViewDelegate.swift -// SparkCore -// -// Created by Michael Zimmermann on 29.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public protocol RatingInputUIViewDelegate: AnyObject { - /// The rating value was changed. - /// - Parameters: - /// - rating: The updated rating input. - /// - rating: The new rating value. - func rating(_ rating: RatingInputUIView, didChangeRating rating: CGFloat) -} diff --git a/core/Sources/Components/Rating/View/UIKit/RatingInputUIViewSnapshotTests.swift b/core/Sources/Components/Rating/View/UIKit/RatingInputUIViewSnapshotTests.swift deleted file mode 100644 index 04d9dfc2e..000000000 --- a/core/Sources/Components/Rating/View/UIKit/RatingInputUIViewSnapshotTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// RatingInputUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by Michael Zimmermann on 30.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -@testable import SparkCore - -final class RatingInputUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = RatingInputScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: false) - for configuration in configurations { - let view = RatingInputUIView( - theme: self.theme, - intent: .main, - rating: configuration.rating - ) - - if configuration.state == .disabled { - view.isEnabled = false - } else if configuration.state == .pressed { - view.isHighlighted = true - } - - view.backgroundColor = UIColor.lightGray - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Rating/View/UIKit/StarUIView.swift b/core/Sources/Components/Rating/View/UIKit/StarUIView.swift deleted file mode 100644 index 194630b3e..000000000 --- a/core/Sources/Components/Rating/View/UIKit/StarUIView.swift +++ /dev/null @@ -1,240 +0,0 @@ -// -// StarUIView.swift -// SparkCore -// -// Created by michael.zimmermann on 06.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// -//: A UIKit based Playground for presenting user interface - -import UIKit - -/// StarUIView -/// Render a star. -/// The star may be configured by various attributes: -/// - number of vertices -/// - corner radius -/// - vertex size -/// - border color -/// - fill color -/// -public final class StarUIView: UIView { - - // MARK: - Public variables - /// The number of vertices the star has - public var numberOfVertices: Int { - get { - return self.configuration.numberOfVertices - } - set { - self.configuration.numberOfVertices = newValue - } - } - - /// The vertex size determins how deep the inner angle of the star is. - /// This value is a percentage of the radius and should be in the range [0...1]. - public var vertexSize: CGFloat { - get { - return self.configuration.vertexSize - } - set { - self.configuration.vertexSize = newValue - } - } - - /// The cornerRadiusSize is a proportional value - /// This value is a percentage of the radius and should be in the range [0...1]. - public var cornerRadiusSize: CGFloat { - get { - return self.configuration.cornerRadiusSize - } - set { - self.configuration.cornerRadiusSize = newValue - } - } - - /// The fill mode. - /// The fill mode determines how to round the rating value to fill the star. - public var fillMode: StarFillMode { - didSet { - self.setNeedsDisplay() - } - } - - /// The rating. - /// The ration should be a value between 0...1. Any number greater than 1 will be taken as a full star. Any number smaller than 0 will be treated as 0. - public var rating: CGFloat { - didSet { - self.setNeedsDisplay() - } - } - - /// The line width. - /// The width of the border of the non filled star. - public var lineWidth: CGFloat { - didSet { - self.setNeedsDisplay() - } - } - - /// The borderColor defines the color of the unfilled portion of the star. - public var borderColor: UIColor { - didSet { - self.setNeedsDisplay() - } - } - - /// The fillColor defines the color of the filled portion of the star. - public var fillColor: UIColor { - didSet { - self.setNeedsDisplay() - } - } - - public var configuration: StarConfiguration { - didSet { - guard self.configuration != oldValue else { return } - self.setNeedsDisplay() - } - } - - /// IsCachingEnabled. - /// Calculated stars will be cached for performance reasons. This may be disabled and the star will be calculated everytime when a redraw is required. - public var isCachingEnabled = true - - public override var bounds: CGRect { - didSet { - self.setNeedsDisplay() - } - } - - // MARK: - Private variables - private var cache: CGLayerCaching - - private var normalizedRating: CGFloat { - return self.fillMode.rating(of: self.rating) - } - - // MARK: - Initializer - /// Create a StarUIView with the following parameters - /// - /// - Parameters: - /// - rating: the value of the rating. This should be a number in the range [0...1] - /// - fillMode: the fill mode of the start. The star will be filled according to the rating and the fillMode. - /// - lineWidth: the width of the outer border. - /// - borderColor: The color of the border of the unfilled part of the star. - /// - fillColor: The color of the filled part of the star. - /// - configuration: StarConfiguration, a configuration of the star appearance. The default is `default`. - public convenience init( - rating: CGFloat = StarDefaults.rating, - fillMode: StarFillMode = .half, - lineWidth: CGFloat = StarDefaults.lineWidth, - borderColor: UIColor, - fillColor: UIColor, - configuration: StarConfiguration = .default - ) { - self.init( - rating: rating, - fillMode: fillMode, - lineWidth: lineWidth, - borderColor: borderColor, - fillColor: fillColor, - configuration: configuration, - cache: CGLayerCache()) - } - - init( - rating: CGFloat, - fillMode: StarFillMode, - lineWidth: CGFloat, - borderColor: UIColor, - fillColor: UIColor, - configuration: StarConfiguration, - cache: CGLayerCaching - ) { - self.fillMode = fillMode - self.rating = rating - self.lineWidth = lineWidth - self.configuration = configuration - self.borderColor = borderColor - self.fillColor = fillColor - self.cache = cache - - super.init(frame: .zero) - self.backgroundColor = .clear - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func draw(_ rect: CGRect) { - guard let ctx = UIGraphicsGetCurrentContext() else { - super.draw(rect) - return - } - self.drawLayer(context: ctx, rect: rect) - } - - // MARK: - Private functions - private func drawLayer(context: CGContext, rect: CGRect) { - - let star = Star( - numberOfVertices: self.numberOfVertices, - vertexSize: self.vertexSize, - cornerRadiusSize: self.cornerRadiusSize - ) - - context.saveGState() - - let cacheKey = self.cacheKey(rect: rect) - - let starLayer: CGLayer - if self.isCachingEnabled, let layer = self.cache.object(forKey: cacheKey) { - starLayer = layer - } else { - let shapeLayer = ShapeLayer( - shape: star, - fillColor: self.fillColor.cgColor, - strokeColor: self.borderColor.cgColor, - fillPercentage: self.normalizedRating, - strokeWidth: self.lineWidth) - - starLayer = shapeLayer.layer(graphicsContext: context, size: rect.size) - if self.isCachingEnabled { - self.cache.setObject(starLayer, forKey: self.cacheKey(rect: rect)) - } - } - - context.draw(starLayer, in: rect) - context.restoreGState() - } - - // MARK: Internal functions - internal func cacheKey(rect: CGRect) -> NSString { - let key = [Self.self, - self.numberOfVertices, - self.normalizedRating, - self.lineWidth, - self.vertexSize, - self.cornerRadiusSize, - self.borderColor.rgb, - self.fillColor.rgb, - min(rect.width, rect.height), - ].map{ "\($0)" } - .joined(separator: "_") - return NSString(string: key) - } -} - -// MARK: Private extension -private extension UIColor { - var rgb: String { - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - var alpha: CGFloat = 0 - self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) - return "\(red)-\(green)-\(blue)-\(alpha)" - } -} diff --git a/core/Sources/Components/Rating/View/UIKit/StarUIViewTests.swift b/core/Sources/Components/Rating/View/UIKit/StarUIViewTests.swift deleted file mode 100644 index e8b401675..000000000 --- a/core/Sources/Components/Rating/View/UIKit/StarUIViewTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// StarUIViewTests.swift -// SparkCoreUnitTests -// -// Created by michael.zimmermann on 08.11.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class StarUIViewTests: XCTestCase { - - var cache: CGLayerCachingGeneratedMock! - var sut: StarUIView! - - override func setUp() { - super.setUp() - self.cache = CGLayerCachingGeneratedMock() - self.sut = StarUIView( - rating: 0.4, - fillMode: .full, - lineWidth: 2, - borderColor: .red, - fillColor: .blue, - configuration: .init(numberOfVertices: 6, vertexSize: 0.5, cornerRadiusSize: 0.12), - cache: self.cache) - } - - func test_cache_name() throws { - // When - let key = sut.cacheKey(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - XCTAssertEqual(key, "StarUIView_6_0.0_2.0_0.5_0.12_1.0-0.0-0.0-1.0_0.0-0.0-1.0-1.0_100.0") - } -} diff --git a/core/Sources/Components/Slider/AccessibilityIdentifiier/SliderAccessibilityIdentifier.swift b/core/Sources/Components/Slider/AccessibilityIdentifiier/SliderAccessibilityIdentifier.swift deleted file mode 100644 index 15671d89d..000000000 --- a/core/Sources/Components/Slider/AccessibilityIdentifiier/SliderAccessibilityIdentifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// SliderAccessibilityIdentifier.swift -// SparkCore -// -// Created by louis.borlee on 12.12.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the slider. -public enum SliderAccessibilityIdentifier { - - // MARK: - Properties - - /// The text label accessibility identifier. - public static let slider = "spark-slider" -} diff --git a/core/Sources/Components/Slider/Constant/SliderConstants.swift b/core/Sources/Components/Slider/Constant/SliderConstants.swift deleted file mode 100644 index ca5702562..000000000 --- a/core/Sources/Components/Slider/Constant/SliderConstants.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// SliderConstants.swift -// Spark -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum SliderConstants { - static let handleSize = CGSize(width: 32, height: 32) - static let activeIndicatorSize = CGSize(width: 40, height: 40) - static let barHeight: CGFloat = 4.0 -} diff --git a/core/Sources/Components/Slider/Handle/View/SliderHandle.swift b/core/Sources/Components/Slider/Handle/View/SliderHandle.swift deleted file mode 100644 index cac8d85a5..000000000 --- a/core/Sources/Components/Slider/Handle/View/SliderHandle.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SliderHandle.swift -// SparkCore -// -// Created by louis.borlee on 13/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -struct SliderHandle: View { - - @ObservedObject var viewModel: SliderHandleViewModel - - @Binding var isEditing: Bool - - init(viewModel: SliderHandleViewModel, - isEditing: Binding) { - self.viewModel = viewModel - _isEditing = isEditing - } - - var body: some View { - ZStack(alignment: .center) { - if self.isEditing { - self.activeIndicatorStroke() - self.activeIndicatorHalo() - } - Circle() - .fill() - .foregroundColor(self.viewModel.color.color) - .frame(width: SliderConstants.handleSize.width, height: SliderConstants.handleSize.height) - } - } - - @ViewBuilder - private func activeIndicatorStroke() -> some View { - Circle() - .strokeBorder(self.viewModel.color.color, lineWidth: 1.0) - .frame(width: SliderConstants.activeIndicatorSize.width, height: SliderConstants.activeIndicatorSize.height) - } - - @ViewBuilder - private func activeIndicatorHalo() -> some View { - Circle() - .fill(self.viewModel.activeIndicatorColor) - .frame(width: SliderConstants.activeIndicatorSize.width - 2, height: SliderConstants.activeIndicatorSize.height - 2) - } -} diff --git a/core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift b/core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift deleted file mode 100644 index 183c3f85d..000000000 --- a/core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// SliderHandleUIControl.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine - -final class SliderHandleUIControl: UIControl { - - private let handleView = UIView() - private let activeIndicatorView = UIView() - - private var cancellables = Set() - - let viewModel: SliderHandleViewModel - - override var isHighlighted: Bool { - didSet { - self.activeIndicatorView.isHidden = !self.isHighlighted - } - } - - init(viewModel: SliderHandleViewModel) { - self.viewModel = viewModel - super.init(frame: .init( - origin: .zero, - size: .init( - width: SliderConstants.handleSize.width, - height: SliderConstants.handleSize.height - ) - )) - self.setupHandleView() - self.setupActiveHandleView() - self.subscribeToViewModelChanges() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupHandleView() { - self.handleView.frame = self.bounds - self.handleView.isUserInteractionEnabled = false - self.handleView.layer.cornerRadius = SliderConstants.handleSize.height / 2 - self.addSubview(self.handleView) - } - - private func setupActiveHandleView() { - self.activeIndicatorView.setBorderWidth(1.0) - self.activeIndicatorView.isUserInteractionEnabled = false - self.activeIndicatorView.isHidden = true - self.activeIndicatorView.layer.cornerRadius = SliderConstants.activeIndicatorSize.height / 2 - - self.activeIndicatorView.frame.size = .init(width: SliderConstants.activeIndicatorSize.width, height: SliderConstants.activeIndicatorSize.height) - self.activeIndicatorView.center = self.handleView.center - - self.insertSubview(self.activeIndicatorView, belowSubview: self.handleView) - } - - private func subscribeToViewModelChanges() { - self.viewModel.$color.subscribe(in: &self.cancellables) { [weak self] newColor in - guard let self else { return } - self.handleView.backgroundColor = newColor.uiColor - self.activeIndicatorView.setBorderColor(from: newColor) - } - self.viewModel.$activeIndicatorColor.subscribe(in: &self.cancellables) { [weak self] newActiveIndicatorColor in - guard let self else { return } - self.activeIndicatorView.backgroundColor = newActiveIndicatorColor.uiColor - } - } - - override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - let beginTracking = super.beginTracking(touch, with: event) - if let supercontrol = superview as? UIControl { - return supercontrol.beginTracking(touch, with: event) - } - return beginTracking - } - - override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - let continueTracking = super.continueTracking(touch, with: event) - if let supercontrol = superview as? UIControl { - return supercontrol.continueTracking(touch, with: event) - } - return continueTracking - } - - override func cancelTracking(with event: UIEvent?) { - super.cancelTracking(with: event) - if let supercontrol = superview as? UIControl { - supercontrol.cancelTracking(with: event) - } - } - - override func endTracking(_ touch: UITouch?, with event: UIEvent?) { - super.endTracking(touch, with: event) - if let supercontrol = superview as? UIControl { - supercontrol.endTracking(touch, with: event) - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - // CGColors need to be refreshed on trait changes - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.activeIndicatorView.setBorderColor(from: self.viewModel.color) - } - } -} diff --git a/core/Sources/Components/Slider/Handle/ViewModel/SliderHandleViewModel.swift b/core/Sources/Components/Slider/Handle/ViewModel/SliderHandleViewModel.swift deleted file mode 100644 index 5e69a7bfd..000000000 --- a/core/Sources/Components/Slider/Handle/ViewModel/SliderHandleViewModel.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SliderHandleViewModel.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import Combine - -final class SliderHandleViewModel: ObservableObject { - - @Published var color: any ColorToken - @Published var activeIndicatorColor: any ColorToken - - init(color: some ColorToken, - activeIndicatorColor: some ColorToken) { - self.color = color - self.activeIndicatorColor = activeIndicatorColor - } -} diff --git a/core/Sources/Components/Slider/Properties/Private/SliderColors+ExtensionTests.swift b/core/Sources/Components/Slider/Properties/Private/SliderColors+ExtensionTests.swift deleted file mode 100644 index ce771f4ab..000000000 --- a/core/Sources/Components/Slider/Properties/Private/SliderColors+ExtensionTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// SliderColors+ExtensionTests.swift -// SparkCore -// -// Created by louis.borlee on 08/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension SliderColors { - static func mocked(colors: Colors) -> SliderColors { - return .init( - track: colors.feedback.alert, - indicator: colors.accent.accentVariant, - handle: colors.states.neutralPressed, - handleActiveIndicator: colors.basic.onBasicContainer - ) - } -} diff --git a/core/Sources/Components/Slider/Properties/Private/SliderColors.swift b/core/Sources/Components/Slider/Properties/Private/SliderColors.swift deleted file mode 100644 index 0cdc07f59..000000000 --- a/core/Sources/Components/Slider/Properties/Private/SliderColors.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SliderColors.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct SliderColors: Equatable { - let track: any ColorToken - let indicator: any ColorToken - let handle: any ColorToken - let handleActiveIndicator: any ColorToken - - func withOpacity(_ opacity: CGFloat) -> SliderColors { - return .init( - track: self.track.opacity(opacity), - indicator: self.indicator.opacity(opacity), - handle: self.handle.opacity(opacity), - handleActiveIndicator: self.handleActiveIndicator.opacity(opacity) - ) - } - - static func == (lhs: SliderColors, rhs: SliderColors) -> Bool { - return lhs.track.equals(rhs.track) && - lhs.indicator.equals(rhs.indicator) && - lhs.handle.equals(rhs.handle) && - lhs.handleActiveIndicator.equals(rhs.handleActiveIndicator) - } -} diff --git a/core/Sources/Components/Slider/Properties/Private/SliderColorsTests.swift b/core/Sources/Components/Slider/Properties/Private/SliderColorsTests.swift deleted file mode 100644 index 7aa1f17d3..000000000 --- a/core/Sources/Components/Slider/Properties/Private/SliderColorsTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SliderColorsTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SliderColorsTests: XCTestCase { - - private let colors: Colors = ColorsGeneratedMock.mocked() - private let dims: Dims = DimsGeneratedMock.mocked() - - func test_withOpacity() { - // GIVEN - let track = self.colors.feedback.alertContainer - let indicator = self.colors.states.accentVariantPressed.opacity(self.dims.dim5) - let handle = self.colors.main.onMainContainer - let handleActiveIndicator = self.colors.base.surface - let sut = SliderColors( - track: track, - indicator: indicator, - handle: handle, - handleActiveIndicator: handleActiveIndicator - ) - let dim = self.dims.dim3 - let expectedResult = SliderColors( - track: track.opacity(dim), - indicator: indicator.opacity(dim), - handle: handle.opacity(dim), - handleActiveIndicator: handleActiveIndicator.opacity(dim) - ) - - // WHEN - let result = sut.withOpacity(dim) - - // THEN - XCTAssertEqual(result, expectedResult) - } - -} diff --git a/core/Sources/Components/Slider/Properties/Private/SliderRadii+ExtensionTests.swift b/core/Sources/Components/Slider/Properties/Private/SliderRadii+ExtensionTests.swift deleted file mode 100644 index d0ee82b08..000000000 --- a/core/Sources/Components/Slider/Properties/Private/SliderRadii+ExtensionTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SliderRadii+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 08/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension SliderRadii { - static func mocked() -> SliderRadii { - return .init( - trackRadius: 0.123, - indicatorRadius: 49.3 - ) - } -} diff --git a/core/Sources/Components/Slider/Properties/Private/SliderRadii.swift b/core/Sources/Components/Slider/Properties/Private/SliderRadii.swift deleted file mode 100644 index da0874528..000000000 --- a/core/Sources/Components/Slider/Properties/Private/SliderRadii.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SliderRadii.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct SliderRadii: Equatable { - let trackRadius: CGFloat - let indicatorRadius: CGFloat -} diff --git a/core/Sources/Components/Slider/Properties/Public/SliderIntent.swift b/core/Sources/Components/Slider/Properties/Public/SliderIntent.swift deleted file mode 100644 index 8d2172f43..000000000 --- a/core/Sources/Components/Slider/Properties/Public/SliderIntent.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SliderIntent.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The various intents of sliders. -@frozen -public enum SliderIntent: CaseIterable { - case basic - case success - case error - case alert - case accent - case main - case neutral - case support - case info -} diff --git a/core/Sources/Components/Slider/Properties/Public/SliderShape.swift b/core/Sources/Components/Slider/Properties/Public/SliderShape.swift deleted file mode 100644 index 96121be98..000000000 --- a/core/Sources/Components/Slider/Properties/Public/SliderShape.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// SliderShape.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The various shapes of sliders. -@frozen -public enum SliderShape: CaseIterable { - case square - case rounded -} diff --git a/core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateStepsUseCase.swift b/core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateStepsUseCase.swift deleted file mode 100644 index 151a47c77..000000000 --- a/core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateStepsUseCase.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// SliderCreateStepsUseCase.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum SliderCreateValuesFromStepsUseCasableError: Error { - case invalidRange - case invalidStep -} - -// sourcery: AutoMockable -protocol SliderCreateValuesFromStepsUseCasable { - func execute(from: Float, - to: Float, - steps: Float) throws -> [Float] -} - -final class SliderCreateValuesFromStepsUseCase: SliderCreateValuesFromStepsUseCasable { - func execute(from: Float, to: Float, steps: Float) throws -> [Float] { - guard from < to else { throw SliderCreateValuesFromStepsUseCasableError.invalidRange } - guard steps > .zero, - steps <= (to - from) else { throw SliderCreateValuesFromStepsUseCasableError.invalidStep } - - var values = Array(stride(from: from, through: to, by: steps)) - // Last value should be added when `to` % `step` is > 0 - if values.contains(to) == false { - values.append(to) - } - return values - } -} diff --git a/core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateValuesFromStepUseCaseTests.swift b/core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateValuesFromStepUseCaseTests.swift deleted file mode 100644 index 71e342641..000000000 --- a/core/Sources/Components/Slider/UseCase/CreateValuesFromSteps/SliderCreateValuesFromStepUseCaseTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// SliderCreateValuesFromStepUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SliderCreateValuesFromStepsUseCaseTests: XCTestCase { - - private let sut = SliderCreateValuesFromStepsUseCase() - - func test_execute_throws_invalid_range() { - XCTAssertThrowsError(try self.sut.execute(from: 800, to: 200, steps: 4), "Execute should throw") { error in - XCTAssertEqual( - error as? SliderCreateValuesFromStepsUseCasableError, - SliderCreateValuesFromStepsUseCasableError.invalidRange, - "Error should be \(SliderCreateValuesFromStepsUseCasableError.invalidRange) but is \(error)" - ) - } - } - - func test_execute_throws_invalid_step_less_than_zero() { - XCTAssertThrowsError(try self.sut.execute(from: 200, to: 800, steps: -1), "Execute should throw") { error in - XCTAssertEqual( - error as? SliderCreateValuesFromStepsUseCasableError, - SliderCreateValuesFromStepsUseCasableError.invalidStep, - "Error should be \(SliderCreateValuesFromStepsUseCasableError.invalidStep) but is \(error)" - ) - } - - } - - func test_execute_throws_invalid_step_zero() { - XCTAssertThrowsError(try self.sut.execute(from: 200, to: 800, steps: .zero), "Execute should throw") { error in - XCTAssertEqual( - error as? SliderCreateValuesFromStepsUseCasableError, - SliderCreateValuesFromStepsUseCasableError.invalidStep, - "Error should be \(SliderCreateValuesFromStepsUseCasableError.invalidStep) but is \(error)" - ) - } - - } - - func test_execute_throws_invalid_step_greater_than_to_minus_from() { - XCTAssertThrowsError(try self.sut.execute(from: 200, to: 800, steps: 800), "Execute should throw") { error in - XCTAssertEqual( - error as? SliderCreateValuesFromStepsUseCasableError, - SliderCreateValuesFromStepsUseCasableError.invalidStep, - "Error should be \(SliderCreateValuesFromStepsUseCasableError.invalidStep) but is \(error)" - ) - } - } - - func test_execute_adding_last_value() throws { - let values = try XCTUnwrap(self.sut.execute(from: 0, to: 1, steps: 0.7), - "Couldn't unwrap values") - XCTAssertEqual(values, [0, 0.7, 1]) - } - - func test_execute() throws { - let values = try XCTUnwrap(self.sut.execute(from: 50_000, to: 200_000, steps: 50_000), - "Couldn't unwrap values") - XCTAssertEqual(values, [ - 50_000, - 100_000, - 150_000, - 200_000 - ]) - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueInBoundsUseCase.swift b/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueInBoundsUseCase.swift deleted file mode 100644 index 7b1d58f7a..000000000 --- a/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueInBoundsUseCase.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SliderGetClosestValueUseCase.swift -// SparkCore -// -// Created by louis.borlee on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -protocol SliderGetClosestValueUseCasable { - func execute(value: V, in values: [V]) -> V where V: BinaryFloatingPoint -} - -final class SliderGetClosestValueUseCase: SliderGetClosestValueUseCasable { - func execute(value: V, in values: [V]) -> V where V: BinaryFloatingPoint { - guard let closestValue = values.min(by: { - return abs($0 - value) <= abs($1 - value) - }) else { return value } - return closestValue - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCasableMock+Tests.swift b/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCasableMock+Tests.swift deleted file mode 100644 index 4b18400da..000000000 --- a/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCasableMock+Tests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// File.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 02/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -// swiftlint:disable force_cast -final class SliderGetClosestValueUseCasableMock: SparkCore.SliderGetClosestValueUseCasable where U: BinaryFloatingPoint { - - // MARK: - Initialization - - init() {} - - // MARK: - execute - - var executeWithValueAndValuesCallsCount = 0 - var executeWithValueAndValuesCalled: Bool { - return executeWithValueAndValuesCallsCount > 0 - } - var executeWithValueAndValuesReceivedArguments: (value: U, values: [U])? - var executeWithValueAndValuesReceivedInvocations: [(value: U, values: [U])] = [] - var executeWithValueAndValuesReturnValue: U! - var _executeWithValueAndValues: ((U, [U]) -> U?)? - - func execute(value: V, in values: [V]) -> V where V: BinaryFloatingPoint { - guard let castedValue = value as? U, - let castedValues = values as? [U] else { - fatalError("\(U.self) is not equal to \(V.self)") - } - executeWithValueAndValuesCallsCount += 1 - executeWithValueAndValuesReceivedArguments = (value: castedValue, values: castedValues) - executeWithValueAndValuesReceivedInvocations.append((value: castedValue, values: castedValues)) - return (_executeWithValueAndValues.map{ $0(castedValue, castedValues) } ?? executeWithValueAndValuesReturnValue) as! V - } - - // MARK: Reset - - func reset() { - executeWithValueAndValuesCallsCount = 0 - executeWithValueAndValuesReceivedArguments = nil - executeWithValueAndValuesReceivedInvocations = [] - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCaseTests.swift b/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCaseTests.swift deleted file mode 100644 index 338734924..000000000 --- a/core/Sources/Components/Slider/UseCase/GetClosestValue/SliderGetClosestValueUseCaseTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// SliderGetClosestValueUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SliderGetClosestValueUseCaseTests: XCTestCase { - - func test_execute_under_minimum_value() { - // GIVEN - let useCase = SliderGetClosestValueUseCase() - let values: [CGFloat] = [10, 20, 30] - let value: CGFloat = 0 - let expectedClosestValue: CGFloat = 10 - - // WHEN - let closestValue = useCase.execute(value: value, in: values) - - // THEN - XCTAssertEqual(closestValue, expectedClosestValue) - } - - func test_execute_over_maximum_value() { - // GIVEN - let useCase = SliderGetClosestValueUseCase() - let values: [Double] = [0.1, 0.2, 0.3] - let value: Double = 0.4 - let expectedClosestValue: Double = 0.3 - - // WHEN - let closestValue = useCase.execute(value: value, in: values) - - // THEN - XCTAssertEqual(closestValue, expectedClosestValue) - } - - func test_execute_lower_rounding() { - // GIVEN - let useCase = SliderGetClosestValueUseCase() - let values: [Float] = [0.0, 0.50, 1.0, 1.50] - let value: Float = 0.74 - let expectedClosestValue: Float = 0.50 - - // WHEN - let closestValue = useCase.execute(value: value, in: values) - - // THEN - XCTAssertEqual(closestValue, expectedClosestValue) - } - - func test_execute_upper_rounding() { - // GIVEN - let useCase = SliderGetClosestValueUseCase() - let values: [CGFloat] = [0.0, 0.50, 1.0, 1.50] - let value: CGFloat = 0.76 - let expectedClosestValue: CGFloat = 1.0 - - // WHEN - let closestValue = useCase.execute(value: value, in: values) - - // THEN - XCTAssertEqual(closestValue, expectedClosestValue) - } - - func test_execute_inbetween() { - // GIVEN - let useCase = SliderGetClosestValueUseCase() - let values: [CGFloat] = [0.0, 0.50, 1.0, 1.50] - let value: CGFloat = 0.75 - let expectedClosestValue: CGFloat = 1.0 - - // WHEN - let closestValue = useCase.execute(value: value, in: values) - - // THEN - XCTAssertEqual(closestValue, expectedClosestValue) - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCasableGeneratedMock+ExtensionTests.swift b/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCasableGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 6ee5c3f16..000000000 --- a/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCasableGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SliderGetColorsUseCasableGeneratedMock+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension SliderGetColorsUseCasableGeneratedMock { - static func mocked(returnedColors colors: SliderColors) -> SliderGetColorsUseCasableGeneratedMock { - let mock = SliderGetColorsUseCasableGeneratedMock() - mock._executeWithThemeAndIntent = { _, _ in - return colors - } - return mock - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCase.swift b/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCase.swift deleted file mode 100644 index 296d4e58b..000000000 --- a/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCase.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// SliderGetColorsUseCase.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -// sourcery: AutoMockable -protocol SliderGetColorsUseCasable { - func execute(theme: Theme, - intent: SliderIntent) -> SliderColors -} - -final class SliderGetColorsUseCase: SliderGetColorsUseCasable { - func execute(theme: Theme, - intent: SliderIntent) -> SliderColors { - let colors = theme.colors - let dims = theme.dims - - let sliderColors: SliderColors - let trackColor = colors.base.onBackground.opacity(dims.dim4) - switch intent { - case .basic: - sliderColors = .init( - track: trackColor, - indicator: colors.basic.basic, - handle: colors.basic.basic, - handleActiveIndicator: colors.basic.basicContainer - ) - case .success: - sliderColors = .init( - track: trackColor, - indicator: colors.feedback.success, - handle: colors.feedback.success, - handleActiveIndicator: colors.feedback.successContainer - ) - case .error: - sliderColors = .init( - track: trackColor, - indicator: colors.feedback.error, - handle: colors.feedback.error, - handleActiveIndicator: colors.feedback.errorContainer - ) - case .alert: - sliderColors = .init( - track: trackColor, - indicator: colors.feedback.alert, - handle: colors.feedback.alert, - handleActiveIndicator: colors.feedback.alertContainer - ) - case .accent: - sliderColors = .init( - track: trackColor, - indicator: colors.accent.accent, - handle: colors.accent.accent, - handleActiveIndicator: colors.accent.accentContainer - ) - case .main: - sliderColors = .init( - track: trackColor, - indicator: colors.main.main, - handle: colors.main.main, - handleActiveIndicator: colors.main.mainContainer - ) - case .neutral: - sliderColors = .init( - track: trackColor, - indicator: colors.feedback.neutral, - handle: colors.feedback.neutral, - handleActiveIndicator: colors.feedback.neutralContainer - ) - case .support: - sliderColors = .init( - track: trackColor, - indicator: colors.support.support, - handle: colors.support.support, - handleActiveIndicator: colors.support.supportContainer - ) - case .info: - sliderColors = .init( - track: trackColor, - indicator: colors.feedback.info, - handle: colors.feedback.info, - handleActiveIndicator: colors.feedback.infoContainer - ) - } - return sliderColors - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCaseTests.swift b/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCaseTests.swift deleted file mode 100644 index 77517430a..000000000 --- a/core/Sources/Components/Slider/UseCase/GetColors/SliderGetColorsUseCaseTests.swift +++ /dev/null @@ -1,192 +0,0 @@ -// -// SliderGetColorsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SliderGetColorsUseCaseTests: XCTestCase { - - private let theme: Theme = ThemeGeneratedMock.mocked() - private var colors: Colors { - return self.theme.colors - } - private var dims: Dims { - return self.theme.dims - } - - // MARK: - Basic - func test_execute_intent_basic_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.basic.basic, - handle: self.colors.basic.basic, - handleActiveIndicator: self.colors.basic.basicContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .basic) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Success - func test_execute_intent_success_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.feedback.success, - handle: self.colors.feedback.success, - handleActiveIndicator: self.colors.feedback.successContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .success) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Error - func test_execute_intent_error_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.feedback.error, - handle: self.colors.feedback.error, - handleActiveIndicator: self.colors.feedback.errorContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .error) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Alert - func test_execute_intent_alert_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.feedback.alert, - handle: self.colors.feedback.alert, - handleActiveIndicator: self.colors.feedback.alertContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .alert) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Accent - func test_execute_intent_accent_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.accent.accent, - handle: self.colors.accent.accent, - handleActiveIndicator: self.colors.accent.accentContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .accent) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Main - func test_execute_intent_main_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.main.main, - handle: self.colors.main.main, - handleActiveIndicator: self.colors.main.mainContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .main) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Neutral - func test_execute_intent_neutral_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.feedback.neutral, - handle: self.colors.feedback.neutral, - handleActiveIndicator: self.colors.feedback.neutralContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .neutral) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Support - func test_execute_intent_support_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.support.support, - handle: self.colors.support.support, - handleActiveIndicator: self.colors.support.supportContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .support) - - // THEN - XCTAssertEqual(colors, expectedColors) - } - - // MARK: - Info - func test_execute_intent_info_enabled() { - // GIVEN - let useCase = SliderGetColorsUseCase() - let expectedColors = SliderColors( - track: self.colors.base.onBackground.opacity(self.dims.dim4), - indicator: self.colors.feedback.info, - handle: self.colors.feedback.info, - handleActiveIndicator: self.colors.feedback.infoContainer - ) - - // WHEN - let colors = useCase.execute(theme: self.theme, - intent: .info) - - // THEN - XCTAssertEqual(colors, expectedColors) - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCasableGeneratedMock+ExtensionTests.swift b/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCasableGeneratedMock+ExtensionTests.swift deleted file mode 100644 index ac8530522..000000000 --- a/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCasableGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SliderGetCornerRadiiUseCasableGeneratedMock+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension SliderGetCornerRadiiUseCasableGeneratedMock { - static func mocked(expectedRadii radii: SliderRadii) -> SliderGetCornerRadiiUseCasableGeneratedMock { - let mock = SliderGetCornerRadiiUseCasableGeneratedMock() - mock._executeWithThemeAndShape = { _, _ in - return radii - } - return mock - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCase.swift b/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCase.swift deleted file mode 100644 index 1cb80eb93..000000000 --- a/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCase.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SliderGetCornerRadiiUseCase.swift -// SparkCore -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SliderGetCornerRadiiUseCasable { - func execute(theme: Theme, - shape: SliderShape) -> SliderRadii -} - -final class SliderGetCornerRadiiUseCase: SliderGetCornerRadiiUseCasable { - func execute(theme: Theme, - shape: SliderShape) -> SliderRadii { - let radius: CGFloat - switch shape { - case .rounded: - radius = theme.border.radius.small - case .square: - radius = theme.border.radius.none - } - return SliderRadii(trackRadius: radius, indicatorRadius: radius) - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCaseTests.swift b/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCaseTests.swift deleted file mode 100644 index 46a887281..000000000 --- a/core/Sources/Components/Slider/UseCase/GetCornerRadii/SliderGetCornerRadiiUseCaseTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// SliderGetCornerRadiiUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 23/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SliderGetCornerRadiiUseCaseTests: XCTestCase { - - private let theme: Theme = ThemeGeneratedMock.mocked() - - func test_execute_shape_rounded() { - // GIVEN - let sut = SliderGetCornerRadiiUseCase() - let expectedRadii = SliderRadii( - trackRadius: self.theme.border.radius.small, - indicatorRadius: self.theme.border.radius.small - ) - - // WHEN - let radii = sut.execute(theme: self.theme, shape: .rounded) - - // THEN - XCTAssertEqual(radii, expectedRadii) - } - - func test_execute_shape_square() { - // GIVEN - let sut = SliderGetCornerRadiiUseCase() - let expectedRadii = SliderRadii( - trackRadius: self.theme.border.radius.none, - indicatorRadius: self.theme.border.radius.none - ) - - // WHEN - let radii = sut.execute(theme: self.theme, shape: .square) - - // THEN - XCTAssertEqual(radii, expectedRadii) - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCasableMock+Tests.swift b/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCasableMock+Tests.swift deleted file mode 100644 index 4bdbb33e0..000000000 --- a/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCasableMock+Tests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// SliderGetStepValuesInBoundsUseCaseMock.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 02/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -// swiftlint:disable force_cast -final class SliderGetStepValuesInBoundsUseCasableMock: SparkCore.SliderGetStepValuesInBoundsUseCasable where U: BinaryFloatingPoint, U.Stride: BinaryFloatingPoint { - - // MARK: - Initialization - - init() {} - - // MARK: - execute - - var executeWithBoundsAndStepCallsCount = 0 - var executeWithBoundsAndStepCalled: Bool { - return executeWithBoundsAndStepCallsCount > 0 - } - var executeWithBoundsAndStepReceivedArguments: (bounds: ClosedRange, step: U.Stride)? - var executeWithBoundsAndStepReceivedInvocations: [(bounds: ClosedRange, step: U.Stride)] = [] - var executeWithBoundsAndStepReturnValue: [U]! - var _executeWithBoundsAndStep: ((ClosedRange, U.Stride) -> Any?)? - - func execute(bounds: ClosedRange, step: V.Stride) -> [V] where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - guard let castedBounds = bounds as? ClosedRange, - let castedStep = step as? U.Stride else { - fatalError("\(U.self) is not equal to \(V.self)") - } - executeWithBoundsAndStepCallsCount += 1 - executeWithBoundsAndStepReceivedArguments = (bounds: castedBounds, step: castedStep) - executeWithBoundsAndStepReceivedInvocations.append((bounds: castedBounds, step: castedStep)) - return (_executeWithBoundsAndStep.map{ $0(castedBounds, castedStep) } ?? executeWithBoundsAndStepReturnValue) as! [V] - } - - // MARK: Reset - - func reset() { - executeWithBoundsAndStepCallsCount = 0 - executeWithBoundsAndStepReceivedArguments = nil - executeWithBoundsAndStepReceivedInvocations = [] - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCase.swift b/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCase.swift deleted file mode 100644 index 97cda5532..000000000 --- a/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCase.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SliderGetStepValuesInBoundsUseCase.swift -// SparkCore -// -// Created by louis.borlee on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -protocol SliderGetStepValuesInBoundsUseCasable { - func execute(bounds: ClosedRange, step: V.Stride) -> [V] where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint -} - -final class SliderGetStepValuesInBoundsUseCase: SliderGetStepValuesInBoundsUseCasable { - func execute(bounds: ClosedRange, step: V.Stride) -> [V] where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - return stride(from: bounds.lowerBound, through: bounds.upperBound, by: step).sorted() - } -} diff --git a/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCaseTests.swift b/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCaseTests.swift deleted file mode 100644 index 446bf82a8..000000000 --- a/core/Sources/Components/Slider/UseCase/GetStepValues/SliderGetStepValuesInBoundsUseCaseTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// SliderGetStepValuesInBoundsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SliderGetStepValuesInBoundsUseCaseTests: XCTestCase { - - func test_execute_with_step_multiplier_of_upperBound() { - // GIVEN - let useCase = SliderGetStepValuesInBoundsUseCase() - let bounds: ClosedRange = 0...1 - let step: Double = 0.25 - let expectedStepValues: [Double] = [0, 0.25, 0.50, 0.75, 1.0] - - // WHEN - let stepValues = useCase.execute(bounds: bounds, step: step) - - // THEN - XCTAssertEqual(stepValues, expectedStepValues) - } - - func test_execute_with_step_not_a_multiplier_of_upperBound() { - // GIVEN - let useCase = SliderGetStepValuesInBoundsUseCase() - let bounds: ClosedRange = 0...10 - let step: Float = 3 - let expectedStepValues: [Float] = [0, 3, 6, 9] - - // WHEN - let stepValues = useCase.execute(bounds: bounds, step: step) - - // THEN - XCTAssertEqual(stepValues, expectedStepValues) - } -} diff --git a/core/Sources/Components/Slider/View/SliderScenario+SnapshotTests.swift b/core/Sources/Components/Slider/View/SliderScenario+SnapshotTests.swift deleted file mode 100644 index 7872fa211..000000000 --- a/core/Sources/Components/Slider/View/SliderScenario+SnapshotTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// SliderScenario+SnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by louis.borlee on 14/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -struct SliderScenario: CustomStringConvertible { - let description: String - let intents: [SliderIntent] - let states: [State] - let shape: SliderShape - let modes: [ComponentSnapshotTestMode] - let values: [Value] - - enum State: CaseIterable { - case normal, disabled, highlighted - } - - enum Value: Float { - case min = 0 - case medium = 0.5 - case max = 1.0 - } - - static let test1 = SliderScenario( - description: "Test1", - intents: SliderIntent.allCases, - states: [.highlighted], - shape: .square, - modes: ComponentSnapshotTestConstants.Modes.all, - values: [.medium] - ) - - static let test2 = SliderScenario( - description: "Test2", - intents: [.basic], - states: [.normal], - shape: .square, - modes: ComponentSnapshotTestConstants.Modes.default, - values: [.min, .max] - ) - - static let test3 = SliderScenario( - description: "Test3", - intents: [.basic], - states: State.allCases, - shape: .square, - modes: ComponentSnapshotTestConstants.Modes.all, - values: [.medium] - ) - - static let test4 = SliderScenario( - description: "Test4", - intents: [.basic], - states: [.normal], - shape: .rounded, - modes: ComponentSnapshotTestConstants.Modes.default, - values: [.medium] - ) - - func getTestName(intent: SliderIntent, state: State, value: Value) -> String { - return "\(self)-\(intent)-\(state)-\(self.shape)-\(value)" - } -} diff --git a/core/Sources/Components/Slider/View/SwiftUI/Slider.swift b/core/Sources/Components/Slider/View/SwiftUI/Slider.swift deleted file mode 100644 index b9dd6cd9f..000000000 --- a/core/Sources/Components/Slider/View/SwiftUI/Slider.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// Slider.swift -// SparkCore -// -// Created by louis.borlee on 15/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import Combine - -public struct Slider: View where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - - @ObservedObject private var viewModel: SingleSliderViewModel - - @State private var isEditing: Bool = false - - @Binding var value: V - - private var onEditingChanged: (Bool) -> Void - - private init(value: Binding, - in bounds: ClosedRange, - step: V.Stride?, - theme: Theme, - shape: SliderShape, - intent: SliderIntent, - onEditingChanged: @escaping (Bool) -> Void = { _ in }) { - self._value = value - - let viewModel = SingleSliderViewModel(theme: theme, shape: shape, intent: intent) - viewModel.bounds = bounds - viewModel.step = step - viewModel.resetBoundsIfNeeded() - viewModel.setValue(value.wrappedValue) - - self.viewModel = viewModel - - self.onEditingChanged = onEditingChanged - } - - public init(theme: Theme, - shape: SliderShape, - intent: SliderIntent, - value: Binding, - in bounds: ClosedRange = 0...1, - onEditingChanged: @escaping (Bool) -> Void = { _ in }) { - self.init(value: value, - in: bounds, - step: nil, - theme: theme, - shape: shape, - intent: intent, - onEditingChanged: onEditingChanged) - } - - public init(theme: Theme, - shape: SliderShape, - intent: SliderIntent, - value: Binding, - in bounds: ClosedRange = 0...1, - step: V.Stride = 1, - onEditingChanged: @escaping (Bool) -> Void = { _ in }) { - self.init(value: value, - in: bounds, - step: step != .zero ? step : nil, - theme: theme, - shape: shape, - intent: intent, - onEditingChanged: onEditingChanged) - } - - public var body: some View { - GeometryReader(content: { geometry in - let sliderHandleCenterX = self.getHandleXPosition(frameWidth: geometry.size.width) - ZStack { - HStack(spacing: .zero) { - RoundedRectangle(cornerRadius: self.viewModel.indicatorRadius) - .foregroundColor(self.viewModel.indicatorColor.color) - .frame(width: sliderHandleCenterX) - RoundedRectangle(cornerRadius: self.viewModel.trackRadius) - .foregroundColor(self.viewModel.trackColor.color) - } - .frame(height: SliderConstants.barHeight) - SliderHandle( - viewModel: .init(color: self.viewModel.handleColor, activeIndicatorColor: self.viewModel.handleActiveIndicatorColor), - isEditing: self.$isEditing) - .position(x: sliderHandleCenterX, - y: geometry.size.height / 2.0) - } - .gesture( - DragGesture(minimumDistance: .zero) - .onChanged { value in - self.isEditing = true - self.moveHandle(to: value.location.x, width: geometry.size.width) - } - .onEnded { value in - self.isEditing = false - } - ) - }) - .compositingGroup() - .opacity(self.viewModel.dim) - .frame(height: SliderConstants.handleSize.height) - .onChange(of: self.isEditing, perform: { value in - self.onEditingChanged(value) - }) - .onChange(of: self.viewModel.value, perform: { value in - self.value = value - }) - .isEnabledChanged { isEnabled in - self.viewModel.isEnabled = isEnabled - } - .accessibilityElement() - .accessibilityIdentifier(SliderAccessibilityIdentifier.slider) - .accessibilityValue(self.getAccessibilityValue()) - .accessibilityAdjustableAction { direction in - switch direction { - case .decrement: - self.viewModel.decrementValue() - case .increment: - self.viewModel.incrementValue() - @unknown default: - break - } - } - } - - private func moveHandle(to: CGFloat, width: CGFloat) { - let absoluteX = max(SliderConstants.handleSize.width / 2, min(to, width - SliderConstants.handleSize.width / 2)) - let relativeX = (absoluteX - SliderConstants.handleSize.width / 2) / (width - SliderConstants.handleSize.width) - let newValue = V(relativeX) * (self.viewModel.bounds.upperBound - self.viewModel.bounds.lowerBound) + self.viewModel.bounds.lowerBound - self.viewModel.setValue(newValue) - } - - private func getHandleXPosition(frameWidth: CGFloat) -> CGFloat { - guard self.viewModel.bounds.lowerBound != self.viewModel.bounds.upperBound else { - return SliderConstants.handleSize.width / 2 - } - let value = (max(self.viewModel.bounds.lowerBound, self.value) - self.viewModel.bounds.lowerBound) / (self.viewModel.bounds.upperBound - self.viewModel.bounds.lowerBound) - return (frameWidth - SliderConstants.handleSize.width) * CGFloat(value) + SliderConstants.handleSize.width / 2 - } - - private func getAccessibilityValue() -> String { - let percentage = ((self.value - self.viewModel.bounds.lowerBound) * 100) / (self.viewModel.bounds.upperBound - self.viewModel.bounds.lowerBound) - return "\(Int(round(percentage)))%" - } -} diff --git a/core/Sources/Components/Slider/View/UIKit/SliderUIControl.swift b/core/Sources/Components/Slider/View/UIKit/SliderUIControl.swift deleted file mode 100644 index e7f9d8dc2..000000000 --- a/core/Sources/Components/Slider/View/UIKit/SliderUIControl.swift +++ /dev/null @@ -1,288 +0,0 @@ -// -// SliderUIControl.swift -// SparkCore -// -// Created by louis.borlee on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine - -public final class SliderUIControl: UIControl where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - - private let viewModel: SingleSliderViewModel - private var cancellables = Set() - private var _isTracking: Bool = false - - // MARK: - Public properties - /// The slider's current theme. - public var theme: Theme { - get { return self.viewModel.theme } - set { self.viewModel.theme = newValue } - } - - /// The slider's current intent. - public var intent: SliderIntent { - get { return self.viewModel.intent } - set { self.viewModel.intent = newValue } - } - - /// The slider's current shape (`square` or `rounded`). - public var shape: SliderShape { - get { return self.viewModel.shape } - set { self.viewModel.shape = newValue } - } - - /// A Boolean value indicating whether changes in the slider’s value generate continuous update events. - public var isContinuous: Bool { - get { return self.viewModel.isContinuous } - set { self.viewModel.isContinuous = newValue } - } - - /// The bounds of the slider. - public var range: ClosedRange { - get { return self.viewModel.bounds } - set { - self.viewModel.bounds = newValue - self.setAccessibilityValue(with: self.value) - } - } - - /// The distance between each valid value. - public var step: V.Stride? { - get { return self.viewModel.step } - set { self.viewModel.step = newValue } - } - - public override var isEnabled: Bool { - didSet { - self.viewModel.isEnabled = self.isEnabled - self.isUserInteractionEnabled = self.isEnabled - } - } - - /// The slider’s current value. - public private(set) var value: V = .zero { - didSet { - switch (self._isTracking, self.isContinuous) { - case (false, _): - break // valueChanged event should only trigger when isTracking is true same as UISlider - case (true, false): // valueChanged event should not be sent while tracking when isContinuous is false - break - case (true, true): - self.sendActions(for: .valueChanged) - } - self.setNeedsLayout() - } - } - - public override var isHighlighted: Bool { - didSet { - self.handle.isHighlighted = self.isHighlighted - } - } - - private var valueSubject = PassthroughSubject() - /// Value changes are sent to the publisher. - /// Alternative: use addAction(UIAction, for: .valueChanged). - public var valuePublisher: some Publisher { - return self.valueSubject - } - - // MARK: - Subviews - private let indicatorView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: SliderConstants.barHeight))) - private let trackView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: SliderConstants.barHeight))) - private let handle: SliderHandleUIControl - - init(viewModel: SingleSliderViewModel) { - self.viewModel = viewModel - self.handle = SliderHandleUIControl( - viewModel: .init(color: viewModel.handleColor, - activeIndicatorColor: viewModel.handleActiveIndicatorColor) - ) - super.init(frame: .init(origin: .zero, size: .init(width: 0, height: SliderConstants.handleSize.height))) - self.alpha = self.viewModel.dim - self.setupBar() - self.subscribeToViewModel() - self.translatesAutoresizingMaskIntoConstraints = false - let defaultHeightConstraint = self.heightAnchor.constraint(equalToConstant: SliderConstants.handleSize.height) - defaultHeightConstraint.priority = .defaultHigh - NSLayoutConstraint.activate([ - defaultHeightConstraint - ]) - self.addSubview(self.handle) - self.addAction(UIAction(handler: { [weak self] _ in - guard let self else { return } - self.valueSubject.send(self.value) - }), for: .valueChanged) - - self.setupAccessibility() - - self.addPanGestureToPreventCancelTracking() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - /// SliderUIControler initializer - /// - Parameters: - /// - theme: The slider's current theme - /// - shape: The slider's current shape (`square` or `rounded`) - /// - intent: The slider's current intent - public convenience init( - theme: Theme, - shape: SliderShape, - intent: SliderIntent - ) { - self.init(viewModel: .init(theme: theme, shape: shape, intent: intent)) - } - - public override func layoutSubviews() { - super.layoutSubviews() - - self.viewModel.resetBoundsIfNeeded() - - self.indicatorView.center.y = self.frame.height / 2 - self.handle.center.y = self.frame.height / 2 - self.trackView.center.y = self.frame.height / 2 - - if self.range.lowerBound < self.range.upperBound { - let value = (max(self.range.lowerBound, self.value) - self.range.lowerBound) / (self.range.upperBound - self.range.lowerBound) - self.handle.center.x = (self.frame.width - SliderConstants.handleSize.width) * CGFloat(value) + SliderConstants.handleSize.width / 2 - } else { - self.handle.center.x = SliderConstants.handleSize.width / 2 - } - - self.indicatorView.frame.size.width = self.handle.center.x - - self.trackView.frame.origin.x = self.handle.center.x - self.trackView.frame.size.width = self.frame.width - self.trackView.frame.origin.x - } - - /// Sets the slider’s current value, allowing you to animate the change visually. - /// - Parameters: - /// - value: The new value to assign to the value property - /// - animated: Specify `true` to animate the change in value; otherwise, specify `false` to update the slider’s appearance immediately. Animations are performed asynchronously and do not block the calling thread. - public func setValue(_ value: V, animated: Bool = false) { - if animated { - UIView.animate(withDuration: 0.3) { [weak self] in - guard let self else { return } - self.viewModel.setValue(value) - self.layoutSubviews() - } - } else { - self.viewModel.setValue(value) - } - } - - private func setupBar() { - self.indicatorView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] // Left - self.indicatorView.isUserInteractionEnabled = false - self.addSubview(self.indicatorView) - - self.trackView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] // Right - self.trackView.isUserInteractionEnabled = false - self.addSubview(self.trackView) - } - - private func subscribeToViewModel() { - // Indicator - self.viewModel.$indicatorColor.subscribe(in: &self.cancellables) { [weak self] newIndicatorColor in - self?.indicatorView.backgroundColor = newIndicatorColor.uiColor - } - self.viewModel.$indicatorRadius.subscribe(in: &self.cancellables) { [weak self] newIndicatorRadius in - self?.indicatorView.layer.cornerRadius = newIndicatorRadius / 2.0 // "/ 2.0" for top / bottom - } - - // Track - self.viewModel.$trackColor.subscribe(in: &self.cancellables) { [weak self] newTrackColor in - self?.trackView.backgroundColor = newTrackColor.uiColor - } - self.viewModel.$trackRadius.subscribe(in: &self.cancellables) { [weak self] newTrackRadius in - self?.trackView.layer.cornerRadius = newTrackRadius / 2.0 // "/ 2.0" for top / bottom - } - - // Handle - self.viewModel.$handleColor.subscribe(in: &self.cancellables) { [weak self] newHandleColor in - self?.handle.viewModel.color = newHandleColor - } - - self.viewModel.$handleActiveIndicatorColor.subscribe(in: &self.cancellables) { [weak self] newHandleActiveIndicatorColor in - self?.handle.viewModel.activeIndicatorColor = newHandleActiveIndicatorColor - } - - // Value - self.viewModel.$value.subscribe(in: &self.cancellables) { [weak self] newValue in - guard let self, - self.value != newValue else { return } - self.value = newValue - self.setAccessibilityValue(with: newValue) - } - - // Dim - self.viewModel.$dim.subscribe(in: &self.cancellables) { [weak self] newDim in - self?.alpha = newDim - } - } - - private func setupAccessibility() { - self.isAccessibilityElement = true - self.accessibilityIdentifier = SliderAccessibilityIdentifier.slider - self.accessibilityTraits.insert(.adjustable) - self.setAccessibilityValue(with: self.value) - } - - private func setAccessibilityValue(with value: V) { - let percentage = ((self.value - self.range.lowerBound) * 100) / (self.range.upperBound - self.range.lowerBound) - self.accessibilityValue = "\(Int(round(percentage)))%" - } - - // MARK: - Tracking - public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - super.beginTracking(touch, with: event) - self._isTracking = true - let location = touch.location(in: self) - self.isHighlighted = true - self.moveHandle(to: location.x) - return true - } - - public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - let continueTracking = super.continueTracking(touch, with: event) - - let location = touch.location(in: self) - - self.moveHandle(to: location.x) - - return continueTracking - } - - public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { - super.endTracking(touch, with: event) - if self._isTracking, - self.isContinuous == false { - self.sendActions(for: .valueChanged) - } - self._isTracking = false - self.isHighlighted = false - } - - private func moveHandle(to: CGFloat) { - let absoluteX = max(SliderConstants.handleSize.width / 2, min(to, self.frame.width - SliderConstants.handleSize.width / 2)) - let relativeX = (absoluteX - SliderConstants.handleSize.width / 2) / (self.frame.width - SliderConstants.handleSize.width) - - self.setValue( - V(relativeX) * (self.viewModel.bounds.upperBound - self.viewModel.bounds.lowerBound) + self.viewModel.bounds.lowerBound - ) - } - - public override func accessibilityIncrement() { - self.viewModel.incrementValue() - } - - public override func accessibilityDecrement() { - self.viewModel.decrementValue() - } -} diff --git a/core/Sources/Components/Slider/View/UIKit/SliderUIControlSnapshotTests.swift b/core/Sources/Components/Slider/View/UIKit/SliderUIControlSnapshotTests.swift deleted file mode 100644 index 26de693a8..000000000 --- a/core/Sources/Components/Slider/View/UIKit/SliderUIControlSnapshotTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// SliderUIControlSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by louis.borlee on 14/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class SliderUIControlSnapshotTests: UIKitComponentSnapshotTestCase { - - private let theme = SparkTheme.shared - - private func _test(scenario: SliderScenario) { - let configurations = self.createConfigurations(from: scenario) - for configuration in configurations { - self.assertSnapshot(matching: configuration.view, modes: scenario.modes, sizes: [.medium], testName: configuration.testName) - } - } - - func test1() { - self._test(scenario: SliderScenario.test1) - } - - func test2() { - self._test(scenario: SliderScenario.test2) - } - - func test3() { - self._test(scenario: SliderScenario.test3) - } - - func test4() { - self._test(scenario: SliderScenario.test4) - } - - private func createConfigurations(from scenario: SliderScenario) -> [(testName: String, view: SliderUIControl)] { - var sliders = [(testName: String, view: SliderUIControl)]() - for intent in scenario.intents { - for state in scenario.states { - for value in scenario.values { - let slider = SliderUIControl( - theme: self.theme, - shape: scenario.shape, - intent: intent - ) - slider.backgroundColor = .systemBackground - slider.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - slider.heightAnchor.constraint(equalToConstant: 44), - slider.widthAnchor.constraint(equalToConstant: 200) - ]) - slider.setValue(value.rawValue) - switch state { - case .normal: slider.isEnabled = true - case .disabled: slider.isEnabled = false - case .highlighted: slider.isHighlighted = true - } - let testName = scenario.getTestName(intent: intent, state: state, value: value) - sliders.append((testName: testName, view: slider)) - } - } - } - return sliders - } -} diff --git a/core/Sources/Components/Slider/ViewModel/Base/SliderViewModel.swift b/core/Sources/Components/Slider/ViewModel/Base/SliderViewModel.swift deleted file mode 100644 index c34426a00..000000000 --- a/core/Sources/Components/Slider/ViewModel/Base/SliderViewModel.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// SliderViewModel.swift -// SparkCore -// -// Created by louis.borlee on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -class SliderViewModel: ObservableObject where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - - // MARK: - Private Properties - private let getColorsUseCase: SliderGetColorsUseCasable - private let getCornerRadiiUseCase: SliderGetCornerRadiiUseCasable - private let getStepValuesInBoundsUseCase: SliderGetStepValuesInBoundsUseCasable - private let getClosestValueUseCase: SliderGetClosestValueUseCasable - - // MARK: - Internal Properties - var theme: Theme { - didSet { - self.setDim() - self.setColors() - self.setRadii() - } - } - var shape: SliderShape { - didSet { - guard oldValue != self.shape else { return } - self.setRadii() - } - } - var intent: SliderIntent { - didSet { - guard oldValue != self.intent else { return } - self.setColors() - } - } - - var isEnabled = true { - didSet { - guard oldValue != self.isEnabled else { return } - self.setDim() - } - } - var isContinuous = true - - // TODO: var numberOfSteps - - var step: V.Stride? { - didSet { - guard self.step != oldValue else { return } - self.setStepValues() - } - } - private(set) var stepValues: [V]? - - // MARK: - Published Colors - @Published var trackColor: any ColorToken - @Published var indicatorColor: any ColorToken - @Published var handleColor: any ColorToken - @Published var handleActiveIndicatorColor: any ColorToken - - // MARK: - Published Radii - @Published var trackRadius: CGFloat - @Published var indicatorRadius: CGFloat - - // MARK: - Published Bounds - @Published var bounds: ClosedRange = 0...1 { - didSet { - guard self.bounds != oldValue else { return } - self.setStepValues() - } - } - - // MARK: - Published Dim - @Published var dim: CGFloat - - required init(theme: Theme, - shape: SliderShape, - intent: SliderIntent, - getColorsUseCase: SliderGetColorsUseCasable = SliderGetColorsUseCase(), - getCornerRadiiUseCase: SliderGetCornerRadiiUseCasable = SliderGetCornerRadiiUseCase(), - getStepValuesInBoundsUseCase: SliderGetStepValuesInBoundsUseCasable = SliderGetStepValuesInBoundsUseCase(), - getClosestValueUseCase: SliderGetClosestValueUseCasable = SliderGetClosestValueUseCase()) { - self.theme = theme - self.shape = shape - self.intent = intent - self.getColorsUseCase = getColorsUseCase - self.getCornerRadiiUseCase = getCornerRadiiUseCase - self.getStepValuesInBoundsUseCase = getStepValuesInBoundsUseCase - self.getClosestValueUseCase = getClosestValueUseCase - - self.dim = self.theme.dims.none - - let colors = getColorsUseCase.execute(theme: self.theme, intent: self.intent) - self.trackColor = colors.track - self.indicatorColor = colors.indicator - self.handleColor = colors.handle - self.handleActiveIndicatorColor = colors.handleActiveIndicator - - let radii = getCornerRadiiUseCase.execute(theme: self.theme, shape: self.shape) - self.trackRadius = radii.trackRadius - self.indicatorRadius = radii.indicatorRadius - } - - private func setDim() { - self.dim = self.isEnabled ? self.theme.dims.none : self.theme.dims.dim3 - } - - private func setColors() { - let colors = self.getColorsUseCase.execute(theme: self.theme, intent: self.intent) - self.trackColor = colors.track - self.indicatorColor = colors.indicator - self.handleColor = colors.handle - self.handleActiveIndicatorColor = colors.handleActiveIndicator - } - - private func setRadii() { - let radii = getCornerRadiiUseCase.execute(theme: self.theme, shape: self.shape) - self.trackRadius = radii.trackRadius - self.indicatorRadius = radii.indicatorRadius - } - - private func setStepValues() { - if let step { - self.stepValues = self.getStepValuesInBoundsUseCase.execute(bounds: self.bounds, step: step) - } else { - self.stepValues = nil - } - } - - func resetBoundsIfNeeded() { - guard let lastValue = self.stepValues?.last, - lastValue < self.bounds.upperBound else { - return - } - self.bounds = self.bounds.lowerBound...lastValue - } - - func getClosestValue(fromValue value: V) -> V { - let boundedValue = max(self.bounds.lowerBound, min(self.bounds.upperBound, value)) - guard let stepValues else { - return boundedValue - } - return self.getClosestValueUseCase.execute(value: boundedValue, in: stepValues) - } -} diff --git a/core/Sources/Components/Slider/ViewModel/Base/SliderViewModelTests.swift b/core/Sources/Components/Slider/ViewModel/Base/SliderViewModelTests.swift deleted file mode 100644 index 089aa6695..000000000 --- a/core/Sources/Components/Slider/ViewModel/Base/SliderViewModelTests.swift +++ /dev/null @@ -1,641 +0,0 @@ -// -// SliderViewModelTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 02/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import Combine -@testable import SparkCore - -final class SliderViewModelTests: SliderViewModelWithMocksTests { - - override func setUp() { - super.setUp() - self.viewModel = SliderViewModel( - - theme: self.theme, - shape: self.shape, - intent: self.intent, - getColorsUseCase: self.getColorsUseCase, - getCornerRadiiUseCase: self.getCornerRadiiUseCase, - getStepValuesInBoundsUseCase: self.getStepValuesInBoundsUseCase, - getClosestValueUseCase: self.getClosestValueUseCase - ) - self.setupPublishers() - } - - // MARK: - init - func test_init() throws { - // GIVEN / WHEN - Inits from setUp() - // THEN - Simple variables - XCTAssertIdentical(self.viewModel.theme as? ThemeGeneratedMock, self.theme, "Wrong theme") - XCTAssertEqual(self.viewModel.intent, self.intent, "Wrong theme") - XCTAssertEqual(self.viewModel.shape, self.shape, "Wrong shape") - XCTAssertEqual(self.viewModel.dim, self.theme.dims.none, "Wrong dim") - XCTAssertNil(self.viewModel.step, "step should be nil") - XCTAssertNil(self.viewModel.stepValues, "stepValues should be nil") - - // THEN - Corner Radii - XCTAssertEqual(self.getCornerRadiiUseCase.executeWithThemeAndShapeCallsCount, 1, "getCornerRadiiUseCase.executeWithThemeAndShape should be called once") - let radiiReceivedArguments = try XCTUnwrap(self.getCornerRadiiUseCase.executeWithThemeAndShapeReceivedArguments, "Couldn't unwrap radiiReceivedArguments") - XCTAssertEqual(radiiReceivedArguments.shape, self.shape, "Wrong radiiReceivedArguments.shape") - XCTAssertIdentical(radiiReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong radiiReceivedArguments.theme") - XCTAssertEqual(self.viewModel.trackRadius, self.expectedRadii.trackRadius, "Wrong trackRadius") - XCTAssertEqual(self.viewModel.indicatorRadius, self.expectedRadii.indicatorRadius, "Wrong indicatorRadius") - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntent should be called once") - let colorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentReceivedArguments, "Couldn't unwrap colorsReceivedArguments") - XCTAssertEqual(colorsReceivedArguments.intent, self.intent, "Wrong colorsReceivedArguments.intent") - XCTAssertIdentical(colorsReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong colorsReceivedArguments.theme") - XCTAssertEqual(self.viewModel.trackColor.uiColor, self.expectedColors.track.uiColor, "Wrong trackColor") - XCTAssertEqual(self.viewModel.indicatorColor.uiColor, self.expectedColors.indicator.uiColor, "Wrong indicatorColor") - XCTAssertEqual(self.viewModel.handleColor.uiColor, self.expectedColors.handle.uiColor, "Wrong handleColor") - XCTAssertEqual(self.viewModel.handleActiveIndicatorColor.uiColor, self.expectedColors.handleActiveIndicator.uiColor, "Wrong handleActiveIndicatorColor") - - // THEN - StepValuesInBounds - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - - // THEN - ClosestValues - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should have been called once") - XCTAssertEqual(self.publishers.handleColor.sinkCount, 1, "$handleColor should have been called once") - XCTAssertEqual(self.publishers.handleActiveIndicatorColor.sinkCount, 1, "$handleActiveIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.trackColor.sinkCount, 1, "$trackColor should have been called once") - XCTAssertEqual(self.publishers.indicatorColor.sinkCount, 1, "$indicatorColor should have been called once") - XCTAssertEqual(self.publishers.trackRadius.sinkCount, 1, "$trackRadius should have been called once") - XCTAssertEqual(self.publishers.indicatorRadius.sinkCount, 1, "$indicatorRadius should have been called once") - } - - // MARK: - Theme - func test_theme_didSet() throws { - // GIVEN - Inits from setUp() - let newTheme = ThemeGeneratedMock() - newTheme.colors = ColorsGeneratedMock.mocked() - newTheme.dims = DimsGeneratedMock.mocked() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.theme = newTheme - - // THEN - Var - XCTAssertIdentical(self.viewModel.theme as? ThemeGeneratedMock, newTheme, "Wrong theme") - - // THEN - Corner Radii - XCTAssertEqual(self.getCornerRadiiUseCase.executeWithThemeAndShapeCallsCount, 1, "getCornerRadiiUseCase.executeWithThemeAndShape should be called once") - let radiiReceivedArguments = try XCTUnwrap(self.getCornerRadiiUseCase.executeWithThemeAndShapeReceivedArguments, "Couldn't unwrap radiiReceivedArguments") - XCTAssertIdentical(radiiReceivedArguments.theme as? ThemeGeneratedMock, newTheme, "Wrong radiiReceivedArguments.theme") - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntent should be called once") - let colorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentReceivedArguments, "Couldn't unwrap colorsReceivedArguments") - XCTAssertIdentical(colorsReceivedArguments.theme as? ThemeGeneratedMock, newTheme, "Wrong colorsReceivedArguments.theme") - - // THEN - StepValuesInBounds - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - - // THEN - ClosestValues - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should have been called once") - XCTAssertEqual(self.publishers.handleColor.sinkCount, 1, "$handleColor should have been called once") - XCTAssertEqual(self.publishers.handleActiveIndicatorColor.sinkCount, 1, "$handleActiveIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.trackColor.sinkCount, 1, "$trackColor should have been called once") - XCTAssertEqual(self.publishers.indicatorColor.sinkCount, 1, "$indicatorColor should have been called once") - XCTAssertEqual(self.publishers.trackRadius.sinkCount, 1, "$trackRadius should have been called once") - XCTAssertEqual(self.publishers.indicatorRadius.sinkCount, 1, "$indicatorRadius should have been called once") - } - - // MARK: - Is Enabled - func test_isEnabled_didSet_not_equal() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.isEnabled = false - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should be called once") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_isEnabled_didSet_equal() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.isEnabled = true - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - // MARK: - Intent - func test_intent_didSet_not_equal() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.intent = .neutral - - // THEN - UseCases - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntent should have been called once") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertEqual(self.publishers.handleColor.sinkCount, 1, "$handleColor should have been called once") - XCTAssertEqual(self.publishers.handleActiveIndicatorColor.sinkCount, 1, "$handleActiveIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.trackColor.sinkCount, 1, "$trackColor should have been called once") - XCTAssertEqual(self.publishers.indicatorColor.sinkCount, 1, "$indicatorColor should have been called once") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_intent_didSet_equal() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.intent = self.intent - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - // MARK: - Shape - func test_shape_didSet_not_equal() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.shape = .square - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertEqual(self.getCornerRadiiUseCase.executeWithThemeAndShapeCallsCount, 1, "getCornerRadiiUseCase.executeWithThemeAndShape should have been called once") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertEqual(self.publishers.trackRadius.sinkCount, 1, "$trackRadius should have been called once") - XCTAssertEqual(self.publishers.indicatorRadius.sinkCount, 1, "$indicatorRadius should have been called once") - } - - func test_shape_didSet_equal() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.shape = self.shape - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - // MARK: - Step - func test_step_same_value() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.step = nil - - // THEN - XCTAssertNil(self.viewModel.stepValues, "stepValues should be nil") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_step_new_value() throws { - // GIVEN - Inits from setUp() - let expectedStepValues: [Float] = [0, 1, 2] - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = expectedStepValues - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.step = 0.3 - - // THEN - XCTAssertEqual(self.viewModel.stepValues, expectedStepValues, "Wrong stepValues") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - XCTAssertEqual(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCallsCount, 1, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn have been called") - let receivedArguments = try XCTUnwrap(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReceivedArguments, "Couldn't unwrap receivedArguments") - XCTAssertEqual(receivedArguments.step, 0.3, "Wrong received step") - XCTAssertEqual(receivedArguments.bounds, 0...1, "Wrong received bounds") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_step_nil_value() throws { - // GIVEN - Inits from setUp() - let expectedStepValues: [Float] = [0, 1, 2] - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = expectedStepValues - self.viewModel.step = 0.3 - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.step = nil - - // THEN - XCTAssertNil(self.viewModel.stepValues, "stepValues should be nil") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - // MARK: - Bounds - func test_bounds_same_value() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.bounds = 0...1 - - // THEN - XCTAssertNil(self.viewModel.stepValues, "stepValues should be nil") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_bounds_new_value() throws { - // GIVEN - Inits from setUp() - let expectedStepValues: [Float] = [0, 1, 2] - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = expectedStepValues - self.viewModel.step = 0.4 // Changing bounds will not change stepValues unless step isn't nil - self.resetUseCases() // Removes previous executes - self.publishers.reset() // Removes previous publishes - - // WHEN - self.viewModel.bounds = 0...4 - - // THEN - XCTAssertEqual(self.viewModel.stepValues, expectedStepValues, "Wrong stepValues") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - XCTAssertEqual(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCallsCount, 1, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn have been called") - let receivedArguments = try XCTUnwrap(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReceivedArguments, "Couldn't unwrap receivedArguments") - XCTAssertEqual(receivedArguments.step, 0.4, "Wrong received step") - XCTAssertEqual(receivedArguments.bounds, 0...4, "Wrong received bounds") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - // MARK: - Get Closest Value - func test_getClosestValue_default_variables() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - let returnedValue = self.viewModel.getClosestValue(fromValue: 0.4) - - // THEN - XCTAssertEqual(returnedValue, 0.4, "Wrong returnedValue") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_getClosestValue_custom_variables() throws { - // GIVEN - Inits from setUp() - let stepValues: [Float] = [0, 3, 6, 9] - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = stepValues - self.getClosestValueUseCase.executeWithValueAndValuesReturnValue = 6 - self.viewModel.step = 3 - self.viewModel.bounds = 0...10 - self.resetUseCases() // Removes previous executes - self.publishers.reset() // Removes previous publishes - - // WHEN - let returnedValue = self.viewModel.getClosestValue(fromValue: 5) - - // THEN - XCTAssertEqual(returnedValue, 6.0, "Wrong returnedValue") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertEqual(self.getClosestValueUseCase.executeWithValueAndValuesCallsCount, 1, "getClosestValueUseCase.executeWithValueAndValues should have been called once") - let receivedArguments = try XCTUnwrap(self.getClosestValueUseCase.executeWithValueAndValuesReceivedArguments, "Couldn't unwrap receivedArguments") - XCTAssertEqual(receivedArguments.value, 5, "Wrong received value") - XCTAssertEqual(receivedArguments.values, stepValues, "Wrong received value") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_getClosestValue_value_over_bounds() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - let returnedValue = self.viewModel.getClosestValue(fromValue: 2) - - // THEN - XCTAssertEqual(returnedValue, 1, "Wrong returnedValue") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_getClosestValue_value_under_bounds() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - let returnedValue = self.viewModel.getClosestValue(fromValue: -1) - - // THEN - XCTAssertEqual(returnedValue, 0, "Wrong returnedValue") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - // MARK: Reset Bounds If Needed - func test_resetBoundsIfNeeded_is_not_needed() { - // GIVEN - Inits from setUp() - let expectedStepValues: [Float] = [0, 0.3, 1] - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = expectedStepValues - self.viewModel.step = 0.4 // Changing bounds will not change stepValues unless step isn't nil - self.resetUseCases() // Removes previous executes - self.publishers.reset() // Removes previous publishes - - // WHEN - self.viewModel.resetBoundsIfNeeded() - - // THEN - XCTAssertEqual(self.viewModel.bounds, 0...1, "Wrong bounds") - XCTAssertEqual(self.viewModel.stepValues, expectedStepValues, "Wrong stepValues") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCalled, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } - - func test_resetBoundsIfNeeded_is_needed() throws { - // GIVEN - Inits from setUp() - let expectedStepValues: [Float] = [0, 0.3, 0.6, 0.9] - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = expectedStepValues - self.viewModel.step = 0.3 // Changing bounds will not change stepValues unless step isn't nil - self.resetUseCases() // Removes previous executes - self.publishers.reset() // Removes previous publishes - - // WHEN - self.viewModel.resetBoundsIfNeeded() - - // THEN - XCTAssertEqual(self.viewModel.bounds, 0...0.9, "Wrong bounds") - XCTAssertEqual(self.viewModel.stepValues, expectedStepValues, "Wrong stepValues") - - // THEN - UseCases - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentCalled, "getColorsUseCase.executeWithThemeAndIntent shouldn't have been called") - XCTAssertFalse(self.getCornerRadiiUseCase.executeWithThemeAndShapeCalled, "getCornerRadiiUseCase.executeWithThemeAndShape shouldn't have been called") - XCTAssertFalse(self.getClosestValueUseCase.executeWithValueAndValuesCalled, "getClosestValueUseCase.executeWithValueAndValues shouldn't have been called") - XCTAssertEqual(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepCallsCount, 1, "getStepValuesInBoundsUseCase.executeWithBoundsAndStep should have been called once") - let receivedArguments = try XCTUnwrap(self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReceivedArguments, "Couldn't unwrap receivedArguments") - XCTAssertEqual(receivedArguments.step, 0.3, "Wrong received step") - XCTAssertEqual(receivedArguments.bounds, 0...0.9, "Wrong received bounds") - - // THEN - Publishers - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim shouldn't have been called") - XCTAssertFalse(self.publishers.handleColor.sinkCalled, "$handleColor shouldn't have been called") - XCTAssertFalse(self.publishers.handleActiveIndicatorColor.sinkCalled, "$handleActiveIndicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackColor.sinkCalled, "$trackColor shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorColor.sinkCalled, "$indicatorColor shouldn't have been called") - XCTAssertFalse(self.publishers.trackRadius.sinkCalled, "$trackRadius shouldn't have been called") - XCTAssertFalse(self.publishers.indicatorRadius.sinkCalled, "$indicatorRadius shouldn't have been called") - } -} - -class SliderPublishers { - var cancellables = Set() - var dim: PublisherMock.Publisher> - var trackColor: PublisherMock.Publisher> - var handleColor: PublisherMock.Publisher> - var indicatorColor: PublisherMock.Publisher> - var handleActiveIndicatorColor: PublisherMock.Publisher> - var trackRadius: PublisherMock.Publisher> - var indicatorRadius: PublisherMock.Publisher> - - init(dim: PublisherMock.Publisher>, - trackColor: PublisherMock.Publisher>, - handleColor: PublisherMock.Publisher>, - indicatorColor: PublisherMock.Publisher>, - handleActiveIndicatorColor: PublisherMock.Publisher>, - trackRadius: PublisherMock.Publisher>, - indicatorRadius: PublisherMock.Publisher>) { - self.dim = dim - self.trackColor = trackColor - self.handleColor = handleColor - self.indicatorColor = indicatorColor - self.handleActiveIndicatorColor = handleActiveIndicatorColor - self.trackRadius = trackRadius - self.indicatorRadius = indicatorRadius - } - - func load() { - self.cancellables = Set() - [self.dim, self.trackRadius, self.indicatorRadius].forEach { - $0.loadTesting(on: &self.cancellables) - } - [self.trackColor, self.handleColor, self.indicatorColor, self.handleActiveIndicatorColor].forEach { - $0.loadTesting(on: &self.cancellables) - } - } - - func reset() { - [self.dim, self.trackRadius, self.indicatorRadius].forEach { - $0.reset() - } - [self.trackColor, self.handleColor, self.indicatorColor, self.handleActiveIndicatorColor].forEach { - $0.reset() - } - } -} diff --git a/core/Sources/Components/Slider/ViewModel/Base/SliderViewModelWithMocksTests.swift b/core/Sources/Components/Slider/ViewModel/Base/SliderViewModelWithMocksTests.swift deleted file mode 100644 index 6436391bf..000000000 --- a/core/Sources/Components/Slider/ViewModel/Base/SliderViewModelWithMocksTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// SliderViewModelWithMocksTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 03/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import Combine -@testable import SparkCore - -class SliderViewModelWithMocksTests: XCTestCase { - let intent = SliderIntent.info - let shape = SliderShape.rounded - let expectedRadii = SliderRadii.mocked() - - var theme: ThemeGeneratedMock! - var viewModel: SliderViewModel! - var expectedColors: SliderColors! - var getColorsUseCase: SliderGetColorsUseCasableGeneratedMock! - var getCornerRadiiUseCase: SliderGetCornerRadiiUseCasableGeneratedMock! - var getStepValuesInBoundsUseCase: SliderGetStepValuesInBoundsUseCasableMock! - var getClosestValueUseCase: SliderGetClosestValueUseCasableMock! - - var publishers: SliderPublishers! - - func setupPublishers() { - self.publishers = SliderPublishers( - dim: PublisherMock(publisher: self.viewModel.$dim), - trackColor: PublisherMock(publisher: self.viewModel.$trackColor), - handleColor: PublisherMock(publisher: self.viewModel.$handleColor), - indicatorColor: PublisherMock(publisher: self.viewModel.$indicatorColor), - handleActiveIndicatorColor: PublisherMock(publisher: self.viewModel.$handleActiveIndicatorColor), - trackRadius: PublisherMock(publisher: self.viewModel.$trackRadius), - indicatorRadius: PublisherMock(publisher: self.viewModel.$indicatorRadius) - ) - self.publishers.load() - } - - func resetUseCases() { - self.getColorsUseCase.reset() - self.getCornerRadiiUseCase.reset() - self.getClosestValueUseCase.reset() - self.getStepValuesInBoundsUseCase.reset() - } - - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - self.expectedColors = SliderColors.mocked(colors: self.theme.colors) - self.getColorsUseCase = SliderGetColorsUseCasableGeneratedMock.mocked(returnedColors: self.expectedColors) - self.getCornerRadiiUseCase = SliderGetCornerRadiiUseCasableGeneratedMock.mocked(expectedRadii: self.expectedRadii) - self.getStepValuesInBoundsUseCase = SliderGetStepValuesInBoundsUseCasableMock() - self.getClosestValueUseCase = SliderGetClosestValueUseCasableMock() - } -} diff --git a/core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModel.swift b/core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModel.swift deleted file mode 100644 index 6deaf3e9b..000000000 --- a/core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SingleSliderViewModel.swift -// SparkCore -// -// Created by louis.borlee on 02/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -final class SingleSliderViewModel: SliderViewModel where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - - // MARK: - Published values - @Published private(set) var value: V = 0.0 - - func setValue(_ value: V) { - let newValue = super.getClosestValue(fromValue: value) - self.value = newValue - } - - private func getDefaultIncrementValue() -> V { - return 10 * (bounds.upperBound - bounds.lowerBound) / 100.0 - } - - func incrementValue() { - if let step { - self.setValue(self.value.advanced(by: step)) - } else { - self.setValue(self.value + self.getDefaultIncrementValue()) - } - } - - func decrementValue() { - if let step { - self.setValue(self.value.advanced(by: -step)) - } else { - self.setValue(self.value - self.getDefaultIncrementValue()) - } - } -} diff --git a/core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModelTests.swift b/core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModelTests.swift deleted file mode 100644 index 59dc9171a..000000000 --- a/core/Sources/Components/Slider/ViewModel/Single/SingleSliderViewModelTests.swift +++ /dev/null @@ -1,291 +0,0 @@ -// -// SingleSliderViewModelTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 03/01/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import Combine -@testable import SparkCore - -final class SingleSliderViewModelTests: SliderViewModelWithMocksTests { - var singleViewModel: SingleSliderViewModel! - override var viewModel: SliderViewModel! { - get { return self.singleViewModel } - set { self.singleViewModel = newValue as? SingleSliderViewModel } - } - - var singlePublishers: SingleSliderPublishers! - override var publishers: SliderPublishers! { - get { return self.singlePublishers } - set { self.singlePublishers = newValue as? SingleSliderPublishers } - } - - override func setUp() { - super.setUp() - self.singleViewModel = SingleSliderViewModel( - theme: self.theme, - shape: self.shape, - intent: self.intent, - getColorsUseCase: self.getColorsUseCase, - getCornerRadiiUseCase: self.getCornerRadiiUseCase, - getStepValuesInBoundsUseCase: self.getStepValuesInBoundsUseCase, - getClosestValueUseCase: self.getClosestValueUseCase - ) - self.setupPublishers() - } - - override func setupPublishers() { - self.singlePublishers = SingleSliderPublishers( - dim: PublisherMock(publisher: self.viewModel.$dim), - trackColor: PublisherMock(publisher: self.viewModel.$trackColor), - handleColor: PublisherMock(publisher: self.viewModel.$handleColor), - indicatorColor: PublisherMock(publisher: self.viewModel.$indicatorColor), - handleActiveIndicatorColor: PublisherMock(publisher: self.viewModel.$handleActiveIndicatorColor), - trackRadius: PublisherMock(publisher: self.viewModel.$trackRadius), - indicatorRadius: PublisherMock(publisher: self.viewModel.$indicatorRadius), - value: PublisherMock(publisher: self.singleViewModel.$value) - ) - self.publishers.load() - } - - // MARK: - init - func test_init() throws { - // GIVEN / WHEN - Inits from setUp() - // THEN - Simple variables - XCTAssertEqual(self.singleViewModel.value, 0.0, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - // MARK: - Set Value - func test_setValue() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.setValue(0.5) - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.5, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_setValue_under_bounds() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.setValue(-1) - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_setValue_over_bounds() { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.setValue(2) - - // THEN - XCTAssertEqual(self.singleViewModel.value, 1, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_setValue_closest_value() { - // GIVEN - Inits from setUp() - self.getClosestValueUseCase.executeWithValueAndValuesReturnValue = 0.4 - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = [0, 0.4, 1] - self.viewModel.step = 0.3 - self.resetUseCases() // Removes previous executes - self.publishers.reset() // Removes previous publishes - - // WHEN - self.singleViewModel.setValue(0.5) - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.4, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - // MARK: - Increment value - func test_incrementValue_withoutStep() { - // GIVEN - Inits from setUp() - self.singleViewModel.setValue(0.5) - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.incrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.6, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_incrementValue_withoutStep_customBounds() { - // GIVEN - Inits from setUp() - self.singleViewModel.bounds = 200...800 - self.singleViewModel.setValue(400) - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.incrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 460, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_incrementValue_withoutStep_overBound() { - // GIVEN - Inits from setUp() - self.singleViewModel.setValue(0.95) - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.incrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 1.0, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_incrementValue_withStep() { - // GIVEN - Inits from setUp() - self.getClosestValueUseCase.executeWithValueAndValuesReturnValue = 0.8 - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = [0, 0.3, 0.8, 1.0] - self.singleViewModel.setValue(0.5) - self.singleViewModel.step = 0.3 - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.incrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.8, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - // MARK: - Decrement value - func test_decrementValue_withoutStep() { - // GIVEN - Inits from setUp() - self.singleViewModel.setValue(0.5) - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.decrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.4, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_decrementValue_withoutStep_customBounds() { - // GIVEN - Inits from setUp() - self.singleViewModel.bounds = 200...800 - self.singleViewModel.setValue(400) - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.decrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 340, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_decrementValue_withoutStep_overBound() { - // GIVEN - Inits from setUp() - self.singleViewModel.setValue(0.05) - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.decrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.0, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } - - func test_decrementValue_withStep() { - // GIVEN - Inits from setUp() - self.getClosestValueUseCase.executeWithValueAndValuesReturnValue = 0.2 - self.getStepValuesInBoundsUseCase.executeWithBoundsAndStepReturnValue = [0, 0.2, 0.8, 1.0] - self.singleViewModel.setValue(0.5) - self.singleViewModel.step = 0.3 - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.singleViewModel.decrementValue() - - // THEN - XCTAssertEqual(self.singleViewModel.value, 0.2, "Wrong value") - - // THEN - Publishers - XCTAssertEqual(self.singlePublishers.value.sinkCount, 1, "$value should have been called once") - } -} - -final class SingleSliderPublishers: SliderPublishers { - var value: PublisherMock.Publisher> - - init(dim: PublisherMock.Publisher>, - trackColor: PublisherMock.Publisher>, - handleColor: PublisherMock.Publisher>, - indicatorColor: PublisherMock.Publisher>, - handleActiveIndicatorColor: PublisherMock.Publisher>, - trackRadius: PublisherMock.Publisher>, - indicatorRadius: PublisherMock.Publisher>, - value: PublisherMock.Publisher>) { - self.value = value - super.init(dim: dim, trackColor: trackColor, handleColor: handleColor, indicatorColor: indicatorColor, handleActiveIndicatorColor: handleActiveIndicatorColor, trackRadius: trackRadius, indicatorRadius: indicatorRadius) - } - - override func load() { - super.load() - self.value.loadTesting(on: &super.cancellables) - } - - override func reset() { - super.reset() - self.value.reset() - } -} diff --git a/core/Sources/Components/Spinner/AccessibilityIdentifiier/SpinnerAccessibilityIdentifier.swift b/core/Sources/Components/Spinner/AccessibilityIdentifiier/SpinnerAccessibilityIdentifier.swift deleted file mode 100644 index 6d243b875..000000000 --- a/core/Sources/Components/Spinner/AccessibilityIdentifiier/SpinnerAccessibilityIdentifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// SpinnerAccessibilityIdentifier.swift -// SparkCore -// -// Created by michael.zimmermann on 13.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the spinner. -public enum SpinnerAccessibilityIdentifier { - - // MARK: - Properties - - /// The text label accessibility identifier. - public static let spinner = "spark-spinner" -} diff --git a/core/Sources/Components/Spinner/Enum/SpinnerIntent.swift b/core/Sources/Components/Spinner/Enum/SpinnerIntent.swift deleted file mode 100644 index 4fbd19d9d..000000000 --- a/core/Sources/Components/Spinner/Enum/SpinnerIntent.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SpinnerIntent.swift -// SparkCore -// -// Created by michael.zimmermann on 07.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// `SpinnerIntent` determines the color of the spinner according to the theme. -public enum SpinnerIntent: CaseIterable { - case alert - case error - case info - case neutral - case main - case support - case success - case accent - case basic -} diff --git a/core/Sources/Components/Spinner/Enum/SpinnerSize.swift b/core/Sources/Components/Spinner/Enum/SpinnerSize.swift deleted file mode 100644 index 6853759d3..000000000 --- a/core/Sources/Components/Spinner/Enum/SpinnerSize.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// SpinnerSize.swift -// SparkCore -// -// Created by michael.zimmermann on 07.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// `SpinnerSize` is the size of the spinner. At the moment, only two sizes are available, medium and small. -public enum SpinnerSize: CaseIterable { - case medium - case small -} diff --git a/core/Sources/Components/Spinner/SpinnerViewModel.swift b/core/Sources/Components/Spinner/SpinnerViewModel.swift deleted file mode 100644 index 407e61621..000000000 --- a/core/Sources/Components/Spinner/SpinnerViewModel.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// SpinnerViewModel.swift -// SparkCore -// -// Created by michael.zimmermann on 10.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation - -/// `SpinnerViewModel` is the view model for both the SwiftUI `SpinnerView` as well as the UIKit `SpinnerUIView`. -/// The view model is responsible for returning the varying attributes to the views, i.e. colors and size. These are determined by the theme, intent and spinnerSize. -/// When the theme or the intent change the new values are calculated and published. -final class SpinnerViewModel: ObservableObject { - enum Constants { - enum Size { - static let small = 20.0 - static let medium = 28.0 - } - static let stroke = 2.0 - static let duration = 1.0 - } - - // MARK: - Private Properties - private let useCase: any GetSpinnerIntentColorUseCasable - - // MARK: - Public Properties - var theme: Theme { - didSet { - self.intentColor = self.useCase.execute(colors: theme.colors, intent: intent) - } - } - - var intent: SpinnerIntent { - didSet { - guard self.intent != oldValue else { return } - self.intentColor = self.useCase.execute(colors: theme.colors, intent: intent) - } - } - - var spinnerSize: SpinnerSize { - didSet { - guard self.spinnerSize != oldValue else { return } - self.size = self.spinnerSize.numeric - } - } - - let duration = Constants.duration - let strokeWidth = Constants.stroke - - // MARK: Published Properties - @Published var size: CGFloat - @Published var intentColor: any ColorToken - @Published var isSpinning: Bool = false - - // MARK: Init - /// Init - /// Parameters: - /// - theme: the current `Theme` - /// - intent: the `SpinnerIntent`, which will determine the color of the spinner - /// - spinnerSize: the `SpinnerSize` - /// - userCase: `GetSpinnerIntentColorUseCasable` has a default value `GetSpinnerIntentColorUseCase` - init(theme: Theme, - intent: SpinnerIntent, - spinnerSize: SpinnerSize, - useCase: any GetSpinnerIntentColorUseCasable = GetSpinnerIntentColorUseCase()) { - self.theme = theme - self.intent = intent - self.spinnerSize = spinnerSize - self.useCase = useCase - self.size = spinnerSize.numeric - self.intentColor = useCase.execute(colors: theme.colors, intent: intent) - } -} - -// MARK: - Private helpers -private extension SpinnerSize { - var numeric: CGFloat { - switch self { - case .small: return SpinnerViewModel.Constants.Size.small - case .medium: return SpinnerViewModel.Constants.Size.medium - } - } -} diff --git a/core/Sources/Components/Spinner/SpinnerViewModelTests.swift b/core/Sources/Components/Spinner/SpinnerViewModelTests.swift deleted file mode 100644 index a1f8b0f99..000000000 --- a/core/Sources/Components/Spinner/SpinnerViewModelTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// SpinnerViewModelTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 11.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import XCTest -import SwiftUI - -final class SpinnerViewModelTests: XCTestCase { - - // MARK: - Private properties - private var useCase: GetSpinnerIntentColorUseCasableGeneratedMock! - private var subscriptions = Set() - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.useCase = GetSpinnerIntentColorUseCasableGeneratedMock() - let colorToken = ColorTokenGeneratedMock.mock(Color.red) - self.useCase.executeWithColorsAndIntentReturnValue = colorToken - } - - // MARK: - Tests - func test_variables_published_on_init() { - let sut = sut(intent: .main, spinnerSize: .small) - let expect = expectation(description: "All publishers should have published") - - Publishers.Zip(sut.$intentColor, sut.$size).subscribe(in: &self.subscriptions) { colorToken, size in - - XCTAssertEqual(colorToken.color, Color.red) - XCTAssertEqual(size, SpinnerViewModel.Constants.Size.small) - expect.fulfill() - } - - wait(for: [expect], timeout: 0.1) - } - - func test_non_published_variables_on_init() { - let sut = sut(intent: .error, spinnerSize: .medium) - - XCTAssertEqual(sut.size, SpinnerViewModel.Constants.Size.medium, "Expected size to be \(SpinnerViewModel.Constants.Size.medium)") - XCTAssertEqual(sut.intentColor.color, Color.red, "Expected intent.color to be red") - XCTAssertEqual(sut.duration, SpinnerViewModel.Constants.duration, "Expected duration to be \(SpinnerViewModel.Constants.duration)") - XCTAssertEqual(sut.strokeWidth, SpinnerViewModel.Constants.stroke, "Expected strokeWidth to be \(SpinnerViewModel.Constants.stroke)") - } - - func test_colors_republished_on_theme_update() { - let sut = sut(intent: .main, spinnerSize: .small) - let expect = expectation(description: "All publisher should have published") - expect.expectedFulfillmentCount = 2 - - sut.$intentColor.subscribe(in: &self.subscriptions) { colorToken in - XCTAssertEqual(colorToken.color, Color.red) - expect.fulfill() - } - - sut.theme = ThemeGeneratedMock.mocked() - - wait(for: [expect], timeout: 0.1) - } - - func test_size_republished_on_size_update() { - let sut = sut(intent: .main, spinnerSize: .small) - let expect = expectation(description: "All publisher should have published") - expect.expectedFulfillmentCount = 2 - - var expectedSize = 20.0 - sut.$size.subscribe(in: &self.subscriptions) { size in - XCTAssertEqual(size, expectedSize) - expectedSize = 28 - expect.fulfill() - } - - sut.spinnerSize = .medium - - wait(for: [expect], timeout: 0.1) - } - - // MARK: - Private helper function - private func sut(intent: SpinnerIntent, spinnerSize: SpinnerSize) -> SpinnerViewModel { - return .init(theme: ThemeGeneratedMock.mocked(), - intent: intent, - spinnerSize: spinnerSize, - useCase: self.useCase - ) - } -} diff --git a/core/Sources/Components/Spinner/SwiftUI/SpinnerView.swift b/core/Sources/Components/Spinner/SwiftUI/SpinnerView.swift deleted file mode 100644 index c5d7222bf..000000000 --- a/core/Sources/Components/Spinner/SwiftUI/SpinnerView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// SpinnerView.swift -// SparkCore -// -// Created by michael.zimmermann on 10.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// SpinnerView is a single indeterminate spinner. -/// The spinner can have a size of `small` or `medium` and have different intents which determine the color of the spinner. -/// The spinner spin animation is 1 second linear infinite. -public struct SpinnerView: View { - - // MARK: - Type Alias - private typealias AccessibilityIdentifier = SpinnerAccessibilityIdentifier - - // MARK: - Private Properties - @ObservedObject private var viewModel: SpinnerViewModel - @State private var rotationDegrees = 0.0 - - @ScaledMetric private var size: CGFloat - @ScaledMetric private var strokeWidth: CGFloat - - // MARK: - Init - /// init - /// Parameters: - /// - theme: The current `Theme` - /// - intent: The `SpinnerIntent` intent used for coloring the spinner. The default is `main` - /// - spinnerSize: The defined size of the spinner`SpinnerSize`. The default is `small` - public init(theme: Theme, - intent: SpinnerIntent = .main, - spinnerSize: SpinnerSize = .small) { - self.init(viewModel: SpinnerViewModel(theme: theme, intent: intent, spinnerSize: spinnerSize)) - } - - init(viewModel: SpinnerViewModel) { - self.viewModel = viewModel - self._size = ScaledMetric(wrappedValue: viewModel.size) - self._strokeWidth = ScaledMetric(wrappedValue: viewModel.strokeWidth) - } - - public var body: some View { - Circle() - .trim(from: 0, to: 0.5) - .stroke(lineWidth: self.strokeWidth) - .foregroundColor(self.viewModel.intentColor.color) - .frame(width: self.size, height: self.size) - .rotationEffect(.degrees(self.rotationDegrees)) - .animation(self.animation(), value: self.viewModel.isSpinning) - .task { - self.rotationDegrees = 360.0 - self.viewModel.isSpinning = true - } - .accessibilityIdentifier(AccessibilityIdentifier.spinner) - } - - // MARK: - Public modifiers - public func spinnerSize(_ spinnerSize: SpinnerSize) -> Self { - self.viewModel.spinnerSize = spinnerSize - return self - } - - public func intent(_ intent: SpinnerIntent) -> Self { - self.viewModel.intent = intent - return self - } - - // MARK: - Private helpers - private func animation() -> Animation? { - guard self.viewModel.isSpinning else { - return nil - } - return .linear(duration: self.viewModel.duration) - .repeatForever(autoreverses: false) - } -} - diff --git a/core/Sources/Components/Spinner/UIKit/SpinnerUIView.swift b/core/Sources/Components/Spinner/UIKit/SpinnerUIView.swift deleted file mode 100644 index 9d7125912..000000000 --- a/core/Sources/Components/Spinner/UIKit/SpinnerUIView.swift +++ /dev/null @@ -1,158 +0,0 @@ -// -// ChipUIView.swift -// SparkCore -// -// Created by michael.zimmermann on 07.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation -import QuartzCore -import UIKit -import SwiftUI - -/// SpinnerView is a single indeterminate spinner. -/// The spinner can have a size of `small` or `medium` and have different intents which determine the color of the spinner. -/// The spinner spin animation is 1 second linear infinite. -public final class SpinnerUIView: UIView { - - // MARK: - Type Alias - private typealias AccessibilityIdentifier = SpinnerAccessibilityIdentifier - - // MARK: - Private attributes - private let viewModel: SpinnerViewModel - private var subscriptions = Set() - - @ScaledUIMetric private var size: CGFloat - @ScaledUIMetric private var strokeWidth: CGFloat - - // MARK: - Public modifiable attributes - /// The current theme - public var theme: Theme { - get { return self.viewModel.theme } - set { self.viewModel.theme = newValue } - } - - /// The spinner size (`medium` or `small`) - public var spinnerSize: SpinnerSize { - get { return self.viewModel.spinnerSize } - set { self.viewModel.spinnerSize = newValue } - } - - /// The spinner intent - public var intent: SpinnerIntent { - get { return self.viewModel.intent } - set { self.viewModel.intent = newValue} - } - - // MARK: - Init - /// init - /// Parameters: - /// - theme: The current `Theme` - /// - intent: The `SpinnerIntent` intent used for coloring the spinner. The default is `main` - /// - spinnerSize: The defined size of the spinner`SpinnerSize`. The default is `small` - public convenience init(theme: Theme, - intent: SpinnerIntent = .main, - spinnerSize: SpinnerSize = .small) { - self.init(viewModel: SpinnerViewModel(theme: theme, intent: intent, spinnerSize: spinnerSize)) - } - - init(viewModel: SpinnerViewModel) { - self.viewModel = viewModel - let size = ScaledUIMetric(wrappedValue: viewModel.size) - self._size = size - let strokeWidth = ScaledUIMetric(wrappedValue: viewModel.strokeWidth) - self._strokeWidth = strokeWidth - - super.init(frame: CGRect.zero) - - self.translatesAutoresizingMaskIntoConstraints = false - self.setContentHuggingPriority(.required, for: .horizontal) - self.setContentHuggingPriority(.required, for: .vertical) - self.setContentCompressionResistancePriority(.required, for: .horizontal) - self.setContentCompressionResistancePriority(.required, for: .vertical) - self.backgroundColor = .clear - - self.setupSubscriptions() - self.accessibilityIdentifier = AccessibilityIdentifier.spinner - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override var intrinsicContentSize: CGSize { - return CGSize(width: self.size, height: self.size) - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - let oldSize = self.size - let oldStrokeWidth = self.strokeWidth - - self._size.update(traitCollection: self.traitCollection) - self._strokeWidth.update(traitCollection: self.traitCollection) - - if self.size != oldSize || self.strokeWidth != oldStrokeWidth { - self.setNeedsLayout() - } - } - - public override func draw(_ rect: CGRect) { - super.draw(rect) - - guard - let ctx = UIGraphicsGetCurrentContext() - else { return } - - let center = rect.width / 2 - let centerPoint = CGPoint(x: center, y: center) - - let spinnerArc = UIBezierPath.arc(arcCenter: centerPoint, radius: (rect.width - self.strokeWidth) / 2 ) - spinnerArc.lineWidth = self.strokeWidth - ctx.setStrokeColor(self.viewModel.intentColor.uiColor.cgColor) - spinnerArc.stroke() - - self.animate() - } - - // MARK: - Private functions - private func setupSubscriptions() { - self.viewModel.$size.subscribe(in: &self.subscriptions) { [weak self] size in - guard let self else { return } - let oldSize = self.size - self.size = size - - if oldSize != self.size { - self.invalidateIntrinsicContentSize() - } - } - - self.viewModel.$intentColor.subscribe(in: &self.subscriptions) { [weak self] _ in - self?.setNeedsDisplay() - } - } - - private func animate() { - self.layer.removeAllAnimations() - - let fullRotation = CABasicAnimation(keyPath: "transform.rotation.z") - fullRotation.fromValue = 0 - fullRotation.toValue = 2 * CGFloat.pi - fullRotation.duration = self.viewModel.duration - fullRotation.repeatCount = .infinity - - self.layer.add(fullRotation, forKey: "Spinner.360") - } -} - -// MARK: - Private helper extensions -private extension UIBezierPath { - static func arc(arcCenter: CGPoint, - radius: CGFloat) -> UIBezierPath { - return UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat.pi, clockwise: true) - } -} - diff --git a/core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCase.swift b/core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCase.swift deleted file mode 100644 index 900337d6f..000000000 --- a/core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCase.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// GetSpinnerIntentColorUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 10.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol GetSpinnerIntentColorUseCasable { - func execute(colors: any Colors, intent: SpinnerIntent) -> any ColorToken -} - -/// GetSpinnerIntentColorUseCase -/// Use case to determin the colors of the Spinner by the intent -/// Functions: -/// - execute: returns a color token for given colors and an intent -struct GetSpinnerIntentColorUseCase: GetSpinnerIntentColorUseCasable { - - // MARK: - Functions - /// - /// Calculate the color of the spinner depending on the intent - /// - /// - Parameters: - /// - colors: Colors from the theme - /// - intent: `SpinnerIntent`. - /// - /// - Returns: ``ColorToken`` of the spinner. - func execute(colors: any Colors, intent: SpinnerIntent) -> any ColorToken { - switch intent { - case .main: return colors.main.main - case .support: return colors.support.support - case .alert: return colors.feedback.alert - case .error: return colors.feedback.error - case .info: return colors.feedback.info - case .neutral: return colors.feedback.neutral - case .success: return colors.feedback.success - case .accent: return colors.accent.accent - case .basic: return colors.basic.basic - } - } -} diff --git a/core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCaseTests.swift b/core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCaseTests.swift deleted file mode 100644 index e7927f086..000000000 --- a/core/Sources/Components/Spinner/UseCases/GetSpinnerIntentColorUseCaseTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// GetSpinnerIntentColorUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 11.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class GetSpinnerIntentColorUseCaseTests: XCTestCase { - - // MARK: - Private properties - private var sut: GetSpinnerIntentColorUseCase! - private var colors: ColorsGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.sut = GetSpinnerIntentColorUseCase() - self.colors = ColorsGeneratedMock.mocked() - } - - // MARK: - Tests - func test_execute_main() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .main).color, self.colors.main.main.color) - } - - func test_execute_support() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .support).color, self.colors.support.support.color) - } - - func test_execute_alert() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .alert).color, self.colors.feedback.alert.color) - } - - func test_execute_error() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .error).color, self.colors.feedback.error.color) - } - - func test_execute_info() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .info).color, self.colors.feedback.info.color) - } - - func test_execute_neutral() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .neutral).color, self.colors.feedback.neutral.color) - } - - func test_execute_success() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .success).color, self.colors.feedback.success.color) - } - - func test_execute_accent() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .accent).color, self.colors.accent.accent.color) - } - - func test_execute_basic() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .basic).color, self.colors.basic.basic.color) - } -} diff --git a/core/Sources/Components/Switch/AccessibilityIdentifier/SwitchAccessibilityIdentifier.swift b/core/Sources/Components/Switch/AccessibilityIdentifier/SwitchAccessibilityIdentifier.swift deleted file mode 100644 index 83391f384..000000000 --- a/core/Sources/Components/Switch/AccessibilityIdentifier/SwitchAccessibilityIdentifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SwitchAccessibilityIdentifier.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The accessibility identifiers for the switch. -public enum SwitchAccessibilityIdentifier { - - // MARK: - Properties - - /// The toggle view accessibility identifier. - public static let toggleView = "spark-switch-toggleView" - /// The toggle dot view accessibility identifier. - public static let toggleDotView = "spark-switch-toggle-dotView" - /// The toggle dot image view accessibility identifier. - public static let toggleDotImageView = "spark-switch-toggle-dotImageView" - /// The text accessibility identifier. - public static let text = "spark-switch-text" -} diff --git a/core/Sources/Components/Switch/Constants/SwitchConstants.swift b/core/Sources/Components/Switch/Constants/SwitchConstants.swift deleted file mode 100644 index 76d0c7111..000000000 --- a/core/Sources/Components/Switch/Constants/SwitchConstants.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SwitchConstants.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum SwitchConstants { - /// Animation duration is 200ms - static let animationDuration = 0.2 - - /// Padding for toggle dot image - static let toggleDotImagePadding: CGFloat = 5 - - /// Toggles sizes - enum ToggleSizes { - /// Toggle width - static let width: CGFloat = 56 - /// Toggle height - static let height: CGFloat = 32 - /// Toggle padding - static let padding: CGFloat = 4 - } -} diff --git a/core/Sources/Components/Switch/Either/SwitchImagesEither.swift b/core/Sources/Components/Switch/Either/SwitchImagesEither.swift deleted file mode 100644 index 811e275ad..000000000 --- a/core/Sources/Components/Switch/Either/SwitchImagesEither.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// SwitchImagesEither.swift -// SparkCore -// -// Created by robin.lemaire on 04/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -typealias SwitchImagesEither = Either diff --git a/core/Sources/Components/Switch/Enum/SwitchAlignment.swift b/core/Sources/Components/Switch/Enum/SwitchAlignment.swift deleted file mode 100644 index c826f772d..000000000 --- a/core/Sources/Components/Switch/Enum/SwitchAlignment.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// SwitchAlignment.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The alignment of the switch. -public enum SwitchAlignment: CaseIterable { - /// Switch is on the left, text is on the right - case left - /// Switch is on the right, text is on the left - case right -} diff --git a/core/Sources/Components/Switch/Enum/SwitchIntent.swift b/core/Sources/Components/Switch/Enum/SwitchIntent.swift deleted file mode 100644 index 5547a5d5f..000000000 --- a/core/Sources/Components/Switch/Enum/SwitchIntent.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SwitchIntent.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The intent of the switch. -public enum SwitchIntent: CaseIterable { - case alert - case error - case info - case neutral - case main - case support - case success - case accent - case basic -} diff --git a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors+ExtensionTests.swift b/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors+ExtensionTests.swift deleted file mode 100644 index dec7bcd24..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors+ExtensionTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SwitchColors.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension SwitchColors { - - // MARK: - Properties - - static func mocked( - toggleBackgroundColors: SwitchStatusColors = .mocked(), - toggleDotForegroundColors: SwitchStatusColors = .mocked(), - toggleDotBackgroundColor: any ColorToken = ColorTokenGeneratedMock.random(), - textForegroundColor: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - toggleBackgroundColors: toggleBackgroundColors, - toggleDotForegroundColors: toggleDotForegroundColors, - toggleDotBackgroundColor: toggleDotBackgroundColor, - textForegroundColor: textForegroundColor - ) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors.swift b/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors.swift deleted file mode 100644 index fd964a2e5..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColors.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// SwitchStatusColors.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct SwitchColors { - - // MARK: - Properties - - let toggleBackgroundColors: SwitchStatusColors - let toggleDotForegroundColors: SwitchStatusColors - let toggleDotBackgroundColor: any ColorToken - let textForegroundColor: any ColorToken -} - -// MARK: Hashable & Equatable - -extension SwitchColors: Hashable, Equatable { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.toggleBackgroundColors) - hasher.combine(self.toggleDotForegroundColors) - hasher.combine(self.toggleDotBackgroundColor) - hasher.combine(self.textForegroundColor) - } - - static func == (lhs: SwitchColors, rhs: SwitchColors) -> Bool { - return lhs.toggleBackgroundColors == rhs.toggleBackgroundColors && - lhs.toggleDotForegroundColors == rhs.toggleDotForegroundColors && - lhs.toggleDotBackgroundColor.equals(rhs.toggleDotBackgroundColor) && - lhs.textForegroundColor.equals(rhs.textForegroundColor) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColorsTests.swift b/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColorsTests.swift deleted file mode 100644 index 238daf879..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchColorsTests.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SwitchColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class SwitchColorsTests: XCTestCase { - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = SwitchColors( - toggleBackgroundColors: SwitchStatusColors( - onColorToken: colors.feedback.info, - offColorToken: colors.feedback.alert), - toggleDotForegroundColors: SwitchStatusColors( - onColorToken: colors.main.main, - offColorToken: colors.support.support), - toggleDotBackgroundColor: colors.base.background, - textForegroundColor: colors.main.onMain) - - let colors2 = SwitchColors( - toggleBackgroundColors: SwitchStatusColors( - onColorToken: colors.feedback.info, - offColorToken: colors.feedback.alert), - toggleDotForegroundColors: SwitchStatusColors( - onColorToken: colors.main.main, - offColorToken: colors.support.support), - toggleDotBackgroundColor: colors.base.background, - textForegroundColor: colors.main.onMain) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = SwitchColors( - toggleBackgroundColors: SwitchStatusColors( - onColorToken: colors.feedback.info, - offColorToken: colors.feedback.alert), - toggleDotForegroundColors: SwitchStatusColors( - onColorToken: colors.main.main, - offColorToken: colors.support.support), - toggleDotBackgroundColor: colors.base.background, - textForegroundColor: colors.main.onMain) - - let colors2 = SwitchColors( - toggleBackgroundColors: SwitchStatusColors( - onColorToken: colors.feedback.error, - offColorToken: colors.feedback.alert), - toggleDotForegroundColors: SwitchStatusColors( - onColorToken: colors.main.main, - offColorToken: colors.support.support), - toggleDotBackgroundColor: colors.base.background, - textForegroundColor: colors.main.onMain) - - XCTAssertNotEqual(colors1, colors2) - } - -} diff --git a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors+ExtensionTests.swift b/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors+ExtensionTests.swift deleted file mode 100644 index 99c50ebb6..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors+ExtensionTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// SwitchStatusColors+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 07/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension SwitchStatusColors { - - // MARK: - Properties - - static func mocked( - onColorToken: any ColorToken = ColorTokenGeneratedMock.random(), - offColorToken: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - onColorToken: onColorToken, - offColorToken: offColorToken - ) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors.swift b/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors.swift deleted file mode 100644 index 16ad84ab7..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColors.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// SwitchStatusColors.swift -// SparkCore -// -// Created by robin.lemaire on 07/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct SwitchStatusColors { - - // MARK: - Properties - - let onColorToken: any ColorToken - let offColorToken: any ColorToken -} - -// MARK: Hashable & Equatable - -extension SwitchStatusColors: Hashable, Equatable { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.onColorToken) - hasher.combine(self.offColorToken) - } - - static func == (lhs: SwitchStatusColors, rhs: SwitchStatusColors) -> Bool { - return lhs.onColorToken.equals(rhs.onColorToken) && - lhs.offColorToken.equals(rhs.offColorToken) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColorsTests.swift b/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColorsTests.swift deleted file mode 100644 index bca69684c..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Colors/SwitchStatusColorsTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SwitchStatusColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class SwitchStatusColorsTests: XCTestCase { - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = SwitchStatusColors( - onColorToken: colors.feedback.info, - offColorToken: colors.feedback.alert) - - let colors2 = SwitchStatusColors( - onColorToken: colors.feedback.info, - offColorToken: colors.feedback.alert) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = SwitchStatusColors( - onColorToken: colors.feedback.info, - offColorToken: colors.feedback.alert) - - let colors2 = SwitchStatusColors( - onColorToken: colors.main.main, - offColorToken: colors.support.support) - - XCTAssertNotEqual(colors1, colors2) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState+ExtensionTests.swift b/core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState+ExtensionTests.swift deleted file mode 100644 index 4eac36e53..000000000 --- a/core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState+ExtensionTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SwitchImagesState+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore -import UIKit - -extension SwitchImagesState { - - // MARK: - Properties - - static func mocked( - currentImage: ImageEither = .left(IconographyTests.shared.switchOn), - images: SwitchImagesEither = .left(SwitchUIImages( - on: IconographyTests.shared.switchOn, - off: IconographyTests.shared.switchOff - )), - onImageOpacity: Double = 1, - offImageOpacity: Double = 0 - ) -> Self { - return .init( - currentImage: currentImage, - images: images, - onImageOpacity: onImageOpacity, - offImageOpacity: offImageOpacity - ) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState.swift b/core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState.swift deleted file mode 100644 index 69b2a4b5a..000000000 --- a/core/Sources/Components/Switch/Model/Internal/ImagesState/SwitchImagesState.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SwitchImagesState.swift -// SparkCore -// -// Created by robin.lemaire on 12/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct SwitchImagesState: Equatable { - - // MARK: - Properties - - let currentImage: ImageEither - let images: SwitchImagesEither - let onImageOpacity: Double - var offImageOpacity: Double -} diff --git a/core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition+ExtensionTests.swift b/core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition+ExtensionTests.swift deleted file mode 100644 index 5c9001763..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SwitchPosition.swift -// SparkCore -// -// Created by robin.lemaire on 15/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension SwitchPosition { - - // MARK: - Properties - - static func mocked( - isToggleOnLeft: Bool = true, - horizontalSpacing: CGFloat = 10 - ) -> Self { - return .init( - isToggleOnLeft: isToggleOnLeft, - horizontalSpacing: horizontalSpacing - ) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition.swift b/core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition.swift deleted file mode 100644 index 81be3f45b..000000000 --- a/core/Sources/Components/Switch/Model/Internal/Position/SwitchPosition.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// SwitchPosition.swift -// SparkCore -// -// Created by robin.lemaire on 15/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct SwitchPosition: Equatable { - - // MARK: - Properties - - let isToggleOnLeft: Bool - let horizontalSpacing: CGFloat -} diff --git a/core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState+ExtensionTests.swift b/core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState+ExtensionTests.swift deleted file mode 100644 index 98d5df824..000000000 --- a/core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SwitchToggleState.swift -// SparkCore -// -// Created by robin.lemaire on 24/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension SwitchToggleState { - - // MARK: - Properties - - static func mocked( - interactionEnabled: Bool = true, - opacity: CGFloat = 1.0 - ) -> Self { - return .init( - interactionEnabled: interactionEnabled, - opacity: opacity - ) - } -} diff --git a/core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState.swift b/core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState.swift deleted file mode 100644 index 5a32fd41a..000000000 --- a/core/Sources/Components/Switch/Model/Internal/ToggleState/SwitchToggleState.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// SwitchToggleState.swift -// SparkCore -// -// Created by robin.lemaire on 24/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct SwitchToggleState: Equatable { - - // MARK: - Properties - - let interactionEnabled: Bool - let opacity: CGFloat -} diff --git a/core/Sources/Components/Switch/Model/Public/Images/SwitchImages.swift b/core/Sources/Components/Switch/Model/Public/Images/SwitchImages.swift deleted file mode 100644 index 9290da3c5..000000000 --- a/core/Sources/Components/Switch/Model/Public/Images/SwitchImages.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// SwitchImages.swift -// SparkCore -// -// Created by robin.lemaire on 03/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct SwitchImages: Equatable { - - // MARK: - Properties - - public let on: Image - public let off: Image - - // MARK: - Initialization - - public init(on: Image, off: Image) { - self.on = on - self.off = off - } -} diff --git a/core/Sources/Components/Switch/Model/Public/Images/SwitchUIImages.swift b/core/Sources/Components/Switch/Model/Public/Images/SwitchUIImages.swift deleted file mode 100644 index 1a5da6488..000000000 --- a/core/Sources/Components/Switch/Model/Public/Images/SwitchUIImages.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// SwitchUIImages.swift -// SparkCore -// -// Created by robin.lemaire on 03/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -public struct SwitchUIImages: Equatable { - - // MARK: - Properties - - public let on: UIImage - public let off: UIImage - - // MARK: - Initialization - - public init(on: UIImage, off: UIImage) { - self.on = on - self.off = off - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCase.swift b/core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCase.swift deleted file mode 100644 index 322e53ebd..000000000 --- a/core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCase.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SwitchGetColorUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol SwitchGetColorUseCaseable { - func execute(intent: SwitchIntent, - colors: Colors) -> any ColorToken -} - -struct SwitchGetColorUseCase: SwitchGetColorUseCaseable { - - // MARK: - Methods - - func execute( - intent: SwitchIntent, - colors: Colors - ) -> any ColorToken { - switch intent { - case .alert: - return colors.feedback.alert - - case .error: - return colors.feedback.error - - case .info: - return colors.feedback.info - - case .neutral: - return colors.feedback.neutral - - case .main: - return colors.main.main - - case .support: - return colors.support.support - - case .success: - return colors.feedback.success - - case .accent: - return colors.accent.accent - - case .basic: - return colors.basic.basic - } - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCaseTests.swift b/core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCaseTests.swift deleted file mode 100644 index dd64bf39b..000000000 --- a/core/Sources/Components/Switch/UseCase/GetColor/SwitchGetColorUseCaseTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// SwitchGetColorUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class SwitchGetColorUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let colorsMock = ColorsGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_when_intent_is_alert_case() throws { - try self.testExecute( - givenIntent: .alert, - expectedColorToken: self.colorsMock.feedback.alert - ) - } - - func test_execute_when_intent_is_error_case() throws { - try self.testExecute( - givenIntent: .error, - expectedColorToken: self.colorsMock.feedback.error - ) - } - - func test_execute_when_intent_is_info_case() throws { - try self.testExecute( - givenIntent: .info, - expectedColorToken: self.colorsMock.feedback.info - ) - } - - func test_execute_when_intent_is_neutral_case() throws { - try self.testExecute( - givenIntent: .neutral, - expectedColorToken: self.colorsMock.feedback.neutral - ) - } - - func test_execute_when_intent_is_main_case() throws { - try self.testExecute( - givenIntent: .main, - expectedColorToken: self.colorsMock.main.main - ) - } - - func test_execute_when_intent_is_support_case() throws { - try self.testExecute( - givenIntent: .support, - expectedColorToken: self.colorsMock.support.support - ) - } - - func test_execute_when_intent_is_success_case() throws { - try self.testExecute( - givenIntent: .success, - expectedColorToken: self.colorsMock.feedback.success - ) - } - - func test_execute_when_intent_is_accent_case() throws { - try self.testExecute( - givenIntent: .accent, - expectedColorToken: self.colorsMock.accent.accent - ) - } - - func test_execute_when_intent_is_basic_case() throws { - try self.testExecute( - givenIntent: .basic, - expectedColorToken: self.colorsMock.basic.basic - ) - } -} - -// MARK: - Execute Testing - -private extension SwitchGetColorUseCaseTests { - - func testExecute( - givenIntent: SwitchIntent, - expectedColorToken: any ColorToken - ) throws { - // GIVEN - let useCase = SwitchGetColorUseCase() - - // WHEN - let colorToken = useCase.execute( - intent: givenIntent, - colors: self.colorsMock - ) - - // THEN - XCTAssertIdentical(colorToken as? ColorTokenGeneratedMock, - expectedColorToken as? ColorTokenGeneratedMock, - "Wrong color for .\(givenIntent) case") - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCase.swift b/core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCase.swift deleted file mode 100644 index 098f7f012..000000000 --- a/core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCase.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// SwitchGetColorUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SwitchGetColorsUseCaseable { - func execute(intent: SwitchIntent, - colors: Colors, - dims: Dims) -> SwitchColors -} - -struct SwitchGetColorsUseCase: SwitchGetColorsUseCaseable { - - // MARK: - Properties - - private let getColorUseCase: any SwitchGetColorUseCaseable - - // MARK: - Initialization - - init(getColorUseCase: any SwitchGetColorUseCaseable = SwitchGetColorUseCase()) { - self.getColorUseCase = getColorUseCase - } - - // MARK: - Methods - - func execute( - intent: SwitchIntent, - colors: Colors, - dims: Dims - ) -> SwitchColors { - - // Get color from use case - let color = self.getColorUseCase.execute( - intent: intent, - colors: colors - ) - - let statusAndStateColors = SwitchStatusColors( - onColorToken: color, - offColorToken: colors.base.onSurface.opacity(dims.dim4) - ) - - return .init( - toggleBackgroundColors: statusAndStateColors, - toggleDotForegroundColors: statusAndStateColors, - toggleDotBackgroundColor: colors.base.surface, - textForegroundColor: colors.base.onSurface - ) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCaseTests.swift b/core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCaseTests.swift deleted file mode 100644 index b3eec220b..000000000 --- a/core/Sources/Components/Switch/UseCase/GetColors/SwitchGetColorsUseCaseTests.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// SwitchGetColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class SwitchGetColorsUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute() throws { - // GIVEN - let intentMock = SwitchIntent.alert - let colorTokenMock = ColorTokenGeneratedMock.random() - - let colorsMock = ColorsGeneratedMock.mocked() - let dimsMock = DimsGeneratedMock.mocked() - - let getColorUseCaseMock = SwitchGetColorUseCaseableGeneratedMock() - getColorUseCaseMock.executeWithIntentAndColorsReturnValue = colorTokenMock - - let expectedOnColorToken = colorTokenMock - let expectedOffColorToken = colorsMock.base.onSurface.opacity(dimsMock.dim4) - - let useCase = SwitchGetColorsUseCase(getColorUseCase: getColorUseCaseMock) - - // WHEN - - let colors = useCase.execute( - intent: intentMock, - colors: colorsMock, - dims: dimsMock - ) - - // ** - // Status background colors - XCTAssertEqual( - colors.toggleBackgroundColors.onColorToken.hashValue, - expectedOnColorToken.hashValue, - "Wrong onColorToken on toggleBackgroundColors" - ) - XCTAssertEqual( - colors.toggleBackgroundColors.offColorToken.hashValue, - expectedOffColorToken.hashValue, - "Wrong offColorToken on toggleBackgroundColors" - ) - // ** - - XCTAssertEqual( - colors.toggleDotBackgroundColor.hashValue, - colorsMock.base.surface.hashValue, - "Wrong toggleBackgroundColor color" - ) - - // ** - // State foreground colors - XCTAssertEqual( - colors.toggleDotForegroundColors.onColorToken.hashValue, - expectedOnColorToken.hashValue, - "Wrong onColorToken on toggleDotForegroundColors" - ) - XCTAssertEqual( - colors.toggleDotForegroundColors.offColorToken.hashValue, - expectedOffColorToken.hashValue, - "Wrong offColorToken on toggleDotForegroundColors" - ) - // ** - - XCTAssertEqual( - colors.textForegroundColor.hashValue, - colorsMock.base.onSurface.hashValue, - "Wrong textForegroundColor color" - ) - - // ** - // GetColorUseCase - let getColorUseCaseArgs = getColorUseCaseMock.executeWithIntentAndColorsReceivedArguments - XCTAssertEqual( - getColorUseCaseMock.executeWithIntentAndColorsCallsCount, - 1, - "Wrong call number on execute on getColorUseCase" - ) - XCTAssertEqual( - getColorUseCaseArgs?.intent, - intentMock, - "Wrong intent parameter on execute on getColorUseCaseMock" - ) - XCTAssertIdentical( - getColorUseCaseArgs?.colors as? ColorsGeneratedMock, - colorsMock, - "Wrong colors parameter on execute on getColorUseCaseMock" - ) - // ** - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCase.swift b/core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCase.swift deleted file mode 100644 index 17baf0a5f..000000000 --- a/core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCase.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SwitchGetImagesStateUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SwitchGetImagesStateUseCaseable { - func execute(isOn: Bool, - images: SwitchImagesEither) -> SwitchImagesState -} - -struct SwitchGetImagesStateUseCase: SwitchGetImagesStateUseCaseable { - - // MARK: - Methods - - func execute( - isOn: Bool, - images: SwitchImagesEither - ) -> SwitchImagesState { - let currentImage: ImageEither - switch images { - case .left(let images): - currentImage = .left(isOn ? images.on : images.off) - case .right(let images): - currentImage = .right(isOn ? images.on : images.off) - } - - return .init( - currentImage: currentImage, - images: images, - onImageOpacity: isOn ? 1 : 0, - offImageOpacity: isOn ? 0 : 1 - ) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCaseTests.swift b/core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCaseTests.swift deleted file mode 100644 index 4d37df8ea..000000000 --- a/core/Sources/Components/Switch/UseCase/GetImagesState/SwitchGetImagesStateUseCaseTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// SwitchGetImagesStateUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class SwitchGetImagesStateUseCaseTests: XCTestCase { - - // MARK: - SwiftUI Properties - - private let onImageMock = Image("switchOn") - private let offImageMock = Image("switchOff") - - private lazy var imagesMock: SwitchImagesEither = { - return .right( - .init( - on: self.onImageMock, - off: self.offImageMock - ) - ) - }() - - // MARK: - UIKit Properties - - private let onUIImageMock = IconographyTests.shared.switchOn - private let offUIImageMock = IconographyTests.shared.switchOff - - private lazy var uiImagesMock: SwitchImagesEither = { - return .left( - .init( - on: self.onUIImageMock, - off: self.offUIImageMock - ) - ) - }() - - // MARK: - UIKit Images Tests - - func test_execute_when_isOn_is_true_and_is_UIKit_images() throws { - try self.testExecute( - givenIsOn: true, - givenImages: self.uiImagesMock, - givenIsSwiftUIVersion: false, - expectedImage: .left(self.onUIImageMock) - ) - } - - func test_execute_when_isOn_is_false_and_is_UIKit_images() throws { - try self.testExecute( - givenIsOn: false, - givenImages: self.uiImagesMock, - givenIsSwiftUIVersion: false, - expectedImage: .left(self.offUIImageMock) - ) - } - - // MARK: - Swift Images Tests - - func test_execute_when_isOn_is_true_and_is_SwiftUI_images() throws { - try self.testExecute( - givenIsOn: true, - givenImages: self.imagesMock, - givenIsSwiftUIVersion: true, - expectedImage: .right(self.onImageMock) - ) - } - - func test_execute_when_isOn_is_false_and_is_SwiftUI_images() throws { - try self.testExecute( - givenIsOn: false, - givenImages: self.imagesMock, - givenIsSwiftUIVersion: true, - expectedImage: .right(self.offImageMock) - ) - } -} - -// MARK: - Execute Testing - -private extension SwitchGetImagesStateUseCaseTests { - - func testExecute( - givenIsOn: Bool, - givenImages: SwitchImagesEither, - givenIsSwiftUIVersion: Bool, - expectedImage: ImageEither - ) throws { - // GIVEN - let errorPrefixMessage = " for \(givenIsOn) isOn" - - let useCase = SwitchGetImagesStateUseCase() - - // WHEN - let imageState = useCase.execute( - isOn: givenIsOn, - images: givenImages - ) - - // THEN - if givenIsSwiftUIVersion { - if givenIsOn { - XCTAssertEqual( - imageState.currentImage.rightValue, - givenImages.rightValue.on, - "Wrong on image" + errorPrefixMessage - ) - } else { - XCTAssertEqual( - imageState.currentImage.rightValue, - givenImages.rightValue.off, - "Wrong off image" + errorPrefixMessage - ) - } - } else { - if givenIsOn { - XCTAssertEqual( - imageState.currentImage.leftValue, - givenImages.leftValue.on, - "Wrong on UIImage" + errorPrefixMessage - ) - } else { - XCTAssertEqual( - imageState.currentImage.leftValue, - givenImages.leftValue.off, - "Wrong off UIImage" + errorPrefixMessage - ) - } - } - - XCTAssertEqual( - imageState.images, - givenImages, - "Wrong on images" + errorPrefixMessage - ) - - XCTAssertEqual( - imageState.onImageOpacity, - givenIsOn ? 1 : 0, - "Wrong on onImageOpacity" + errorPrefixMessage - ) - XCTAssertEqual( - imageState.offImageOpacity, - givenIsOn ? 0 : 1, - "Wrong on images" + errorPrefixMessage - ) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCase.swift b/core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCase.swift deleted file mode 100644 index 90fb32cf2..000000000 --- a/core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCase.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SwitchGetPositionUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SwitchGetPositionUseCaseable { - func execute(alignment: SwitchAlignment, - spacing: LayoutSpacing, - containsText: Bool) -> SwitchPosition -} - -struct SwitchGetPositionUseCase: SwitchGetPositionUseCaseable { - - // MARK: - Methods - - func execute( - alignment: SwitchAlignment, - spacing: LayoutSpacing, - containsText: Bool - ) -> SwitchPosition { - var horizontalSpacing: CGFloat - let isToggleOnLeft: Bool - switch alignment { - case .left: - horizontalSpacing = spacing.medium - isToggleOnLeft = true - - case .right: - horizontalSpacing = spacing.xxxLarge - isToggleOnLeft = false - } - - // No text ? No space ! - if !containsText { - horizontalSpacing = 0 - } - - return .init( - isToggleOnLeft: isToggleOnLeft, - horizontalSpacing: horizontalSpacing - ) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCaseTests.swift b/core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCaseTests.swift deleted file mode 100644 index 2712fa41a..000000000 --- a/core/Sources/Components/Switch/UseCase/GetPosition/SwitchGetPositionUseCaseTests.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// SwitchGetPositionUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class SwitchGetPositionUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let spacingMock = LayoutSpacingGeneratedMock.mocked() - - // MARK: - Alignment Tests - - func test_execute_when_switchAlignment_is_left_case() throws { - try self.testExecute( - givenAlignment: .left, - givenContainsText: true, - expectedPosition: .init( - isToggleOnLeft: true, - horizontalSpacing: self.spacingMock.medium - ) - ) - } - - func test_execute_when_switchAlignment_is_right_case() throws { - try self.testExecute( - givenAlignment: .left, - givenContainsText: true, - expectedPosition: .init( - isToggleOnLeft: true, - horizontalSpacing: self.spacingMock.medium - ) - ) - } - - // MARK: - Contains Text Tests - - func test_execute_when_containsText_is_false() throws { - try self.testExecute( - givenAlignment: .left, - givenContainsText: false, - expectedPosition: .init( - isToggleOnLeft: true, - horizontalSpacing: 0 - ) - ) - } -} - -// MARK: - Execute Testing - -private extension SwitchGetPositionUseCaseTests { - - func testExecute( - givenAlignment: SwitchAlignment, - givenContainsText: Bool, - expectedPosition: SwitchPosition - ) throws { - // GIVEN - let errorPrefixMessage = " for .\(givenAlignment) alignment case - containsText = .\(givenContainsText)" - - let useCase = SwitchGetPositionUseCase() - - // WHEN - let position = useCase.execute( - alignment: givenAlignment, - spacing: self.spacingMock, - containsText: givenContainsText - ) - - // THEN - XCTAssertEqual(position.isToggleOnLeft, - expectedPosition.isToggleOnLeft, - "Wrong isToggleOnLeft position" + errorPrefixMessage) - XCTAssertEqual(position.horizontalSpacing, - expectedPosition.horizontalSpacing, - "Wrong horizontalSpacing position" + errorPrefixMessage) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCase.swift b/core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCase.swift deleted file mode 100644 index 1e8a205a9..000000000 --- a/core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCase.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SwitchGetToggleColorUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 23/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SwitchGetToggleColorUseCaseable { - func execute(isOn: Bool, - statusAndStateColor: SwitchStatusColors) -> any ColorToken -} - -struct SwitchGetToggleColorUseCase: SwitchGetToggleColorUseCaseable { - - // MARK: - Methods - - func execute( - isOn: Bool, - statusAndStateColor: SwitchStatusColors - ) -> any ColorToken { - return isOn ? statusAndStateColor.onColorToken : statusAndStateColor.offColorToken - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCaseTests.swift b/core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCaseTests.swift deleted file mode 100644 index c325e7784..000000000 --- a/core/Sources/Components/Switch/UseCase/GetToggleColor/SwitchGetToggleColorUseCaseTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SwitchGetToggleColorUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 23/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class SwitchGetToggleColorUseCaseTests: XCTestCase { - - // MARK: - Properties - - private var statusAndStateColorMock = SwitchStatusColors.mocked() - - // MARK: - Tests - - func test_execute_when_isOn_is_true() throws { - try self.testExecute( - givenIsOn: true, - expectedColorToken: self.statusAndStateColorMock.onColorToken - ) - } - - func test_execute_when_isOn_is_false() throws { - try self.testExecute( - givenIsOn: false, - expectedColorToken: self.statusAndStateColorMock.offColorToken - ) - } -} - -// MARK: - Execute Testing - -private extension SwitchGetToggleColorUseCaseTests { - - func testExecute( - givenIsOn: Bool, - expectedColorToken: any ColorToken - ) throws { - // GIVEN - let errorPrefixMessage = " for \(givenIsOn) isOn" - - let useCase = SwitchGetToggleColorUseCase() - - // WHEN - let colorToken = useCase.execute( - isOn: givenIsOn, - statusAndStateColor: statusAndStateColorMock - ) - - // THEN - XCTAssertEqual(colorToken as? ColorTokenGeneratedMock, - expectedColorToken as? ColorTokenGeneratedMock, - "Wrong interactionEnabled" + errorPrefixMessage) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCase.swift b/core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCase.swift deleted file mode 100644 index 68c85567b..000000000 --- a/core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCase.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SwitchGetToggleStateUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SwitchGetToggleStateUseCaseable { - func execute(isEnabled: Bool, - dims: Dims) -> SwitchToggleState -} - -struct SwitchGetToggleStateUseCase: SwitchGetToggleStateUseCaseable { - - // MARK: - Methods - - func execute( - isEnabled: Bool, - dims: Dims - ) -> SwitchToggleState { - let opacity = isEnabled ? dims.none : dims.dim3 - - return .init( - interactionEnabled: isEnabled, - opacity: opacity - ) - } -} diff --git a/core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCaseTests.swift b/core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCaseTests.swift deleted file mode 100644 index 9afa85e17..000000000 --- a/core/Sources/Components/Switch/UseCase/GetToggleState/SwitchGetToggleStateUseCaseTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// SwitchGetToggleStateUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class SwitchGetToggleStateUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let dimsMock = DimsGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_when_isEnabled_is_true() throws { - try self.testExecute( - givenIsEnabled: true, - expectedInteractionState: .init(interactionEnabled: true, opacity: self.dimsMock.none) - ) - } - - func test_execute_when_isEnabled_is_false() throws { - try self.testExecute( - givenIsEnabled: false, - expectedInteractionState: .init(interactionEnabled: false, opacity: self.dimsMock.dim3) - ) - } -} - -// MARK: - Execute Testing - -private extension SwitchGetToggleStateUseCaseTests { - - func testExecute( - givenIsEnabled: Bool, - expectedInteractionState: SwitchToggleState - ) throws { - // GIVEN - let errorPrefixMessage = " for \(givenIsEnabled) givenIsEnabled" - - let useCase = SwitchGetToggleStateUseCase() - - // GIVEN - let interactionState = useCase.execute( - isEnabled: givenIsEnabled, - dims: self.dimsMock - ) - - // THEN - XCTAssertEqual(interactionState.interactionEnabled, - expectedInteractionState.interactionEnabled, - "Wrong interactionEnabled" + errorPrefixMessage) - XCTAssertEqual(interactionState.opacity, - expectedInteractionState.opacity, - "Wrong opacity" + errorPrefixMessage) - } -} diff --git a/core/Sources/Components/Switch/View/Common/SwitchSutSnapshotTests.swift b/core/Sources/Components/Switch/View/Common/SwitchSutSnapshotTests.swift deleted file mode 100644 index 770fd6308..000000000 --- a/core/Sources/Components/Switch/View/Common/SwitchSutSnapshotTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// SwitchSutSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 14/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI -import XCTest - -struct SwitchSutSnapshotTests { - - // MARK: - Type Alias - - typealias SwitchAttributedStringEither = Either - - // MARK: - Properties - - let intent: SwitchIntent - let isOn: Bool - let alignment: SwitchAlignment - let isEnabled: Bool - let images: SwitchImagesEither? - let text: String? - let attributedText: SwitchAttributedStringEither? - - // MARK: - Getter - - func testName(on function: String = #function) -> String { - return [ - function, - "\(self.intent)", - self.isOn ? "isOn" : "isOff", - "\(self.alignment)" + "Aligment", - self.isEnabled ? "isEnabled" : "isDisabled", - self.images != nil ? "withImages" : "withoutImages", - self.text != nil ? "withText" : "withoutText", - self.attributedText != nil ? "withAttributedText" : "withoutAttributedText", - ].joined(separator: "-") - } - - // MARK: - Cases - - /// Test all colors for all intent, isOn and IsEnabled cases - static func allColorsCases(isSwiftUIComponent: Bool) throws -> [Self] { - let intentPossibilities = SwitchIntent.allCases - let isOnPossibilities = Bool.allCases - let isEnabledPossibilities = Bool.allCases - - return intentPossibilities.flatMap { intent in - isOnPossibilities.flatMap { isOn in - isEnabledPossibilities.map { isEnabled -> SwitchSutSnapshotTests in - .init( - intent: intent, - isOn: isOn, - alignment: .left, - isEnabled: isEnabled, - images: nil, - text: "My Color Switch", - attributedText: nil - ) - } - } - } - } - - /// Test all contents for all images, text and attributedText cases - static func allContentsCases(isSwiftUIComponent: Bool) throws -> [Self] { - typealias ContentCases = (images: SwitchImagesEither?, text: String?, attributedText: SwitchAttributedStringEither?) - - let images: SwitchImagesEither = try isSwiftUIComponent ? .right(Image.images) : .left(UIImage.images) - - let attributedText: SwitchAttributedStringEither = isSwiftUIComponent ? .right(AttributedString("My Attributed Switch")) : .left(.init(string: "My Attributed Switch")) - - let items: [ContentCases] = [ - (images: images, text: "My Full Content Switch", attributedText: nil), // Images + text - (images: nil, text: "My Content Switch", attributedText: nil), // Only text - (images: images, text: nil, attributedText: attributedText), // Images + attributed text - (images: nil, text: nil, attributedText: attributedText), // Only attributed text - (images: nil, text: nil, attributedText: nil) // Nothing - ] - - return items.map { item -> SwitchSutSnapshotTests in - .init( - intent: .main, - isOn: true, - alignment: .left, - isEnabled: true, - images: item.images, - text: item.text, - attributedText: item.attributedText - ) - } - } - - /// Test all positions for all alignment cases - static func allPositionsCases(isSwiftUIComponent: Bool) throws -> [Self] { - let alignmentPossibilities = SwitchAlignment.allCases - let isMultilineTextPossibilities = Bool.allCases - - return alignmentPossibilities.flatMap { alignment in - isMultilineTextPossibilities.map { isMultilineText -> SwitchSutSnapshotTests in - .init( - intent: .main, - isOn: true, - alignment: alignment, - isEnabled: true, - images: nil, - text: isMultilineText ? "Multiline switch.\nMore text.\nAnd more text." : "My Text", - attributedText: nil - ) - } - } - } -} - -// MARK: - Private Extensions - -private extension Image { - - static let images: SwitchImages = .init( - on: Image("switchOn"), - off: Image("switchOff") - ) -} - -private extension UIImage { - - static var images: SwitchUIImages { - get throws { - .init( - on: IconographyTests.shared.switchOn, - off: IconographyTests.shared.switchOff - ) - } - } -} diff --git a/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift b/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift deleted file mode 100644 index 340abb0c3..000000000 --- a/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// SwitchSubviewType.swift -// SparkCore -// -// Created by robin.lemaire on 11/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum SwitchSubviewType { - case space - case text - case toggle - - // MARK: - Methods - - static func allCases(isLeftAlignment: Bool, showSpace: Bool) -> [Self] { - var cases: [Self] - - // Left or right alignement ? - if isLeftAlignment { - cases = [.toggle, .text] - } else { - cases = [.text, .toggle] - } - - // Add spaces ? - if showSpace { - cases.insert(.space, at: 1) - } - - return cases - } -} diff --git a/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift b/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift deleted file mode 100644 index bc5c62775..000000000 --- a/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// SwitchSubviewTypeTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 14/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class SwitchSubviewTypeTests: XCTestCase { - - // MARK: - Tests - - func test_allCases_when_isLeftAlignment_is_true_and_showSpace_is_false() { - // GIVEN / WHEN - let allCases = SwitchSubviewType.allCases( - isLeftAlignment: true, - showSpace: false - ) - - // THEN - XCTAssertEqual( - allCases, - [.toggle, .text] - ) - } - - func test_allCases_when_isLeftAlignment_is_true_and_showSpace_is_true() { - // GIVEN / WHEN - let allCases = SwitchSubviewType.allCases( - isLeftAlignment: true, - showSpace: true - ) - - // THEN - XCTAssertEqual( - allCases, - [.toggle, .space, .text] - ) - } - - func test_allCases_when_isLeftAlignment_is_false_and_showSpace_is_false() { - // GIVEN / WHEN - let allCases = SwitchSubviewType.allCases( - isLeftAlignment: false, - showSpace: false - ) - - // THEN - XCTAssertEqual( - allCases, - [.text, .toggle] - ) - } - - func test_allCases_when_isLeftAlignment_is_false_and_showSpace_is_true() { - // GIVEN / WHEN - let allCases = SwitchSubviewType.allCases( - isLeftAlignment: false, - showSpace: true - ) - - // THEN - XCTAssertEqual( - allCases, - [.text, .space, .toggle] - ) - } -} diff --git a/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift b/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift deleted file mode 100644 index 4a10d2336..000000000 --- a/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift +++ /dev/null @@ -1,289 +0,0 @@ -// -// SwitchView.swift -// SparkCore -// -// Created by robin.lemaire on 11/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct SwitchView: View { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = SwitchAccessibilityIdentifier - private typealias Constants = SwitchConstants - - // MARK: - Properties - - @ObservedObject private var viewModel: SwitchViewModel - - @Binding private var isOn: Bool - - @ScaledMetric private var contentStackViewSpacingMultiplier: CGFloat = .scaledMetricMultiplier - @ScaledMetric private var toggleHeight: CGFloat = Constants.ToggleSizes.height - @ScaledMetric private var toggleWidth: CGFloat = Constants.ToggleSizes.width - @ScaledMetric private var togglePadding: CGFloat = Constants.ToggleSizes.padding - @ScaledMetric private var toggleDotPadding: CGFloat = Constants.toggleDotImagePadding - - // MARK: - Initialization - - /// Initialize a new switch view - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - intent: The intent of the switch. - /// - alignment: The alignment of the switch. - /// - isOn: The Binding value of the switch. - public init( - theme: any Theme, - intent: SwitchIntent, - alignment: SwitchAlignment, - isOn: Binding - ) { - self.viewModel = .init( - for: .swiftUI, - theme: theme, - isOn: isOn.wrappedValue, - alignment: alignment, - intent: intent, - isEnabled: true, - images: nil, - text: nil, - attributedText: nil - ) - self._isOn = isOn - } - - // MARK: - View - - public var body: some View { - HStack(alignment: .top) { - ForEach(self.subviewsTypes(), id: \.self) { - self.makeSubview(from: $0) - } - } - .onChange(of: self.viewModel.isOnChanged) { isOn in - guard let isOn else { return } - self.isOn = isOn - } - .isEnabledChanged { isEnabled in - self.viewModel.set(isEnabled: isEnabled) - } - .disabled(!self.viewModel.isEnabled) - } - - // MARK: - Subview Maker - - private func subviewsTypes() -> [SwitchSubviewType] { - return SwitchSubviewType.allCases( - isLeftAlignment: self.viewModel.isToggleOnLeft == true, - showSpace: self.viewModel.horizontalSpacing ?? 0 > 0 - ) - } - - @ViewBuilder - private func makeSubview(from type: SwitchSubviewType) -> some View { - switch type { - case .space: - self.space() - case .text: - self.text() - case .toggle: - self.toggle() - } - } - - // MARK: - Subview Builder - - @ViewBuilder - private func text() -> some View { - if let text = self.viewModel.displayedText?.text { - Text(text) - .font(self.viewModel.textFontToken?.font) - .foregroundColor(self.viewModel.textForegroundColorToken?.color ?? .clear) - .applyStyle() - } else if let attributedText = self.viewModel.displayedText?.attributedText?.rightValue { - Text(attributedText) - .applyStyle() - } - } - - @ViewBuilder - private func space() -> some View { - Spacer() - .frame( - width: self.viewModel.horizontalSpacing.scaledMetric( - with: self.contentStackViewSpacingMultiplier - ) - ) - } - - @ViewBuilder - private func toggle() -> some View { - ZStack { - RoundedRectangle( - cornerRadius: self.viewModel.theme.border.radius.full - ) - .fill(self.viewModel.toggleBackgroundColorToken?.color ?? .clear) - .opacity(self.viewModel.toggleOpacity ?? .zero) - - HStack { - // Left Space - if let showSpace = self.viewModel.showToggleLeftSpace, - showSpace { - Spacer() - } - ZStack { - // Dot - Circle() - .fill(self.viewModel.toggleDotBackgroundColorToken?.color ?? .clear) - .aspectRatio(1, contentMode: .fit) - .accessibilityIdentifier(AccessibilityIdentifier.toggleDotView) - - ZStack { - // On icon - self.viewModel.toggleDotImagesState?.images.rightValue.on - .applyStyle( - isForOnImage: true, - viewModel: self.viewModel - ) - - // Off icon - self.viewModel.toggleDotImagesState?.images.rightValue.off - .applyStyle( - isForOnImage: false, - viewModel: self.viewModel - ) - } - .opacity(self.viewModel.toggleOpacity ?? .zero) - .padding(.init( - all: self.toggleDotPadding - )) - .animation( - .custom, - value: self.viewModel.toggleDotImagesState - ) - } - - // Right Space - if let showSpace = self.viewModel.showToggleLeftSpace, - !showSpace { - Spacer() - } - } - .padding(.init( - all: self.togglePadding - )) - .animation( - .custom, - value: self.viewModel.showToggleLeftSpace - ) - } - .frame( - width: self.toggleWidth, - height: self.toggleHeight - ) - .accessibilityAddTraits(self.getAccessibilityTraits()) - .accessibilityIdentifier(AccessibilityIdentifier.toggleView) - .accessibilityValue(isOn ? "1" : "0") - .accessibilityRepresentation { - Toggle(isOn: $isOn) { } - } - .onTapGesture { - withAnimation(.custom) { - self.viewModel.toggle() - } - } - } - - private func getAccessibilityTraits() -> AccessibilityTraits { - var traits: AccessibilityTraits = [.isButton] - if #available(iOS 17, *) { - _ = traits.insert(.isToggle) - } - return traits - } - - // MARK: - Modifier - - /// Set disabled - /// - Parameters: - /// - isDisabled: true = disabled, false = enabled - public func disabled(_ isDisabled: Bool) -> Self { - self.viewModel.set(isEnabled: !isDisabled) - return self - } - - /// Set the images on switch. - /// - Parameters: - /// - images: The optional images of the switch. - /// - Returns: Current Switch View. - public func images(_ images: SwitchImages?) -> Self { - self.viewModel.set(images: images.map { .right($0) }) - return self - } - - /// Set the text of the switch. - /// - Parameters: - /// - text: The optional text of the switch. - /// - Returns: Current Switch View. - public func text(_ text: String?) -> Self { - self.viewModel.set(text: text) - return self - } - - /// Set the attributed text of the switch. - /// - Parameters: - /// - text: The optional attributed text of the switch. - /// - Returns: Current Switch View. - public func attributedText(_ attributedText: AttributedString?) -> Self { - self.viewModel.set(attributedText: attributedText.map { .right($0) }) - return self - } -} - -// MARK: - Extension - -private extension Text { - - func applyStyle() -> some View { - return self.frame( - maxHeight: .infinity, - alignment: .center - ) - .accessibilityIdentifier(SwitchAccessibilityIdentifier.text) - } -} - -private extension Image { - - @ViewBuilder - func applyStyle( - isForOnImage: Bool, - viewModel: SwitchViewModel - ) -> some View { - if isForOnImage { - self.applyImageStyle(viewModel: viewModel) - .opacity(viewModel.toggleDotImagesState?.onImageOpacity ?? 0) - } else { - self.applyImageStyle(viewModel: viewModel) - .opacity(viewModel.toggleDotImagesState?.offImageOpacity ?? 0) - } - } - - func applyImageStyle(viewModel: SwitchViewModel) -> some View { - self.resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(viewModel.toggleDotForegroundColorToken?.color) - .accessibilityIdentifier(SwitchAccessibilityIdentifier.toggleDotImageView) - - } -} - -private extension Animation { - - static var custom: Animation { - return Animation.easeOut(duration: SwitchConstants.animationDuration) - } -} diff --git a/core/Sources/Components/Switch/View/SwiftUI/SwitchViewSnapshotTests.swift b/core/Sources/Components/Switch/View/SwiftUI/SwitchViewSnapshotTests.swift deleted file mode 100644 index 643f4263c..000000000 --- a/core/Sources/Components/Switch/View/SwiftUI/SwitchViewSnapshotTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// SwitchViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 14/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -import SwiftUI - -@testable import SparkCore - -final class SwitchViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test_swiftUI_switch_colors() throws { - let suts = try SwitchSutSnapshotTests.allColorsCases(isSwiftUIComponent: true) - self.test(suts: suts) - } - - func test_swiftUI_switch_contens() throws { - let suts = try SwitchSutSnapshotTests.allContentsCases(isSwiftUIComponent: true) - self.test(suts: suts) - } - - func test_swiftUI_switch_positions() throws { - let suts = try SwitchSutSnapshotTests.allPositionsCases(isSwiftUIComponent: true) - self.test(suts: suts) - } -} - -// MARK: - Testing - -private extension SwitchViewSnapshotTests { - - func test(suts: [SwitchSutSnapshotTests], function: String = #function) { - for sut in suts { - var view = SwitchView( - theme: self.theme, - intent: sut.intent, - alignment: sut.alignment, - isOn: .constant(sut.isOn) - ) - .disabled(!sut.isEnabled) - - // Images + Text - if let images = sut.images, let text = sut.text { - view = view - .images(images.rightValue) - .text(text) - } else if let images = sut.images, let attributedText = sut.attributedText { // Images + Attributed Text - view = view - .images(images.rightValue) - .attributedText(attributedText.rightValue) - } else if let text = sut.text { // Only Text - view = view - .text(text) - } else if let attributedText = sut.attributedText { // Only Attributed Text - view = view - .attributedText(attributedText.rightValue) - } - - self.assertSnapshotInDarkAndLight( - matching: view - .background(self.theme.colors.base.background.color) - .fixedSize(), - testName: sut.testName(on: function) - ) - } - } -} diff --git a/core/Sources/Components/Switch/View/UIKit/SwitchUIView.swift b/core/Sources/Components/Switch/View/UIKit/SwitchUIView.swift deleted file mode 100644 index 9e03630a0..000000000 --- a/core/Sources/Components/Switch/View/UIKit/SwitchUIView.swift +++ /dev/null @@ -1,903 +0,0 @@ -// -// SwitchUIView.swift -// SparkCore -// -// Created by robin.lemaire on 12/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine -import SwiftUI - -// TODO: Add Pressed state (rounded rect) - -/// The delegate for the UIKit switch. -public protocol SwitchUIViewDelegate: AnyObject { - /// When isOn value is changed - /// - Parameters: - /// - switchView: The current switch view. - /// - isOn: The current value of the switch. - func switchDidChange(_ switchView: SwitchUIView, isOn: Bool) -} - -/// The UIKit version for the switch. -public final class SwitchUIView: UIView { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = SwitchAccessibilityIdentifier - private typealias Constants = SwitchConstants - - // MARK: - Components - - private lazy var contentStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - self.toggleView, - self.textLabel - ] - ) - stackView.axis = .horizontal - stackView.alignment = .firstBaseline - stackView.isBaselineRelativeArrangement = true - - return stackView - }() - - private lazy var toggleView: UIView = { - let view = UIView() - view.accessibilityTraits = [.button] - if #available(iOS 17.0, *) { - view.accessibilityTraits.insert(.toggleButton) - } - view.isAccessibilityElement = true - view.accessibilityIdentifier = AccessibilityIdentifier.toggleView - view.addSubview(self.toggleContentStackView) - view.isUserInteractionEnabled = true - view.setContentCompressionResistancePriority(.required, - for: .vertical) - view.setContentCompressionResistancePriority(.required, - for: .horizontal) - return view - }() - - private lazy var toggleContentStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - self.toggleLeftSpaceView, - self.toggleDotView, - self.toggleRightSpaceView - ] - ) - stackView.axis = .horizontal - stackView.isUserInteractionEnabled = false - return stackView - }() - - private var toggleLeftSpaceView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - private lazy var toggleDotView: UIView = { - let view = UIView() - view.accessibilityIdentifier = AccessibilityIdentifier.toggleDotView - view.addSubview(self.toggleDotImageView) - return view - }() - - private var toggleDotImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.accessibilityIdentifier = AccessibilityIdentifier.toggleDotImageView - return imageView - }() - - private var toggleRightSpaceView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - private var textLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - label.textAlignment = .left - label.adjustsFontForContentSizeCategory = true - label.accessibilityIdentifier = AccessibilityIdentifier.text - label.setContentCompressionResistancePriority(.required, - for: .vertical) - return label - }() - - // MARK: - Public Properties - - /// The delegate used to notify about some changed on switch. - public weak var delegate: SwitchUIViewDelegate? - - private let isOnChangedSubject = PassthroughSubject() - /// The publisher used to notify when isOn value changed on switch. - public private(set) lazy var isOnChangedPublisher: AnyPublisher = self.isOnChangedSubject.eraseToAnyPublisher() - - /// The spark theme of the switch. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.set(theme: newValue) - } - } - - /// The value of the switch (retrieve and set without animation). - public var isOn: Bool { - get { - return self.viewModel.isOn - } - set { - self.isOnAnimated = false - self.viewModel.set(isOn: newValue) - } - } - - /// The alignment of the switch. - public var alignment: SwitchAlignment { - get { - return self.viewModel.alignment - } - set { - self.viewModel.set(alignment: newValue) - } - } - - /// The intent of the switch. - public var intent: SwitchIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.set(intent: newValue) - } - } - - /// The state of the switch: enabled or not (retrieve and set without animation). . - public var isEnabled: Bool { - get { - return self.viewModel.isEnabled - } - set { - self.isEnabledAnimated = false - self.viewModel.set(isEnabled: newValue) - } - } - - /// The images of the switch. - public var images: SwitchUIImages? { - get { - return self.viewModel.images?.leftValue - } - set { - self.viewModel.set(images: newValue.map { .left($0) }) - } - } - - /// The text of the switch. - public var text: String? { - get { - return self.textLabel.text - } - set { - self.textLabel.text = newValue - self.viewModel.set(text: newValue) - } - } - - /// The attributed text of the switch. - public var attributedText: NSAttributedString? { - get { - return self.textLabel.attributedText - } - set { - self.textLabel.attributedText = newValue - self.viewModel.set(attributedText: newValue.map { .left($0) }) - } - } - - // MARK: - Private Properties - - private let viewModel: SwitchViewModel - - private var toggleWidthConstraint: NSLayoutConstraint? - private var toggleHeightConstraint: NSLayoutConstraint? - - private var toggleContentLeadingConstraint: NSLayoutConstraint? - private var toggleContentTopConstraint: NSLayoutConstraint? - private var toggleContentTrailingConstraint: NSLayoutConstraint? - private var toggleContentBottomConstraint: NSLayoutConstraint? - - private var toggleDotLeadingConstraint: NSLayoutConstraint? - private var toggleDotTopConstraint: NSLayoutConstraint? - private var toggleDotTrailingConstraint: NSLayoutConstraint? - private var toggleDotBottomConstraint: NSLayoutConstraint? - - @ScaledUIMetric private var contentStackViewSpacing: CGFloat = .zero - @ScaledUIMetric private var toggleHeight: CGFloat = Constants.ToggleSizes.height - @ScaledUIMetric private var toggleWidth: CGFloat = Constants.ToggleSizes.width - @ScaledUIMetric private var toggleSpacing: CGFloat = Constants.ToggleSizes.padding - @ScaledUIMetric private var toggleDotSpacing: CGFloat = Constants.toggleDotImagePadding - - private var isEnabledAnimated: Bool = false - private var isOnAnimated: Bool = false - - private var subscriptions = Set() - - // MARK: - Initialization - - /// Initialize a new switch view without images and text. - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - isOn: The value of the switch. - /// - alignment: The alignment of the switch. - /// - intent: The intent of the switch. - /// - isEnabled: The state of the switch: enabled or not. - public convenience init( - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool - ) { - self.init( - theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: nil, - text: nil, - attributedText: nil - ) - } - - /// Initialize a new switch view with images and without text. - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - isOn: The value of the switch. - /// - alignment: The alignment of the switch. - /// - intent: The intent of the switch. - /// - isEnabled: The state of the switch: enabled or not. - /// - images: The images of the switch. - public convenience init( - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - images: SwitchUIImages - ) { - self.init( - theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: images, - text: nil, - attributedText: nil - ) - } - - /// Initialize a new switch view without images and with text. - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - isOn: The value of the switch. - /// - alignment: The alignment of the switch. - /// - intent: The intent of the switch. - /// - isEnabled: The state of the switch: enabled or not. - /// - text: The text of the switch. - public convenience init( - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - text: String - ) { - self.init( - theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: nil, - text: text, - attributedText: nil - ) - } - - /// Initialize a new switch view without images and with attributedText. - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - isOn: The value of the switch. - /// - alignment: The alignment of the switch. - /// - intent: The intent of the switch. - /// - isEnabled: The state of the switch: enabled or not. - /// - attributedText: The attributed text of the switch. - public convenience init( - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - attributedText: NSAttributedString - ) { - self.init( - theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: nil, - text: nil, - attributedText: attributedText - ) - } - - /// Initialize a new switch view with images and text. - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - isOn: The value of the switch. - /// - alignment: The alignment of the switch. - /// - intent: The intent of the switch. - /// - isEnabled: The state of the switch: enabled or not. - /// - images: The images of the switch. - /// - text: The text of the switch. - public convenience init( - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - images: SwitchUIImages, - text: String - ) { - self.init( - theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: images, - text: text, - attributedText: nil - ) - } - - /// Initialize a new switch view with images and attributed text. - /// - Parameters: - /// - theme: The spark theme of the switch. - /// - isOn: The value of the switch. - /// - alignment: The alignment of the switch. - /// - intent: The intent of the switch. - /// - isEnabled: The state of the switch: enabled or not. - /// - images: The images of the switch. - /// - attributedText: The attributed text of the switch. - public convenience init( - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - images: SwitchUIImages, - attributedText: NSAttributedString - ) { - self.init( - theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: images, - text: nil, - attributedText: attributedText - ) - } - - private init( - _ theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - images: SwitchUIImages?, - text: String?, - attributedText: NSAttributedString? - ) { - self.viewModel = .init( - for: .uiKit, - theme: theme, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: images.map { .left($0) }, - text: text, - attributedText: attributedText.map { .left($0) } - ) - - super.init(frame: .zero) - - // Setup - self.setupView() - self.setupProperties( - text: text, - attributedText: attributedText - ) - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - private func setupView() { - // Add subview - self.addSubview(self.contentStackView) - - // View properties - self.backgroundColor = .clear - - // Setup gestures - self.setupGesturesRecognizer() - - // Setup constraints - self.setupConstraints() - - // Setup publisher subcriptions - self.setupSubscriptions() - - // Load view model - self.viewModel.load() - - // Accessibility value - self.setupAccessibilityValue(isOn: self.isOn) - - // Updates - self.updateToggleContentViewSpacings() - self.updateToggleViewSize() - self.updateToggleDotImageViewSpacings() - self.updateContentStackViewSpacing() - } - - private func setupProperties( - text: String?, - attributedText: NSAttributedString? - ) { - // Label - // Only one of the text/attributedText can be set in the init - if let text { - self.textLabel.text = text - } else if let attributedText { - self.textLabel.attributedText = attributedText - } - } - - private func setupAccessibilityValue(isOn: Bool) { - self.toggleView.accessibilityValue = isOn ? "1" : "0" - } - - // MARK: - Layout - - public override func layoutSubviews() { - super.layoutSubviews() - - self.toggleView.layoutIfNeeded() - self.toggleView.setCornerRadius(self.theme.border.radius.full) - self.toggleDotView.setCornerRadius(self.theme.border.radius.full) - } - - // MARK: - Instrinsic Content Size - - public override var intrinsicContentSize: CGSize { - // Calculate height - let toggleHeight = self.toggleHeight - let labelHeight = self.textLabel.intrinsicContentSize.height - let height = max(toggleHeight, labelHeight) - - // Calculate width - let toggleWidth = self.toggleWidth - let contentStackViewSpacing = self.contentStackViewSpacing - var width = toggleWidth + contentStackViewSpacing - - if let attributedText { - let computedSize = attributedText.boundingRect( - with: CGSize( - width: CGFloat.greatestFiniteMagnitude, - height: CGFloat.greatestFiniteMagnitude - ), - options: .usesLineFragmentOrigin, - context: nil) - width += computedSize.width - } else if text != nil { - width += self.textLabel.intrinsicContentSize.width - } - return CGSize(width: width, height: height) - } - - public override func invalidateIntrinsicContentSize() { - self.textLabel.invalidateIntrinsicContentSize() - - super.invalidateIntrinsicContentSize() - } - - // MARK: - Gesture - - private func setupGesturesRecognizer() { - self.setupToggleTapGestureRecognizer() - self.setupToggleSwipeGestureRecognizer() - } - - private func setupToggleTapGestureRecognizer() { - let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.toggleTapGestureAction)) - self.toggleView.addGestureRecognizer(gestureRecognizer) - } - - private func setupToggleSwipeGestureRecognizer() { - let rightGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.toggleSwipeGestureAction)) - rightGestureRecognizer.direction = .right - self.toggleView.addGestureRecognizer(rightGestureRecognizer) - - let leftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.toggleSwipeGestureAction)) - leftGestureRecognizer.direction = .left - self.toggleView.addGestureRecognizer(leftGestureRecognizer) - } - - // MARK: - Constraints - - private func setupConstraints() { - // Global - self.setupViewConstraints() - self.setupContentStackViewConstraints() - - // Toggle View and subviews - self.setupToggleViewConstraints() - self.setupToggleContentStackViewConstraints() - self.setupToggleDotViewConstraints() - self.setupToggleDotImageViewConstraints() - - // Text Label - self.setupTextLabelContraints() - } - - private func setupViewConstraints() { - self.translatesAutoresizingMaskIntoConstraints = false - } - - private func setupContentStackViewConstraints() { - self.contentStackView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.stickEdges( - from: self.contentStackView, - to: self, - insets: .zero - ) - } - - private func setupToggleViewConstraints() { - self.toggleView.translatesAutoresizingMaskIntoConstraints = false - - self.toggleWidthConstraint = self.toggleView.widthAnchor.constraint(equalToConstant: .zero) - self.toggleHeightConstraint = self.toggleView.heightAnchor.constraint(equalToConstant: .zero) - } - - private func setupToggleContentStackViewConstraints() { - self.toggleContentStackView.translatesAutoresizingMaskIntoConstraints = false - - self.toggleContentLeadingConstraint = self.toggleContentStackView.leadingAnchor.constraint(equalTo: self.toggleView.leadingAnchor) - self.toggleContentLeadingConstraint?.isActive = true - self.toggleContentTopConstraint = self.toggleContentStackView.topAnchor.constraint(equalTo: self.toggleView.topAnchor) - self.toggleContentTopConstraint?.isActive = true - self.toggleContentTrailingConstraint = self.toggleContentStackView.trailingAnchor.constraint(equalTo: self.toggleView.trailingAnchor) - self.toggleContentTrailingConstraint?.isActive = true - self.toggleContentBottomConstraint = self.toggleContentStackView.bottomAnchor.constraint(equalTo: self.toggleView.bottomAnchor) - self.toggleContentBottomConstraint?.isActive = true - } - - private func setupToggleDotViewConstraints() { - self.toggleDotView.translatesAutoresizingMaskIntoConstraints = false - self.toggleDotView.widthAnchor.constraint(equalTo: self.toggleDotView.heightAnchor).isActive = true - } - - private func setupToggleDotImageViewConstraints() { - self.toggleDotImageView.translatesAutoresizingMaskIntoConstraints = false - - self.toggleDotLeadingConstraint = self.toggleDotImageView.leadingAnchor.constraint(equalTo: self.toggleDotView.leadingAnchor) - self.toggleDotLeadingConstraint?.isActive = true - self.toggleDotTopConstraint = self.toggleDotImageView.topAnchor.constraint(equalTo: self.toggleDotView.topAnchor) - self.toggleDotTopConstraint?.isActive = true - self.toggleDotTrailingConstraint = self.toggleDotImageView.trailingAnchor.constraint(equalTo: self.toggleDotView.trailingAnchor) - self.toggleDotTrailingConstraint?.isActive = true - self.toggleDotBottomConstraint = self.toggleDotImageView.bottomAnchor.constraint(equalTo: self.toggleDotView.bottomAnchor) - self.toggleDotBottomConstraint?.isActive = true - } - - private func setupTextLabelContraints() { - self.textLabel.translatesAutoresizingMaskIntoConstraints = false - self.textLabel.heightAnchor.constraint(greaterThanOrEqualTo: self.toggleView.heightAnchor).isActive = true - } - - // MARK: - Setter - - /// Sets the state of the switch to the on or off position, optionally animating the transition. - public func setOn(_ isOn: Bool, animated: Bool) { - self.isOnAnimated = animated - self.viewModel.set(isOn: isOn) - } - - /// Sets the enable status of the switch, optionally animating the color. - public func setEnabled(_ isEnabled: Bool, animated: Bool) { - self.isEnabledAnimated = animated - self.viewModel.set(isEnabled: isEnabled) - } - - // MARK: - Actions - - @objc private func toggleTapGestureAction(_ sender: UITapGestureRecognizer) { - self.viewModel.toggle() - } - - @objc private func toggleSwipeGestureAction(_ sender: UISwipeGestureRecognizer) { - switch sender.direction { - case .left where self.isOn, .right where !self.isOn: - self.viewModel.toggle() - - default: - break - } - } - - // MARK: - Update UI - - private func updateToggleContentViewSpacings() { - // Reload spacing only if value changed - if self.toggleSpacing != self.toggleContentLeadingConstraint?.constant { - - self.toggleContentLeadingConstraint?.constant = self.toggleSpacing - self.toggleContentTopConstraint?.constant = self.toggleSpacing - self.toggleContentTrailingConstraint?.constant = -self.toggleSpacing - self.toggleContentBottomConstraint?.constant = -self.toggleSpacing - - self.toggleContentStackView.updateConstraintsIfNeeded() - self.invalidateIntrinsicContentSize() - } - } - - private func updateToggleViewSize() { - // Reload size only if value changed - var valueChanged = false - - if self.toggleWidth > 0 && self.toggleWidth != self.toggleWidthConstraint?.constant { - self.toggleWidthConstraint?.constant = self.toggleWidth - self.toggleWidthConstraint?.isActive = true - valueChanged = true - } - - if self.toggleHeight > 0 && self.toggleHeight != self.toggleHeightConstraint?.constant { - self.toggleHeightConstraint?.constant = self.toggleHeight - self.toggleHeightConstraint?.isActive = true - valueChanged = true - } - if valueChanged { - self.toggleView.updateConstraints() - self.invalidateIntrinsicContentSize() - } - } - - private func updateToggleDotImageViewSpacings() { - // Reload spacing only if value changed - if self.toggleDotSpacing != self.toggleDotLeadingConstraint?.constant { - self.toggleDotLeadingConstraint?.constant = self.toggleDotSpacing - self.toggleDotTopConstraint?.constant = self.toggleDotSpacing - self.toggleDotTrailingConstraint?.constant = -self.toggleDotSpacing - self.toggleDotBottomConstraint?.constant = -self.toggleDotSpacing - - self.toggleDotImageView.updateConstraintsIfNeeded() - self.invalidateIntrinsicContentSize() - } - } - - private func updateContentStackViewSpacing() { - // Reload spacing only if value changed and constraint is active - if self.contentStackViewSpacing != self.contentStackView.spacing { - self.contentStackView.spacing = contentStackViewSpacing - self.invalidateIntrinsicContentSize() - } - } - - // MARK: - Subscribe - - private func setupSubscriptions() { - // ** - // Is On - self.viewModel.$isOnChanged.subscribe(in: &self.subscriptions) { [weak self] isOn in - guard let self, let isOn else { return } - self.setupAccessibilityValue(isOn: isOn) - self.delegate?.switchDidChange(self, isOn: isOn) - self.isOnChangedSubject.send(isOn) - } - - // ** - // Interaction - self.viewModel.$isToggleInteractionEnabled.subscribe(in: &self.subscriptions) { [weak self] isEnabled in - guard let self, let isEnabled else { return } - - self.toggleView.isUserInteractionEnabled = isEnabled - if !isEnabled { - self.toggleView.accessibilityTraits.insert(.notEnabled) - } else { - self.toggleView.accessibilityTraits.remove(.notEnabled) - } - } - self.viewModel.$toggleOpacity.subscribe(in: &self.subscriptions) { [weak self] toggleOpacity in - guard let self, let toggleOpacity else { return } - - // Animate only if new alpha is different from current alpha - - let isAnimated = self.isEnabledAnimated && self.toggleView.alpha != toggleOpacity - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Constants.animationDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - self?.toggleView.alpha = toggleOpacity - } - } - // ** - - // ** - // Colors - self.viewModel.$toggleBackgroundColorToken.subscribe(in: &self.subscriptions) { [weak self] colorToken in - guard let self, let colorToken else { return } - - // Animate only if there is currently an color on View and if new color is different from current color - let isAnimated = self.isOnAnimated && self.toggleView.backgroundColor != nil && self.toggleView.backgroundColor != colorToken.uiColor - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Constants.animationDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - self?.toggleView.backgroundColor = colorToken.uiColor - } - } - self.viewModel.$toggleDotBackgroundColorToken.subscribe(in: &self.subscriptions) { [weak self] colorToken in - guard let self, let colorToken else { return } - - self.toggleDotView.backgroundColor = colorToken.uiColor - } - self.viewModel.$toggleDotForegroundColorToken.subscribe(in: &self.subscriptions) { [weak self] colorToken in - guard let self, let colorToken else { return } - - // Animate only if there is currently an color on View and if new color is different from current color - let isAnimated = self.isOnAnimated && self.toggleDotImageView.tintColor != nil && self.toggleDotImageView.tintColor != colorToken.uiColor - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Constants.animationDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - self?.toggleDotImageView.tintColor = colorToken.uiColor - } - } - self.viewModel.$textForegroundColorToken.subscribe(in: &self.subscriptions) { [weak self] colorToken in - guard let self, let colorToken else { return } - - self.textLabel.textColor = colorToken.uiColor - } - // ** - - // Aligments - self.viewModel.$isToggleOnLeft.subscribe(in: &self.subscriptions) { [weak self] isToggleOnLeft in - guard let self, let isToggleOnLeft else { return } - - self.contentStackView.semanticContentAttribute = isToggleOnLeft ? .forceLeftToRight : .forceRightToLeft - } - self.viewModel.$horizontalSpacing.subscribe(in: &self.subscriptions) { [weak self] horizontalSpacing in - guard let self, let horizontalSpacing else { return } - - self.contentStackViewSpacing = horizontalSpacing - self._contentStackViewSpacing.update(traitCollection: self.traitCollection) - self.updateContentStackViewSpacing() - } - - // ** - // Show spaces - self.viewModel.$showToggleLeftSpace.subscribe(in: &self.subscriptions) { [weak self] showLeftSpace in - guard let self, let showLeftSpace else { return } - - // showLeftSpace MUST be different to continue - // Or if the both space have the same isHidden (default state) - guard self.toggleLeftSpaceView.isHidden == showLeftSpace || - self.toggleRightSpaceView.isHidden == self.toggleLeftSpaceView.isHidden else { - return - } - - // Lock interaction before animation - let currentUserInteraction = self.viewModel.isToggleInteractionEnabled ?? true - self.toggleView.isUserInteractionEnabled = false - - let animationType: UIExecuteAnimationType = self.isOnAnimated ? .animated(duration: Constants.animationDuration) : .unanimated - - UIView.execute(animationType: animationType) { [weak self] in - self?.toggleLeftSpaceView.isHidden = !showLeftSpace - self?.toggleRightSpaceView.isHidden = showLeftSpace - } completion: { [weak self] _ in - // Unlock interaction after animation - self?.toggleView.isUserInteractionEnabled = currentUserInteraction - } - } - // ** - - // Show images - self.viewModel.$toggleDotImagesState.subscribe(in: &self.subscriptions) { [weak self] toggleDotImagesState in - guard let self else { return } - - let image = toggleDotImagesState?.currentImage.leftValue - - // Animate only if there is currently an image on ImageView and new image is exists - let isAnimated = self.isOnAnimated && self.toggleDotImageView.image != nil && image != nil - let animationType: UIExecuteAnimationType = isAnimated ? .animated(duration: Constants.animationDuration) : .unanimated - - UIView.execute( - with: self.toggleDotImageView, - animationType: animationType, - options: .transitionCrossDissolve, - instructions: { [weak self] in - self?.toggleDotImageView.image = image - }, - completion: nil - ) - } - - // Displayed Text - self.viewModel.$displayedText.subscribe(in: &self.subscriptions) { [weak self] _ in - self?.invalidateIntrinsicContentSize() - } - - // Text Label Font - self.viewModel.$textFontToken.subscribe(in: &self.subscriptions) { [weak self] fontToken in - guard let self, let fontToken else { return } - - self.textLabel.font = fontToken.uiFont - } - } - - // MARK: - Trait Collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - self.invalidateIntrinsicContentSize() - - // Update size content - self._contentStackViewSpacing.update(traitCollection: self.traitCollection) - self.updateContentStackViewSpacing() - - self._toggleSpacing.update(traitCollection: self.traitCollection) - self.updateToggleContentViewSpacings() - self.updateToggleDotImageViewSpacings() - - self._toggleWidth.update(traitCollection: self.traitCollection) - self._toggleHeight.update(traitCollection: self.traitCollection) - self.updateToggleViewSize() - } - - // MARK: - Label priorities - - func setLabelContentCompressionResistancePriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentCompressionResistancePriority(priority, - for: axis) - } - - func setLabelContentHuggingPriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentHuggingPriority(priority, - for: axis) - } -} diff --git a/core/Sources/Components/Switch/View/UIKit/SwitchUIViewSnapshotTests.swift b/core/Sources/Components/Switch/View/UIKit/SwitchUIViewSnapshotTests.swift deleted file mode 100644 index 8d92a5d16..000000000 --- a/core/Sources/Components/Switch/View/UIKit/SwitchUIViewSnapshotTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// SwitchUIViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 13/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting - -@testable import SparkCore - -final class SwitchUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test_uikit_switch_colors() throws { - let suts = try SwitchSutSnapshotTests.allColorsCases(isSwiftUIComponent: false) - self.test(suts: suts) - } - - func test_uikit_switch_contens() throws { - let suts = try SwitchSutSnapshotTests.allContentsCases(isSwiftUIComponent: false) - self.test(suts: suts) - } - - func test_uikit_switch_positions() throws { - let suts = try SwitchSutSnapshotTests.allPositionsCases(isSwiftUIComponent: false) - self.test(suts: suts) - } -} - -// MARK: - Testing - -private extension SwitchUIViewSnapshotTests { - - func test(suts: [SwitchSutSnapshotTests], function: String = #function) { - for sut in suts { - var view: SwitchUIView! - - // Images + Text - if let images = sut.images, let text = sut.text { - view = SwitchUIView( - theme: self.theme, - isOn: sut.isOn, - alignment: sut.alignment, - intent: sut.intent, - isEnabled: sut.isEnabled, - images: images.leftValue, - text: text - ) - } else if let images = sut.images, let attributedText = sut.attributedText { // Images + Attributed Text - view = SwitchUIView( - theme: self.theme, - isOn: sut.isOn, - alignment: sut.alignment, - intent: sut.intent, - isEnabled: sut.isEnabled, - images: images.leftValue, - attributedText: attributedText.leftValue - ) - } else if let text = sut.text { // Only Text - view = SwitchUIView( - theme: self.theme, - isOn: sut.isOn, - alignment: sut.alignment, - intent: sut.intent, - isEnabled: sut.isEnabled, - text: text - ) - } else if let attributedText = sut.attributedText { // Only Attributed Text - view = SwitchUIView( - theme: self.theme, - isOn: sut.isOn, - alignment: sut.alignment, - intent: sut.intent, - isEnabled: sut.isEnabled, - attributedText: attributedText.leftValue - ) - } else { // Without image and text - view = SwitchUIView( - theme: self.theme, - isOn: sut.isOn, - alignment: sut.alignment, - intent: sut.intent, - isEnabled: sut.isEnabled - ) - } - - view.backgroundColor = self.theme.colors.base.background.uiColor - - self.assertSnapshotInDarkAndLight( - matching: view, - testName: sut.testName(on: function) - ) - } - } -} diff --git a/core/Sources/Components/Switch/ViewModel/SwitchViewModel.swift b/core/Sources/Components/Switch/ViewModel/SwitchViewModel.swift deleted file mode 100644 index 315486cef..000000000 --- a/core/Sources/Components/Switch/ViewModel/SwitchViewModel.swift +++ /dev/null @@ -1,273 +0,0 @@ -// -// SwitchViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 23/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -final class SwitchViewModel: ObservableObject { - - // MARK: - Properties - - private(set) var isOn: Bool - - private let frameworkType: FrameworkType - private(set) var theme: Theme - private(set) var alignment: SwitchAlignment - private(set) var intent: SwitchIntent - private(set) var isEnabled: Bool - private(set) var images: SwitchImagesEither? - - // MARK: - Published Properties - - @Published var isOnChanged: Bool? - - @Published private (set) var isToggleInteractionEnabled: Bool? - @Published private (set) var toggleOpacity: CGFloat? - - @Published private (set) var toggleBackgroundColorToken: (any ColorToken)? - @Published private (set) var toggleDotBackgroundColorToken: (any ColorToken)? - @Published private (set) var toggleDotForegroundColorToken: (any ColorToken)? - @Published private (set) var textForegroundColorToken: (any ColorToken)? - - @Published private (set) var isToggleOnLeft: Bool? - @Published private (set) var horizontalSpacing: CGFloat? - - @Published private (set) var showToggleLeftSpace: Bool? - - @Published private (set) var toggleDotImagesState: SwitchImagesState? - - @Published private (set) var displayedText: DisplayedText? - @Published private (set) var textFontToken: TypographyFontToken? - - // MARK: - Private Properties - - private var colors: SwitchColors? - private let displayedTextViewModel: DisplayedTextViewModel - - private let dependencies: any SwitchViewModelDependenciesProtocol - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - theme: Theme, - isOn: Bool, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - images: SwitchImagesEither?, - text: String?, - attributedText: AttributedStringEither?, - dependencies: any SwitchViewModelDependenciesProtocol = SwitchViewModelDependencies() - ) { - self.isOn = isOn - self.frameworkType = frameworkType - - self.theme = theme - self.alignment = alignment - self.intent = intent - self.isEnabled = isEnabled - self.images = images - self.displayedTextViewModel = dependencies.makeDisplayedTextViewModel( - text: text, - attributedText: attributedText - ) - - self.dependencies = dependencies - - // Load the values directly on init just for SwiftUI - if frameworkType == .swiftUI { - self.updateAll() - } - } - - // MARK: - Load - - func load() { - // Update all values when UIKit view is ready to receive published values - if self.frameworkType == .uiKit { - self.updateAll() - } - } - - // MARK: - Action - - func toggle() { - // Update content only if user interaction is enabled - if self.isToggleInteractionEnabled ?? false { - self.isOn.toggle() - - // Manual action: update isOnChanged value - self.isOnChanged = self.isOn - - self.colorsDidUpdate() - self.toggleStateDidUpdate() - self.toggleDotImageDidUpdate() - self.toggleSpacesVisibilityDidUpdate() - } - } - - // MARK: - Setter - - func set(isOn: Bool) { - if self.isOn != isOn { - self.isOn = isOn - - self.colorsDidUpdate() - self.toggleDotImageDidUpdate() - self.toggleSpacesVisibilityDidUpdate() - } - } - - func set(theme: Theme) { - self.theme = theme - - self.updateAll() - } - - func set(alignment: SwitchAlignment) { - if self.alignment != alignment { - self.alignment = alignment - - self.alignmentDidUpdate() - } - } - - func set(intent: SwitchIntent) { - if self.intent != intent { - self.intent = intent - - self.colorsDidUpdate(reloadColorsFromUseCase: true) - } - } - - func set(isEnabled: Bool) { - if self.isEnabled != isEnabled { - self.isEnabled = isEnabled - - self.colorsDidUpdate() - self.toggleStateDidUpdate() - } - } - - func set(images: SwitchImagesEither?) { - if self.images != images { - self.images = images - - self.toggleDotImageDidUpdate() - } - } - - func set(text: String?) { - // Reload text properties (font and color) if consumer set a new text - if self.displayedTextViewModel.textChanged(text) { - self.displayTextDidUpdate() - self.alignmentDidUpdate() - - if text != nil { - self.textFontDidUpdate() - self.textForegroundColorTokenDidUpdate() - } - } - } - - func set(attributedText: AttributedStringEither?) { - if self.displayedTextViewModel.attributedTextChanged(attributedText) { - self.displayTextDidUpdate() - self.alignmentDidUpdate() - } - } - - // MARK: - Private Update - - private func updateAll() { - self.colorsDidUpdate(reloadColorsFromUseCase: true) - self.alignmentDidUpdate() - self.toggleStateDidUpdate() - self.toggleDotImageDidUpdate() - self.toggleSpacesVisibilityDidUpdate() - self.displayTextDidUpdate() - self.textFontDidUpdate() - } - - private func colorsDidUpdate(reloadColorsFromUseCase: Bool = false) { - if reloadColorsFromUseCase { - self.colors = self.dependencies.getColorsUseCase.execute( - intent: self.intent, - colors: self.theme.colors, - dims: self.theme.dims - ) - } - - guard let colors = self.colors else { - return - } - - self.toggleBackgroundColorToken = self.dependencies.getToggleColorUseCase.execute( - isOn: self.isOn, - statusAndStateColor: colors.toggleBackgroundColors - ) - self.toggleDotBackgroundColorToken = colors.toggleDotBackgroundColor - self.toggleDotForegroundColorToken = self.dependencies.getToggleColorUseCase.execute( - isOn: self.isOn, - statusAndStateColor: colors.toggleDotForegroundColors - ) - self.textForegroundColorTokenDidUpdate() - } - - private func textForegroundColorTokenDidUpdate() { - guard let colors = self.colors else { - return - } - - self.textForegroundColorToken = colors.textForegroundColor - } - - private func alignmentDidUpdate() { - let position = self.dependencies.getPositionUseCase.execute( - alignment: self.alignment, - spacing: self.theme.layout.spacing, - containsText: self.displayedTextViewModel.containsText - ) - - self.isToggleOnLeft = position.isToggleOnLeft - self.horizontalSpacing = position.horizontalSpacing - } - - private func toggleStateDidUpdate() { - let interactionState = self.dependencies.getToggleStateUseCase.execute( - isEnabled: self.isEnabled, - dims: self.theme.dims - ) - - self.isToggleInteractionEnabled = interactionState.interactionEnabled - self.toggleOpacity = interactionState.opacity - } - - private func toggleDotImageDidUpdate() { - if let images = self.images { - self.toggleDotImagesState = self.dependencies.getImagesStateUseCase.execute( - isOn: self.isOn, - images: images - ) - } else { - self.toggleDotImagesState = nil - } - } - - private func toggleSpacesVisibilityDidUpdate() { - self.showToggleLeftSpace = self.isOn - } - - private func displayTextDidUpdate() { - self.displayedText = self.displayedTextViewModel.displayedText - } - - private func textFontDidUpdate() { - self.textFontToken = self.theme.typography.body1 - } -} diff --git a/core/Sources/Components/Switch/ViewModel/SwitchViewModelDependencies.swift b/core/Sources/Components/Switch/ViewModel/SwitchViewModelDependencies.swift deleted file mode 100644 index 567d82929..000000000 --- a/core/Sources/Components/Switch/ViewModel/SwitchViewModelDependencies.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SwitchViewModelDependencies.swift -// SparkCore -// -// Created by robin.lemaire on 03/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol SwitchViewModelDependenciesProtocol { - var getColorsUseCase: SwitchGetColorsUseCaseable { get } - var getImagesStateUseCase: SwitchGetImagesStateUseCaseable { get } - var getToggleColorUseCase: SwitchGetToggleColorUseCaseable { get } - var getPositionUseCase: SwitchGetPositionUseCaseable { get } - var getToggleStateUseCase: SwitchGetToggleStateUseCaseable { get } - - func makeDisplayedTextViewModel(text: String?, - attributedText: AttributedStringEither?) -> DisplayedTextViewModel -} - -struct SwitchViewModelDependencies: SwitchViewModelDependenciesProtocol { - - // MARK: - Properties - - let getColorsUseCase: SwitchGetColorsUseCaseable - var getImagesStateUseCase: SwitchGetImagesStateUseCaseable - let getToggleColorUseCase: SwitchGetToggleColorUseCaseable - let getPositionUseCase: SwitchGetPositionUseCaseable - let getToggleStateUseCase: SwitchGetToggleStateUseCaseable - - // MARK: - Initialization - - init( - getColorsUseCase: SwitchGetColorsUseCaseable = SwitchGetColorsUseCase(), - getImagesStateUseCase: SwitchGetImagesStateUseCaseable = SwitchGetImagesStateUseCase(), - getToggleColorUseCase: SwitchGetToggleColorUseCaseable = SwitchGetToggleColorUseCase(), - getPositionUseCase: SwitchGetPositionUseCaseable = SwitchGetPositionUseCase(), - getToggleStateUseCase: SwitchGetToggleStateUseCaseable = SwitchGetToggleStateUseCase() - ) { - self.getColorsUseCase = getColorsUseCase - self.getImagesStateUseCase = getImagesStateUseCase - self.getToggleColorUseCase = getToggleColorUseCase - self.getPositionUseCase = getPositionUseCase - self.getToggleStateUseCase = getToggleStateUseCase - } - - // MARK: - Maker - - func makeDisplayedTextViewModel( - text: String?, - attributedText: AttributedStringEither? - ) -> DisplayedTextViewModel { - return DisplayedTextViewModelDefault( - text: text, - attributedText: attributedText - ) - } -} diff --git a/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift b/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift deleted file mode 100644 index 3bea2544f..000000000 --- a/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift +++ /dev/null @@ -1,1645 +0,0 @@ -// -// SwitchViewModelTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 26/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import Combine - -@testable import SparkCore - -final class SwitchViewModelTests: XCTestCase { - - // MARK: - Properties - - private var subscriptions = Set() - - // MARK: - Setup - - override func tearDown() { - super.tearDown() - - // Clear publishers - self.subscriptions.removeAll() - } - - // MARK: - Init Tests - - func test_properties_on_init_when_frameworkType_is_UIKit() throws { - try self.testPropertiesOnInit( - givenFrameworkType: .uiKit - ) - } - - func test_properties_on_init_when_frameworkType_is_SwiftUI() throws { - try self.testPropertiesOnInit( - givenFrameworkType: .swiftUI - ) - } - - private func testPropertiesOnInit( - givenFrameworkType: FrameworkType - ) throws { - // GIVEN - let isOnMock = true - let alignmentMock: SwitchAlignment = .left - let intentMock: SwitchIntent = .alert - let isEnabledMock = true - - let isUIKit = givenFrameworkType == .uiKit - - // WHEN - let stub = Stub( - frameworkType: givenFrameworkType, - isOn: isOnMock, - alignment: alignmentMock, - intent: intentMock, - isEnabled: isEnabledMock, - isImages: true - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // THEN - XCTAssertEqual(viewModel.isOn, - isOnMock, - "Wrong isOn value") - XCTAssertIdentical(viewModel.theme as? ThemeGeneratedMock, - stub.themeMock, - "Wrong theme value") - XCTAssertEqual(viewModel.alignment, - alignmentMock, - "Wrong alignment value") - XCTAssertEqual(viewModel.intent, - intentMock, - "Wrong intent value") - XCTAssertEqual(viewModel.isEnabled, - isEnabledMock, - "Wrong isEnabled value") - XCTAssertEqual(viewModel.images?.leftValue, - stub.imagesMock, - "Wrong images value") - - XCTAssertNil(viewModel.isOnChanged, - "Wrong isOnChanged value") - - // ** - // Published properties - let publishedExpectedContainsValue = (isUIKit ? false : true) - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testColors(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testPosition(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testToggleDotImage(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testDisplayedText(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testTextFont(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testShowToggleLeftSpace(on: stub, expectedValue: isUIKit ? nil : true) - - self.testAllPublishedSinkCount( - on: stub, - expectedIsOnChangedPublishedSinkCount: 1, - expectedIsToggleInteractionEnabledPublishedSinkCount: 1, - expectedToggleOpacityPublishedSinkCount: 1, - expectedToggleBackgroundColorTokenPublishedSinkCount: 1, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: 1, - expectedToggleDotForegroundColorTokenPublishedSinkCount: 1, - expectedTextForegroundColorTokenPublishedSinkCount: 1, - expectedIsToggleOnLeftPublishedSinkCount: 1, - expectedHorizontalSpacingPublishedSinkCount: 1, - expectedShowToggleLeftSpacePublishedSinkCount: 1, - expectedToggleDotImagePublishedSinkCount: 1, - expectedDisplayedTextPublishedSinkCount: 1, - expectedTextFontTokenPublishedSinkCount: 1 - ) - // ** - - // ** - // Use Cases - let useCaseNumberOfCalls = (isUIKit ? 0 : 1) - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls - ) - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: isUIKit ? 0 : 2 - ) - self.testGetPositionUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls - ) - self.testGetToggleStateUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls - ) - // ** - - } - - // MARK: - Load Tests - - func test_published_properties_on_load_when_frameworkType_is_UIKit() throws { - try self.testPublishedPropertiesOnLoad( - givenFrameworkType: .uiKit - ) - } - - func test_published_properties_on_load_when_frameworkType_is_SwiftUI() throws { - try self.testPublishedPropertiesOnLoad( - givenFrameworkType: .swiftUI - ) - } - - func testPublishedPropertiesOnLoad( - givenFrameworkType: FrameworkType - ) throws { - // GIVEN - let isOnMock = true - let alignmentMock: SwitchAlignment = .left - let intentMock: SwitchIntent = .alert - let isEnabledMock = true - - let isUIKit = givenFrameworkType == .uiKit - - // WHEN - let stub = Stub( - frameworkType: givenFrameworkType, - isOn: isOnMock, - alignment: alignmentMock, - intent: intentMock, - isEnabled: isEnabledMock, - isImages: true - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - viewModel.load() - - // THEN - // ** - // Published properties - let publishedExpectedContainsValue = (isUIKit ? true : false) - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testColors(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testPosition(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testToggleDotImage(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testDisplayedText(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testTextFont(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testShowToggleLeftSpace(on: stub, expectedValue: isUIKit ? stub.isOnMock : nil) - - let publishedSinkCount = isUIKit ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedIsToggleInteractionEnabledPublishedSinkCount: publishedSinkCount, - expectedToggleOpacityPublishedSinkCount: publishedSinkCount, - expectedToggleBackgroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedToggleDotForegroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedTextForegroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedIsToggleOnLeftPublishedSinkCount: publishedSinkCount, - expectedHorizontalSpacingPublishedSinkCount: publishedSinkCount, - expectedShowToggleLeftSpacePublishedSinkCount: publishedSinkCount, - expectedToggleDotImagePublishedSinkCount: publishedSinkCount, - expectedDisplayedTextPublishedSinkCount: publishedSinkCount, - expectedTextFontTokenPublishedSinkCount: publishedSinkCount - ) - // ** - - // ** - // Use Cases - let useCaseNumberOfCalls = (isUIKit ? 1 : 0) - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls, - givenTheme: stub.themeMock, - givenIntent: intentMock - ) - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls, - givenIsOn: isOnMock, - givenImages: stub.imagesMock - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: isUIKit ? 2 : 0, - givenIsOn: isOnMock - ) - self.testGetPositionUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls, - givenTheme: stub.themeMock, - givenAlignment: alignmentMock - ) - self.testGetToggleStateUseCaseMock( - on: stub, - numberOfCalls: useCaseNumberOfCalls, - givenTheme: stub.themeMock, - givenIsEnabled: isEnabledMock - ) - // ** - } - - // MARK: - Toggle Tests - - func test_toggle_when_isToggleInteractionEnabled_is_true_should_update_switch() { - // GIVEN - let expectedIsOn = true - - let isEnabledMock = false - - let stub = Stub( - isOn: false, - alignment: .right, - intent: .support, - isEnabled: isEnabledMock, - isImages: true, - userInteractionEnabled: true - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.toggle() - - // THEN - XCTAssertEqual(viewModel.isOn, - expectedIsOn, - "Wrong isOn value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: expectedIsOn) - self.testToggleState(on: stub) - self.testColors(on: stub) - self.testPosition(on: stub, expectedContainsValue: false) - self.testToggleDotImage(on: stub) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: expectedIsOn) - - self.testAllPublishedSinkCount( - on: stub, - expectedIsOnChangedPublishedSinkCount: 1, - expectedIsToggleInteractionEnabledPublishedSinkCount: 1, - expectedToggleOpacityPublishedSinkCount: 1, - expectedToggleBackgroundColorTokenPublishedSinkCount: 1, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: 1, - expectedToggleDotForegroundColorTokenPublishedSinkCount: 1, - expectedTextForegroundColorTokenPublishedSinkCount: 1, - expectedIsToggleOnLeftPublishedSinkCount: 1, - expectedShowToggleLeftSpacePublishedSinkCount: 1, - expectedToggleDotImagePublishedSinkCount: 1 - ) - // ** - - // ** - // Use Cases - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: 1 - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: 2, - givenIsOn: expectedIsOn - ) - self.testGetToggleStateUseCaseMock( - on: stub, - numberOfCalls: 1, - givenTheme: stub.themeMock, - givenIsEnabled: isEnabledMock - ) - // ** - } - - func test_toggle_when_isToggleInteractionEnabled_is_false_should_do_nothing() { - // GIVEN - let isOnMock = false - let alignmentMock: SwitchAlignment = .right - let intentMock: SwitchIntent = .support - let isEnabledMock = false - - let stub = Stub( - isOn: isOnMock, - alignment: alignmentMock, - intent: intentMock, - isEnabled: isEnabledMock, - isImages: false, - userInteractionEnabled: false - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.toggle() - - // THEN - XCTAssertEqual(viewModel.isOn, - isOnMock, - "Wrong isOn value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors(on: stub, expectedContainsValue: false) - self.testPosition(on: stub, expectedContainsValue: false) - self.testToggleDotImage(on: stub, expectedContainsValue: false) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - self.testAllPublishedSinkCount(on: stub) - // ** - - // ** - // Use Cases - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - self.testGetToggleStateUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - // ** - } - - // MARK: - Setter Tests - - func test_set_isOn_with_different_new_value() { - self.testSetIsOn( - givenIsDifferentNewValue: true - ) - } - - func test_set_isOn_with_same_new_value() { - self.testSetIsOn( - givenIsDifferentNewValue: false - ) - } - - func testSetIsOn( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue = true - let newValue = givenIsDifferentNewValue ? !defaultValue : defaultValue - - let stub = Stub( - isOn: defaultValue, - isImages: true - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(isOn: newValue) - - // THEN - XCTAssertEqual(viewModel.isOn, - newValue, - "Wrong isOn value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testPosition(on: stub, expectedContainsValue: false) - self.testToggleDotImage(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: givenIsDifferentNewValue ? newValue : nil) - - let publishedExpectedContainsValue = givenIsDifferentNewValue ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedToggleBackgroundColorTokenPublishedSinkCount: publishedExpectedContainsValue, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: publishedExpectedContainsValue, - expectedToggleDotForegroundColorTokenPublishedSinkCount: publishedExpectedContainsValue, - expectedTextForegroundColorTokenPublishedSinkCount: publishedExpectedContainsValue, - expectedIsToggleOnLeftPublishedSinkCount: publishedExpectedContainsValue, - expectedShowToggleLeftSpacePublishedSinkCount: publishedExpectedContainsValue, - expectedToggleDotImagePublishedSinkCount: publishedExpectedContainsValue - ) - // ** - - // ** - // Use Cases - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 1 : 0 - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 2 : 0, - givenIsOn: newValue - ) - // ** - } - - func test_set_theme() { - // GIVEN - let newTheme = ThemeGeneratedMock.mocked() - - let isOnMock = false - let alignmentMock: SwitchAlignment = .right - let intentMock: SwitchIntent = .support - let isEnabledMock = false - - let stub = Stub( - isOn: isOnMock, - alignment: alignmentMock, - intent: intentMock, - isEnabled: isEnabledMock, - isImages: true - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(theme: newTheme) - - // THEN - XCTAssertIdentical(viewModel.theme as? ThemeGeneratedMock, - newTheme, - "Wrong theme value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub) - self.testColors(on: stub) - self.testPosition(on: stub) - self.testToggleDotImage(on: stub) - self.testDisplayedText(on: stub) - self.testTextFont(on: stub, givenNewTheme: newTheme) - self.testShowToggleLeftSpace(on: stub, expectedValue: stub.isOnMock) - - self.testAllPublishedSinkCount( - on: stub, - expectedIsToggleInteractionEnabledPublishedSinkCount: 1, - expectedToggleOpacityPublishedSinkCount: 1, - expectedToggleBackgroundColorTokenPublishedSinkCount: 1, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: 1, - expectedToggleDotForegroundColorTokenPublishedSinkCount: 1, - expectedTextForegroundColorTokenPublishedSinkCount: 1, - expectedIsToggleOnLeftPublishedSinkCount: 1, - expectedHorizontalSpacingPublishedSinkCount: 1, - expectedShowToggleLeftSpacePublishedSinkCount: 1, - expectedToggleDotImagePublishedSinkCount: 1, - expectedDisplayedTextPublishedSinkCount: 1, - expectedTextFontTokenPublishedSinkCount: 1 - ) - // ** - - // ** - // Use Cases - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: 1, - givenTheme: newTheme, - givenIntent: intentMock - ) - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: 1 - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: 2, - givenIsOn: isOnMock - ) - self.testGetPositionUseCaseMock( - on: stub, - numberOfCalls: 1, - givenTheme: newTheme, - givenAlignment: alignmentMock - ) - self.testGetToggleStateUseCaseMock( - on: stub, - numberOfCalls: 1, - givenTheme: newTheme, - givenIsEnabled: isEnabledMock - ) - // ** - } - - func test_set_alignment_with_different_new_value() { - self.testSetAlignment( - givenIsDifferentNewValue: true - ) - } - - func test_set_alignment_with_same_new_value() { - self.testSetAlignment( - givenIsDifferentNewValue: false - ) - } - - func testSetAlignment( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: SwitchAlignment = .right - let newValue: SwitchAlignment = givenIsDifferentNewValue ? .left : defaultValue - - let stub = Stub( - alignment: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(alignment: newValue) - - // THEN - XCTAssertEqual(viewModel.alignment, - newValue, - "Wrong alignment value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors(on: stub, expectedContainsValue: false) - self.testPosition(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testToggleDotImage(on: stub, expectedContainsValue: false) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - let publishedSinkCount = givenIsDifferentNewValue ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedIsToggleOnLeftPublishedSinkCount: publishedSinkCount, - expectedHorizontalSpacingPublishedSinkCount: publishedSinkCount - ) - // ** - - // Use Cases - self.testGetPositionUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenTheme: stub.themeMock, - givenAlignment: newValue - ) - } - - func test_set_intent_with_different_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: true - ) - } - - func test_set_intent_with_same_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: false - ) - } - - private func testSetIntent( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: SwitchIntent = .main - let newValue: SwitchIntent = givenIsDifferentNewValue ? .error : defaultValue - - let isOnMock = true - - let stub = Stub( - isOn: isOnMock, - intent: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(intent: newValue) - - // THEN - XCTAssertEqual(viewModel.intent, - newValue, - "Wrong intent value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testPosition(on: stub, expectedContainsValue: false) - self.testToggleDotImage(on: stub, expectedContainsValue: false) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - let publishedSinkCount = givenIsDifferentNewValue ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedToggleBackgroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedToggleDotForegroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedTextForegroundColorTokenPublishedSinkCount: publishedSinkCount - ) - // ** - - // ** - // Use Cases - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenTheme: stub.themeMock, - givenIntent: newValue - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 2 : 0, - givenIsOn: isOnMock - ) - // ** - } - - func test_set_isEnabled_with_different_new_value() { - self.testSetIsEnabled( - givenIsDifferentNewValue: true - ) - } - - func test_set_isEnabled_with_same_new_value() { - self.testSetIsEnabled( - givenIsDifferentNewValue: false - ) - } - - func testSetIsEnabled( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue = true - let newValue = givenIsDifferentNewValue ? false : defaultValue - - let intentMock: SwitchIntent = .neutral - let isOnMock = true - - let stub = Stub( - isOn: isOnMock, - intent: intentMock, - isEnabled: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(isEnabled: newValue) - - // THEN - XCTAssertEqual(viewModel.isEnabled, - newValue, - "Wrong isEnabled value") - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testColors(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testPosition(on: stub, expectedContainsValue: false) - self.testToggleDotImage(on: stub, expectedContainsValue: false) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - let publishedSinkCount = givenIsDifferentNewValue ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedIsToggleInteractionEnabledPublishedSinkCount: publishedSinkCount, - expectedToggleOpacityPublishedSinkCount: publishedSinkCount, - expectedToggleBackgroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedToggleDotForegroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedTextForegroundColorTokenPublishedSinkCount: publishedSinkCount - ) - // ** - - // ** - // Use Cases - self.testGetColorsUseCaseMock( - on: stub, - numberOfCalls: 0 - ) - self.testGetToggleColorUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 2 : 0, - givenIsOn: isOnMock - ) - self.testGetToggleStateUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenTheme: stub.themeMock, - givenIsEnabled: newValue - ) - // ** - } - - func test_set_images_with_different_new_value() { - self.testSetImages( - givenIsDifferentNewValue: true - ) - } - - func test_set_images_with_same_new_value() { - self.testSetImages( - givenIsDifferentNewValue: false - ) - } - - func testSetImages( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let newValue = SwitchUIImages(on: UIImage(), off: UIImage()) - - let isOnMock = true - - let stub = Stub( - isOn: isOnMock, - isImages: false - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.set(images: givenIsDifferentNewValue ? .left(newValue) : nil) - - // THEN - if givenIsDifferentNewValue { - XCTAssertEqual(viewModel.images?.leftValue, - newValue, - "Wrong images value") - } else { - XCTAssertNil(viewModel.images?.leftValue, - "Wrong images value") - } - - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors(on: stub, expectedContainsValue: false) - self.testPosition(on: stub, expectedContainsValue: false) - self.testToggleDotImage(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testDisplayedText(on: stub, expectedContainsValue: false) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - self.testAllPublishedSinkCount( - on: stub, - expectedToggleDotImagePublishedSinkCount: givenIsDifferentNewValue ? 1 : 0 - ) - // ** - - // Use Cases - self.testGetImageUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 1 : 0, - givenIsOn: isOnMock, - givenImages: givenIsDifferentNewValue ? newValue : nil - ) - } - - func test_set_text_when_displayedTextViewModel_textChanged_return_true_and_new_value_is_NOT_nil() { - self.testSetText( - newValueIsNil: false, - givenIsDifferentNewValue: true - ) - } - - func test_set_text_when_displayedTextViewModel_textChanged_return_true_and_new_value_is_nil() { - self.testSetText( - newValueIsNil: true, - givenIsDifferentNewValue: true - ) - } - - func test_set_text_when_displayedTextViewModel_textChanged_return_false() { - self.testSetText( - givenIsDifferentNewValue: false - ) - } - - private func testSetText( - newValueIsNil: Bool = true, - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let newValue: String? = newValueIsNil ? nil : "My New Text" - - let stub = Stub( - text: "Text" - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - stub.displayedTextViewModelMock.textChangedWithTextReturnValue = givenIsDifferentNewValue - viewModel.set(text: newValue) - - // THEN - - // ** - // Published properties - let publishedExpectedContainsValue = (givenIsDifferentNewValue && !newValueIsNil) - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors( - on: stub, - expectedContainsValue: false, - expectedTextForegroundColorContainsValue: publishedExpectedContainsValue - ) - self.testPosition(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testToggleDotImage(on: stub, expectedContainsValue: false) - self.testDisplayedText(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testTextFont(on: stub, expectedContainsValue: publishedExpectedContainsValue) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - let publishedSinkCount = (givenIsDifferentNewValue && !newValueIsNil) ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedTextForegroundColorTokenPublishedSinkCount: publishedSinkCount, - expectedIsToggleOnLeftPublishedSinkCount: publishedSinkCount, - expectedHorizontalSpacingPublishedSinkCount: givenIsDifferentNewValue ? 1 : 0, - expectedDisplayedTextPublishedSinkCount: givenIsDifferentNewValue ? 1 : 0, - expectedTextFontTokenPublishedSinkCount: publishedSinkCount - ) - // ** - - // ** - // DisplayedText ViewModel - XCTAssertEqual( - stub.displayedTextViewModelMock.textChangedWithTextCallsCount, - 1, - "Wrong call number on textChanged on displayedTextViewModel" - ) - XCTAssertEqual( - stub.displayedTextViewModelMock.textChangedWithTextReceivedText, - newValue, - "Wrong textChanged parameter on displayedTextViewModel" - ) - // ** - - // Use Cases - self.testGetPositionUseCaseMock( - on: stub, - numberOfCalls: givenIsDifferentNewValue ? 1 : 0 - ) - } - - func test_set_attributedText_when_displayedTextViewModel_attributedTextChanged_return_true() { - self.testSetAttributedText( - givenIsDifferentNewValue: true - ) - } - - func test_set_attributedText_when_displayedTextViewModel_attributedTextChanged_return_false() { - self.testSetAttributedText( - givenIsDifferentNewValue: false - ) - } - - private func testSetAttributedText( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let newValue = NSAttributedString(string: "My new AT Switch") - - let stub = Stub( - attributedText: .init(string: "My AT Switch") - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - viewModel.load() // Needed to get colors from usecase one time - - // Reset all dependencies mocked data - stub.resetMockedData() - - // WHEN - stub.displayedTextViewModelMock.attributedTextChangedWithAttributedTextReturnValue = givenIsDifferentNewValue - viewModel.set(attributedText: .left(newValue)) - - // THEN - // ** - // Published properties - self.testIsOnChanged(on: stub, expectedValue: nil) - self.testToggleState(on: stub, expectedContainsValue: false) - self.testColors(on: stub, expectedContainsValue: false) - self.testPosition(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testToggleDotImage(on: stub, expectedContainsValue: false) - self.testDisplayedText(on: stub, expectedContainsValue: givenIsDifferentNewValue) - self.testTextFont(on: stub, expectedContainsValue: false) - self.testShowToggleLeftSpace(on: stub, expectedValue: nil) - - let publishedSinkCount = (givenIsDifferentNewValue) ? 1 : 0 - self.testAllPublishedSinkCount( - on: stub, - expectedIsToggleOnLeftPublishedSinkCount: publishedSinkCount, - expectedHorizontalSpacingPublishedSinkCount: publishedSinkCount, - expectedDisplayedTextPublishedSinkCount: publishedSinkCount - - ) - // ** - - // ** - // DisplayedText ViewModel - XCTAssertEqual( - stub.displayedTextViewModelMock.attributedTextChangedWithAttributedTextCallsCount, - 1, - "Wrong call number on attributedText on displayedTextViewModel" - ) - XCTAssertEqual( - stub.displayedTextViewModelMock.attributedTextChangedWithAttributedTextReceivedAttributedText?.leftValue, - newValue, - "Wrong attributedText parameter on displayedTextViewModel" - ) - // ** - } -} - -// MARK: - Testing Dependencies - -private extension SwitchViewModelTests { - - func testGetColorsUseCaseMock( - on stub: Stub, - numberOfCalls: Int, - givenTheme: Theme? = nil, - givenIntent: SwitchIntent? = nil - ) { - XCTAssertEqual(stub.getColorsUseCaseMock.executeWithIntentAndColorsAndDimsCallsCount, - numberOfCalls, - "Wrong call number on execute on getColorsUseCase") - - if numberOfCalls > 0, let givenTheme, let givenIntent { - let getColorsUseCaseArgs = stub.getColorsUseCaseMock.executeWithIntentAndColorsAndDimsReceivedArguments - XCTAssertEqual(getColorsUseCaseArgs?.intent, - givenIntent, - "Wrong intent parameter on execute on getColorsUseCase") - XCTAssertIdentical(try XCTUnwrap(getColorsUseCaseArgs?.colors as? ColorsGeneratedMock), - givenTheme.colors as? ColorsGeneratedMock, - "Wrong colors parameter on execute on getColorsUseCase") - XCTAssertIdentical(try XCTUnwrap(getColorsUseCaseArgs?.dims as? DimsGeneratedMock), - givenTheme.dims as? DimsGeneratedMock, - "Wrong dims parameter on execute on getColorsUseCase") - } - } - - func testGetImageUseCaseMock( - on stub: Stub, - numberOfCalls: Int, - givenIsOn: Bool? = nil, - givenImages: SwitchUIImages? = nil - ) { - XCTAssertEqual(stub.getImagesStateUseCaseMock.executeWithIsOnAndImagesCallsCount, - numberOfCalls, - "Wrong call number on execute on getImageUseCase") - - if numberOfCalls > 0, let givenIsOn { - let getImageUseCaseArgs = stub.getImagesStateUseCaseMock.executeWithIsOnAndImagesReceivedArguments - XCTAssertEqual(getImageUseCaseArgs?.isOn, - givenIsOn, - "Wrong isOn parameter on execute on getImageUseCase") - - if let givenImages { - XCTAssertEqual(getImageUseCaseArgs?.images.leftValue, - givenImages, - "Wrong images parameter on execute on getImageUseCase") - } else { - XCTAssertNil(getImageUseCaseArgs?.images, - "images parameter should be nil on execute on getImageUseCase") - } - } - } - - func testGetToggleColorUseCaseMock( - on stub: Stub, - numberOfCalls: Int, - givenIsOn: Bool? = nil - ) { - let givenStatusAndStateColors = [ - stub.colorsMock.toggleBackgroundColors, - stub.colorsMock.toggleDotForegroundColors - ] - - XCTAssertEqual(stub.getToggleColorUseCaseMock.executeWithIsOnAndStatusAndStateColorCallsCount, - numberOfCalls, - "Wrong call number on execute on getToggleColorUseCase") - - if numberOfCalls > 0, let givenIsOn { - let getToggleColorUseCaseInvocations = stub.getToggleColorUseCaseMock.executeWithIsOnAndStatusAndStateColorReceivedInvocations - - guard getToggleColorUseCaseInvocations.count == numberOfCalls else { - XCTFail("Wrong invocation number on execute on getToggleColorUseCase") - return - } - - guard givenStatusAndStateColors.count == 2 else { - XCTFail("Wrong number of item into givenStatusAndStateColors") - return - } - - for (index, args) in getToggleColorUseCaseInvocations.enumerated() { - let givenStatusAndStateColorsIndex = (index % 2 == 0) ? 0 : 1 - - XCTAssertEqual(args.isOn, - givenIsOn, - "Wrong isOn parameter on \(index) execute on getToggleColorUseCase") - XCTAssertEqual(try XCTUnwrap(args.statusAndStateColor), - givenStatusAndStateColors[givenStatusAndStateColorsIndex], - "Wrong statusAndStateColor parameter on \(index) execute on getToggleColorUseCase") - } - } - } - - func testGetPositionUseCaseMock( - on stub: Stub, - numberOfCalls: Int, - givenTheme: Theme? = nil, - givenAlignment: SwitchAlignment? = nil - ) { - XCTAssertEqual(stub.getPositionUseCaseMock.executeWithAlignmentAndSpacingAndContainsTextCallsCount, - numberOfCalls, - "Wrong call number on execute on getPositionUseCase") - - if numberOfCalls > 0, let givenTheme, let givenAlignment { - let getPositionUseCaseArgs = stub.getPositionUseCaseMock.executeWithAlignmentAndSpacingAndContainsTextReceivedArguments - XCTAssertEqual(getPositionUseCaseArgs?.alignment, - givenAlignment, - "Wrong alignment parameter on execute on getPositionUseCase") - XCTAssertIdentical(try XCTUnwrap(getPositionUseCaseArgs?.spacing as? LayoutSpacingGeneratedMock), - givenTheme.layout.spacing as? LayoutSpacingGeneratedMock, - "Wrong spacing parameter on execute on getPositionUseCase") - XCTAssertEqual(getPositionUseCaseArgs?.containsText, - stub.displayedTextViewModelMock.containsText, - "Wrong containsText parameter on execute on getContentUseCase") - } - } - - func testGetToggleStateUseCaseMock( - on stub: Stub, - numberOfCalls: Int, - givenTheme: Theme? = nil, - givenIsEnabled: Bool? = nil - ) { - XCTAssertEqual(stub.getToggleStateUseCaseMock.executeWithIsEnabledAndDimsCallsCount, - numberOfCalls, - "Wrong call number on execute on getToggleStateUseCase") - - if numberOfCalls > 0, let givenTheme, let givenIsEnabled { - let getToggleStateUseCaseArgs = stub.getToggleStateUseCaseMock.executeWithIsEnabledAndDimsReceivedArguments - XCTAssertEqual(getToggleStateUseCaseArgs?.isEnabled, - givenIsEnabled, - "Wrong isEnabled parameter on execute on getToggleStateUseCase") - XCTAssertIdentical(try XCTUnwrap(getToggleStateUseCaseArgs?.dims as? DimsGeneratedMock), - givenTheme.dims as? DimsGeneratedMock, - "Wrong dims parameter on execute on getToggleStateUseCase") - } - } -} - -// MARK: - Testing Publisher - -private extension SwitchViewModelTests { - - func testAllPublishedSinkCount( - on stub: Stub, - expectedIsOnChangedPublishedSinkCount: Int = 0, - expectedIsToggleInteractionEnabledPublishedSinkCount: Int = 0, - expectedToggleOpacityPublishedSinkCount: Int = 0, - expectedToggleBackgroundColorTokenPublishedSinkCount: Int = 0, - expectedToggleDotBackgroundColorTokenPublishedSinkCount: Int = 0, - expectedToggleDotForegroundColorTokenPublishedSinkCount: Int = 0, - expectedTextForegroundColorTokenPublishedSinkCount: Int = 0, - expectedIsToggleOnLeftPublishedSinkCount: Int = 0, - expectedHorizontalSpacingPublishedSinkCount: Int = 0, - expectedShowToggleLeftSpacePublishedSinkCount: Int = 0, - expectedToggleDotImagePublishedSinkCount: Int = 0, - expectedDisplayedTextPublishedSinkCount: Int = 0, - expectedTextFontTokenPublishedSinkCount: Int = 0 - ) { - XCTAssertPublisherSinkCountEqual( - on: stub.isOnChangedPublisherMock, - expectedIsOnChangedPublishedSinkCount - ) - - XCTAssertPublisherSinkCountEqual( - on: stub.isToggleInteractionEnabledPublisherMock, - expectedIsToggleInteractionEnabledPublishedSinkCount - ) - XCTAssertPublisherSinkCountEqual( - on: stub.toggleOpacityPublisherMock, - expectedToggleOpacityPublishedSinkCount - ) - - XCTAssertPublisherSinkCountEqual( - on: stub.toggleBackgroundColorTokenPublisherMock, - expectedToggleBackgroundColorTokenPublishedSinkCount - ) - XCTAssertPublisherSinkCountEqual( - on: stub.toggleDotBackgroundColorTokenPublisherMock, - expectedToggleDotBackgroundColorTokenPublishedSinkCount - ) - XCTAssertPublisherSinkCountEqual( - on: stub.toggleDotForegroundColorTokenPublisherMock, - expectedToggleDotForegroundColorTokenPublishedSinkCount - ) - XCTAssertPublisherSinkCountEqual( - on: stub.textForegroundColorTokenPublisherMock, - expectedTextForegroundColorTokenPublishedSinkCount - ) - - XCTAssertPublisherSinkCountEqual( - on: stub.horizontalSpacingPublisherMock, - expectedHorizontalSpacingPublishedSinkCount - ) - XCTAssertPublisherSinkCountEqual( - on: stub.showToggleLeftSpacePublisherMock, - expectedShowToggleLeftSpacePublishedSinkCount - ) - - XCTAssertPublisherSinkCountEqual( - on: stub.toggleDotImagesStatePublisherMock, - expectedToggleDotImagePublishedSinkCount - ) - - XCTAssertPublisherSinkCountEqual( - on: stub.displayedTextPublisherMock, - expectedDisplayedTextPublishedSinkCount - ) - - XCTAssertPublisherSinkCountEqual( - on: stub.textFontTokenPublisherMock, - expectedTextFontTokenPublishedSinkCount - ) - } - - func testIsOnChanged( - on stub: Stub, - expectedValue: Bool? = true - ) { - if let expectedValue { - XCTAssertPublisherSinkValueEqual( - on: stub.isOnChangedPublisherMock, - expectedValue - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.isOnChangedPublisherMock - ) - } - } - - func testPosition( - on stub: Stub, - expectedContainsValue: Bool = true - ) { - if expectedContainsValue { - XCTAssertPublisherSinkValueEqual( - on: stub.isToggleOnLeftPublisherMock, - stub.positionMock.isToggleOnLeft - ) - XCTAssertPublisherSinkValueEqual( - on: stub.horizontalSpacingPublisherMock, - stub.positionMock.horizontalSpacing - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.isToggleOnLeftPublisherMock - ) - XCTAssertPublisherSinkValueNil( - on: stub.horizontalSpacingPublisherMock - ) - } - } - - func testColors( - on stub: Stub, - expectedContainsValue: Bool = true, - expectedTextForegroundColorContainsValue: Bool = false - ) { - if expectedContainsValue { - XCTAssertPublisherSinkValueIdentical( - on: stub.toggleBackgroundColorTokenPublisherMock, - stub.colorTokenMock - ) - XCTAssertPublisherSinkValueIdentical( - on: stub.toggleDotBackgroundColorTokenPublisherMock, - stub.colorsMock.toggleDotBackgroundColor as? ColorTokenGeneratedMock - ) - XCTAssertPublisherSinkValueIdentical( - on: stub.toggleDotForegroundColorTokenPublisherMock, - stub.colorTokenMock - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.toggleBackgroundColorTokenPublisherMock - ) - XCTAssertPublisherSinkValueNil( - on: stub.toggleDotBackgroundColorTokenPublisherMock - ) - XCTAssertPublisherSinkValueNil( - on: stub.toggleDotForegroundColorTokenPublisherMock - ) - } - - if expectedContainsValue || expectedTextForegroundColorContainsValue { - XCTAssertPublisherSinkValueIdentical( - on: stub.textForegroundColorTokenPublisherMock, - stub.colorsMock.textForegroundColor as? ColorTokenGeneratedMock - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.textForegroundColorTokenPublisherMock - ) - } - } - - func testToggleState( - on stub: Stub, - expectedContainsValue: Bool = true - ) { - if expectedContainsValue { - XCTAssertPublisherSinkValueEqual( - on: stub.isToggleInteractionEnabledPublisherMock, - stub.toggleStateMock.interactionEnabled - ) - XCTAssertPublisherSinkValueEqual( - on: stub.toggleOpacityPublisherMock, - stub.toggleStateMock.opacity - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.isToggleInteractionEnabledPublisherMock - ) - XCTAssertPublisherSinkValueNil( - on: stub.toggleOpacityPublisherMock - ) - } - } - - func testToggleDotImage( - on stub: Stub, - expectedContainsValue: Bool = true - ) { - if expectedContainsValue { - XCTAssertPublisherSinkValueEqual( - on: stub.toggleDotImagesStatePublisherMock, - .mocked() - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.toggleDotImagesStatePublisherMock - ) - } - } - - func testDisplayedText( - on stub: Stub, - givenNewTheme: Theme? = nil, - expectedContainsValue: Bool = true - ) { - if expectedContainsValue { - XCTAssertPublisherSinkValueEqual( - on: stub.displayedTextPublisherMock, - .mocked() - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.displayedTextPublisherMock - ) - } - } - - func testTextFont( - on stub: Stub, - givenNewTheme: Theme? = nil, - expectedContainsValue: Bool = true - ) { - if expectedContainsValue { - let themeMock = givenNewTheme ?? stub.themeMock - XCTAssertPublisherSinkValueIdentical( - on: stub.textFontTokenPublisherMock, - themeMock.typography.body1 as? TypographyFontTokenGeneratedMock - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.textFontTokenPublisherMock - ) - } - } - - func testShowToggleLeftSpace( - on stub: Stub, - expectedValue: Bool? = nil - ) { - if let expectedValue { - XCTAssertPublisherSinkValueEqual( - on: stub.showToggleLeftSpacePublisherMock, - expectedValue - ) - } else { - XCTAssertPublisherSinkValueNil( - on: stub.showToggleLeftSpacePublisherMock - ) - } - } -} - -// MARK: - Stub - -private final class Stub { - - // MARK: - Properties - - let viewModel: SwitchViewModel - - // MARK: - Data Properties - - let frameworkType: FrameworkType - let isOnMock: Bool - - let themeMock = ThemeGeneratedMock.mocked() - - let colorsMock = SwitchColors.mocked() - - let imageMock = IconographyTests.shared.switchOn - let imagesMock = SwitchUIImages(on: UIImage(), off: UIImage()) - - let colorTokenMock = ColorTokenGeneratedMock.random() - - let positionMock = SwitchPosition.mocked() - - let toggleStateMock: SwitchToggleState - - // MARK: - Dependencies Properties - - let getColorsUseCaseMock: SwitchGetColorsUseCaseableGeneratedMock - let getImagesStateUseCaseMock: SwitchGetImagesStateUseCaseableGeneratedMock - let getToggleColorUseCaseMock: SwitchGetToggleColorUseCaseableGeneratedMock - let getPositionUseCaseMock: SwitchGetPositionUseCaseableGeneratedMock - let getToggleStateUseCaseMock: SwitchGetToggleStateUseCaseableGeneratedMock - let displayedTextViewModelMock: DisplayedTextViewModelGeneratedMock - - let dependenciesMock: SwitchViewModelDependenciesProtocolGeneratedMock - - // MARK: - Publisher Properties - - let isOnChangedPublisherMock: PublisherMock.Publisher> - let isToggleInteractionEnabledPublisherMock: PublisherMock.Publisher> - let toggleOpacityPublisherMock: PublisherMock.Publisher> - let toggleBackgroundColorTokenPublisherMock: PublisherMock.Publisher> - let toggleDotBackgroundColorTokenPublisherMock: PublisherMock.Publisher> - let toggleDotForegroundColorTokenPublisherMock: PublisherMock.Publisher> - let textForegroundColorTokenPublisherMock: PublisherMock.Publisher> - let isToggleOnLeftPublisherMock: PublisherMock.Publisher> - let horizontalSpacingPublisherMock: PublisherMock.Publisher> - let showToggleLeftSpacePublisherMock: PublisherMock.Publisher> - let toggleDotImagesStatePublisherMock: PublisherMock.Publisher> - let displayedTextPublisherMock: PublisherMock.Publisher> - let textFontTokenPublisherMock: PublisherMock.Publisher> - - // MARK: - Initialization - - init( - frameworkType: FrameworkType = .uiKit, - isOn: Bool = true, - alignment: SwitchAlignment = .left, - intent: SwitchIntent = .alert, - isEnabled: Bool = true, - isImages: Bool = false, - text: String? = nil, - attributedText: NSAttributedString? = nil, - userInteractionEnabled: Bool = true - ) { - // Data properties - self.frameworkType = frameworkType - self.isOnMock = isOn - - let toggleStateMock = SwitchToggleState.mocked( - interactionEnabled: userInteractionEnabled - ) - self.toggleStateMock = toggleStateMock - - // ** - // Dependencies - let getColorsUseCaseMock = SwitchGetColorsUseCaseableGeneratedMock() - getColorsUseCaseMock.executeWithIntentAndColorsAndDimsReturnValue = self.colorsMock - self.getColorsUseCaseMock = getColorsUseCaseMock - - let getImagesStateUseCaseMock = SwitchGetImagesStateUseCaseableGeneratedMock() - getImagesStateUseCaseMock.executeWithIsOnAndImagesReturnValue = .mocked() - self.getImagesStateUseCaseMock = getImagesStateUseCaseMock - - let getToggleColorUseCaseMock = SwitchGetToggleColorUseCaseableGeneratedMock() - getToggleColorUseCaseMock.executeWithIsOnAndStatusAndStateColorReturnValue = self.colorTokenMock - self.getToggleColorUseCaseMock = getToggleColorUseCaseMock - - let getPositionUseCaseMock = SwitchGetPositionUseCaseableGeneratedMock() - getPositionUseCaseMock.executeWithAlignmentAndSpacingAndContainsTextReturnValue = self.positionMock - self.getPositionUseCaseMock = getPositionUseCaseMock - - let getToggleStateUseCaseMock = SwitchGetToggleStateUseCaseableGeneratedMock() - getToggleStateUseCaseMock.executeWithIsEnabledAndDimsReturnValue = toggleStateMock - self.getToggleStateUseCaseMock = getToggleStateUseCaseMock - - let displayedTextViewModelMock = DisplayedTextViewModelGeneratedMock() - displayedTextViewModelMock.underlyingDisplayedTextType = .text - displayedTextViewModelMock.displayedText = .mocked() - self.displayedTextViewModelMock = displayedTextViewModelMock - - let dependenciesMock = SwitchViewModelDependenciesProtocolGeneratedMock() - dependenciesMock.underlyingGetColorsUseCase = self.getColorsUseCaseMock - dependenciesMock.underlyingGetImagesStateUseCase = self.getImagesStateUseCaseMock - dependenciesMock.underlyingGetToggleColorUseCase = self.getToggleColorUseCaseMock - dependenciesMock.underlyingGetPositionUseCase = self.getPositionUseCaseMock - dependenciesMock.underlyingGetToggleStateUseCase = self.getToggleStateUseCaseMock - dependenciesMock.makeDisplayedTextViewModelWithTextAndAttributedTextReturnValue = self.displayedTextViewModelMock - self.dependenciesMock = dependenciesMock - // ** - - // ** - // View Model - let imagesEither: SwitchImagesEither? - if isImages { - imagesEither = .left(self.imagesMock) - } else { - imagesEither = nil - } - - let attributedTextEither: AttributedStringEither? - if let attributedText { - attributedTextEither = .left(attributedText) - } else { - attributedTextEither = nil - } - - let viewModel = SwitchViewModel( - for: frameworkType, - theme: self.themeMock, - isOn: isOn, - alignment: alignment, - intent: intent, - isEnabled: isEnabled, - images: imagesEither, - text: text, - attributedText: attributedTextEither, - dependencies: dependenciesMock - ) - self.viewModel = viewModel - // ** - - // ** - // Publishers - self.isOnChangedPublisherMock = .init(publisher: viewModel.$isOnChanged) - self.isToggleInteractionEnabledPublisherMock = .init(publisher: viewModel.$isToggleInteractionEnabled) - self.toggleOpacityPublisherMock = .init(publisher: viewModel.$toggleOpacity) - self.toggleBackgroundColorTokenPublisherMock = .init(publisher: viewModel.$toggleBackgroundColorToken) - self.toggleDotBackgroundColorTokenPublisherMock = .init(publisher: viewModel.$toggleDotBackgroundColorToken) - self.toggleDotForegroundColorTokenPublisherMock = .init(publisher: viewModel.$toggleDotForegroundColorToken) - self.textForegroundColorTokenPublisherMock = .init(publisher: viewModel.$textForegroundColorToken) - self.isToggleOnLeftPublisherMock = .init(publisher: viewModel.$isToggleOnLeft) - self.horizontalSpacingPublisherMock = .init(publisher: viewModel.$horizontalSpacing) - self.showToggleLeftSpacePublisherMock = .init(publisher: viewModel.$showToggleLeftSpace) - self.toggleDotImagesStatePublisherMock = .init(publisher: viewModel.$toggleDotImagesState) - self.displayedTextPublisherMock = .init(publisher: viewModel.$displayedText) - self.textFontTokenPublisherMock = .init(publisher: viewModel.$textFontToken) - // ** - } - - func subscribePublishers(on subscriptions: inout Set) { - self.isOnChangedPublisherMock.loadTesting(on: &subscriptions) - self.isToggleInteractionEnabledPublisherMock.loadTesting(on: &subscriptions) - self.toggleOpacityPublisherMock.loadTesting(on: &subscriptions) - self.toggleBackgroundColorTokenPublisherMock.loadTesting(on: &subscriptions) - self.toggleDotBackgroundColorTokenPublisherMock.loadTesting(on: &subscriptions) - self.toggleDotForegroundColorTokenPublisherMock.loadTesting(on: &subscriptions) - self.textForegroundColorTokenPublisherMock.loadTesting(on: &subscriptions) - self.isToggleOnLeftPublisherMock.loadTesting(on: &subscriptions) - self.horizontalSpacingPublisherMock.loadTesting(on: &subscriptions) - self.showToggleLeftSpacePublisherMock.loadTesting(on: &subscriptions) - self.toggleDotImagesStatePublisherMock.loadTesting(on: &subscriptions) - self.displayedTextPublisherMock.loadTesting(on: &subscriptions) - self.textFontTokenPublisherMock.loadTesting(on: &subscriptions) - } - - func resetMockedData() { - // Clear UseCases Mock - let useCases: [ResetGeneratedMock] = [ - self.getColorsUseCaseMock, - self.getImagesStateUseCaseMock, - self.getToggleColorUseCaseMock, - self.getPositionUseCaseMock, - self.getToggleStateUseCaseMock - ] - useCases.forEach { $0.reset() } - - // Reset published sink counter - self.isOnChangedPublisherMock.reset() - self.isToggleInteractionEnabledPublisherMock.reset() - self.toggleOpacityPublisherMock.reset() - self.toggleBackgroundColorTokenPublisherMock.reset() - self.toggleDotBackgroundColorTokenPublisherMock.reset() - self.toggleDotForegroundColorTokenPublisherMock.reset() - self.textForegroundColorTokenPublisherMock.reset() - self.isToggleOnLeftPublisherMock.reset() - self.horizontalSpacingPublisherMock.reset() - self.showToggleLeftSpacePublisherMock.reset() - self.toggleDotImagesStatePublisherMock.reset() - self.displayedTextPublisherMock.reset() - self.textFontTokenPublisherMock.reset() - } -} diff --git a/core/Sources/Components/Tab/AccessibilityIdentifier/TabAccessibilityIdentifier.swift b/core/Sources/Components/Tab/AccessibilityIdentifier/TabAccessibilityIdentifier.swift deleted file mode 100644 index 565fbaa81..000000000 --- a/core/Sources/Components/Tab/AccessibilityIdentifier/TabAccessibilityIdentifier.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// TabAccessibilityIdentifier.swift -// SparkCore -// -// Created by michael.zimmermann on 04.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum TabAccessibilityIdentifier { - public static let tabItem = "spark-tab-item" - public static let tab = "spark-tab" -} diff --git a/core/Sources/Components/Tab/Enum/TabIntent.swift b/core/Sources/Components/Tab/Enum/TabIntent.swift deleted file mode 100644 index 2699e9715..000000000 --- a/core/Sources/Components/Tab/Enum/TabIntent.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TabIntent.swift -// SparkCore -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// `TabIntent` determines the color of the tab tint color. -public enum TabIntent: CaseIterable { - case basic - case main - case support -} diff --git a/core/Sources/Components/Tab/Enum/TabSize.swift b/core/Sources/Components/Tab/Enum/TabSize.swift deleted file mode 100644 index d90179302..000000000 --- a/core/Sources/Components/Tab/Enum/TabSize.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TabSize.swift -// SparkCore -// -// Created by michael.zimmermann on 02.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// The size of the content of the tabs -public enum TabSize: CaseIterable { - case xs - case sm - case md -} diff --git a/core/Sources/Components/Tab/Properties/TabItemColors.swift b/core/Sources/Components/Tab/Properties/TabItemColors.swift deleted file mode 100644 index 1c0748e72..000000000 --- a/core/Sources/Components/Tab/Properties/TabItemColors.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// TabItemColors.swift -// SparkCore -// -// Created by alican.aycil on 28.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Colors of the tab item: -/// - label: defines the color of the text and the tint color of the icon -/// - line: The color of the base line -/// - background: The background color of the tab item. -struct TabItemColors: Equatable, Updateable { - var icon: any ColorToken { - return self.label - } - var label: any ColorToken - var line: any ColorToken - var background: any ColorToken - var opacity: CGFloat - - init(label: any ColorToken, - line: any ColorToken, - background: any ColorToken, - opacity: CGFloat = 1) { - self.label = label - self.line = line - self.background = background - self.opacity = opacity - } - - static func == (lhs: TabItemColors, rhs: TabItemColors) -> Bool { - return colorsEqual(lhs.label, rhs.label) && colorsEqual(lhs.line, rhs.line) && colorsEqual(lhs.background, rhs.background) && lhs.opacity == rhs.opacity - } - - private static func colorsEqual(_ lhs: any ColorToken, _ rhs: any ColorToken) -> Bool { - return lhs.color == rhs.color && lhs.uiColor == rhs.uiColor - } -} diff --git a/core/Sources/Components/Tab/Properties/TabItemContent.swift b/core/Sources/Components/Tab/Properties/TabItemContent.swift deleted file mode 100644 index bbd773df9..000000000 --- a/core/Sources/Components/Tab/Properties/TabItemContent.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// TabItemContent.swift -// SparkCore -// -// Created by michael.zimmermann on 04.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public protocol TitleContaining { - var hasTitle: Bool { get } -} - -/// The content of a tab item. -public struct TabItemContent: TitleContaining, Equatable, Updateable { - public static func == (lhs: TabItemContent, rhs: TabItemContent) -> Bool { - return lhs.id == rhs.id - } - - /// A unique id of each tab item - public var id = UUID() - - /// An optional icon of a tab item - public var icon: Image? - - /// An optional title of a tab item - public var title: String? - - /// An optional attributed title. If a title is set, this will have preference over an attributed title - public var attributedTitle: AttributedString? - - /// An optional badge - public var badge: BadgeView? - - /// Return true if either title or attributed title are not nil. - /// An empty string in either title or attributed title will be treated as having a title and `hasTitle` will return true. - public var hasTitle: Bool { - return self.title != nil || self.attributedTitle != nil - } - - /// Initialization - /// - Parameters: - /// - icon: An optional image - /// - title: An optional title - public init(icon: Image? = nil, title: String? = nil) { - self.icon = icon - self.title = title - } -} diff --git a/core/Sources/Components/Tab/Properties/TabItemHeights.swift b/core/Sources/Components/Tab/Properties/TabItemHeights.swift deleted file mode 100644 index b45bd0d01..000000000 --- a/core/Sources/Components/Tab/Properties/TabItemHeights.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// TabItemHeights.swift -// SparkCore -// -// Created by michael.zimmermann on 08.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Heights of a tab item. -/// - separatorLineHeight: The height of the bottom line. -/// - itemHeight: The height of the item -/// - iconHeight: The height of the icon -struct TabItemHeights: Equatable, Updateable { - var separatorLineHeight: CGFloat - var itemHeight: CGFloat - var iconHeight: CGFloat -} diff --git a/core/Sources/Components/Tab/Properties/TabItemSpacings.swift b/core/Sources/Components/Tab/Properties/TabItemSpacings.swift deleted file mode 100644 index cdea65206..000000000 --- a/core/Sources/Components/Tab/Properties/TabItemSpacings.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// TabItemSpacings.swift -// SparkCore -// -// Created by alican.aycil on 28.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Spacings of the tab item: -/// - verticalEdge: The vertical edge determines top and bottom spacing of the tab item. -/// - horizontalEdge: The horizontal edge determines leading and trailing spacing of the tab item. -/// - content: The content determines the space between the icon, text and badge. -struct TabItemSpacings: Equatable { - - let verticalEdge: CGFloat - let horizontalEdge: CGFloat - let content: CGFloat - - static func == (lhs: TabItemSpacings, rhs: TabItemSpacings) -> Bool { - return lhs.verticalEdge == rhs.verticalEdge && - lhs.horizontalEdge == lhs.horizontalEdge && - lhs.content == rhs.content - } -} diff --git a/core/Sources/Components/Tab/Properties/TabState.swift b/core/Sources/Components/Tab/Properties/TabState.swift deleted file mode 100644 index 9703a2927..000000000 --- a/core/Sources/Components/Tab/Properties/TabState.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// TabState.swift -// SparkCore -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// `TabState` determines the current state of the tab. -struct TabState: Equatable, Updateable { - var isEnabled: Bool - var isPressed: Bool - var isSelected: Bool - - init( - isEnabled: Bool = true, - isPressed: Bool = false, - isSelected: Bool = false - ) { - self.isEnabled = isEnabled - self.isPressed = isPressed - self.isSelected = isSelected - } -} diff --git a/core/Sources/Components/Tab/Properties/TabStateAttributes.swift b/core/Sources/Components/Tab/Properties/TabStateAttributes.swift deleted file mode 100644 index 6a281c724..000000000 --- a/core/Sources/Components/Tab/Properties/TabStateAttributes.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// TabStateAttributes.swift -// SparkCore -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Attributes available for the states of the tab: -/// - spacings: Spacings of the tab item. -/// - colors: Colors of the tab item. -/// - opacity: The opacity of the tab item. -/// - separatorLineHeight: The lineHeight of the tab item. -struct TabStateAttributes: Equatable { - - let spacings: TabItemSpacings - let colors: TabItemColors - let heights: TabItemHeights - let font: TypographyFontToken - - static func == (lhs: TabStateAttributes, rhs: TabStateAttributes) -> Bool { - return lhs.spacings == rhs.spacings && - lhs.colors == rhs.colors && - lhs.heights == rhs.heights && - lhs.font.font == rhs.font.font - } -} diff --git a/core/Sources/Components/Tab/Properties/TabUIItemContent.swift b/core/Sources/Components/Tab/Properties/TabUIItemContent.swift deleted file mode 100644 index 95556fab7..000000000 --- a/core/Sources/Components/Tab/Properties/TabUIItemContent.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// TabItemContent.swift -// SparkCore -// -// Created by alican.aycil on 25.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// Contents of the tab: -/// - icon: The icon of the tab item -/// - text: The text of the tab item. -public struct TabUIItemContent: TitleContaining, Equatable, Updateable { - public var icon: UIImage? - public var title: String? - - /// Return true if the title is not nil. - /// If the title is an empty string, `hasTitle` will return true. - public var hasTitle: Bool { - return title != nil - } - - public init( - title: String - ) { - self.icon = nil - self.title = title - } - - public init( - icon: UIImage - ) { - self.icon = icon - self.title = nil - } - - public init( - icon: UIImage? = nil, - title: String? = nil - ) { - self.icon = icon - self.title = title - } - -} diff --git a/core/Sources/Components/Tab/Properties/TabsAttributes.swift b/core/Sources/Components/Tab/Properties/TabsAttributes.swift deleted file mode 100644 index 8a73b73c5..000000000 --- a/core/Sources/Components/Tab/Properties/TabsAttributes.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// TabAttributes.swift -// SparkCore -// -// Created by michael.zimmermann on 30.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -/// Specific attributes of the tab control. -/// - lineHeight: The height of the bottom line. -/// - lineColor: The color of the bottom line. -/// - backgroundColor: The background color of the tab control. -struct TabsAttributes: Equatable { - let lineHeight: CGFloat - let itemHeight: CGFloat - let lineColor: any ColorToken - let backgroundColor: any ColorToken - - static func == (lhs: TabsAttributes, rhs: TabsAttributes) -> Bool { - return lhs.backgroundColor.equals(rhs.backgroundColor) && - lhs.lineColor.equals(rhs.lineColor) && - lhs.lineHeight == rhs.lineHeight && - lhs.itemHeight == rhs.itemHeight - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabGetFontUseCase.swift b/core/Sources/Components/Tab/UseCases/TabGetFontUseCase.swift deleted file mode 100644 index f0796f266..000000000 --- a/core/Sources/Components/Tab/UseCases/TabGetFontUseCase.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// TabGetFontUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 02.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TabGetFontUseCaseable { - func execute(typography: Typography, - size: TabSize - ) -> TypographyFontToken -} - -/// Use case which returns the font according to the tab size -struct TabGetFontUseCase: TabGetFontUseCaseable { - - /// Calculate the font according to the tab size - /// - Parameters: - /// - typograph, the current typograph from which to select a font - /// - size: the given tab size - /// - /// - Returns: TypographyFontToken - func execute(typography: Typography, - size: TabSize - ) -> TypographyFontToken { - switch size { - case .xs: return typography.caption - case .sm: return typography.body2 - case .md: return typography.body1 - } - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabGetFontUseCaseTests.swift b/core/Sources/Components/Tab/UseCases/TabGetFontUseCaseTests.swift deleted file mode 100644 index a285e3108..000000000 --- a/core/Sources/Components/Tab/UseCases/TabGetFontUseCaseTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// TabGetFontUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 02.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class TabGetFontUseCaseTests: XCTestCase { - - // MARK: - Properties - var typography: TypographyGeneratedMock! - var sut: TabGetFontUseCase! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.typography = TypographyGeneratedMock.mocked() - self.sut = TabGetFontUseCase() - } - - // MARK: - Tests - func test_size_md() throws { - let font = sut.execute(typography: self.typography, size: .md) - - XCTAssertEqual(font.font, typography.body1.font) - } - - func test_size_xs() throws { - let font = sut.execute(typography: self.typography, size: .xs) - - XCTAssertEqual(font.font, typography.caption.font) - } - - func test_size_sm() throws { - let font = sut.execute(typography: self.typography, size: .sm) - - XCTAssertEqual(font.font, typography.body2.font) - } - -} diff --git a/core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCase.swift b/core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCase.swift deleted file mode 100644 index f33ead9b3..000000000 --- a/core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCase.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// TabGetIntentColorUseCase.swift -// SparkCore -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TabGetIntentColorUseCaseble { - func execute(colors: any Colors, intent: TabIntent) -> any ColorToken -} - -/// TabGetColorUseCase -/// Use case to determin the colors of the Tab by the intent -/// Functions: -/// - execute: returns a color token for given colors and an intent -struct TabGetIntentColorUseCase: TabGetIntentColorUseCaseble { - - // MARK: - Functions - /// - /// Calculate the color of the tab depending on the intent - /// - /// - Parameters: - /// - colors: Colors from the theme - /// - intent: `TabIntent`. - /// - /// - Returns: ``ColorToken`` return color of the tab. - func execute(colors: any Colors, intent: TabIntent) -> any ColorToken { - switch intent { - case .basic: return colors.basic.basic - case .main: return colors.main.main - case .support: return colors.support.support - } - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCaseTests.swift b/core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCaseTests.swift deleted file mode 100644 index 6a49c8b08..000000000 --- a/core/Sources/Components/Tab/UseCases/TabGetIntentColorUseCaseTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// TabGetIntentColorUseCaseTests.swift -// SparkCoreTests -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class TabGetIntentColorUseCaseTests: XCTestCase { - - // MARK: - Private properties - private var sut: TabGetIntentColorUseCase! - private var colors: ColorsGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.sut = TabGetIntentColorUseCase() - self.colors = ColorsGeneratedMock.mocked() - } - - // MARK: - Tests - func test_execute_main() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .main).color, self.colors.main.main.color) - } - - func test_execute_support() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .support).color, self.colors.support.support.color) - } - - func test_execute_basic() { - XCTAssertEqual(self.sut.execute(colors: self.colors, intent: .basic).color, self.colors.basic.basic.color) - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCase.swift b/core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCase.swift deleted file mode 100644 index 8caee42d3..000000000 --- a/core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCase.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// TabGetStateAttributesUseCase.swift -// SparkCore -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TabGetStateAttributesUseCasable { - func execute(theme: Theme, - intent: TabIntent, - state: TabState, - tabSize: TabSize, - hasTitle: Bool) -> TabStateAttributes -} - -/// TabGetStateAttributesUseCase -/// Use case to determine the attributes of the Tab -/// Functions: -/// - execute: returns attributes for given theme, intent and state -struct TabGetStateAttributesUseCase: TabGetStateAttributesUseCasable { - - private let getIntentColorUseCase: any TabGetIntentColorUseCaseble - private let getFontUseCase: any TabGetFontUseCaseable - - // MARK: - Initializer - init(getIntentColorUseCase: any TabGetIntentColorUseCaseble = TabGetIntentColorUseCase(), - getTabFontUseCase: any TabGetFontUseCaseable = TabGetFontUseCase() - ) { - self.getIntentColorUseCase = getIntentColorUseCase - self.getFontUseCase = getTabFontUseCase - } - - // MARK: - Functions - /// - /// Calculate the attribute of the tab depending on the theme, intent and state - /// - /// - Parameters: - /// - theme: current theme - /// - intent: `TabIntent`. - /// - state: `TabState`. - /// - size: `TabSize` - /// - /// - Returns: ``TabStateAttributes`` return attributes of the tab. - func execute(theme: Theme, - intent: TabIntent, - state: TabState, - tabSize: TabSize, - hasTitle: Bool - ) -> TabStateAttributes { - - let size = hasTitle ? tabSize : TabSize.md - - let font = self.getFontUseCase.execute(typography: theme.typography, size: size) - - let spacings = TabItemSpacings( - verticalEdge: theme.layout.spacing.medium, - horizontalEdge: theme.layout.spacing.large, - content: theme.layout.spacing.medium - ) - - let colors = TabItemColors( - label: theme.colors.base.onSurface, - line: theme.colors.base.outline, - background: ColorTokenDefault.clear - ) - - let heights = TabItemHeights( - separatorLineHeight: theme.border.width.small, - itemHeight: size.itemHeight, - iconHeight: size.iconHeight - ) - - if !state.isEnabled { - return TabStateAttributes( - spacings: spacings, - colors: colors.update(\.opacity, value: theme.dims.dim3), - heights: heights, - font: font - ) - } - - if state.isPressed { - let pressedColors = TabItemColors( - label: theme.colors.base.onSurface.opacity(theme.dims.dim1), - line: theme.colors.base.outline, - background: theme.colors.states.surfacePressed) - - return TabStateAttributes( - spacings: spacings, - colors: pressedColors, - heights: heights, - font: font - ) - } - - if state.isSelected { - let intentColor = self.getIntentColorUseCase.execute(colors: theme.colors, intent: intent) - let selectedColors = TabItemColors( - label: intentColor, - line: intentColor, - background: ColorTokenDefault.clear - ) - return TabStateAttributes( - spacings: spacings, - colors: selectedColors, - heights: heights.update(\.separatorLineHeight, value: theme.border.width.medium), - font: font - ) - } - - return TabStateAttributes( - spacings: spacings, - colors: colors, - heights: heights, - font: font - ) - } -} - -private extension CGFloat { - static let medium: CGFloat = 40 - static let small: CGFloat = 36 - static let extraSmall: CGFloat = 34 - - static let fontMd: CGFloat = 16 - static let fontSm: CGFloat = 14 - static let fontXs: CGFloat = 12 -} - -extension TabSize { - var itemHeight: CGFloat { - switch self { - case .md: return .medium - case .sm: return .small - case .xs: return .extraSmall - } - } -} - -private extension TabSize { - var iconHeight: CGFloat { - switch self { - case .md: return .fontMd - case .sm: return .fontSm - case .xs: return .fontXs - } - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCaseTests.swift b/core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCaseTests.swift deleted file mode 100644 index 024865116..000000000 --- a/core/Sources/Components/Tab/UseCases/TabGetStateAttributesUseCaseTests.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// TabGetStateAttributesUseCaseTests.swift -// SparkCoreTests -// -// Created by alican.aycil on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class TabGetStateAttributesUseCaseTests: XCTestCase { - - // MARK: - Private properties - private var sut: TabGetStateAttributesUseCase! - private var theme: ThemeGeneratedMock! - private var getIntentColorUseCase: TabGetIntentColorUseCasebleGeneratedMock! - private var getFontUseCase: TabGetFontUseCaseableGeneratedMock! - private var spacings: TabItemSpacings! - private var colors: TabItemColors! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - self.getIntentColorUseCase = TabGetIntentColorUseCasebleGeneratedMock() - self.getFontUseCase = TabGetFontUseCaseableGeneratedMock() - - self.sut = TabGetStateAttributesUseCase( - getIntentColorUseCase: getIntentColorUseCase, - getTabFontUseCase: getFontUseCase - ) - self.spacings = TabItemSpacings( - verticalEdge: self.theme.layout.spacing.medium, - horizontalEdge: self.theme.layout.spacing.large, - content: self.theme.layout.spacing.medium - ) - self.colors = TabItemColors( - label: self.theme.colors.base.onSurface, - line: self.theme.colors.base.outline, - background: ColorTokenDefault.clear - ) - - self.getFontUseCase.executeWithTypographyAndSizeReturnValue = self.theme.typography.body2 - } - - // MARK: - Tests - func test_selected() { - let mockedColor = ColorTokenGeneratedMock(uiColor: .black) - self.getIntentColorUseCase.executeWithColorsAndIntentReturnValue = mockedColor - let stateAttribute = sut.execute( - theme: self.theme, - intent: .main, - state: .selected, - tabSize: .md, - hasTitle: true - ) - let selectedColors = TabItemColors( - label: mockedColor, - line: mockedColor, - background: ColorTokenDefault.clear - ) - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.medium, - itemHeight: 40, - iconHeight: 16 - ) - - let expectedAttribute = TabStateAttributes( - spacings: self.spacings, - colors: selectedColors, - heights: expectedHeights, - font: self.theme.typography.body1 - ) - XCTAssertEqual(stateAttribute, expectedAttribute) - } - - func test_enabled() { - let stateAttribute = sut.execute( - theme: self.theme, - intent: .main, - state: .enabled, - tabSize: .md, - hasTitle: true - ) - - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.small, - itemHeight: 40, - iconHeight: 16 - ) - - let expectedAttribute = TabStateAttributes( - spacings: self.spacings, - colors: self.colors, - heights: expectedHeights, - font: self.theme.typography.body1 - ) - XCTAssertEqual(stateAttribute, expectedAttribute) - } - - func test_pressed() { - let stateAttribute = sut.execute( - theme: self.theme, - intent: .main, - state: .pressed, - tabSize: .sm, - hasTitle: true - ) - - self.colors = TabItemColors( - label: self.theme.colors.base.onSurface.opacity(self.theme.dims.dim1), - line: self.theme.colors.base.outline, - background: self.theme.colors.states.surfacePressed - ) - - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.small, - itemHeight: 36, - iconHeight: 14 - ) - - let expectedAttribute = TabStateAttributes( - spacings: self.spacings, - colors: self.colors, - heights: expectedHeights, - font: self.theme.typography.body1 - ) - XCTAssertEqual(stateAttribute, expectedAttribute) - } - - func test_disabled() { - let stateAttribute = sut.execute( - theme: self.theme, - intent: .main, - state: .disabled, - tabSize: .xs, - hasTitle: true - ) - - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.small, - itemHeight: 34, - iconHeight: 12 - ) - - let expectedAttribute = TabStateAttributes( - spacings: self.spacings, - colors: self.colors.update(\.opacity, value: theme.dims.dim3), - heights: expectedHeights, - font: self.theme.typography.body1 - ) - XCTAssertEqual(stateAttribute, expectedAttribute) - } - - func test_no_title() { - let stateAttribute = sut.execute( - theme: self.theme, - intent: .main, - state: .disabled, - tabSize: .xs, - hasTitle: false - ) - - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.small, - itemHeight: 40, - iconHeight: 16 - ) - - let expectedAttribute = TabStateAttributes( - spacings: self.spacings, - colors: self.colors.update(\.opacity, value: theme.dims.dim3), - heights: expectedHeights, - font: self.theme.typography.body1 - ) - XCTAssertEqual(stateAttribute, expectedAttribute) - } -} - -private extension TabState { - static var enabled: TabState { - return .init() - } - - static var selected: TabState { - return .init(isSelected: true) - } - - static var pressed: TabState { - return .init(isPressed: true) - } - - static var disabled: TabState { - return .init(isEnabled: false) - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCase.swift b/core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCase.swift deleted file mode 100644 index 1be82ae47..000000000 --- a/core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCase.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// TabsGetAttributesUseCase.swift -// SparkCore -// -// Created by michael.zimmermann on 30.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TabsGetAttributesUseCaseable { - func execute(theme: Theme, size: TabSize, isEnabled: Bool) -> TabsAttributes -} - -/// TabGetColorUseCase -/// Use case to determin the colors of the Tab by the intent -/// Functions: -/// - execute: returns a color token for given colors and an intent -struct TabsGetAttributesUseCase: TabsGetAttributesUseCaseable { - - // MARK: - Functions - /// - /// Calculate the attributes of the tab control depending on wheter it is enabled or not. - /// - /// - Parameters: - /// - theme: The theme - /// - size: The tab size - /// - isEnabled: Bool. - /// - /// - Returns: ``TabsAttributes`` containing colors and line hight of the tab control. - func execute(theme: Theme, size: TabSize, isEnabled: Bool) -> TabsAttributes { - - var lineColor = theme.colors.base.outline - if !isEnabled { - lineColor = lineColor.opacity(theme.dims.dim3) - } - - return TabsAttributes( - lineHeight: theme.border.width.small, - itemHeight: size.itemHeight, - lineColor: lineColor, - backgroundColor: ColorTokenDefault.clear - ) - } -} diff --git a/core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCaseTests.swift b/core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCaseTests.swift deleted file mode 100644 index 0c9e63cc6..000000000 --- a/core/Sources/Components/Tab/UseCases/TabsGetAttributesUseCaseTests.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// TabsGetAttributesUseCaseTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 01.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -final class TabsGetAttributesUseCaseTests: XCTestCase { - - // MARK: - Properties - var sut: TabsGetAttributesUseCase! - var theme: Theme! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.theme = ThemeGeneratedMock.mocked( - lineHeight: 5.0, - lineColor: .red, - backgroundColor: .green, - opacity: 0.1 - ) - - self.sut = TabsGetAttributesUseCase() - } - - // MARK: - Tests - func test_execute_enabled() { - - let expectedAttributes = TabsAttributes( - lineHeight: 5, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenDefault.clear - ) - - let attributes = self.sut.execute(theme: self.theme, size: .md, isEnabled: true) - - XCTAssertEqual(attributes, expectedAttributes) - } - - func test_execute_disabled() { - - let attributes = self.sut.execute(theme: self.theme, size: .md, isEnabled: false) - - XCTAssertEqual(attributes.lineColor.uiColor, .red.withAlphaComponent(0.1), "Expect line to have an opacity.") - XCTAssertEqual(attributes.backgroundColor.uiColor, .clear, "Expect background to remain the same.") - } -} - -// MARK: - Setup Mocks -private extension ThemeGeneratedMock { - static func mocked(lineHeight: CGFloat, - lineColor: UIColor, - backgroundColor: UIColor, - opacity: CGFloat - ) -> ThemeGeneratedMock{ - let border = BorderGeneratedMock() - let width = BorderWidthGeneratedMock() - width.small = 5 - border.width = width - - let colors = ColorsGeneratedMock() - let base = ColorsBaseGeneratedMock() - let surface = ColorTokenGeneratedMock(uiColor: backgroundColor) - let outline = ColorTokenGeneratedMock(uiColor: lineColor) - base.surface = surface - base.outline = outline - colors.base = base - - let dims = DimsGeneratedMock() - dims.dim3 = opacity - - let theme = ThemeGeneratedMock() - theme.border = border - theme.colors = colors - theme.dims = dims - - return theme - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabApportionsSizeView.swift b/core/Sources/Components/Tab/View/SwiftUI/TabApportionsSizeView.swift deleted file mode 100644 index a7e712d4b..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabApportionsSizeView.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// TabApportionsSizeView.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -/// TabApportionsSizeView is the similar to a SegmentControl with apportionsSegmentWidthsByContent == true. -struct TabApportionsSizeView: View { - private let intent: TabIntent - - @ObservedObject private var viewModel: TabViewModel - @Binding private var selectedIndex: Int - @ScaledMetric private var factor: CGFloat = 1.0 - @State private var screenWidth: CGFloat = UIScreen.main.bounds.width - @State private var axis: Axis.Set = .horizontal - @State private var apportionsContentWidth: CGFloat = 0 - - private var lineHeight: CGFloat { - return self.viewModel.tabsAttributes.lineHeight * self.factor - } - - private var itemHeight: CGFloat { - return self.viewModel.tabsAttributes.itemHeight * self.factor - } - - /// Initializer - /// - Parameters: - /// - viewModel: the current view model - /// - intent: the tab intent. - /// - selectedIndex: Binding of the index of the current selected tab. - init(viewModel: TabViewModel, - intent: TabIntent, - selectedIndex: Binding - ) { - self.intent = intent - self._selectedIndex = selectedIndex - self.viewModel = viewModel - } - - // MARK: - View - var body: some View { - ZStack(alignment: .bottom) { - GeometryReader { geometry in - VStack(alignment: .trailing) { - Spacer() - - TabBackgroundLine( - lineHeight: self.lineHeight, - width: geometry.size.width, - color: self.viewModel.tabsAttributes.lineColor.color) - .onAppear { - self.screenWidth = geometry.size.width - self.updateAxis() - } - .onChange(of: geometry.size.width) { width in - self.screenWidth = width - self.updateAxis() - } - } - .frame(height: self.itemHeight) - } - - ScrollView(self.axis, showsIndicators: false) { - self.tabItems() - .frame(height: self.itemHeight) - .accessibilityIdentifier(TabAccessibilityIdentifier.tab) - } - } - .frame(height: self.itemHeight) - } - - // MARK: - Private functions - @ViewBuilder - private func tabItems() -> some View { - ScrollViewReader { proxy in - HStack(spacing: 0) { - ForEach(Array(self.viewModel.content.enumerated()), id: \.element.id) { index, content in - self.tabItem(index: index, content: content, proxy: proxy) - } - Spacer() - } - } - .onChange(of: self.viewModel.content) { _ in - self.apportionsContentWidth = 0 - } - } - - @ViewBuilder - private func tabItem(index: Int, content: TabItemContent, proxy: ScrollViewProxy) -> some View { - TabSingleItem( - viewModel: self.viewModel, - intent: self.intent, - content: content, - proxy: proxy, - selectedIndex: self.$selectedIndex, - index: index) - .background { - GeometryReader { geometry in - Color.clear - .onAppear { - self.updateApportionsContentWidth(geometry.size.width, index: index) - } - .onChange(of: geometry.size) { size in - self.updateApportionsContentWidth(geometry.size.width, index: index) - } - } - } - } - - private func updateApportionsContentWidth(_ width: CGFloat, index: Int) { - self.apportionsContentWidth += width - self.updateAxis() - } - - private func updateAxis() { - self.axis = self.apportionsContentWidth > self.screenWidth ? .horizontal : [] - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabBackgroundLine.swift b/core/Sources/Components/Tab/View/SwiftUI/TabBackgroundLine.swift deleted file mode 100644 index 0df089339..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabBackgroundLine.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// TabBackgroundLine.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -struct TabBackgroundLine: View { - let lineHeight: CGFloat - let width: CGFloat - let color: Color - - var body: some View { - Rectangle() - .frame(maxWidth: .infinity) - .frame(height: self.lineHeight) - .frame(width: self.width) - .foregroundColor(self.color) - - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabEqualSizeView.swift b/core/Sources/Components/Tab/View/SwiftUI/TabEqualSizeView.swift deleted file mode 100644 index 74ab61c60..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabEqualSizeView.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// TabEqualSizeView.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -/// TabApportionsSizeView is the similar to a SegmentControl with apportionsSegmentWidthsByContent == false. -struct TabEqualSizeView: View { - private let intent: TabIntent - - @StateObject private var widthStates = WidthStates() - @ObservedObject private var viewModel: TabViewModel - @Binding private var selectedIndex: Int - @ScaledMetric private var factor: CGFloat = 1.0 - @State private var minItemWidth: CGFloat = 40.0 - @State private var screenWidth: CGFloat = 0 - @State private var axis: Axis.Set = .horizontal - - private var tabsWidth: CGFloat { - return self.minItemWidth * CGFloat(self.viewModel.content.count) - } - - private var lineHeight: CGFloat { - return self.viewModel.tabsAttributes.lineHeight * self.factor - } - - private var itemHeight: CGFloat { - return self.viewModel.tabsAttributes.itemHeight * self.factor - } - - /// Initializer - /// - Parameters: - /// - viewModel: the view model - /// - selectedIndex: A binding with the selected index. - /// - tab size: the default value is `md`. - /// - An array of tuples of image and string. - - init(viewModel: TabViewModel, - intent: TabIntent, - selectedIndex: Binding - ) { - self._selectedIndex = selectedIndex - self.intent = intent - self.viewModel = viewModel - } - - // MARK: - View - var body: some View { - ZStack(alignment: .bottom) { - GeometryReader { geometry in - VStack(alignment: .trailing) { - Spacer() - - TabBackgroundLine( - lineHeight: self.lineHeight, - width: geometry.size.width, - color: self.viewModel.tabsAttributes.lineColor.color) - .onAppear { - self.screenWidth = geometry.size.width - self.minItemWidth = geometry.size.width / CGFloat(self.viewModel.content.count) - } - .onChange(of: geometry.size.width) { width in - self.screenWidth = width - self.minItemWidth = self.widthStates.widths[round(width)] ?? width / CGFloat(self.viewModel.content.count) - } - } - .frame(height: self.itemHeight) - } - - ScrollView(self.axis, showsIndicators: false) { - self.tabItems() - .frame(height: self.itemHeight) - .accessibilityIdentifier(TabAccessibilityIdentifier.tab) - } - .onChange(of: self.viewModel.content) { content in - self.minItemWidth = self.screenWidth / CGFloat(content.count) - } - } - .frame(height: self.itemHeight) - } - - // MARK: - Private functions - @ViewBuilder - private func tabItems() -> some View { - ScrollViewReader { proxy in - HStack(spacing: 0) { - ForEach(Array(self.viewModel.content.enumerated()), id: \.element.id) { index, content in - self.tabItem(index: index, content: content, proxy: proxy) - .frame(minWidth: self.minItemWidth) - } - } - } - } - - @ViewBuilder - private func tabItem(index: Int, content: TabItemContent, proxy: ScrollViewProxy) -> some View { - TabSingleItem( - viewModel: self.viewModel, - intent: self.intent, - content: content, - proxy: proxy, - selectedIndex: self.$selectedIndex, - index: index) - .background { - GeometryReader { geometry in - Color.clear - .onAppear { - self.updateMinWidth(geometry.size.width, index: index) - } - .onChange(of: geometry.size) { size in - self.updateMinWidth(geometry.size.width, index: index) - } - } - } - } - - private func updateMinWidth(_ width: CGFloat, index: Int) { - self.minItemWidth = max(self.minItemWidth, width) - self.axis = floor(self.tabsWidth) > self.screenWidth ? .horizontal : [] - self.widthStates.widths[round(self.screenWidth)] = self.minItemWidth - } -} - -private class WidthStates: ObservableObject { - var widths: [CGFloat: CGFloat] = [:] -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabItemView.swift b/core/Sources/Components/Tab/View/SwiftUI/TabItemView.swift deleted file mode 100644 index 83daf0906..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabItemView.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// TabItemView.swift -// SparkCore -// -// Created by michael.zimmermann on 04.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A single tab item used on the tab view. -struct TabItemView: View { - - // MARK: - Private Variables - @ObservedObject private var viewModel: TabItemViewModel - - private let tapAction: () -> Void - - // MARK: - Scaled Metrics - @ScaledMetric private var factor: CGFloat = 1 - - private var lineHeight: CGFloat { - return self.viewModel.tabStateAttributes.heights.separatorLineHeight * self.factor - } - - private var itemHeight: CGFloat { - return self.viewModel.tabStateAttributes.heights.itemHeight * self.factor - } - private var iconHeight: CGFloat { - return self.viewModel.tabStateAttributes.heights.iconHeight * self.factor - } - private var paddingHorizontal: CGFloat { - return self.viewModel.tabStateAttributes.spacings.horizontalEdge * self.factor - } - private var spacing: CGFloat { - return self.viewModel.tabStateAttributes.spacings.content * self.factor - } - - // MARK: Initialization - /// Initializer - /// - Parameters: - /// - viewModel: The view model of the tab item. - /// - tapAction: the action triggered by tapping on the tab. - init( - viewModel: TabItemViewModel, - tapAction: @escaping () -> Void - ) { - self.viewModel = viewModel - self.tapAction = tapAction - } - - // MARK: - View - var body: some View { - Button( - action: { - self.tapAction() - }, - label: { - self.tabContent() - .background(self.viewModel.tabStateAttributes.colors.background.color) - .contentShape(Rectangle()) - }) - .opacity(self.viewModel.tabStateAttributes.colors.opacity) - .buttonStyle(PressedButtonStyle(isPressed: self.$viewModel.isPressed, animationDuration: 0.1)) - } - - // MARK: Private Functions - @ViewBuilder - private func tabContent() -> some View { - HStack(alignment: .center, spacing: self.spacing) { - spacer() - if let icon = self.viewModel.content.icon { - icon - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(self.viewModel.tabStateAttributes.colors.icon.color) - .frame(width: self.iconHeight, height: self.iconHeight) - } - - tabTitle() - .foregroundColor(self.viewModel.tabStateAttributes.colors.label.color) - .font(self.viewModel.tabStateAttributes.font.font) - .fixedSize(horizontal: true, vertical: false) - - if let badge = self.viewModel.content.badge { - badge - } - spacer() - } - .frame(height: self.itemHeight) - .overlay( - Rectangle() - .frame(width: nil, height: self.lineHeight, alignment: .top) - .foregroundColor(self.viewModel.tabStateAttributes.colors.line.color), - alignment: .bottom) - .accessibilityLabel(self.viewModel.content.title ?? TabAccessibilityIdentifier.tabItem) - } - - @ViewBuilder - private func spacer() -> some View { - if !self.viewModel.apportionsSegmentWidthsByContent { - Spacer() - .frame(minWidth: self.paddingHorizontal) - } else { - Spacer().frame(width: self.paddingHorizontal) - } - } - - @ViewBuilder - private func tabTitle() -> some View { - if let title = self.viewModel.content.title { - Text(title) - } else if let attributedTitle = self.viewModel.content.attributedTitle { - Text(attributedTitle) - } else { - EmptyView() - } - } - - // MARK: - Public modifiers - /// Indicates whether the control attempts to adjust segment widths based on their content widths. - func apportionsSegmentWidthsByContent(_ newValue: Bool) -> Self { - self.viewModel.apportionsSegmentWidthsByContent = newValue - return self - } - - /// Add a badge to the view - func badge(_ badge: BadgeView) -> Self { - self.viewModel.content.badge = badge - return self - } - - /// Set the tab as selected - func selected(_ selected: Bool) -> Self { - self.viewModel.updateState(isSelected: selected) - return self - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabItemViewSnapshotTests.swift b/core/Sources/Components/Tab/View/SwiftUI/TabItemViewSnapshotTests.swift deleted file mode 100644 index 62215c48f..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabItemViewSnapshotTests.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// TabItemViewSnapshotTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 13.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class TabItemViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - // MARK: - Properties - let theme = SparkTheme.shared - var image: Image! - var badge: BadgeView! - - // MARK: - Setup - - override func setUp() { - super.setUp() - - self.badge = BadgeView(theme: theme, intent: .danger, value: 99).borderVisible(false) - self.image = Image(systemName: "trash") - } - - // MARK: - Tests - func test_tab_icon_and_title_and_badge() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .main, - content: .init( - icon: Image(systemName: "paperplane"), - title: "Label") - ) - - let sut = TabItemView( - viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(false) - .badge(self.badge) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.light, .dark], sizes: [.medium]) - } - - func test_selected_tab_with_intent_main() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .main, - content: .init(title: "Label") - ) - - let sut = TabItemView(viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(true) - .selected(true) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.light, .dark], sizes: [.medium]) - } - - func test_with_badge_only() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .main, - content: .init() - ) - - let sut = TabItemView(viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(true) - .badge(self.badge) - .selected(true) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.light, .dark], sizes: [.medium]) - } - - func test_with_label_only() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .main, - content: .init(title: "Label") - ) - - let sut = TabItemView(viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.light, .dark], sizes: [.small, .medium, .large, .extraLarge]) - } - - func test_with_icon_only() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .main, - content: .init(icon: Image(systemName: "paperplane")) - ) - - let sut = TabItemView( - viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.light, .dark], sizes: [.medium]) - } - - func test_with_label_and_badge() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .basic, - content: .init(title: "Label") - ) - - let sut = TabItemView(viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(true) - .badge(self.badge) - .selected(true) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.dark, .light], sizes: [.small, .medium, .large, .extraLarge]) - } - - func test_with_icon_and_label() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .basic, - content: .init( - icon: Image(systemName: "paperplane"), - title: "Label") - ) - - let sut = TabItemView(viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(false) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.dark, .light], sizes: [.large]) - } - - func test_with_icon_and_badge() throws { - // GIVEN - let viewModel: TabItemViewModel = - .init( - theme: theme, - intent: .basic, - content: .init( - icon: Image(systemName: "paperplane")) - ) - - let sut = TabItemView(viewModel: viewModel, tapAction: {}) - .apportionsSegmentWidthsByContent(true) - .badge(self.badge) - .background(.systemBackground) - - // THEN - assertSnapshot(matching: sut, modes: [.dark, .light], sizes: [.extraSmall]) - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabSingleItem.swift b/core/Sources/Components/Tab/View/SwiftUI/TabSingleItem.swift deleted file mode 100644 index f5de6fbef..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabSingleItem.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// TabSingleItem.swift -// SparkCore -// -// Created by Michael Zimmermann on 17.01.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -struct TabSingleItem: View { - let intent: TabIntent - let content: TabItemContent - let proxy: ScrollViewProxy - @Binding var selectedIndex: Int - let index: Int - - @ObservedObject private var itemViewModel: TabItemViewModel - - init(viewModel: TabViewModel, intent: TabIntent, content: TabItemContent, proxy: ScrollViewProxy, selectedIndex: Binding, index: Int) { - self.intent = intent - self.content = content - self.proxy = proxy - self._selectedIndex = selectedIndex - self.index = index - - self.itemViewModel = TabItemViewModel( - theme: viewModel.theme, - intent: intent, - tabSize: viewModel.tabSize, - content: content, - apportionsSegmentWidthsByContent: viewModel.apportionsSegmentWidthsByContent - ) - .updateState(isSelected: selectedIndex.wrappedValue == index) - .updateState(isEnabled: viewModel.isTabEnabled(index: index)) - } - - var body: some View { - TabItemView(viewModel: itemViewModel) { - self.selectedIndex = self.index - withAnimation{ - self.proxy.scrollTo(self.content.id) - } - } - .disabled(!self.itemViewModel.isEnabled) - .id(self.content.id) - .accessibilityIdentifier("\(TabAccessibilityIdentifier.tabItem)_\(index)") - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabView.swift b/core/Sources/Components/Tab/View/SwiftUI/TabView.swift deleted file mode 100644 index 50d7e2ce6..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabView.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// TabView.swift -// SparkCore -// -// Created by michael.zimmermann on 04.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// TabView is the similar to a SegmentControl -public struct TabView: View { - private let intent: TabIntent - private var viewModel: TabViewModel - @ObservedObject private var containerViewModel = TabContainerViewModel() - @Binding private var selectedIndex: Int - @Environment(\.isEnabled) private var isEnabled: Bool - - // MARK: - Initialization - /// Initializer - /// - Parameters: - /// - theme: the current theme - /// - intent: the tab intent. The default value is `main`. - /// - tabSize: The tab size, see `TabSize`. The default value is medium `md`. - /// - titles: An array of labels. - public init(theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - titles: [String], - selectedIndex: Binding - ) { - self.init(theme: theme, - intent: intent, - tabSize: tabSize, - content: titles.map{ .init(icon: nil, title: $0) }, - selectedIndex: selectedIndex) - } - - /// Initializer - /// - Parameters: - /// - theme: the current theme - /// - intent: the tab intent. The default value is `main`. - /// - tabSize: The tab size, see `TabSize`. The default value is medium `md`. - /// - icons: An array of images. - public init(theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - icons: [Image], - selectedIndex: Binding - ) { - self.init(theme: theme, - intent: intent, - tabSize: tabSize, - content: icons.map{ .init(icon: $0, title: nil) }, - selectedIndex: selectedIndex - ) - } - - /// Initializer - /// - Parameters: - /// - theme: the current theme - /// - intent: the tab intent. The default value is `main`. - /// - tab size: the default value is `md`. - /// - An array of tuples of image and string. - public init(theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - content: [TabItemContent] = [], - selectedIndex: Binding - ) { - self.intent = intent - self._selectedIndex = selectedIndex - let viewModel = TabViewModel( - theme: theme, - content: content, - tabSize: tabSize - ) - self.viewModel = viewModel - } - - // MARK: - View - public var body: some View { - let viewModel = self.viewModel.setIsEnabled(self.isEnabled) - - if self.containerViewModel.apportionsSegmentWidthsByContent { - TabApportionsSizeView(viewModel: viewModel, intent: self.intent, selectedIndex: self.$selectedIndex) - } else { - TabEqualSizeView(viewModel: viewModel, intent: self.intent, selectedIndex: self.$selectedIndex) - } - } - - // MARK: - Public view modifiers - /// Indicates whether the control attempts to adjust segment widths based on their content widths. - public func apportionsSegmentWidthsByContent(_ value: Bool) -> Self { - self.containerViewModel.apportionsSegmentWidthsByContent = value - self.viewModel.apportionsSegmentWidthsByContent = value - return self - } - - /// Disable the tab of the index - public func disabled(_ disabled: Bool, index: Int) -> Self { - self.viewModel.disableTab(disabled, index: index) - return self - } - - /// Set the selected tab - public func selected(index: Int) -> Self { - self.selectedIndex = index - return self - } - - /// Change the content of the tabs - public func content(_ content: [TabItemContent]) -> Self { - self.viewModel.content = content - return self - } - - /// Add a badge of a specific tab - public func badge(_ badge: BadgeView?, index: Int) -> Self { - guard var content = self.viewModel.content[safe: index] else { return self } - - content.badge = badge - self.viewModel.content[index] = content - return self - } -} diff --git a/core/Sources/Components/Tab/View/SwiftUI/TabViewSnapshotTests.swift b/core/Sources/Components/Tab/View/SwiftUI/TabViewSnapshotTests.swift deleted file mode 100644 index 11f1ab983..000000000 --- a/core/Sources/Components/Tab/View/SwiftUI/TabViewSnapshotTests.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// TabViewSnapshotTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 13.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class TabViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - let theme = SparkTheme.shared - let names = ["paperplane", "folder", "trash", "pencil", "eraser", "scribble", "lasso"] - var badge: BadgeView! - var images: [Image]! - var selectedIndex: Binding! - var index: Int = 0 - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.images = names.map{ Image.init(systemName: $0) } - self.badge = BadgeView(theme: theme, intent: .danger, value: 99).borderVisible(false) - self.selectedIndex = Binding( - get: { - return self.index - }, - set: { - self.index = $0 - }) - } - - // MARK: - Tests - func test_tabs_with_icons_only() throws { - let sut = TabView( - theme: self.theme, - icons: Array(self.images[0..<3]), - selectedIndex: self.selectedIndex) - .badge(self.badge, index: 2) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_icons_only_equal_width() throws { - let sut = TabView( - theme: self.theme, - icons: Array(self.images[0..<3]), - selectedIndex: self.selectedIndex) - .badge(self.badge, index: 2) - .apportionsSegmentWidthsByContent(false) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_text_only() throws { - let sut = TabView( - theme: self.theme, - titles: Array(self.names[0..<3].map(\.capitalized)), - selectedIndex: self.selectedIndex) - .badge(self.badge, index: 1) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut) - } - - func test_tabs_with_text_only_equal_width() throws { - let sut = TabView( - theme: self.theme, - titles: Array(self.names[0..<2].map(\.capitalized)), - selectedIndex: self.selectedIndex) - .apportionsSegmentWidthsByContent(false) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut) - } - - func test_tabs_with_icon_and_text() throws { - let content = Array(Array(zip(images, names.map(\.capitalized)))[0..<3]) - .map(TabItemContent.init(icon:title:)) - - let sut = TabView( - theme: self.theme, - content: content, - selectedIndex: self.selectedIndex) - .badge(self.badge, index: 0) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_icon_and_text_size_small() throws { - let content = Array(Array(zip(images, names.map(\.capitalized)))[0..<3]) - .map(TabItemContent.init(icon:title:)) - - let sut = TabView( - theme: self.theme, - tabSize: .sm, - content: content, - selectedIndex: self.selectedIndex) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_icon_and_text_size_xtra_small() throws { - let content = Array(Array(zip(images, names.map(\.capitalized)))[0..<3]) - .map(TabItemContent.init(icon:title:)) - - let sut = TabView( - theme: self.theme, - tabSize: .xs, - content: content, - selectedIndex: self.selectedIndex) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_many_tabs_with_icon_and_text() throws { - let content = Array(zip(images, names.map(\.capitalized))) - .map(TabItemContent.init(icon:title:)) - - let sut = TabView( - theme: self.theme, - content: content, - selectedIndex: self.selectedIndex) - .apportionsSegmentWidthsByContent(true) - .background(.systemBackground) - .frame(width: 390) - .fixedSize() - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } -} - -extension ShapeStyle where Self == Color { - static var systemBackground: Color { Color(uiColor: .systemBackground) } -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabItemUIView.swift b/core/Sources/Components/Tab/View/UIKit/TabItemUIView.swift deleted file mode 100644 index 1f9baad19..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabItemUIView.swift +++ /dev/null @@ -1,507 +0,0 @@ -// -// TabItemUIView.swift -// SparkCore -// -// Created by michael.zimmermann on 31.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -/// A single component of the tabs view. -/// The standard tab item consists of an icon, label and a badge. -/// The badge is not restricted in type and any UIView may be accepted. . -/// The label and icon are publicly accessible, so the standard label may be replaced with an attributed string. -/// The layout of the tab item is orgianized with a stack view. This stack view is publicly accessible, and further views may be added to it. The developer needs to pay caution, not to break constraints. -public final class TabItemUIView: UIControl { - - private enum Constants { - static let numberOfSpacerViews = 2 - static let indexOfBadge = 3 - } - - // MARK: - Private variables - private var subscriptions = Set() - private var bottomLineHeightConstraint: NSLayoutConstraint? - private var imageViewHeightConstraint: NSLayoutConstraint? - private var heightConstraint: NSLayoutConstraint? - private var spaceConstraint: NSLayoutConstraint? - - private var edgeInsets: UIEdgeInsets { - return UIEdgeInsets(top: self.paddingVertical, - left: 0, - bottom: self.paddingVertical, - right: 0) - } - - private let leadingSpace = UIView.spacer - private let trailingSpace = UIView.spacer - - private var bottomLine: UIView = { - let border = UIView() - border.translatesAutoresizingMaskIntoConstraints = false - border.autoresizingMask = [.flexibleWidth, .flexibleHeight] - border.isUserInteractionEnabled = false - return border - }() - - // An internal property to determine if the segment width should be aligned to the - // content of the tab or to equally size the tabs. - internal var apportionsSegmentWidthsByContent: Bool { - get { - return self.viewModel.apportionsSegmentWidthsByContent - } - set { - self.viewModel.apportionsSegmentWidthsByContent = newValue - self.spaceConstraint?.isActive = self.apportionsSegmentWidthsByContent - self.updateConstraintsIfNeeded() - } - } - - // An internal property used for setting the isSelected tab without triggering an action - internal var backingIsSelected: Bool { - get { - return self.viewModel.isSelected - } - set { - if newValue { - self.accessibilityTraits.insert(.selected) - } else { - self.accessibilityTraits.remove(.selected) - } - self.viewModel.updateState(isSelected: newValue) - } - } - - // MARK: - Scaled metrics - @ScaledUIMetric private var spacing: CGFloat - @ScaledUIMetric private var paddingVertical: CGFloat - @ScaledUIMetric private var paddingHorizontal: CGFloat - @ScaledUIMetric private var borderLineHeight: CGFloat - @ScaledUIMetric var height: CGFloat - @ScaledUIMetric private var iconHeight: CGFloat - - @ObservedObject var viewModel: TabItemViewModel - - // MARK: - Public variables - /// The label shown in the tab item. - /// - /// The attributes may be changed as required, e.g. using an attributed string instead of a standard string. - public private(set) var label: UILabel = { - let label = UILabel() - label.contentMode = .scaleAspectFit - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = true - label.isUserInteractionEnabled = false - label.numberOfLines = 1 - label.setContentCompressionResistancePriority(.defaultHigh, - for: .horizontal) - label.setContentCompressionResistancePriority(.required, - for: .vertical) - return label - }() - - /// The image view containing the icon. - /// - /// The attributes of the icon can be changed directly or replaced by changing the imageView. - public private(set) var imageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.isAccessibilityElement = false - imageView.isUserInteractionEnabled = false - imageView.adjustsImageSizeForAccessibilityContentSizeCategory = true - - imageView.setContentCompressionResistancePriority(.required, for: .horizontal) - imageView.setContentCompressionResistancePriority(.required, for: .vertical) - return imageView - }() - - /// The badge which is rendered to the right of the label. - /// - /// The badge will typically be used for rendering a BadgeUIView, but it is not restricted to this type. Any type of view may be added as a badge. - /// It is possible to add further views to the tab, by directly accessing the stackView. - public var badge: UIView? { - didSet { - guard badge != oldValue else { return } - - if let currentBadge = oldValue { - self.stackView.detachArrangedSubview(currentBadge) - } - - if let newBadge = self.badge { - newBadge.isUserInteractionEnabled = false - - self.stackView.insertArrangedSubview(newBadge, at: Constants.indexOfBadge) - // A hack to get the stack view to render properly. - // When only an icon is being displayed and a badge is added, - // then the badge is rendered on top of the icon. - // By toggeling the visibility, the problem seems to be corrected. - newBadge.isHidden.toggle() - newBadge.isHidden.toggle() - } - - self.invalidateIntrinsicContentSize() - } - } - - /// The stack view containing the single items of the tab. - /// - /// The stack view is publicly accessible, so that the contents of the tab may be changed to special needs. - public var stackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .center - stackView.isLayoutMarginsRelativeArrangement = true - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.isUserInteractionEnabled = false - return stackView - }() - - /// The current theme of the view. - /// - /// By changing the theme, the colors and spacings of the tab item will change according to the new theme. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// The current intent of the view. - /// - /// By chaning the intent, the colors of the selected tab item will change. - public var intent: TabIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - /// The icon of the component. - /// - /// The icon is the leftmost component of the tab item. - /// If the icon is nil, no label will be shown on the tab item. To change the attributes of the icon, the image view is publicly accessible. - public var icon: UIImage? { - get { - return self.imageView.image - } - set { - guard self.imageView.image != newValue else { return } - self.addOrRemoveIcon(newValue) - } - } - - /// The standard title of the tab item. - /// - /// The title is shown to the right of the icon. - /// If the title is nil, no label will be added to the tab item. To change the attributes of the text, you can directly access the label of this component. - public var title: String? { - get { - return self.label.text - } - set { - guard self.label.text != newValue else { return } - self.addOrRemoveTitle(newValue) - } - } - - /// The current tab size - public var tabSize: TabSize { - get { - return self.viewModel.tabSize - } - set { - self.viewModel.tabSize = newValue - } - } - - /// A Boolean value indicating whether the control is in the selected state. - /// - /// Set the value of this property to true to select it or false to deselect it. The colors and design of the tab item change whether it is selected or not. - /// The default value of this property is false for a newly created control. - public override var isSelected: Bool { - get { - return self.backingIsSelected - } - set { - guard newValue != self.backingIsSelected else { return } - if newValue { - self.sendActions(for: .otherSegmentSelected) - } - self.backingIsSelected = newValue - } - } - - /// A Boolean value indicating whether the control is in the enabled state. - /// - /// Set the value of this property to true to enable the control or false to disable it. An enabled control is capable of responding to user interactions, whereas a disabled control ignores touch events and may draw itself differently. - /// The default value of this property is true for a newly created control. You can set a control’s initial enabled state in your storyboard file. - public override var isEnabled: Bool { - get { - return self.viewModel.isEnabled - } - set { - if newValue { - self.accessibilityTraits.remove(.notEnabled) - } else { - self.accessibilityTraits.insert(.notEnabled) - } - self.viewModel.updateState(isEnabled: newValue) - } - } - - public override var intrinsicContentSize: CGSize { - var itemsWidth: CGFloat = 0 - - if self.label.isNotHidden { - itemsWidth += self.label.intrinsicContentSize.width - } - - if self.imageView.isNotHidden { - itemsWidth += self.iconHeight - } - - if let badge = self.badge, self.isNotHidden { - itemsWidth += badge.intrinsicContentSize.width == UIView.noIntrinsicMetric ? self.height : badge.intrinsicContentSize.width - } - - let numberOfSpacings = max(self.stackView.arrangedSubviews.filter(\.isNotHidden).count - (Constants.numberOfSpacerViews + 1), 0) - let spacingsWidth = CGFloat(numberOfSpacings) * self.spacing - - let totalWidth = self.paddingHorizontal + (itemsWidth + spacingsWidth) + self.paddingHorizontal - - return CGSize(width: totalWidth, height: self.height) - } - - /// An optional action which can be set. The action will be invoked when the tab is tapped. - public var action: UIAction? { - didSet { - if let action = action { - self.addAction(action, for: .touchUpInside) - } else if let action = oldValue { - self.removeAction(action, for: .touchUpInside) - } - } - } - - // MARK: - Initializers - /// Create a tab item view. - /// - /// - Parameters: - /// - theme: the current theme, which will determine the colors and spacings - /// - intent: the intent of the tab item, the default is basic - /// - content: the content of the tab item - /// - apportionsSegmentWIdthsByContent: Indicates whether the control attempts to adjust segment widths based on their content widths. - public convenience init( - theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - content: TabUIItemContent, - apportionsSegmentWidthsByContent: Bool = false - ) { - let viewModel = TabItemViewModel( - theme: theme, - intent: intent, - tabSize: tabSize, - content: content) - viewModel.apportionsSegmentWidthsByContent = apportionsSegmentWidthsByContent - - self.init(viewModel: viewModel) - } - - internal init( - viewModel: TabItemViewModel - ) { - - self.viewModel = viewModel - - self._spacing = ScaledUIMetric(wrappedValue: viewModel.tabStateAttributes.spacings.content) - self._paddingVertical = ScaledUIMetric(wrappedValue: viewModel.tabStateAttributes.spacings.verticalEdge) - self._paddingHorizontal = ScaledUIMetric(wrappedValue: viewModel.tabStateAttributes.spacings.horizontalEdge) - self._borderLineHeight = ScaledUIMetric(wrappedValue: viewModel.tabStateAttributes.heights.separatorLineHeight) - self._height = ScaledUIMetric(wrappedValue: viewModel.tabStateAttributes.heights.itemHeight) - self._iconHeight = ScaledUIMetric(wrappedValue: viewModel.tabStateAttributes.heights.iconHeight) - - super.init(frame: .zero) - - self.setupView() - self.setupConstraints() - self.enableTouch() - self.setupSubscriptions() - self.isAccessibilityElement = true - self.accessibilityTraits.insert(.button) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public functions - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - guard self.traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { - return - } - self._spacing.update(traitCollection: self.traitCollection) - self._paddingVertical.update(traitCollection: self.traitCollection) - self._paddingHorizontal.update(traitCollection: self.traitCollection) - self._borderLineHeight.update(traitCollection: self.traitCollection) - self._height.update(traitCollection: self.traitCollection) - self._iconHeight.update(traitCollection: self.traitCollection) - - self.invalidateIntrinsicContentSize() - self.updateLayoutConstraints() - self.setNeedsLayout() - } - - // MARK: - Control functions - public override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - self.viewModel.isPressed = true - } - - public override func touchesEnded(_ touches: Set, with event: UIEvent?) { - super.touchesEnded(touches, with: event) - self.viewModel.isPressed = false - } - - public override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - super.touchesCancelled(touches, with: event) - self.viewModel.isPressed = false - } - - // MARK: - Private functions - private func setupSubscriptions() { - self.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { [weak self] attributes in - guard let self else { return } - self.setupColors(attributes: attributes) - self.updateSizes(attributes: attributes) - self.updateLayoutConstraints() - self.invalidateIntrinsicContentSize() - self.setNeedsLayout() - } - } - - private func setupView() { - self.accessibilityIdentifier = TabAccessibilityIdentifier.tabItem - self.stackView.spacing = self.spacing - self.stackView.layoutMargins = self.edgeInsets - - self.addSubviewSizedEqually(self.stackView) - - self.stackView.addArrangedSubview(self.leadingSpace) - self.stackView.addArrangedSubview(self.imageView) - self.stackView.addArrangedSubview(self.label) - self.stackView.addArrangedSubview(self.trailingSpace) - - self.addSubview(self.bottomLine) - self.bringSubviewToFront(self.bottomLine) - - self.setupColors(attributes: self.viewModel.tabStateAttributes) - - self.addOrRemoveIcon(self.viewModel.content.icon) - self.addOrRemoveTitle(self.viewModel.content.title) - } - - private func setupColors(attributes: TabStateAttributes) { - self.label.font = attributes.font.uiFont - self.label.textColor = attributes.colors.label.uiColor - - self.imageView.tintColor = attributes.colors.label.uiColor - - self.bottomLine.backgroundColor = attributes.colors.line.uiColor - self.stackView.backgroundColor = attributes.colors.background.uiColor - - let opacity = Float(attributes.colors.opacity) - self.bottomLine.layer.opacity = opacity - self.stackView.layer.opacity = opacity - } - - private func updateSizes(attributes: TabStateAttributes) { - self._spacing = ScaledUIMetric(wrappedValue: attributes.spacings.content) - self._paddingVertical = ScaledUIMetric(wrappedValue: attributes.spacings.verticalEdge) - self._paddingHorizontal = ScaledUIMetric(wrappedValue: attributes.spacings.horizontalEdge) - self._borderLineHeight = ScaledUIMetric(wrappedValue: attributes.heights.separatorLineHeight) - self._height = ScaledUIMetric(wrappedValue: attributes.heights.itemHeight) - self._iconHeight = ScaledUIMetric(wrappedValue: attributes.heights.iconHeight) - } - - private func updateLayoutConstraints() { - self.imageViewHeightConstraint?.constant = self.iconHeight - - self.bottomLineHeightConstraint?.constant = self.borderLineHeight - self.heightConstraint?.constant = self.height - self.spaceConstraint?.constant = self.paddingHorizontal - self.spacing - - self.stackView.spacing = self.spacing - self.stackView.layoutMargins = self.edgeInsets - } - - private func setupConstraints() { - let lineHeightConstraint = self.bottomLine.heightAnchor.constraint(equalToConstant: self.borderLineHeight) - let imageHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.iconHeight) - let heightConstraint = self.heightAnchor.constraint(greaterThanOrEqualToConstant: self.height) - - let spaceConstraint = self.leadingSpace.widthAnchor.constraint(equalToConstant: self.paddingHorizontal - self.spacing) - - NSLayoutConstraint.activate([ - lineHeightConstraint, - imageHeightConstraint, - heightConstraint, - self.leadingSpace.widthAnchor.constraint(equalTo: trailingSpace.widthAnchor), - self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor), - self.bottomLine.leadingAnchor.constraint(equalTo: self.stackView.leadingAnchor), - self.bottomLine.trailingAnchor.constraint(equalTo: self.stackView.trailingAnchor), - self.bottomLine.bottomAnchor.constraint(equalTo: self.stackView.bottomAnchor) - ]) - spaceConstraint.isActive = self.apportionsSegmentWidthsByContent - self.bottomLineHeightConstraint = lineHeightConstraint - self.imageViewHeightConstraint = imageHeightConstraint - self.heightConstraint = heightConstraint - self.spaceConstraint = spaceConstraint - } - - private func addOrRemoveIcon(_ icon: UIImage?) { - self.viewModel.content.icon = icon - self.imageView.image = icon - self.imageView.tintColor = self.viewModel.tabStateAttributes.colors.icon.uiColor - - self.imageView.isHidden = icon == nil - - self.invalidateIntrinsicContentSize() - self.setNeedsLayout() - } - - private func addOrRemoveTitle(_ text: String?) { - self.viewModel.content.title = text - self.label.font = self.viewModel.tabStateAttributes.font.uiFont - self.label.textColor = self.viewModel.tabStateAttributes.colors.label.uiColor - - self.label.text = text - self.accessibilityLabel = text - self.label.isHidden = text == nil - - self.invalidateIntrinsicContentSize() - self.setNeedsLayout() - } -} - -public extension UIControl.Event { - static let otherSegmentSelected = UIControl.Event(rawValue: 0b0010 << 24) -} - -private extension UIView { - static var spacer: UIView { - let spacer = UIView() - spacer.translatesAutoresizingMaskIntoConstraints = false - return spacer - } -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabItemUIViewSnapshotTests.swift b/core/Sources/Components/Tab/View/UIKit/TabItemUIViewSnapshotTests.swift deleted file mode 100644 index 711c4c78f..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabItemUIViewSnapshotTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// TabItemUIViewSnapshotTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 02.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class TabItemUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - let theme = SparkTheme.shared - var image: UIImage! - var badge: BadgeUIView! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.badge = BadgeUIView(theme: theme, intent: .danger, isBorderVisible: false) - // swiftlint:disable force_unwrapping - self.image = UIImage(systemName: "trash")! - } - - // MARK: - Tests - func test_tab_with_badge() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init(title: "Label")) - sut.apportionsSegmentWidthsByContent = false - sut.backgroundColor = UIColor.systemBackground - - self.badge.value = 99 - sut.badge = self.badge - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_selected_tab_with_intent_main() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init(title: "Label")) - sut.apportionsSegmentWidthsByContent = true - sut.backgroundColor = UIColor.systemBackground - - sut.isSelected = true - self.badge.value = 99 - sut.badge = self.badge - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_with_badge_only() throws { - - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init() - ) - sut.apportionsSegmentWidthsByContent = true - sut.backgroundColor = UIColor.systemBackground - - self.badge.value = 99 - sut.badge = self.badge - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_with_label_only() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init(title: "Label")) - sut.apportionsSegmentWidthsByContent = true - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.small, .medium, .large, .extraLarge]) - } - - func test_with_icon_only() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init(icon: UIImage(systemName: "paperplane")) - ) - sut.apportionsSegmentWidthsByContent = true - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_with_label_and_badge() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init(title: "Label")) - sut.apportionsSegmentWidthsByContent = true - sut.backgroundColor = UIColor.systemBackground - - let badge = BadgeUIView(theme: self.theme, intent: .danger, value: 99) - sut.badge = badge - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.small, .medium, .large, .extraLarge]) - } - - func test_with_icon_and_label() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init( - icon: UIImage(systemName: "paperplane"), - title: "Label" - ) - ) - sut.apportionsSegmentWidthsByContent = true - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.large]) - } - - func test_with_icon_and_badge() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init(icon: UIImage(systemName: "paperplane")) - ) - let badge = BadgeUIView(theme: self.theme, intent: .danger, value: 99) - sut.apportionsSegmentWidthsByContent = true - sut.badge = badge - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.extraSmall]) - } - - func test_with_icon_and_label_and_badge() throws { - let sut = TabItemUIView( - theme: self.theme, - intent: .main, - content: .init( - icon: UIImage(systemName: "paperplane"), - title: "Label" - ) - ) - let badge = BadgeUIView(theme: self.theme, intent: .danger, value: 99) - sut.apportionsSegmentWidthsByContent = true - sut.badge = badge - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.small]) - } -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabItemUIViewTests.swift b/core/Sources/Components/Tab/View/UIKit/TabItemUIViewTests.swift deleted file mode 100644 index 87d7f05c8..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabItemUIViewTests.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// TabItemUIViewTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -final class TabItemUIViewTests: TestCase { - - let theme = SparkTheme.shared - var sut: TabItemUIView! - var subscriptions = Set() - - override func setUp() { - super.setUp() - self.sut = TabItemUIView( - theme: theme, - intent: .main, - content: .init(icon: .init(systemName: "trash"), - title: "Label") - ) - } - - func test_theme_change_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.theme = SparkTheme.shared - - // Then - waitForExpectations(timeout: 1) - } - - func test_intent_change_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.intent = .support - - // Then - waitForExpectations(timeout: 1) - } - - func test_size_change_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.tabSize = .xs - - // Then - waitForExpectations(timeout: 1) - } - - func test_setting_selected_triggers_other_segment_selected() { - // Given - let badge = UIView() - self.sut.badge = badge - let expect = expectation(description: "Content not to be changed") - - let action = UIAction{ _ in - expect.fulfill() - } - - self.sut.addAction(action, for: .otherSegmentSelected) - - // When - self.sut.isSelected = true - - // Then - waitForExpectations(timeout: 1) - } - - func test_setting_not_selected_doesnt_trigger_other_segment_selected() { - // Given - let badge = UIView() - self.sut.badge = badge - let expect = expectation(description: "Content not to be changed") - expect.isInverted = true - - let action = UIAction{ _ in - expect.fulfill() - } - - self.sut.addAction(action, for: .otherSegmentSelected) - - // When - self.sut.isSelected = false - - // Then - waitForExpectations(timeout: 1) - } - - func test_set_enabled_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - self.sut.isEnabled = false - - self.sut.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.isEnabled = true - - // Then - waitForExpectations(timeout: 1) - } - - func test_when_pressed_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.touchesBegan(Set(), with: nil) - - // Then - waitForExpectations(timeout: 1) - } - - func test_when_pressed_action_sent() { - // Given - let touchDownExpectation = expectation(description: "TouchDown action sent") - let action = UIAction{ _ in - touchDownExpectation.fulfill() - } - - self.sut.addAction(action, for: .touchDown) - - // When - self.sut.sendActions(for: .touchDown) - - // Then - waitForExpectations(timeout: 1) - } - - func test_when_touch_ends_attributes_change() { - // Given - self.sut.viewModel.isPressed = true - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.touchesEnded(Set(), with: nil) - - // Then - waitForExpectations(timeout: 1) - } - - func test_when_touch_ends_action_sent() { - // Given - let touchDownExpectation = expectation(description: "TouchDown action sent") - let action = UIAction{ _ in - touchDownExpectation.fulfill() - } - let actionExpectation = expectation(description: "Class action sent") - - self.sut.action = UIAction{ _ in - actionExpectation.fulfill() - } - - self.sut.addAction(action, for: .touchUpInside) - - // When - self.sut.sendActions(for: .touchUpInside) - - // Then - waitForExpectations(timeout: 1) - } - - func test_when_touch_cancelled_action_sent() { - // Given - let expect = expectation(description: "Touch cancel action sent") - let action = UIAction{ _ in - expect.fulfill() - } - - self.sut.addAction(action, for: .touchCancel) - - // When - self.sut.sendActions(for: .touchCancel) - - // Then - waitForExpectations(timeout: 1) - } -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabUIView.swift b/core/Sources/Components/Tab/View/UIKit/TabUIView.swift deleted file mode 100644 index 414b78d7f..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabUIView.swift +++ /dev/null @@ -1,640 +0,0 @@ -// -// TabUIView.swift -// SparkCore -// -// Created by michael.zimmermann on 08.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation -import UIKit - -public final class TabUIView: UIControl { - - // MARK: - Private variables - private var stackView: UIStackView = { - let stackView = UIStackView() - stackView.spacing = 0 - stackView.axis = .horizontal - stackView.alignment = .fill - stackView.isLayoutMarginsRelativeArrangement = true - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - private let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.isScrollEnabled = true - scrollView.alwaysBounceHorizontal = false - scrollView.alwaysBounceVertical = false - scrollView.translatesAutoresizingMaskIntoConstraints = false - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.isDirectionalLockEnabled = true - return scrollView - }() - - private let bottomLine: UIView = { - let bottomLine = UIView() - bottomLine.translatesAutoresizingMaskIntoConstraints = false - return bottomLine - }() - - private let selectedIndexSubject = PassthroughSubject() - private let viewModel: TabViewModel - private var widthConstraint: NSLayoutConstraint? - private var bottomLineHeightConstraint: NSLayoutConstraint? - private var subscriptions = Set() - - // MARK: - Managing design of the segments - /// All segements of the the tab - public var segments: [TabItemUIView] { - return self.stackView.arrangedSubviews - .compactMap { view in - return view as? TabItemUIView - } - } - - /// The current theme - public var theme: Theme { - didSet { - self.segments.forEach { tab in - tab.theme = theme - } - } - } - - /// The current intent - public var intent: TabIntent { - didSet { - self.segments.forEach { tab in - tab.intent = intent - } - } - } - - /// The current tab size - public var tabSize: TabSize { - get { - return self.viewModel.tabSize - } - set { - self.viewModel.tabSize = newValue - self.segments.forEach { tab in - tab.tabSize = newValue - } - self.invalidateIntrinsicContentSize() - } - } - - /// Disable each segement of the tab - public override var isEnabled: Bool { - didSet { - self.viewModel.setIsEnabled(self.isEnabled) - self.segments.forEach{ $0.isEnabled = self.isEnabled } - - if self.isEnabled { - self.accessibilityTraits.remove(.notEnabled) - } else { - self.accessibilityTraits.insert(.notEnabled) - } - } - } - - /// Indicates the max possible width of the tab. - public var maxWidth: CGFloat = UIScreen.main.bounds.width { - didSet { - self.invalidateIntrinsicContentSize() - } - } - - /// Indicates whether the control attempts to adjust segment widths based on their content widths. - public var apportionsSegmentWidthsByContent: Bool { - get { return viewModel.apportionsSegmentWidthsByContent } - set { - self.viewModel.apportionsSegmentWidthsByContent = newValue - self.segments.forEach{ - $0.apportionsSegmentWidthsByContent = newValue - } - } - } - - public override var intrinsicContentSize: CGSize { - let height = self.stackView - .arrangedSubviews - .filter(\.isNotHidden) - .map(\.intrinsicContentSize.height) - .reduce(0, max) - - return CGSize(width: self.maxWidth, height: height) - } - - // MARK: - Managing interaction with the tab. - /// An optional delegate. - /// Chages to the current selected item of the tabs will be sent to the delegate. - /// An alternative approach would be to add an action just like `UISegmentControl`for `valueChanged` - /// or to subscribe to the publisher. - public weak var delegate: TabUIViewDelegate? - - /// The index of the newly selected tab will be published. - /// This is an alternative to using the delegate or to setting an action for `valueChanged` - public var publisher: some Publisher { - return self.selectedIndexSubject - } - - // MARK: - Initialization - /// Initializer - /// - Parameters: - /// - theme: the current theme - /// - intent: the tab intent. The default value is `basic`. - /// - tabSize: The tab size, see `TabSize`. The default value is medium `md`. - /// - titles: An array of labels. - /// - apportionsSegmentWIdthsByContent: Indicates whether the control attempts to adjust segment widths based on their content widths. - public convenience init( - theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - titles: [String], - apportionsSegmentWidthsByContent: Bool = false - ) { - self.init( - theme: theme, - intent: intent, - tabSize: tabSize, - content: titles.map(TabUIItemContent.init(title:)), - apportionsSegmentWidthsByContent: apportionsSegmentWidthsByContent) - } - - /// Initializer - /// - Parameters: - /// - theme: the current theme - /// - intent: the tab intent. The default value is `basic`. - /// - tabSize: The tab size, see `TabSize`. The default value is medium `md`. - /// - icons: An array of images. - /// - apportionsSegmentWIdthsByContent: Indicates whether the control attempts to adjust segment widths based on their content widths. - public convenience init( - theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - icons: [UIImage], - apportionsSegmentWidthsByContent: Bool = false - ) { - self.init(theme: theme, - intent: intent, - tabSize: tabSize, - content: icons.map(TabUIItemContent.init(icon:)), - apportionsSegmentWidthsByContent: apportionsSegmentWidthsByContent - ) - } - - /// Initializer - /// - Parameters: - /// - theme: the current theme - /// - intent: the tab intent. The default value is `basic`. - /// - tab size: the default value is `md`. - /// - content: An array of TabUIItemContent with of image and string. - /// - apportionsSegmentWIdthsByContent: Indicates whether the control attempts to adjust segment widths based on their content widths. - public init(theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - content: [TabUIItemContent], - apportionsSegmentWidthsByContent: Bool = false - ) { - - self.theme = theme - self.intent = intent - self.viewModel = TabViewModel( - theme: theme, - apportionsSegmentWidthsByContent: apportionsSegmentWidthsByContent, - content: content, - tabSize: tabSize) - - super.init(frame: .zero) - - self.setupViews(items: content) - self.setupConstraints() - self.enableTouch() - self.setupSubscriptions() - self.setupAccessibility() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self.invalidateIntrinsicContentSize() - } - - public override func layoutSubviews() { - super.layoutSubviews() - self.scrollView.alwaysBounceVertical = self.scrollView.bounds.width > self.bounds.width - } - - // MARK: - Managing segment content - - /// Sets the content of a segment to a given image. - public func setImage(_ image: UIImage?, forSegmentAt index: Int) { - guard let segment = self.segments[safe: index] else { return } - segment.icon = image - self.viewModel.content[index].icon = image - - self.invalidateIntrinsicContentSize() - } - - /// Returns the image for a specific segment. - public func imageForSegment(at index: Int) -> UIImage? { - return self.segments[safe: index]?.icon - } - - /// Sets the title of a segment. - public func setTitle(_ title: String?, forSegmentAt index: Int) { - guard let segment = self.segments[safe: index] else { return } - segment.title = title - self.viewModel.content[index].title = title - - self.invalidateIntrinsicContentSize() - } - - /// Returns the title of the specified segment. - public func titleForSegment(at index: Int) -> String? { - return self.segments[safe: index]?.title - } - - /// Set a badge (or any UIView) on the tab at the given index. - public func setBadge(_ badge: UIView?, forSegementAt index: Int) { - guard let segment = self.segments[safe: index] else { return } - segment.badge = badge - self.invalidateIntrinsicContentSize() - } - - /// Return the badge (any UIView) for the specific segment - public func badgeForSegment(at index: Int) -> UIView? { - return self.segments[safe: index]?.badge - } - - // MARK: - Managing segment actions - - /// Fetches the action of the segment at the index you specify, if one exists. - public func actionForSegment(at index: Int) -> UIAction? { - return self.segments[safe: index]?.action - } - - /// Sets the action for the segment at the index you specify. - public func setAction(_ action: UIAction, forSegmentAt index: Int) { - self.segments[safe: index]?.action = action - } - - // MARK: - Managing segments - - /// Returns the number of segments the segmented control has. - public var numberOfSegments: Int { - return segments.count - } - - /// The segment at the givien index. - public func segment(at index: Int) -> TabItemUIView? { - return self.segments[safe: index] - } - - /// The index of the segment with an action that has a matching identifier, or NSNotFound - /// if no matching action is found. - public func segmentIndex(identifiedBy identifier: UIAction.Identifier) -> Int { - return self.segments.firstIndex(where: { $0.action?.identifier == identifier }) ?? NSNotFound - } - - /// Inserts a segment at the last position you specify and gives it an image as content. - public func addSegment(with icon: UIImage, - animated: Bool = false) { - let index = self.numberOfSegments - self.viewModel.content.insert(.init(icon: icon), at: index) - - let tab = TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: .init(icon: icon) - ) - self.insertTab(tab, at: index, animated: animated) - } - - /// Inserts a segment at the last position you specify and gives it a title as content. - public func addSegment(with title: String, - animated: Bool = false) { - let index = self.numberOfSegments - self.viewModel.content.insert(.init(title: title), at: index) - - let tab = TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: .init(title: title) - ) - self.insertTab(tab, at: index, animated: animated) - } - - /// Inserts a segment at the last position you specify and gives it a title as content. - public func addSegment(withImage icon: UIImage, - andTitle title: String, - animated: Bool = false) { - let index = self.numberOfSegments - self.viewModel.content.insert(.init(icon: icon, title: title), at: index) - - let tab = TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: .init(icon: icon, title: title) - ) - self.insertTab(tab, at: index, animated: animated) - } - - /// Inserts a segment at the position you specify and gives it an image as content. - public func insertSegment(with icon: UIImage, - at index: Int, - animated: Bool = false) { - self.viewModel.content.insert(.init(icon: icon), at: index) - let tab = TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: .init(icon: icon) - ) - self.insertTab(tab, at: index, animated: animated) - } - - /// Inserts a segment at the position you specify and gives it a title as content. - public func insertSegment(with title: String, - at index: Int, - animated: Bool = false) { - self.viewModel.content.insert(.init(title: title), at: index) - let tab = TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: .init(title: title) - ) - self.insertTab(tab, at: index, animated: animated) - } - - /// Inserts a segment at the position you specify and gives it a title as content. - public func insertSegment(withImage icon: UIImage, - andTitle title: String, - at index: Int, - animated: Bool = false) { - self.viewModel.content.insert(.init(icon: icon, title: title), at: index) - let tab = TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: .init(icon: icon, title: title) - ) - self.insertTab(tab, at: index, animated: animated) - } - - /// Replace all current segments with segments with just icons - public func setSegments(withImages icons: [UIImage]) { - let content: [TabUIItemContent] = icons.map{ .init(icon: $0, title: nil) } - self.setTabItems(content: content) - } - - /// Replace all current segments with segments with just titles - public func setSegments(withTitles titles: [String]) { - let content: [TabUIItemContent] = titles.map{ .init(icon: nil, title: $0) } - self.setTabItems(content: content) - } - - /// Replace all current segments with segments with icons & titles - public func setSegments(withContent content: [TabUIItemContent]) { - self.setTabItems(content: content) - } - - /// Enables the segment you specify. - public func setEnabled(_ isEnabled: Bool, - at index: Int, - animated: Bool = false) { - doWithAnimation(animated) { - self.segments[safe: index]?.isEnabled = isEnabled - } - } - - /// Returns whether the indicated segment is enabled. - public func isEnabledForSegment(at index: Int) -> Bool { - self.segments[safe: index]?.isEnabled ?? false - } - - /// Removes all segments of the segmented control. - public func removeAllSegments() { - self.stackView.removeArrangedSubviews() - } - - /// Removes the segment you specify from the segmented control, optionally animating the transition. - public func removeSegment(at index: Int, animated: Bool) { - guard let tab = self.stackView.arrangedSubviews[safe: index] else { return } - self.viewModel.content.remove(at: index) - self.removeTab(tab, animated: animated) - } - - /// The index number that identifies the selected segment that the user last touched. - public var selectedSegmentIndex: Int { - get { - return self.segments.firstIndex(where: {$0.isSelected == true}) ?? NSNotFound - } - set { - guard newValue != self.selectedSegmentIndex else { return } - guard newValue < self.numberOfSegments, newValue >= 0 else { return } - self.setSelectedSegment(newValue, animated: false) - } - } - - /// Scroll to the selected segment - public func scrollToSelectedSegement(animated: Bool) { - let frame = self.segments[self.selectedSegmentIndex].frame - - if scrollView.bounds.contains(frame) { - return - } - - var point = CGPoint.zero - if frame.minX < self.scrollView.contentOffset.x { - point.x = frame.minX - } else if frame.maxX > self.scrollView.frame.maxX { - point.x = frame.maxX - self.scrollView.frame.maxX - } - self.scrollView.setContentOffset(point, animated: animated) - } - - // MARK: - Private Functions - private func setupViews(items: [TabUIItemContent]) { - let tabItemViews = items.map{ item in - return TabItemUIView( - theme: theme, - intent: intent, - tabSize: tabSize, - content: .init(icon: item.icon, title: item.title), - apportionsSegmentWidthsByContent: self.apportionsSegmentWidthsByContent - ) - } - - for (index, tabItem) in tabItemViews.enumerated() { - self.setupTabActions(for: tabItem, index: index) - } - self.stackView.addArrangedSubviews(tabItemViews) - - self.addSubviewSizedEqually(scrollView) - self.scrollView.addSubview(self.bottomLine) - self.bottomLine.backgroundColor = self.viewModel.tabsAttributes.lineColor.uiColor - - self.scrollView.addSubview(self.stackView) - self.scrollView.bringSubviewToFront(self.stackView) - - self.selectedSegmentIndex = 0 - self.updateAccessibilityIdentifiers() - - self.scrollView.backgroundColor = self.viewModel.tabsAttributes.backgroundColor.uiColor - } - - private func setupConstraints() { - let scrollContentGuide = self.scrollView.contentLayoutGuide - - NSLayoutConstraint.activate([ - self.stackView.leadingAnchor.constraint(equalTo: scrollContentGuide.leadingAnchor), - self.stackView.trailingAnchor.constraint(lessThanOrEqualTo: scrollContentGuide.trailingAnchor), - self.stackView.topAnchor.constraint(equalTo: scrollContentGuide.topAnchor), - self.stackView.bottomAnchor.constraint(equalTo: scrollContentGuide.bottomAnchor), - self.bottomLine.leadingAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.leadingAnchor), - self.bottomLine.trailingAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.trailingAnchor), - self.bottomLine.bottomAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.bottomAnchor) - ]) - - self.bottomLineHeightConstraint = self.bottomLine.heightAnchor.constraint(equalToConstant: self.viewModel.tabsAttributes.lineHeight) - self.bottomLineHeightConstraint?.isActive = true - - self.widthConstraint = self.stackView.widthAnchor.constraint(greaterThanOrEqualTo: self.widthAnchor) - if !self.apportionsSegmentWidthsByContent { - self.widthConstraint?.isActive = true - } - - stackView.distribution = self.apportionsSegmentWidthsByContent ? .fill : .fillEqually - } - - private func setupAccessibility() { - self.accessibilityIdentifier = TabAccessibilityIdentifier.tab - self.accessibilityTraits.insert(.tabBar) - self.isAccessibilityElement = false - self.accessibilityContainerType = .semanticGroup - } - - private func setTabItems(content: [TabUIItemContent]) { - self.viewModel.content = content - - let items = content.map { - TabItemUIView( - theme: self.theme, - intent: self.intent, - tabSize: self.tabSize, - content: $0 - ) - } - self.stackView.removeArrangedSubviews() - self.stackView.addArrangedSubviews(items) - self.accessibilityElements?.append(contentsOf: items) - - self.updateAccessibilityIdentifiers() - self.invalidateIntrinsicContentSize() - - for (index, item) in items.enumerated() { - self.setupTabActions(for: item, index: index) - } - } - - private func setupTabActions(for tabItem: TabItemUIView, index: Int) { - let pressedAction = UIAction { [weak self] _ in - self?.pressed(index) - } - let unselectAction = UIAction { [weak self] _ in - self?.unselectSegment(index) - } - tabItem.addAction(pressedAction, for: .touchUpInside) - tabItem.addAction(unselectAction, for: .otherSegmentSelected) - } - - private func setupSubscriptions() { - self.viewModel.$tabsAttributes.subscribe(in: &self.subscriptions) { [weak self] tabAttributes in - guard let self else { return } - - self.bottomLineHeightConstraint?.constant = tabAttributes.lineHeight - self.bottomLine.backgroundColor = tabAttributes.lineColor.uiColor - self.scrollView.backgroundColor = tabAttributes.backgroundColor.uiColor - } - - self.viewModel.$apportionsSegmentWidthsByContent.subscribe(in: &self.subscriptions) { [weak self] useContentWidth in - guard let self else { return } - - self.widthConstraint?.isActive = !useContentWidth - self.stackView.distribution = useContentWidth ? .fill : .fillEqually - self.setNeedsUpdateConstraints() - - } - } - - private func pressed(_ index: Int) { - self.setSelectedSegment(index, animated: true) - self.scrollToSelectedSegement(animated: true) - self.selectedIndexSubject.send(index) - self.sendActions(for: .valueChanged) - self.delegate?.segmentSelected(index: index, sender: self) - } - - private func unselectSegment(_ newSelected: Int) { - for (index, segment) in segments.enumerated() { - guard index != newSelected else { return } - segment.backingIsSelected = false - } - } - - private func setSelectedSegment(_ index: Int, animated: Bool = false) { - self.doWithAnimation(animated) { [weak self] in - guard let self else { return } - self.segments[safe: self.selectedSegmentIndex]?.backingIsSelected = false - self.segments[safe: index]?.backingIsSelected = true - } - } - - private func removeTab(_ tab: UIView, animated: Bool) { - self.doWithAnimation(animated) { [weak self] in - self?.stackView.detachArrangedSubview(tab) - } - self.updateAccessibilityIdentifiers() - self.invalidateIntrinsicContentSize() - } - - private func insertTab(_ tab: TabItemUIView, at index: Int, animated: Bool) { - self.setupTabActions(for: tab, index: index) - self.doWithAnimation(animated) { [weak self] in - self?.stackView.insertArrangedSubview(tab, at: index) - } - self.updateAccessibilityIdentifiers() - self.invalidateIntrinsicContentSize() - } - - private func updateAccessibilityIdentifiers() { - for (index, tabItem) in segments.enumerated() { - tabItem.accessibilityIdentifier = "\(TabAccessibilityIdentifier.tabItem)-\(index)" - } - } - - private func doWithAnimation(_ animated: Bool, block: @escaping () -> Void) { - if animated { - UIView.animate(withDuration: 0.5, - delay: 0, - options: .curveEaseInOut, - animations: block) - } else { - block() - } - } -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabUIViewDelegate.swift b/core/Sources/Components/Tab/View/UIKit/TabUIViewDelegate.swift deleted file mode 100644 index 5696859c4..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabUIViewDelegate.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// TabUIViewDelegate.swift -// SparkCore -// -// Created by michael.zimmermann on 10.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -/// The delegate to receive segment selected events. -public protocol TabUIViewDelegate: AnyObject { - /// This method to receive segment event. - /// - Parameters: - /// - index: the index of the segement that is selected. - /// - sender: the sender of the action. - /// - note: This is equivalent to setting the action on the TabUIView `addAction(pressedAction, for: .valueChanged)` - func segmentSelected(index: Int, sender: TabUIView) -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabUIViewSnapshotTests.swift b/core/Sources/Components/Tab/View/UIKit/TabUIViewSnapshotTests.swift deleted file mode 100644 index 448a691ff..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabUIViewSnapshotTests.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// TabUIViewSnapshotTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class TabUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - let theme = SparkTheme.shared - let names = ["paperplane", "folder", "trash", "pencil", "scribble", "lasso"] - var badge: BadgeUIView! - var images: [UIImage]! - - // MARK: - Setup - override func setUp() { - super.setUp() - - self.images = names.map{ - guard let image = UIImage.init(systemName: $0) else { - fatalError("No Image for \($0)") - } - return image - } - self.badge = BadgeUIView(theme: theme, intent: .danger, value: 99, isBorderVisible: false) - } - - // MARK: - Tests - func test_tabs_with_icons_only() throws { - let sut = TabUIView( - theme: self.theme, - icons: Array(self.images[0..<3]), - apportionsSegmentWidthsByContent: true - ) - sut.setBadge(self.badge, forSegementAt: 2) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_icons_only_equal_width() throws { - let sut = TabUIView( - theme: self.theme, - icons: Array(self.images[0..<3]), - apportionsSegmentWidthsByContent: false - ) - sut.setBadge(self.badge, forSegementAt: 2) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_text_only() throws { - let sut = TabUIView( - theme: self.theme, - titles: Array(self.names[0..<3].map(\.capitalized)), - apportionsSegmentWidthsByContent: true - ) - sut.setBadge(self.badge, forSegementAt: 1) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut) - } - - func test_tabs_with_text_only_equal_width() throws { - let sut = TabUIView( - theme: self.theme, - titles: Array(self.names[0..<2].map(\.capitalized)), - apportionsSegmentWidthsByContent: false - ) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut) - } - - func test_tabs_with_icon_and_text() throws { - let content = Array(Array(zip(images, names.map(\.capitalized)))[0..<3]) - .map(TabUIItemContent.init(icon:title:)) - - let sut = TabUIView( - theme: self.theme, - content: content, - apportionsSegmentWidthsByContent: true - ) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - sut.setBadge(self.badge, forSegementAt: 0) - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_icon_and_text_size_small() throws { - let content = Array(Array(zip(images, names.map(\.capitalized)))[0..<3]) - .map(TabUIItemContent.init(icon:title:)) - - let sut = TabUIView( - theme: self.theme, - tabSize: .sm, - content: content, - apportionsSegmentWidthsByContent: true - ) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_tabs_with_icon_and_text_size_xtra_small() throws { - let content = Array(Array(zip(images, names.map(\.capitalized)))[0..<3]) - .map(TabUIItemContent.init(icon:title:)) - - let sut = TabUIView( - theme: self.theme, - tabSize: .xs, - content: content, - apportionsSegmentWidthsByContent: true - ) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } - - func test_many_tabs_with_icon_and_text() throws { - let content = Array(zip(images, names.map(\.capitalized))) - .map(TabUIItemContent.init(icon:title:)) - - let sut = TabUIView( - theme: self.theme, - content: content, - apportionsSegmentWidthsByContent: true - ) - sut.maxWidth = 390 - sut.backgroundColor = UIColor.systemBackground - - assertSnapshotInDarkAndLight(matching: sut, sizes: [.medium]) - } -} diff --git a/core/Sources/Components/Tab/View/UIKit/TabUIViewTests.swift b/core/Sources/Components/Tab/View/UIKit/TabUIViewTests.swift deleted file mode 100644 index 6026efb3f..000000000 --- a/core/Sources/Components/Tab/View/UIKit/TabUIViewTests.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// TabUIViewTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import XCTest - -@testable import SparkCore - -// swiftlint:disable force_unwrapping -final class TabUIViewTests: XCTestCase { - var sut: TabUIView! - var subscriptions = Set() - - override func setUp() { - super.setUp() - - self.sut = TabUIView(theme: SparkTheme.shared, titles: ["Tab 1", "Tab 2", "Tab 3"]) - } - - func test_theme_change_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.segments[0].viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.theme = SparkTheme.shared - - // Then - waitForExpectations(timeout: 1) - } - - func test_intent_change_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.segments[0].viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.intent = .support - - // Then - waitForExpectations(timeout: 1) - } - - func test_size_change_triggers_attributes_change() { - // Given - let expect = expectation(description: "Attributes should be changed") - expect.expectedFulfillmentCount = 2 - - self.sut.segments[0].viewModel.$tabStateAttributes.subscribe(in: &self.subscriptions) { _ in - expect.fulfill() - } - - // When - self.sut.tabSize = .xs - - // Then - waitForExpectations(timeout: 1) - } - - func test_set_enable_sets_state_for_each_segment() { - // When - self.sut.isEnabled = false - - XCTAssertFalse(self.sut.segments[0].isEnabled, "Segment 1 should be disabled") - XCTAssertFalse(self.sut.segments[1].isEnabled, "Segment 2 should be disabled") - XCTAssertFalse(self.sut.segments[2].isEnabled, "Segment 3 should be disabled") - } - - func test_set_attributes_should_work() { - // Given - let image = UIImage() - // When - self.sut.setTitle("Hello", forSegmentAt: 2) - self.sut.setImage(image, forSegmentAt: 2) - - XCTAssertEqual(self.sut.segments[2].title, "Hello", "Expected title on tab to be set correctly") - XCTAssertEqual(self.sut.segments[2].icon, image, "Expected icon on tab to be set correctly") - - XCTAssertEqual(self.sut.titleForSegment(at: 2), "Hello", "Expected same result as accessing text of element directly") - XCTAssertEqual(self.sut.imageForSegment(at: 2), image, "Expected same result as accessing icon of element directly") - } - - func test_tab_change_is_published() { - // Given - let expect = expectation(description: "Expect publisher to publish new selected tab") - var tabTapped = 0 - self.sut.publisher.sink(receiveValue: { selectedTab in - tabTapped = selectedTab - expect.fulfill() - }) - .store(in: &self.subscriptions) - - // When - self.sut.segments[2].sendActions(for: .touchUpInside) - - // Then - waitForExpectations(timeout: 1) - XCTAssertEqual(tabTapped, 2, "Expected tapped tab to be 2.") - } - - func test_tab_change_is_not_published_when_set() { - // Given - let expect = expectation(description: "Expect publisher not to be called") - expect.isInverted = true - - self.sut.publisher.sink(receiveValue: { (selectedTab: Int) in - expect.fulfill() - }) - .store(in: &self.subscriptions) - - // When - self.sut.selectedSegmentIndex = 2 - - // Then - waitForExpectations(timeout: 1) - } - - func test_tab_change_is_sent_to_delegate() { - // Given - let delegate = TabUIViewDelegateGeneratedMock() - self.sut.delegate = delegate - - // When - self.sut.segments[2].sendActions(for: .touchUpInside) - - // Then - XCTAssertEqual( - delegate.segmentSelectedWithIndexAndSenderCallsCount, - 1, - "Expected delegate to be called.") - XCTAssertEqual( - delegate.segmentSelectedWithIndexAndSenderReceivedArguments?.index, - 2, - "Expected delegate to have correct arguments" - ) - } - - func test_tab_change_action_value_changed() { - // Given - let expect = expectation(description: "Expect action to be executed") - - let action = UIAction { _ in - expect.fulfill() - } - - self.sut.addAction(action, for: .valueChanged) - - // When - self.sut.segments[2].sendActions(for: .touchUpInside) - - // Then - waitForExpectations(timeout: 1) - XCTAssertEqual(self.sut.selectedSegmentIndex, 2, "Selected segment should be 2") - } - - func test_setting_selected_state_on_tab_unselects_other() { - // Given - let expect = expectation(description: "Expect action to be executed") - - let action = UIAction { _ in - expect.fulfill() - } - - self.sut.segments[2].addAction(action, for: .otherSegmentSelected) - - // When - self.sut.segments[2].isSelected = true - - // Then - waitForExpectations(timeout: 1) - XCTAssertFalse(self.sut.segments[0].isSelected, "Segment 0 should not be selected") - XCTAssertTrue(self.sut.segments[2].isSelected, "Segment 2 should be selected") - XCTAssertEqual(self.sut.selectedSegmentIndex, 2, "Selected item should be 2") - } - - func test_action_is_executed() { - // Given - let expect = expectation(description: "Expect action to be executed") - let action = UIAction { _ in - expect.fulfill() - } - - self.sut.setAction(action, forSegmentAt: 2) - - // When - self.sut.segments[2].sendActions(for: .touchUpInside) - - // Then - waitForExpectations(timeout: 1) - XCTAssertIdentical(self.sut.actionForSegment(at: 2), action, "Action should be set") - XCTAssertEqual(self.sut.segmentIndex(identifiedBy: action.identifier), 2, "Expect correct segment to be identified.") - } - - func test_insert_with_image() { - // Given - let image = UIImage() - - // When - self.sut.insertSegment(with: image, at: 0) - - XCTAssertIdentical(self.sut.imageForSegment(at: 0), image, "Should contain new segment") - XCTAssertEqual(self.sut.segments.count, 4, "An extra segment has been added.") - XCTAssertTrue(self.sut.segment(at: 1)!.isSelected, "The old selected has moved.") - } - - func test_insert_with_text() { - // When - self.sut.insertSegment(with: "New Tab", at: 0) - - XCTAssertEqual(self.sut.titleForSegment(at: 0), "New Tab", "Should contain new segment") - } - - func test_insert_with_text_and_image() { - // Given - let image = UIImage() - - // When - self.sut.insertSegment(withImage: image, andTitle: "New Tab", at: 0) - - XCTAssertEqual(self.sut.titleForSegment(at: 0), "New Tab", "Should contain new segment") - XCTAssertIdentical(self.sut.imageForSegment(at: 0), image, "Should contain new segment") - } - - func test_set_enabled() { - // When - self.sut.setEnabled(false, at: 0) - - XCTAssertFalse(self.sut.isEnabledForSegment(at: 0), "Segment 0 should be disabled") - } - - func test_remove_all_segments() { - // When - self.sut.removeAllSegments() - - XCTAssertTrue(self.sut.segments.isEmpty, "Expected to have no segments") - } - - func test_remove_first_segment() { - // When - self.sut.removeSegment(at: 0, animated: false) - - XCTAssertEqual(self.sut.segments.count, 2, "Segment expected to be removed") - } -} - diff --git a/core/Sources/Components/Tab/ViewModel/TabContainerViewModel.swift b/core/Sources/Components/Tab/ViewModel/TabContainerViewModel.swift deleted file mode 100644 index 4a457b544..000000000 --- a/core/Sources/Components/Tab/ViewModel/TabContainerViewModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// TabContainerViewModel.swift -// SparkCore -// -// Created by Michael Zimmermann on 06.06.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -final class TabContainerViewModel: ObservableObject { - @Published var apportionsSegmentWidthsByContent: Bool = false -} diff --git a/core/Sources/Components/Tab/ViewModel/TabItemViewModel.swift b/core/Sources/Components/Tab/ViewModel/TabItemViewModel.swift deleted file mode 100644 index 60f2f0a32..000000000 --- a/core/Sources/Components/Tab/ViewModel/TabItemViewModel.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// TabItemViewModel.swift -// SparkCore -// -// Created by alican.aycil on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import UIKit - -/// `TabItemViewModel` is the view model for both the SwiftUI `TabItemView` as well as the UIKit `TabItemUIView`. -/// The view model is responsible for returning the varying attributes to the views, i.e. colors and attributes. These are determined by the theme, intent, tabState, content and tabGetStateAttributesUseCase. -/// When the theme, intent, states or contents change the new values are calculated and published. -final class TabItemViewModel: ObservableObject where Content: TitleContaining { - - // MARK: - Private Properties - private var tabState: TabState { - didSet { - guard tabState != oldValue else { return } - self.updateStateAttributes() - } - } - - private let tabGetStateAttributesUseCase: TabGetStateAttributesUseCasable - - // MARK: Properties - var theme: Theme { - didSet { - self.updateStateAttributes() - } - } - - var intent: TabIntent { - didSet { - guard intent != oldValue else { return } - self.updateStateAttributes() - } - } - - var tabSize: TabSize { - didSet { - guard self.tabSize != oldValue else { return } - self.updateStateAttributes() - } - } - - private (set) var isEnabled: Bool { - get { - self.tabState.isEnabled - } - set { - self.tabState = self.tabState.update(\.isEnabled, value: newValue) - } - } - - private (set) var isSelected: Bool { - get { - self.tabState.isSelected - } - set { - self.tabState = self.tabState.update(\.isSelected, value: newValue) - } - } - - var isPressed: Bool { - get { - self.tabState.isPressed - } - set { - self.tabState = self.tabState.update(\.isPressed, value: newValue) - } - } - - // MARK: Published Properties - @Published var tabStateAttributes: TabStateAttributes - @Published var apportionsSegmentWidthsByContent: Bool - @Published var content: Content { - didSet { - self.updateStateAttributes() - } - } - - // MARK: Init - /// Init - /// Parameters: - /// - theme: the current `Theme` - /// - intent: the `TabIntent`, which will determine the color of the tab item's tint color - /// - tabState: the `TabState` determines the current state of the tab. - /// - content: the `TabUIItemContent` contents of the tab item: - /// - tabGetStateAttributesUseCase: `TabGetStateAttributesUseCasable` has a default value `TabGetStateAttributesUseCase` - init( - theme: Theme, - intent: TabIntent = .basic, - tabSize: TabSize = .md, - tabState: TabState = .init(), - content: Content, - apportionsSegmentWidthsByContent: Bool = false, - tabGetStateAttributesUseCase: TabGetStateAttributesUseCasable = TabGetStateAttributesUseCase() - ) { - self.tabState = tabState - self.theme = theme - self.intent = intent - self.content = content - self.tabSize = tabSize - self.apportionsSegmentWidthsByContent = apportionsSegmentWidthsByContent - self.tabGetStateAttributesUseCase = tabGetStateAttributesUseCase - - self.tabStateAttributes = tabGetStateAttributesUseCase.execute( - theme: theme, - intent: intent, - state: tabState, - tabSize: tabSize, - hasTitle: content.hasTitle - ) - } - - @discardableResult - func updateState(isEnabled: Bool) -> Self { - guard self.isEnabled != isEnabled else { return self } - self.isEnabled = isEnabled - return self - } - - @discardableResult - func updateState(isSelected: Bool) -> Self { - guard self.isSelected != isSelected else { return self } - self.isSelected = isSelected - return self - } - - @discardableResult - func updateState(isPressed: Bool) -> Self { - guard self.isPressed != isPressed else { return self } - self.isPressed = isPressed - return self - } - - // MARK: - Private functions - private func updateStateAttributes() { - self.tabStateAttributes = self.tabGetStateAttributesUseCase.execute( - theme: self.theme, - intent: self.intent, - state: self.tabState, - tabSize: self.tabSize, - hasTitle: self.content.hasTitle - ) - } -} diff --git a/core/Sources/Components/Tab/ViewModel/TabItemViewModelTests.swift b/core/Sources/Components/Tab/ViewModel/TabItemViewModelTests.swift deleted file mode 100644 index 5ef3290f1..000000000 --- a/core/Sources/Components/Tab/ViewModel/TabItemViewModelTests.swift +++ /dev/null @@ -1,317 +0,0 @@ -// -// TabItemViewModelTests.swift -// SparkCoreTests -// -// Created by alican.aycil on 25.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import XCTest - -final class TabItemViewModelTests: XCTestCase { - - // MARK: - Private properties - private var theme: ThemeGeneratedMock! - private var tabGetStateAttributesUseCase: TabGetStateAttributesUseCasableGeneratedMock! - private var cancellables = Set() - private var spacings: TabItemSpacings! - private var colors: TabItemColors! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - tabGetStateAttributesUseCase = TabGetStateAttributesUseCasableGeneratedMock() - - self.spacings = TabItemSpacings( - verticalEdge: self.theme.layout.spacing.medium, - horizontalEdge: self.theme.layout.spacing.large, - content: self.theme.layout.spacing.medium - ) - self.colors = TabItemColors( - label: self.theme.colors.base.outline, - line: self.theme.colors.base.outline, - background: self.theme.colors.base.surface - ) - - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.small, - itemHeight: 40, - iconHeight: 16 - ) - tabGetStateAttributesUseCase.executeWithThemeAndIntentAndStateAndTabSizeAndHasTitleReturnValue = TabStateAttributes( - spacings: self.spacings, - colors: self.colors, - heights: expectedHeights, - font: theme.typography.body1 - ) - } - - // MARK: - Tests - func test_initialization() { - // Given - let title = "Text" - let icon = UIImage(systemName: "pencil.circle") - let sut = self.sut(icon: icon, title: title) - - // Then - XCTAssertIdentical(sut.theme as AnyObject, self.theme, "sut theme should be the same as self.theme") - XCTAssertEqual(sut.intent, .main, "sut intent should be main") - XCTAssertFalse(sut.isSelected, "sut's isSelected parameter should be false") - XCTAssertFalse(sut.isPressed, "sut's isPressed parameter should be false") - XCTAssertTrue(sut.isEnabled, "sut's isDisabled parameter should be false") - } - - func test_usecase_is_executed_on_initialization() { - // Given - _ = self.sut(intent: .support) - let arguments = self.tabGetStateAttributesUseCase.executeWithThemeAndIntentAndStateAndTabSizeAndHasTitleReceivedArguments - - // Then - XCTAssertIdentical(arguments?.theme as AnyObject, self.theme, "sut theme should be the same as self.theme") - XCTAssertEqual(arguments?.intent, .support, "sut intent should be support") - XCTAssertEqual(arguments?.state, TabState(), "sut state should be TabState that has default parameters") - } - - func test_published_attributes_on_initialization() { - // Given - let sut = self.sut() - let expectedHeights = TabItemHeights( - separatorLineHeight: self.theme.border.width.small, - itemHeight: 40, - iconHeight: 16 - ) - - let expectedAttributes = TabStateAttributes( - spacings: self.spacings, - colors: self.colors, - heights: expectedHeights, - font: self.theme.typography.body1 - ) - - let expectation = expectation(description: "wait for attributes") - var givenAttributes: TabStateAttributes? - - // When - sut.$tabStateAttributes.sink(receiveValue: { attributes in - givenAttributes = attributes - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // Then - wait(for: [expectation], timeout: 0.1) - XCTAssertEqual(givenAttributes, expectedAttributes) - } - - func test_published_attributes_on_change() { - // Given - let sut = self.sut() - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - sut.$tabStateAttributes.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.updateState(isSelected: true) - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_attributes_not_published() { - // Given - let sut = self.sut() - let expectation = expectation(description: "wait for attributes") - sut.$tabStateAttributes.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.updateState(isSelected: false) - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_published_is_pressed_on_change() { - // Given - let sut = self.sut() - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - sut.$tabStateAttributes.sink(receiveValue: { _ in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.isPressed = true - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_published_is_disable_on_change() { - // Given - let sut = self.sut() - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - var counter = 0 - sut.$tabStateAttributes.sink(receiveValue: { _ in - counter += 1 - let arguments = self.tabGetStateAttributesUseCase.executeWithThemeAndIntentAndStateAndTabSizeAndHasTitleReceivedArguments - - XCTAssertEqual(arguments?.state.isEnabled, counter == 1) - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.updateState(isEnabled: false) - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_published_attribute_on_size_change() { - // Given - let sut = self.sut(size: .md, title: "Hello") - - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - - sut.$tabStateAttributes.sink(receiveValue: { attributes in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.tabSize = .xs - - // Then - wait(for: [expectation], timeout: 0.1) - XCTAssertEqual(self.tabGetStateAttributesUseCase.executeWithThemeAndIntentAndStateAndTabSizeAndHasTitleReceivedArguments?.tabSize, .xs) - } - - func test_not_published_attribute_when_no_size_change() { - // Given - let sut = self.sut(size: .xs, title: "Hello") - - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 1 - - sut.$tabStateAttributes.sink(receiveValue: { attributes in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.tabSize = .xs - - // Then - wait(for: [expectation], timeout: 0.1) - XCTAssertEqual(self.tabGetStateAttributesUseCase.executeWithThemeAndIntentAndStateAndTabSizeAndHasTitleReceivedArguments?.tabSize, .xs) - } - - func test_when_theme_changes_then_attributes_published() { - // Given - let sut = self.sut(size: .sm, title: "Label") - - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - - sut.$tabStateAttributes.sink(receiveValue: { attributes in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.theme = ThemeGeneratedMock.mocked() - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_when_intent_changes_then_attributes_published() { - // Given - let sut = self.sut(intent: .main, title: "Label") - - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - - sut.$tabStateAttributes.sink(receiveValue: { attributes in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.intent = .support - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_when_intent_does_not_change_then_nothing_published() { - // Given - let sut = self.sut(intent: .main, title: "Label") - - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 1 - - sut.$tabStateAttributes.sink(receiveValue: { attributes in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.intent = .main - - // Then - wait(for: [expectation], timeout: 0.1) - } - - func test_attributes_changed_when_pressed() { - // Given - let sut = self.sut(size: .md, title: "Hello") - - let expectation = expectation(description: "wait for attributes") - expectation.expectedFulfillmentCount = 2 - - sut.$tabStateAttributes.sink(receiveValue: { attributes in - expectation.fulfill() - }) - .store(in: &self.cancellables) - - // When - sut.updateState(isPressed: true) - - // Then - wait(for: [expectation], timeout: 0.1) - XCTAssertEqual(self.tabGetStateAttributesUseCase.executeWithThemeAndIntentAndStateAndTabSizeAndHasTitleReceivedArguments?.tabSize, .md) - } -} - -// MARK: - Helper -private extension TabItemViewModelTests { - - func sut( - intent: TabIntent = .main, - size: TabSize = .md, - icon: UIImage? = nil, - title: String? = nil - ) -> TabItemViewModel { - return TabItemViewModel( - theme: self.theme, - intent: intent, - tabSize: size, - tabState: TabState(), - content: .init(icon: icon, title: title), - tabGetStateAttributesUseCase: self.tabGetStateAttributesUseCase - ) - } -} diff --git a/core/Sources/Components/Tab/ViewModel/TabViewModel.swift b/core/Sources/Components/Tab/ViewModel/TabViewModel.swift deleted file mode 100644 index 3ffdf94ad..000000000 --- a/core/Sources/Components/Tab/ViewModel/TabViewModel.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// TabViewModel.swift -// SparkCore -// -// Created by michael.zimmermann on 30.08.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine - -final class TabViewModel: ObservableObject { - - // MARK: - Private variables - private var useCase: any TabsGetAttributesUseCaseable - - // MARK: - Internal variables - var theme: Theme { - didSet { - self.tabsAttributes = self.useCase.execute(theme: theme, size: self.tabSize, isEnabled: self.isEnabled) - } - } - - // Disable/Enable each tab in the tab control. - // The whole tab is regarded as enabled, if all tabs are enabled. - // When set, each tab will be disabled or enabled. - // To disable a single tab, use the function `disableTab`. - private (set) var isEnabled: Bool { - didSet { - self.tabsAttributes = self.useCase.execute(theme: theme, size: self.tabSize, isEnabled: self.isEnabled) - } - } - - var tabSize: TabSize { - didSet { - guard self.tabSize != oldValue else { return } - self.tabsAttributes = self.useCase.execute(theme: theme, size: self.tabSize, isEnabled: self.isEnabled) - } - } - - var numberOfTabs: Int { - return self.content.count - } - - // MARK: - Published variables - @Published var disabledTabs: [Bool] - @Published var apportionsSegmentWidthsByContent: Bool = false - @Published var tabsAttributes: TabsAttributes - @Published var content: [Content] - @Published var isScrollable = false - - // MARK: - Initializer - init(theme: some Theme, - apportionsSegmentWidthsByContent: Bool = false, - content: [Content], - tabSize: TabSize, - useCase: some TabsGetAttributesUseCaseable = TabsGetAttributesUseCase() - ) { - self.theme = theme - self.tabSize = tabSize - self.apportionsSegmentWidthsByContent = apportionsSegmentWidthsByContent - self.useCase = useCase - self.content = content - self.disabledTabs = content.map{ _ in return false } - self.tabsAttributes = useCase.execute(theme: theme, size: tabSize, isEnabled: true) - self.isEnabled = true - } - - // Disable or enable a single tab. - func disableTab(_ disabled: Bool, index: Int) { - guard index < self.content.count else { return } - guard self.disabledTabs[index] != disabled else { return } - - self.disabledTabs[index] = disabled - } - - func isTabEnabled(index: Int) -> Bool { - return !self.disabledTabs[index] && self.isEnabled - } - - @discardableResult - func setIsEnabled(_ isEnabled: Bool) -> Self { - guard self.isEnabled != isEnabled else { return self } - - self.isEnabled = isEnabled - return self - } -} diff --git a/core/Sources/Components/Tab/ViewModel/TabViewModelTests.swift b/core/Sources/Components/Tab/ViewModel/TabViewModelTests.swift deleted file mode 100644 index cb4048b8d..000000000 --- a/core/Sources/Components/Tab/ViewModel/TabViewModelTests.swift +++ /dev/null @@ -1,211 +0,0 @@ -// -// TabViewModelTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 01.09.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import XCTest - -final class TabViewModelTests: XCTestCase { - - // MARK: - Properties - var useCase: TabsGetAttributesUseCaseableGeneratedMock! - var cancellables = Set() - var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUp() { - super.setUp() - self.useCase = TabsGetAttributesUseCaseableGeneratedMock() - self.theme = ThemeGeneratedMock.mocked() - } - - // MARK: - Tests - func test_attributes_published_on_init() throws { - // Given - let expect = expectation(description: "Expect attributes to be set.") - - let expectedAttributes = TabsAttributes( - lineHeight: 1, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenGeneratedMock(uiColor: .blue) - ) - - let content: [TabItemContent] = [.init(icon: nil, title: "Title")] - self.useCase.executeWithThemeAndSizeAndIsEnabledReturnValue = expectedAttributes - - let sut = TabViewModel( - theme: self.theme, - apportionsSegmentWidthsByContent: false, - content: content, - tabSize: .md, - useCase: self.useCase - ) - - var attributes: TabsAttributes? - // When - sut.$tabsAttributes.subscribe(in: &self.cancellables) { attrs in - attributes = attrs - expect.fulfill() - } - - // Then - wait(for: [expect], timeout: 0.1) - XCTAssertEqual(attributes, expectedAttributes) - } - - func test_attributes_published_on_theme_change() throws { - // Given - let expect = expectation(description: "Expect attributes to be set.") - expect.expectedFulfillmentCount = 2 - - let expectedAttributes = TabsAttributes( - lineHeight: 1, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenGeneratedMock(uiColor: .blue) - ) - let content: [TabItemContent] = [.init(icon: nil, title: "Title")] - - self.useCase.executeWithThemeAndSizeAndIsEnabledReturnValue = expectedAttributes - - let sut = TabViewModel( - theme: self.theme, - apportionsSegmentWidthsByContent: false, - content: content, - tabSize: .md, - useCase: self.useCase - ) - - // When - var attributes: TabsAttributes? - sut.$tabsAttributes.subscribe(in: &self.cancellables) { attrs in - attributes = attrs - expect.fulfill() - } - - sut.theme = ThemeGeneratedMock.mocked() - - // Then - wait(for: [expect], timeout: 0.1) - XCTAssertEqual(attributes, expectedAttributes) - } - - func test_attributes_published_on_enabled_change() throws { - // Given - let expect = expectation(description: "Expect attributes to be set.") - expect.expectedFulfillmentCount = 2 - - let expectedAttributes = TabsAttributes( - lineHeight: 1, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenGeneratedMock(uiColor: .blue) - ) - let content: [TabItemContent] = [.init(icon: nil, title: "Title")] - self.useCase.executeWithThemeAndSizeAndIsEnabledReturnValue = expectedAttributes - - let sut = TabViewModel( - theme: self.theme, - apportionsSegmentWidthsByContent: false, - content: content, - tabSize: .md, - useCase: self.useCase - ) - - // When - var attributes: TabsAttributes? - sut.$tabsAttributes.subscribe(in: &self.cancellables) { attrs in - attributes = attrs - expect.fulfill() - } - - sut.setIsEnabled(false) - - // Then - wait(for: [expect], timeout: 0.1) - XCTAssertEqual(attributes, expectedAttributes) - } - - func test_enable() { - let content: [TabItemContent] = [.init(icon: nil, title: "Title")] - - let expectedAttributes = TabsAttributes( - lineHeight: 1, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenGeneratedMock(uiColor: .blue) - ) - - self.useCase.executeWithThemeAndSizeAndIsEnabledReturnValue = expectedAttributes - - let sut = TabViewModel( - theme: self.theme, - apportionsSegmentWidthsByContent: false, - content: content, - tabSize: .md, - useCase: self.useCase - ) - - sut.setIsEnabled(true) - - XCTAssertEqual(sut.disabledTabs, [false]) - } - - func test_disable() { - let content: [TabItemContent] = [.init(icon: nil, title: "Title")] - - let expectedAttributes = TabsAttributes( - lineHeight: 1, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenGeneratedMock(uiColor: .blue) - ) - - self.useCase.executeWithThemeAndSizeAndIsEnabledReturnValue = expectedAttributes - - let sut = TabViewModel( - theme: self.theme, - apportionsSegmentWidthsByContent: false, - content: content, - tabSize: .md, - useCase: self.useCase - ) - - sut.setIsEnabled(false) - - XCTAssertFalse(sut.isTabEnabled(index: 0)) - } - - func test_disable_single_tab() { - let content: [TabItemContent] = [.init(icon: nil, title: "Title")] - - let expectedAttributes = TabsAttributes( - lineHeight: 1, - itemHeight: 40.0, - lineColor: ColorTokenGeneratedMock(uiColor: .red), - backgroundColor: ColorTokenGeneratedMock(uiColor: .blue) - ) - - self.useCase.executeWithThemeAndSizeAndIsEnabledReturnValue = expectedAttributes - - let sut = TabViewModel( - theme: self.theme, - apportionsSegmentWidthsByContent: false, - content: content, - tabSize: .md, - useCase: self.useCase - ) - - sut.disableTab(true, index: 0) - - XCTAssertEqual(sut.disabledTabs, [true], "Expect tab to be disabled") - XCTAssertTrue(sut.isEnabled, "Expect tab control be enabled") - XCTAssertFalse(sut.isTabEnabled(index: 0), "Expected single tab not to be enabled") - } -} diff --git a/core/Sources/Components/Tag/AccessibilityIdentifier/TagAccessibilityIdentifier.swift b/core/Sources/Components/Tag/AccessibilityIdentifier/TagAccessibilityIdentifier.swift deleted file mode 100644 index ee5f68f49..000000000 --- a/core/Sources/Components/Tag/AccessibilityIdentifier/TagAccessibilityIdentifier.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// TagAccessibilityIdentifier.swift -// SparkCore -// -// Created by robin.lemaire on 29/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The accessibility identifiers for the tag. -public enum TagAccessibilityIdentifier { - - // MARK: - Properties - - /// The text label accessibility identifier. - public static let text = "spark-tag-text" - /// The icon image accessibility identifier. - public static let iconImage = "spark-tag-iconImage" -} diff --git a/core/Sources/Components/Tag/Constants/TagConstants.swift b/core/Sources/Components/Tag/Constants/TagConstants.swift deleted file mode 100644 index 88ff6e237..000000000 --- a/core/Sources/Components/Tag/Constants/TagConstants.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// TagConstants.swift -// SparkCore -// -// Created by robin.lemaire on 28/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -enum TagConstants { - static let height: CGFloat = 20 -} diff --git a/core/Sources/Components/Tag/Enum/TagIntent.swift b/core/Sources/Components/Tag/Enum/TagIntent.swift deleted file mode 100644 index 1ac75b283..000000000 --- a/core/Sources/Components/Tag/Enum/TagIntent.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// TagIntent.swift -// SparkCore -// -// Created by robin.lemaire on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The intent of the tag. -public enum TagIntent: CaseIterable { - case alert - case danger - case info - case neutral - case main - case support - case success - case accent - case basic -} diff --git a/core/Sources/Components/Tag/Enum/TagVariant.swift b/core/Sources/Components/Tag/Enum/TagVariant.swift deleted file mode 100644 index 5c50aa37b..000000000 --- a/core/Sources/Components/Tag/Enum/TagVariant.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TagVariant.swift -// SparkCore -// -// Created by robin.lemaire on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The variant for the tag. -public enum TagVariant: CaseIterable { - /// Background and border color is the same, tint is lighter. - case filled - /// Border and tint color is the same, background is lighter. - case outlined - /// Background and border color is the same, tint is darker. - case tinted -} diff --git a/core/Sources/Components/Tag/Model/Colors/TagColors+ExtensionTests.swift b/core/Sources/Components/Tag/Model/Colors/TagColors+ExtensionTests.swift deleted file mode 100644 index 225864446..000000000 --- a/core/Sources/Components/Tag/Model/Colors/TagColors+ExtensionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// TagColors+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 07/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension TagColors { - - // MARK: - Properties - - static func mocked( - backgroundColor: any ColorToken = ColorTokenGeneratedMock.random(), - borderColor: any ColorToken = ColorTokenGeneratedMock.random(), - foregroundColor: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - backgroundColor: backgroundColor, - borderColor: borderColor, - foregroundColor: foregroundColor - ) - } -} diff --git a/core/Sources/Components/Tag/Model/Colors/TagColors.swift b/core/Sources/Components/Tag/Model/Colors/TagColors.swift deleted file mode 100644 index 49f2c8849..000000000 --- a/core/Sources/Components/Tag/Model/Colors/TagColors.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// TagColors.swift -// SparkCore -// -// Created by robin.lemaire on 28/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct TagColors { - - // MARK: - Properties - - let backgroundColor: any ColorToken - let borderColor: any ColorToken - let foregroundColor: any ColorToken -} - -// MARK: Hashable & Equatable - -extension TagColors: Hashable, Equatable { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.backgroundColor) - hasher.combine(self.borderColor) - hasher.combine(self.foregroundColor) - } - - static func == (lhs: TagColors, rhs: TagColors) -> Bool { - return lhs.backgroundColor.equals(rhs.backgroundColor) && - lhs.borderColor.equals(rhs.borderColor) && - lhs.foregroundColor.equals(rhs.foregroundColor) - } -} diff --git a/core/Sources/Components/Tag/Model/Colors/TagColorsTests.swift b/core/Sources/Components/Tag/Model/Colors/TagColorsTests.swift deleted file mode 100644 index be9ff1e7d..000000000 --- a/core/Sources/Components/Tag/Model/Colors/TagColorsTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// TagColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import XCTest - -@testable import SparkCore - -final class TagColorsTests: XCTestCase { - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = TagColors( - backgroundColor: colors.base.background, - borderColor: colors.main.main, - foregroundColor: colors.feedback.info) - - let colors2 = TagColors( - backgroundColor: colors.base.background, - borderColor: colors.main.main, - foregroundColor: colors.feedback.info) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = TagColors( - backgroundColor: colors.base.background, - borderColor: colors.main.main, - foregroundColor: colors.feedback.info) - - let colors2 = TagColors( - backgroundColor: colors.base.background, - borderColor: colors.main.main, - foregroundColor: colors.feedback.alert) - - XCTAssertNotEqual(colors1, colors2) - } -} diff --git a/core/Sources/Components/Tag/Model/ContentColors/TagContentColors+ExtensionTests.swift b/core/Sources/Components/Tag/Model/ContentColors/TagContentColors+ExtensionTests.swift deleted file mode 100644 index 18318c3a3..000000000 --- a/core/Sources/Components/Tag/Model/ContentColors/TagContentColors+ExtensionTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// TagContentColors+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 07/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension TagContentColors { - - // MARK: - Properties - - static func mocked( - color: any ColorToken = ColorTokenGeneratedMock.random(), - onColor: any ColorToken = ColorTokenGeneratedMock.random(), - containerColor: any ColorToken = ColorTokenGeneratedMock.random(), - onContainerColor: any ColorToken = ColorTokenGeneratedMock.random() - ) -> Self { - return .init( - color: color, - onColor: onColor, - containerColor: containerColor, - onContainerColor: onContainerColor - ) - } -} diff --git a/core/Sources/Components/Tag/Model/ContentColors/TagContentColors.swift b/core/Sources/Components/Tag/Model/ContentColors/TagContentColors.swift deleted file mode 100644 index 582e2cf18..000000000 --- a/core/Sources/Components/Tag/Model/ContentColors/TagContentColors.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// TagContentColors.swift -// SparkCore -// -// Created by robin.lemaire on 29/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct TagContentColors { - - // MARK: - Properties - - let color: any ColorToken - let onColor: any ColorToken - let containerColor: any ColorToken - let onContainerColor: any ColorToken -} - -// MARK: Hashable & Equatable - -extension TagContentColors: Hashable, Equatable { - - func hash(into hasher: inout Hasher) { - hasher.combine(self.color) - hasher.combine(self.onColor) - hasher.combine(self.containerColor) - hasher.combine(self.onContainerColor) - } - - static func == (lhs: TagContentColors, rhs: TagContentColors) -> Bool { - return lhs.color.equals(rhs.color) && - lhs.onColor.equals(rhs.onColor) && - lhs.containerColor.equals(rhs.containerColor) && - lhs.onContainerColor.equals(rhs.onContainerColor) - } -} diff --git a/core/Sources/Components/Tag/Model/ContentColors/TagContentColorsTests.swift b/core/Sources/Components/Tag/Model/ContentColors/TagContentColorsTests.swift deleted file mode 100644 index 9fc463501..000000000 --- a/core/Sources/Components/Tag/Model/ContentColors/TagContentColorsTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// TagContentColorsTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 24.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -import XCTest -import XCTest - -@testable import SparkCore - -final class TagContentColorsTests: XCTestCase { - - func testEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = TagContentColors( - color: colors.main.main, - onColor: colors.main.onMain, - containerColor: colors.base.background, - onContainerColor: colors.base.onBackground) - - let colors2 = TagContentColors( - color: colors.main.main, - onColor: colors.main.onMain, - containerColor: colors.base.background, - onContainerColor: colors.base.onBackground) - - XCTAssertEqual(colors1, colors2) - } - - func testNotEqual() { - let colors = SparkTheme.shared.colors - - let colors1 = TagContentColors( - color: colors.main.main, - onColor: colors.main.onMain, - containerColor: colors.base.background, - onContainerColor: colors.base.onBackground) - - let colors2 = TagContentColors( - color: colors.support.support, - onColor: colors.support.onSupport, - containerColor: colors.base.background, - onContainerColor: colors.base.onBackground) - - XCTAssertNotEqual(colors1, colors2) - } -} - diff --git a/core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCase.swift b/core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCase.swift deleted file mode 100644 index 4ec6296a5..000000000 --- a/core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCase.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// TagGetColorsUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 29/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol TagGetColorsUseCaseable { - func execute(theme: Theme, - intent: TagIntent, - variant: TagVariant) -> TagColors -} - -struct TagGetColorsUseCase: TagGetColorsUseCaseable { - - // MARK: - Properties - - private let getContentColorsUseCase: any TagGetContentColorsUseCaseable - - // MARK: - Initialization - - init(getContentColorsUseCase: any TagGetContentColorsUseCaseable = TagGetContentColorsUseCase()) { - self.getContentColorsUseCase = getContentColorsUseCase - } - - // MARK: - Methods - - func execute(theme: Theme, - intent: TagIntent, - variant: TagVariant) -> TagColors { - let contentColors = self.getContentColorsUseCase.execute( - intent: intent, - colors: theme.colors - ) - - switch variant { - case .filled: - return .init( - backgroundColor: contentColors.color, - borderColor: contentColors.color, - foregroundColor: contentColors.onColor - ) - - case .outlined: - return .init( - backgroundColor: ColorTokenDefault.clear, - borderColor: contentColors.color, - foregroundColor: contentColors.color - ) - - case .tinted: - return .init( - backgroundColor: contentColors.containerColor, - borderColor: contentColors.containerColor, - foregroundColor: contentColors.onContainerColor - ) - } - } -} diff --git a/core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCaseTests.swift b/core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCaseTests.swift deleted file mode 100644 index 486fbbc37..000000000 --- a/core/Sources/Components/Tag/UseCase/GetColors/TagGetColorsUseCaseTests.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// TagGetColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 06/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TagGetColorsUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_variant_filled() throws { - // GIVEN - let getContentColorsUseCase = TagGetContentColorsUseCaseableGeneratedMock() - let getContentColorsUseCaseReturnValue = TagContentColors.mocked() - getContentColorsUseCase.executeWithIntentAndColorsReturnValue = getContentColorsUseCaseReturnValue - let colors = ColorsGeneratedMock() - let theme = ThemeGeneratedMock() - theme.underlyingColors = colors - - let useCase = TagGetColorsUseCase(getContentColorsUseCase: getContentColorsUseCase) - let variant = TagVariant.filled - let intent = TagIntent.alert - - // WHEN - let sut = useCase.execute( - theme: theme, - intent: intent, - variant: variant - ) - - // THEN - XCTAssertEqual( - getContentColorsUseCase.executeWithIntentAndColorsCallsCount, - 1, - "getContentColorsUseCase.execute should be called once" - ) - let receivedArguments = try XCTUnwrap( - getContentColorsUseCase.executeWithIntentAndColorsReceivedArguments, - "Couldn't unwrap getContentColorsUseCase.execute received arguments" - ) - XCTAssertIdentical( - receivedArguments.colors as? ColorsGeneratedMock, - colors, - "Wrong getContentColorsUseCase.executereceived colors" - ) - XCTAssertEqual( - receivedArguments.intent, - .alert, - "Wrong getContentColorsUseCase.executereceived intent" - ) - - let color = try XCTUnwrap( - getContentColorsUseCaseReturnValue.color as? ColorTokenGeneratedMock, - "Couldn't unwrap getContentColorsUseCaseReturnValue.color as a ColorTokenGeneratedMock" - ) - XCTAssertIdentical( - sut.backgroundColor as? ColorTokenGeneratedMock, - color, - "Wrong sut.backgroundColor" - ) - XCTAssertIdentical( - sut.borderColor as? ColorTokenGeneratedMock, - color, - "Wrong sut.borderColor" - ) - - let onColor = try XCTUnwrap( - getContentColorsUseCaseReturnValue.onColor as? ColorTokenGeneratedMock, - "Couldn't unwrap getContentColorsUseCaseReturnValue.onColor as a ColorTokenGeneratedMock" - ) - XCTAssertIdentical( - sut.foregroundColor as? ColorTokenGeneratedMock, - onColor, - "Wrong sut.foregroundColor" - ) - } - - func test_execute_variant_outlined() throws { - // GIVEN - let getContentColorsUseCase = TagGetContentColorsUseCaseableGeneratedMock() - let getContentColorsUseCaseReturnValue = TagContentColors.mocked() - getContentColorsUseCase.executeWithIntentAndColorsReturnValue = getContentColorsUseCaseReturnValue - let colors = ColorsGeneratedMock() - let theme = ThemeGeneratedMock() - theme.underlyingColors = colors - - let useCase = TagGetColorsUseCase(getContentColorsUseCase: getContentColorsUseCase) - let variant = TagVariant.outlined - let intent = TagIntent.danger - - // WHEN - let sut = useCase.execute( - theme: theme, - intent: intent, - variant: variant - ) - - // THEN - XCTAssertEqual( - getContentColorsUseCase.executeWithIntentAndColorsCallsCount, - 1, - "getContentColorsUseCase.execute should be called once" - ) - let receivedArguments = try XCTUnwrap( - getContentColorsUseCase.executeWithIntentAndColorsReceivedArguments, - "Couldn't unwrap getContentColorsUseCase.execute received arguments" - ) - XCTAssertIdentical( - receivedArguments.colors as? ColorsGeneratedMock, - colors, - "Wrong getContentColorsUseCase.executereceived colors" - ) - XCTAssertEqual( - receivedArguments.intent, - .danger, - "Wrong getContentColorsUseCase.executereceived intent" - ) - - let color = try XCTUnwrap( - getContentColorsUseCaseReturnValue.color as? ColorTokenGeneratedMock, - "Couldn't unwrap getContentColorsUseCaseReturnValue.color as a ColorTokenGeneratedMock" - ) - XCTAssertIdentical( - sut.foregroundColor as? ColorTokenGeneratedMock, - color, - "Wrong sut.foregroundColor") - XCTAssertIdentical( - sut.borderColor as? ColorTokenGeneratedMock, - color, - "Wrong sut.borderColor" - ) - - XCTAssertTrue( - sut.backgroundColor.isClear, - "Wrong sut.backgroundColor" - ) - } - - func test_execute_variant_tinted() throws { - // GIVEN - let getContentColorsUseCase = TagGetContentColorsUseCaseableGeneratedMock() - let getContentColorsUseCaseReturnValue = TagContentColors.mocked() - getContentColorsUseCase.executeWithIntentAndColorsReturnValue = getContentColorsUseCaseReturnValue - let colors = ColorsGeneratedMock() - let theme = ThemeGeneratedMock() - theme.underlyingColors = colors - - let useCase = TagGetColorsUseCase(getContentColorsUseCase: getContentColorsUseCase) - let variant = TagVariant.tinted - let intent = TagIntent.success - - // WHEN - let sut = useCase.execute( - theme: theme, - intent: intent, - variant: variant - ) - - // THEN - XCTAssertEqual( - getContentColorsUseCase.executeWithIntentAndColorsCallsCount, - 1, - "getContentColorsUseCase.execute should be called once" - ) - let receivedArguments = try XCTUnwrap( - getContentColorsUseCase.executeWithIntentAndColorsReceivedArguments, - "Couldn't unwrap getContentColorsUseCase.execute received arguments" - ) - XCTAssertIdentical( - receivedArguments.colors as? ColorsGeneratedMock, - colors, - "Wrong getContentColorsUseCase.executereceived colors" - ) - XCTAssertEqual( - receivedArguments.intent, - .success, - "Wrong getContentColorsUseCase.executereceived intent" - ) - - let containerColor = try XCTUnwrap( - getContentColorsUseCaseReturnValue.containerColor as? ColorTokenGeneratedMock, - "Couldn't unwrap getContentColorsUseCaseReturnValue.containerColor as a ColorTokenGeneratedMock" - ) - XCTAssertIdentical( - sut.backgroundColor as? ColorTokenGeneratedMock, - containerColor, - "Wrong sut.foregroundColor") - XCTAssertIdentical( - sut.borderColor as? ColorTokenGeneratedMock, - containerColor, - "Wrong sut.borderColor" - ) - - let onContainerColor = try XCTUnwrap( - getContentColorsUseCaseReturnValue.onContainerColor as? ColorTokenGeneratedMock, - "Couldn't unwrap getContentColorsUseCaseReturnValue.onContainerColor as a ColorTokenGeneratedMock" - ) - XCTAssertIdentical( - sut.foregroundColor as? ColorTokenGeneratedMock, - onContainerColor, - "Wrong sut.foregroundColor" - ) - } -} diff --git a/core/Sources/Components/Tag/UseCase/GetContentColors/TagGetContentColorsUseCase.swift b/core/Sources/Components/Tag/UseCase/GetContentColors/TagGetContentColorsUseCase.swift deleted file mode 100644 index 54d025da1..000000000 --- a/core/Sources/Components/Tag/UseCase/GetContentColors/TagGetContentColorsUseCase.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// TagGetContentColorsUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 29/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -protocol TagGetContentColorsUseCaseable { - func execute(intent: TagIntent, - colors: Colors) -> TagContentColors -} - -struct TagGetContentColorsUseCase: TagGetContentColorsUseCaseable { - - // MARK: - Methods - - func execute(intent: TagIntent, - colors: Colors) -> TagContentColors { - - switch intent { - case .alert: - return .init( - color: colors.feedback.alert, - onColor: colors.feedback.onAlert, - containerColor: colors.feedback.alertContainer, - onContainerColor: colors.feedback.onAlertContainer - ) - - case .danger: - return .init( - color: colors.feedback.error, - onColor: colors.feedback.onError, - containerColor: colors.feedback.errorContainer, - onContainerColor: colors.feedback.onErrorContainer - ) - - case .info: - return .init( - color: colors.feedback.info, - onColor: colors.feedback.onInfo, - containerColor: colors.feedback.infoContainer, - onContainerColor: colors.feedback.onInfoContainer - ) - - case .neutral: - return .init( - color: colors.feedback.neutral, - onColor: colors.feedback.onNeutral, - containerColor: colors.feedback.neutralContainer, - onContainerColor: colors.feedback.onNeutralContainer - ) - - case .main: - return .init( - color: colors.main.main, - onColor: colors.main.onMain, - containerColor: colors.main.mainContainer, - onContainerColor: colors.main.onMainContainer - ) - - case .support: - return .init( - color: colors.support.support, - onColor: colors.support.onSupport, - containerColor: colors.support.supportContainer, - onContainerColor: colors.support.onSupportContainer - ) - - case .success: - return .init( - color: colors.feedback.success, - onColor: colors.feedback.onSuccess, - containerColor: colors.feedback.successContainer, - onContainerColor: colors.feedback.onSuccessContainer - ) - - case .accent: - return .init( - color: colors.accent.accent, - onColor: colors.accent.onAccent, - containerColor: colors.accent.accentContainer, - onContainerColor: colors.accent.onAccentContainer - ) - - case .basic: - return .init( - color: colors.basic.basic, - onColor: colors.basic.onBasic, - containerColor: colors.basic.basicContainer, - onContainerColor: colors.basic.onBasicContainer - ) - } - } -} diff --git a/core/Sources/Components/Tag/UseCase/GetContentColors/TagGetIntentColorsUseCaseTests.swift b/core/Sources/Components/Tag/UseCase/GetContentColors/TagGetIntentColorsUseCaseTests.swift deleted file mode 100644 index b0bf42c3a..000000000 --- a/core/Sources/Components/Tag/UseCase/GetContentColors/TagGetIntentColorsUseCaseTests.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// TagGetContentColorsUseCaseTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 06/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TagGetContentColorsUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_intent_alert() { - // GIVEN - let intent = TagIntent.alert - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.feedback.alert, - onColor: colors.feedback.onAlert, - containerColor: colors.feedback.alertContainer, - onContainerColor: colors.feedback.onAlertContainer - ) - ) - } - - func test_execute_intent_danger() { - // GIVEN - let intent = TagIntent.danger - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.feedback.error, - onColor: colors.feedback.onError, - containerColor: colors.feedback.errorContainer, - onContainerColor: colors.feedback.onErrorContainer - ) - ) - } - - func test_execute_intent_info() { - // GIVEN - let intent = TagIntent.info - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.feedback.info, - onColor: colors.feedback.onInfo, - containerColor: colors.feedback.infoContainer, - onContainerColor: colors.feedback.onInfoContainer - ) - ) - } - - func test_execute_intent_neutral() { - // GIVEN - let intent = TagIntent.neutral - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.feedback.neutral, - onColor: colors.feedback.onNeutral, - containerColor: colors.feedback.neutralContainer, - onContainerColor: colors.feedback.onNeutralContainer - ) - ) - } - - func test_execute_intent_main() { - // GIVEN - let intent = TagIntent.main - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.main.main, - onColor: colors.main.onMain, - containerColor: colors.main.mainContainer, - onContainerColor: colors.main.onMainContainer - ) - ) - } - - func test_execute_intent_support() { - // GIVEN - let intent = TagIntent.support - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.support.support, - onColor: colors.support.onSupport, - containerColor: colors.support.supportContainer, - onContainerColor: colors.support.onSupportContainer - ) - ) - } - - func test_execute_intent_success() { - // GIVEN - let intent = TagIntent.success - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.feedback.success, - onColor: colors.feedback.onSuccess, - containerColor: colors.feedback.successContainer, - onContainerColor: colors.feedback.onSuccessContainer - ) - ) - } - - func test_execute_intent_accent() { - // GIVEN - let intent = TagIntent.accent - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.accent.accent, - onColor: colors.accent.onAccent, - containerColor: colors.accent.accentContainer, - onContainerColor: colors.accent.onAccentContainer - ) - ) - } - - func test_execute_intent_basic() { - // GIVEN - let intent = TagIntent.basic - let useCase = TagGetContentColorsUseCase() - let colors = ColorsGeneratedMock.mocked() - - // WHEN - let sut = useCase.execute(intent: intent, - colors: colors) - - // THEN - XCTAssertEqual( - sut, - .init( - color: colors.basic.basic, - onColor: colors.basic.onBasic, - containerColor: colors.basic.basicContainer, - onContainerColor: colors.basic.onBasicContainer - ) - ) - } -} diff --git a/core/Sources/Components/Tag/View/Common/TagConfigurationSnapshotTests.swift b/core/Sources/Components/Tag/View/Common/TagConfigurationSnapshotTests.swift deleted file mode 100644 index e11a8b518..000000000 --- a/core/Sources/Components/Tag/View/Common/TagConfigurationSnapshotTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// TagConfigurationSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -@testable import SparkCore - -struct TagConfigurationSnapshotTests { - - // MARK: - Properties - - let scenario: TagScenarioSnapshotTests - - let intent: TagIntent - let variant: TagVariant - let content: TagContentType - var width: CGFloat? { - return self.content.isLongText ? 100 : nil - } - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Getter - - func testName() -> String { - return [ - "\(self.scenario.rawValue)", - "\(self.intent)", - "\(self.variant)", - "\(self.content.name)", - ].joined(separator: "-") - } -} - -// MARK: - Enum - -enum TagContentType { - case text(_ value: String) - case longText(_ value: String) - case attributedText(_ value: AttributedStringEither) - case icon(_ image: ImageEither) - case iconAndText(_ image: ImageEither, _ value: String) - case iconAndLongText(_ image: ImageEither, _ value: String) - case iconAndAttributedText(_ image: ImageEither, _ value: AttributedStringEither) - - // MARK: - Properties - - var name: String { - switch self { - case .text: - return "text" - case .longText: - return "longText" - case .attributedText: - return "attributedText" - case .icon: - return "icon" - case .iconAndText: - return "iconAndText" - case .iconAndLongText: - return "iconAndLongText" - case .iconAndAttributedText: - return "iconAndAttributedText" - } - } - - var isLongText: Bool { - switch self { - case .longText, .iconAndLongText: - return true - default: - return false - } - } - - // MARK: - Constants - - enum Constants { - static var text: String = "Text" - static var longText: String = "Very very long long text" - static func attributedText(isSwiftUIComponent: Bool) -> AttributedStringEither { - return .mock( - isSwiftUIComponent: isSwiftUIComponent, - text: "My AT Text", - fontSize: 14 - ) - } - static func icon(isSwiftUIComponent: Bool) -> ImageEither { - return .mock(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Cases - - static func allCasesExceptText(isSwiftUIComponent: Bool) -> [Self] { - return [ - .text(Constants.text), - .longText(Constants.longText), - .attributedText(Constants.attributedText(isSwiftUIComponent: isSwiftUIComponent)), - .icon(Constants.icon(isSwiftUIComponent: isSwiftUIComponent)), - .iconAndText( - Constants.icon(isSwiftUIComponent: isSwiftUIComponent), - Constants.text - ), - .iconAndLongText( - Constants.icon(isSwiftUIComponent: isSwiftUIComponent), - Constants.longText - ), - .iconAndAttributedText( - Constants.icon(isSwiftUIComponent: isSwiftUIComponent), - Constants.attributedText(isSwiftUIComponent: isSwiftUIComponent) - ) - ] - } -} diff --git a/core/Sources/Components/Tag/View/Common/TagScenarioSnapshotTests.swift b/core/Sources/Components/Tag/View/Common/TagScenarioSnapshotTests.swift deleted file mode 100644 index 0865deff0..000000000 --- a/core/Sources/Components/Tag/View/Common/TagScenarioSnapshotTests.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// TagScenarioSnapshotTestsTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 10/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI - -enum TagScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) -> [TagConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1(isSwiftUIComponent: isSwiftUIComponent) - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3(isSwiftUIComponent: isSwiftUIComponent) - case .test4: - return self.test4(isSwiftUIComponent: isSwiftUIComponent) - case .test5: - return self.test5(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all intents - /// - /// Content: - /// - intents: all - /// - variant: tinted - /// - content: icon + text - /// - mode: all - /// - size: default - private func test1(isSwiftUIComponent: Bool) -> [TagConfigurationSnapshotTests] { - let intents = TagIntent.allCases - - return intents.map { - .init( - scenario: self, - intent: $0, - variant: .tinted, - content: .iconAndText( - TagContentType.Constants.icon(isSwiftUIComponent: isSwiftUIComponent), - TagContentType.Constants.text - ), - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 2 - /// - /// Description: To test all variants - /// - /// Content: - /// - intent: main - /// - variant: all - /// - content: text only - /// - mode: all - /// - size: default - private func test2(isSwiftUIComponent: Bool) -> [TagConfigurationSnapshotTests] { - let variants = TagVariant.allCases - - return variants.map { - .init( - scenario: self, - intent: .main, - variant: $0, - content: .text(TagContentType.Constants.text), - modes: Constants.Modes.all, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 3 - /// - /// Description: To test all color for filled variant - /// - /// Content: - /// - intents: all - /// - variant: filled - /// - content: icon + text - /// - mode: default - /// - size: default - private func test3(isSwiftUIComponent: Bool) -> [TagConfigurationSnapshotTests] { - let intents = TagIntent.allCases - - return intents.map { - .init( - scenario: self, - intent: $0, - variant: .filled, - content: .iconAndText( - TagContentType.Constants.icon(isSwiftUIComponent: isSwiftUIComponent), - TagContentType.Constants.text - ), - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 4 - /// - /// Description: To test content resilience - /// - /// Content: - /// - intent: neutral - /// - variant: tinted - /// - content: all (icon only / long text / long text + icon / attributed text / attributed text + icon) - /// - mode: default - /// - size: default - private func test4(isSwiftUIComponent: Bool) -> [TagConfigurationSnapshotTests] { - let contents = TagContentType.allCasesExceptText(isSwiftUIComponent: isSwiftUIComponent) - - return contents.map { - .init( - scenario: self, - intent: .neutral, - variant: .tinted, - content: $0, - modes: Constants.Modes.default, - sizes: Constants.Sizes.default - ) - } - } - - /// Test 6 - /// - /// Description: To test a11y sizes - /// - /// Content: - /// - intent: main - /// - variant: tinted - /// - content: icon + text - /// - mode: default - /// - size: all - private func test5(isSwiftUIComponent: Bool) -> [TagConfigurationSnapshotTests] { - return [ - .init( - scenario: self, - intent: .main, - variant: .tinted, - content: .iconAndText( - TagContentType.Constants.icon(isSwiftUIComponent: isSwiftUIComponent), - TagContentType.Constants.text - ), - modes: Constants.Modes.default, - sizes: Constants.Sizes.all - ) - ] - } -} diff --git a/core/Sources/Components/Tag/View/SwiftUI/TagView.swift b/core/Sources/Components/Tag/View/SwiftUI/TagView.swift deleted file mode 100644 index d1a7d077a..000000000 --- a/core/Sources/Components/Tag/View/SwiftUI/TagView.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// TagView.swift -// SparkCore -// -// Created by robin.lemaire on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -/// The SwiftUI version for the tag -public struct TagView: View { - - // MARK: - Type Alias - - private typealias AccessibilityIdentifier = TagAccessibilityIdentifier - - // MARK: - Private Properties - - @ObservedObject private var viewModel: TagViewModel - @ScaledMetric private var height: CGFloat = TagConstants.height - @ScaledMetric private var smallSpacing: CGFloat - @ScaledMetric private var mediumSpacing: CGFloat - - // MARK: - Initialization - - /// Initialize a new tag with default values. - /// - Parameters: - /// - theme: The spark theme. - /// - intent: The intent of the tag. - /// - variant: The variant of the tag. - /// - /// - Note: You must use the Modifier to add at least iconImage or/and text. - public init( - theme: Theme, - intent: TagIntent, - variant: TagVariant - ) { - let viewModel = TagViewModel( - theme: theme, - intent: intent, - variant: variant - ) - self.viewModel = viewModel - - self._smallSpacing = .init(wrappedValue: viewModel.spacing.small) - self._mediumSpacing = .init(wrappedValue: viewModel.spacing.medium) - } - - // MARK: - View - - public var body: some View { - HStack(spacing: self.smallSpacing) { - // Optional icon image - self.viewModel.iconImage? - .resizable() - .scaledToFit() - .foregroundColor(self.viewModel.colors.foregroundColor.color) - .accessibilityIdentifier(AccessibilityIdentifier.iconImage) - - // Optional Text - self.text() - .lineLimit(1) - .truncationMode(.tail) - .accessibilityIdentifier(AccessibilityIdentifier.text) - } - .padding(.init(vertical: self.smallSpacing, - horizontal: self.mediumSpacing)) - .frame(height: self.height) - .background(self.viewModel.colors.backgroundColor.color) - .border(width: self.viewModel.border.width.small, - radius: self.viewModel.border.radius.full, - colorToken: self.viewModel.colors.borderColor) - } - - // MARK: - View Builder - - @ViewBuilder - private func text() -> some View { - if let text = self.viewModel.text { - Text(text) - .font(self.viewModel.typography.captionHighlight.font) - .foregroundColor(self.viewModel.colors.foregroundColor.color) - } else if let attributedText = self.viewModel.attributedText { // Optional AttributedText - Text(attributedText) - } - } - - // MARK: - Modifier - - /// Add the some accessibility values on tag. - /// - Parameters: - /// - identifier: The accessibility identifier. - /// - label: The label identifier. If value is nil and text is set, the label identifier will be the text value. - /// - Returns: Current Tag View. - public func accessibility(identifier: String, - label: String? = nil) -> some View { - self.modifier(AccessibilityViewModifier(identifier: identifier, - label: label ?? self.viewModel.text)) - } - - /// Set the intent on tag. - /// - Parameters: - /// - intent: The intent of the tag. - /// - Returns: Current Tag View. - @available(*, deprecated, message: "Intent is now directly on the init") - public func intent(_ intent: TagIntent) -> Self { - self.viewModel.setIntent(intent) - return self - } - - /// Set the variant on tag. - /// - Parameters: - /// - variant: The variant of the tag. - /// - Returns: Current Tag View. - @available(*, deprecated, message: "Variant is now directly on the init") - public func variant(_ variant: TagVariant) -> Self { - self.viewModel.setVariant(variant) - return self - } - - /// Set the iconImage on tag. - /// - Parameters: - /// - iconImage: The icon image of the tag. - /// - Nullability: - /// - The image can be nil, in this case, no image is displayed. - /// - If the image is nil, **a text must be added**. - /// - Returns: Current Tag View. - public func iconImage(_ iconImage: Image?) -> Self { - self.viewModel.setIconImage(iconImage) - return self - } - - /// Set the text of the tag. - /// - Parameters: - /// - text: The text of the tag. Can be nil. - /// - Nullability: - /// - The text can be nil, in this case, no text is displayed. - /// - If the text is nil, **an iconImage or attributedText must be added**. - /// - Returns: Current Tag View. - public func text(_ text: String?) -> Self { - self.viewModel.setText(text) - return self - } - - /// Set the attributedText of the tag. - /// - Parameters: - /// - attributedText: The attributedText of the tag. Can be nil. - /// - Nullability: - /// - The attributedText can be nil, in this case, no text is displayed. - /// - If the attributedText is nil, **an iconImage or text must be added**. - /// - Returns: Current Tag View. - public func attributedText(_ attributedText: AttributedString?) -> Self { - self.viewModel.setAttributedText(attributedText) - return self - } -} diff --git a/core/Sources/Components/Tag/View/SwiftUI/TagViewSnapshotTests.swift b/core/Sources/Components/Tag/View/SwiftUI/TagViewSnapshotTests.swift deleted file mode 100644 index 980b48263..000000000 --- a/core/Sources/Components/Tag/View/SwiftUI/TagViewSnapshotTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// TagViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 04/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import SnapshotTesting - -@testable import SparkCore - -final class TagViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = TagScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: true) - for configuration in configurations { - let view = TagView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant - ) - .content(configuration) - .frame(width: configuration.width) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} - -// MARK: - Extension - -private extension TagView { - - @ViewBuilder - func content(_ configuration: TagConfigurationSnapshotTests) -> some View { - switch configuration.content { - case .text(let text), .longText(let text): - self.text(text) - - case .attributedText(let attributedText): - self.attributedText(attributedText.rightValue) - - case .icon(let image): - self.iconImage(image.rightValue) - - case let .iconAndText(image, text), let .iconAndLongText(image, text): - self.iconImage(image.rightValue) - .text(text) - - case let .iconAndAttributedText(image, attributedText): - self.iconImage(image.rightValue) - .attributedText(attributedText.rightValue) - } - } -} - diff --git a/core/Sources/Components/Tag/View/UIKit/TagUIView.swift b/core/Sources/Components/Tag/View/UIKit/TagUIView.swift deleted file mode 100644 index ba5b6e493..000000000 --- a/core/Sources/Components/Tag/View/UIKit/TagUIView.swift +++ /dev/null @@ -1,505 +0,0 @@ -// -// TagUIView.swift -// SparkCore -// -// Created by robin.lemaire on 17/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// The UIKit version for the tag. -public final class TagUIView: UIView { - - // MARK: - Type alias - - private typealias AccessibilityIdentifier = TagAccessibilityIdentifier - - // MARK: - Components - - private lazy var contentStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - self.iconStackView, - self.textLabel - ]) - stackView.axis = .horizontal - return stackView - }() - - private lazy var iconStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - self.topIconSpaceView, - self.iconImageView, - self.bottomIconSpaceView - ]) - stackView.axis = .vertical - return stackView - }() - - private let topIconSpaceView = UIView() - - private var iconImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.accessibilityIdentifier = AccessibilityIdentifier.iconImage - imageView.setContentCompressionResistancePriority(.required, - for: .vertical) - imageView.setContentCompressionResistancePriority(.required, - for: .horizontal) - return imageView - }() - - private let bottomIconSpaceView = UIView() - - private var textLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 1 - label.lineBreakMode = .byTruncatingTail - label.adjustsFontForContentSizeCategory = true - label.accessibilityIdentifier = AccessibilityIdentifier.text - label.setContentCompressionResistancePriority(.required, - for: .vertical) - label.setContentCompressionResistancePriority(.required, - for: .horizontal) - return label - }() - - // MARK: - Public Properties - - /// The spark theme of the tag. - public var theme: Theme { - didSet { - self._colors = self.getColorsFromUseCase() - self.reloadUIFromTheme() - self.reloadUIFromSpacing() - } - } - - /// The intent of the tag. - public var intent: TagIntent { - didSet { - self._colors = self.getColorsFromUseCase() - } - } - - /// The variant of the tag. - public var variant: TagVariant { - didSet { - self._colors = self.getColorsFromUseCase() - } - } - - /// The icon image of the tag. - /// Image can be nil, in this case, no image is displayed. - /// If image is nil, **you must add a text or an attributedText**. - public var iconImage: UIImage? { - didSet { - self.reloadIconImageView() - } - } - - private var _text: String? - - /// The text of the tag. - /// Text can be nil, in this case, no text is displayed. - /// If text is nil, **you must add a iconImage or an attributedText**. - public var text: String? { - set { - self._text = newValue - self._attributedText = nil - self.reloadTextLabel() - } - get { - self._text - } - } - - private var _attributedText: NSAttributedString? - - /// The attributedText of the tag. - /// Text can be nil, in this case, no text is displayed. - /// If attributedText is nil, **you must add a iconImage or a text**. - public var attributedText: NSAttributedString? { - set { - self._attributedText = newValue - self._text = nil - self.reloadTextLabel() - } - get { - self._attributedText - } - } - - // MARK: - Private Properties - - private var heightConstraint: NSLayoutConstraint? - - private var contentStackViewLeadingConstraint: NSLayoutConstraint? - private var contentStackViewTrailingConstraint: NSLayoutConstraint? - - private var topIconSpaceViewTopConstraint: NSLayoutConstraint? - - @ScaledUIMetric private var height: CGFloat = TagConstants.height - @ScaledUIMetric private var iconVerticalSpacing: CGFloat = 0 - @ScaledUIMetric private var contentHorizontalSpacing: CGFloat = 0 - - private var _colors: TagColors? { - didSet { - self.reloadUIFromColors() - } - } - private var colors: TagColors { - // Init value from use case only if value is nil - guard let colors = self._colors else { - let colors = self.getColorsFromUseCase() - self._colors = colors - return colors - } - - return colors - } - - private let getColorsUseCase: any TagGetColorsUseCaseable - - // MARK: - Initialization - - /// Initialize a new tag view with icon image. - /// - Parameters: - /// - theme: The spark theme of the tag. - /// - intent: The intent of the tag. - /// - variant: The variant of the tag. - /// - iconImage: The icon image of the tag. - public convenience init( - theme: Theme, - intent: TagIntent, - variant: TagVariant, - iconImage: UIImage - ) { - self.init( - theme, - intent: intent, - variant: variant, - iconImage: iconImage - ) - } - - /// Initialize a new tag view with text. - /// - Parameters: - /// - theme: The spark theme of the tag. - /// - intent: The intent of the tag. - /// - variant: The variant of the tag. - /// - text: The text of the tag. - public convenience init( - theme: Theme, - intent: TagIntent, - variant: TagVariant, - text: String - ) { - self.init( - theme, - intent: intent, - variant: variant, - text: text - ) - } - - /// Initialize a new tag view with attributedText. - /// - Parameters: - /// - theme: The spark theme of the tag. - /// - intent: The intent of the tag. - /// - variant: The variant of the tag. - /// - attributedText: The attributedText of the tag. - public convenience init( - theme: Theme, - intent: TagIntent, - variant: TagVariant, - attributedText: NSAttributedString - ) { - self.init( - theme, - intent: intent, - variant: variant, - text: attributedText - ) - } - - /// Initialize a new tag view with icon image and text. - /// - Parameters: - /// - theme: The spark theme of the tag. - /// - intent: The intent of the tag. - /// - variant: The variant of the tag. - /// - iconImage: The icon image of the tag. - /// - text: The text of the tag. - public convenience init( - theme: Theme, - intent: TagIntent, - variant: TagVariant, - iconImage: UIImage, - text: String - ) { - self.init( - theme, - intent: intent, - variant: variant, - iconImage: iconImage, - text: text - ) - } - - /// Initialize a new tag view with icon image and text. - /// - Parameters: - /// - theme: The spark theme of the tag. - /// - intent: The intent of the tag. - /// - variant: The variant of the tag. - /// - iconImage: The icon image of the tag. - /// - attributedText: The attributedText of the tag. - public convenience init( - theme: Theme, - intent: TagIntent, - variant: TagVariant, - iconImage: UIImage, - attributedText: NSAttributedString - ) { - self.init( - theme, - intent: intent, - variant: variant, - iconImage: iconImage, - text: attributedText - ) - } - - private init( - _ theme: Theme, - intent: TagIntent, - variant: TagVariant, - iconImage: UIImage? = nil, - text: Any? = nil, - getColorsUseCase: any TagGetColorsUseCaseable = TagGetColorsUseCase() - ) { - self.theme = theme - self.intent = intent - self.variant = variant - self.iconImage = iconImage - self.getColorsUseCase = getColorsUseCase - - super.init(frame: .zero) - - if let text = text as? String { - self.text = text - } else if let attributedText = text as? NSAttributedString { - self.attributedText = attributedText - } - - self.setupView() - self.loadUI() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - private func setupView() { - // Add subview - self.addSubview(self.contentStackView) - - // Setup constraints - self.setupConstraints() - } - - // MARK: - Layout - - public override func layoutSubviews() { - super.layoutSubviews() - - self.setCornerRadius(self.theme.border.radius.full) - } - - // MARK: - Load UI - - private func loadUI() { - self.reloadIconImageView() - self.reloadTextLabel() - self.reloadUIFromTheme() - self.reloadUIFromColors() - self.reloadUIFromSize() - } - - private func reloadIconImageView() { - self.iconImageView.image = self.iconImage - self.iconStackView.isHidden = (self.iconImage == nil) - } - - private func reloadTextLabel() { - self.reloadTextStyle() - if let attributedText = self.attributedText { - self.textLabel.attributedText = attributedText - } else { - self.textLabel.text = self.text - } - self.textLabel.isHidden = (self.text == nil && self.attributedText == nil) - } - - private func reloadTextStyle() { - self.textLabel.font = self.theme.typography.captionHighlight.uiFont - self.textLabel.textColor = self.colors.foregroundColor.uiColor - } - - private func reloadUIFromTheme() { - // Spacing - self.iconVerticalSpacing = self.theme.layout.spacing.small - self._iconVerticalSpacing.update(traitCollection: self.traitCollection) - self.contentHorizontalSpacing = self.theme.layout.spacing.medium - self._contentHorizontalSpacing.update(traitCollection: self.traitCollection) - - // View - self.setBorderWidth(self.theme.border.width.small) - self.setMasksToBounds(true) - - // Subviews - self.reloadTextLabel() - } - - private func reloadUIFromColors() { - // View - self.backgroundColor = self.colors.backgroundColor.uiColor - self.setBorderColor(from: self.colors.borderColor) - - // Subviews - self.iconImageView.tintColor = self.colors.foregroundColor.uiColor - self.reloadTextLabel() - } - - private func reloadUIFromSize() { - self.reloadUIFromHeight() - self.reloadUIFromSpacing() - } - - private func reloadUIFromHeight() { - // Reload height only if value changed - if self.height != self.heightConstraint?.constant { - self.heightConstraint?.constant = self.height - self.updateConstraintsIfNeeded() - } - } - - private func reloadUIFromSpacing() { - self.reloadUIFromIconVerticalSpacing() - self.reloadUIFromContentHorizontalSpacing() - } - - private func reloadUIFromIconVerticalSpacing() { - // Reload spacing only if value changed - let iconVerticalSpacing = self._iconVerticalSpacing.wrappedValue - if iconVerticalSpacing != self.topIconSpaceViewTopConstraint?.constant { - // Subviews - self.contentStackView.spacing = iconVerticalSpacing - - // Constraint - self.topIconSpaceViewTopConstraint?.constant = iconVerticalSpacing - self.topIconSpaceViewTopConstraint?.isActive = true - self.updateConstraintsIfNeeded() - } - } - - private func reloadUIFromContentHorizontalSpacing() { - // Reload spacing only if value changed - let contentHorizontalSpacing = self._contentHorizontalSpacing.wrappedValue - if contentHorizontalSpacing != self.contentStackViewLeadingConstraint?.constant { - self.contentStackViewLeadingConstraint?.constant = contentHorizontalSpacing - self.contentStackViewTrailingConstraint?.constant = -contentHorizontalSpacing - self.contentStackView.updateConstraintsIfNeeded() - } - } - - // MARK: - Constraints - - private func setupConstraints() { - self.setupViewConstraints() - self.setupContentStackViewConstraints() - self.setupIconSpaceViewsContraints() - self.setupIconImageViewConstraints() - } - - private func setupViewConstraints() { - self.translatesAutoresizingMaskIntoConstraints = false - - self.heightConstraint = self.heightAnchor.constraint(equalToConstant: self.height) - self.heightConstraint?.isActive = true - } - - private func setupContentStackViewConstraints() { - self.contentStackView.translatesAutoresizingMaskIntoConstraints = false - - self.contentStackViewLeadingConstraint = self.contentStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor) - self.contentStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true - self.contentStackViewTrailingConstraint = self.contentStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor) - self.contentStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true - - self.contentStackViewLeadingConstraint?.isActive = true - self.contentStackViewTrailingConstraint?.isActive = true - } - - private func setupIconSpaceViewsContraints() { - self.topIconSpaceView.translatesAutoresizingMaskIntoConstraints = false - self.topIconSpaceViewTopConstraint = self.topIconSpaceView.heightAnchor.constraint( - equalToConstant: self.iconVerticalSpacing - ) - - self.bottomIconSpaceView.translatesAutoresizingMaskIntoConstraints = false - self.bottomIconSpaceView.heightAnchor.constraint(equalTo: self.topIconSpaceView.heightAnchor).isActive = true - } - - private func setupIconImageViewConstraints() { - self.iconImageView.translatesAutoresizingMaskIntoConstraints = false - self.iconImageView.widthAnchor.constraint(equalTo: self.iconImageView.heightAnchor).isActive = true - } - - // MARK: - Getter - - private func getColorsFromUseCase() -> TagColors { - return self.getColorsUseCase.execute( - theme: self.theme, - intent: self.intent, - variant: self.variant - ) - } - - // MARK: - Trait Collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // Reload colors ? - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.reloadUIFromColors() - } - - // ** - // Update content size - self._height.update(traitCollection: self.traitCollection) - self._iconVerticalSpacing.update(traitCollection: self.traitCollection) - self._contentHorizontalSpacing.update(traitCollection: self.traitCollection) - self.reloadUIFromSize() - // ** - } -} - -// MARK: - Label priorities -public extension TagUIView { - func setLabelContentCompressionResistancePriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentCompressionResistancePriority(priority, - for: axis) - } - - func setLabelContentHuggingPriority(_ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis) { - self.textLabel.setContentHuggingPriority(priority, - for: axis) - } -} diff --git a/core/Sources/Components/Tag/View/UIKit/TagUIViewSnapshotTests.swift b/core/Sources/Components/Tag/View/UIKit/TagUIViewSnapshotTests.swift deleted file mode 100644 index 2a61986e6..000000000 --- a/core/Sources/Components/Tag/View/UIKit/TagUIViewSnapshotTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// TagUIViewSnapshotTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 04/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting - -@testable import SparkCore - -final class TagUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = TagScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations = scenario.configuration(isSwiftUIComponent: false) - for configuration in configurations { - - var view: TagUIView - switch configuration.content { - case .text(let text), .longText(let text): - view = TagUIView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - text: text - ) - - case .attributedText(let attributedText): - view = TagUIView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - attributedText: attributedText.leftValue - ) - - case .icon(let image): - view = TagUIView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - iconImage: image.leftValue - ) - - case let .iconAndText(image, text), let .iconAndLongText(image, text): - view = TagUIView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - iconImage: image.leftValue, - text: text - ) - - case let .iconAndAttributedText(image, attributedText): - view = TagUIView( - theme: self.theme, - intent: configuration.intent, - variant: configuration.variant, - iconImage: image.leftValue, - attributedText: attributedText.leftValue - ) - } - - view.translatesAutoresizingMaskIntoConstraints = false - if let width = configuration.width { - view.widthAnchor.constraint(equalToConstant: width).isActive = true - } - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/Tag/ViewModel/TagViewModel.swift b/core/Sources/Components/Tag/ViewModel/TagViewModel.swift deleted file mode 100644 index 83be4d346..000000000 --- a/core/Sources/Components/Tag/ViewModel/TagViewModel.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// TagViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 27/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -final class TagViewModel: ObservableObject { - - // MARK: - Public properties - - @Published var colors: TagColors - @Published var typography: Typography - @Published var spacing: LayoutSpacing - @Published var border: Border - - @Published var iconImage: Image? - @Published var text: String? - @Published var attributedText: AttributedString? - - // MARK: - Private properties - - private let theme: Theme - private var intent: TagIntent { - didSet { - self.reloadColors() - } - } - private var variant: TagVariant { - didSet { - self.reloadColors() - } - } - - private let getColorsUseCase: any TagGetColorsUseCaseable - - // MARK: - Initialization - - init( - theme: Theme, - intent: TagIntent = .main, - variant: TagVariant = .filled, - iconImage: Image? = nil, - text: String? = nil, - attributedText: AttributedString? = nil, - getColorsUseCase: any TagGetColorsUseCaseable = TagGetColorsUseCase() - ) { - self.colors = Self.getColors( - for: theme, - intent: intent, - variant: variant, - useCase: getColorsUseCase - ) - self.typography = theme.typography - self.spacing = theme.layout.spacing - self.border = theme.border - - self.iconImage = iconImage - self.text = text - self.attributedText = attributedText - - self.theme = theme - self.intent = intent - self.variant = variant - - self.getColorsUseCase = getColorsUseCase - } - - // MARK: - Load - - private func reloadColors() { - self.colors = Self.getColors( - for: self.theme, - intent: intent, - variant: variant, - useCase: getColorsUseCase - ) - } - - // MARK: - Public Setter - - func setIntent(_ intent: TagIntent) { - self.intent = intent - } - - func setVariant(_ variant: TagVariant) { - self.variant = variant - } - - func setIconImage(_ iconImage: Image?) { - self.iconImage = iconImage - } - - func setText(_ text: String?) { - self.attributedText = nil - self.text = text - } - - func setAttributedText(_ attributedText: AttributedString?) { - self.text = nil - self.attributedText = attributedText - } - - // MARK: - Getter - - private static func getColors( - for theme: Theme, - intent: TagIntent, - variant: TagVariant, - useCase: any TagGetColorsUseCaseable - ) -> TagColors { - return useCase.execute( - theme: theme, - intent: intent, - variant: variant - ) - } -} diff --git a/core/Sources/Components/Tag/ViewModel/TagViewModelTests.swift b/core/Sources/Components/Tag/ViewModel/TagViewModelTests.swift deleted file mode 100644 index 6194e1bb2..000000000 --- a/core/Sources/Components/Tag/ViewModel/TagViewModelTests.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// TagViewModelTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 27/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TagViewModelTests: XCTestCase { - - // MARK: - Properties - - private let themeTypographyMock = TypographyGeneratedMock() - private let themeBorderMock = BorderGeneratedMock() - private let themeSpacingMock = LayoutSpacingGeneratedMock() - private lazy var themeLayoutMock: LayoutGeneratedMock = { - let mock = LayoutGeneratedMock() - mock.underlyingSpacing = self.themeSpacingMock - return mock - }() - private lazy var themeMock: ThemeGeneratedMock = { - let mock = ThemeGeneratedMock() - mock.underlyingTypography = self.themeTypographyMock - mock.underlyingBorder = self.themeBorderMock - mock.underlyingLayout = self.themeLayoutMock - return mock - }() - - private let tagColorsMock = TagColors.mocked() - private lazy var getColorsUseCaseMock: TagGetColorsUseCaseableGeneratedMock = { - let mock = TagGetColorsUseCaseableGeneratedMock() - mock.executeWithThemeAndIntentAndVariantReturnValue = self.tagColorsMock - return mock - }() - - // MARK: - Properties Tests - - func test_default_properties() { - // GIVEN / WHEN - let viewModel = TagViewModel( - theme: self.themeMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // THEN - self.testProperties( - on: viewModel, - expectedIntent: .main, - expectedVariant: .filled, - expectedIconImage: nil, - expectedText: nil, - expectedAttributedText: nil - ) - } - - func test_properties() { - // GIVEN / WHEN - let intentMock: TagIntent = .alert - let variantMock: TagVariant = .outlined - let iconImageMock = Image(systemName: "square.and.arrow.up") - let textMock = "Text" - let attributedTextMock = AttributedString("AT Text") - - let viewModel = TagViewModel( - theme: self.themeMock, - intent: intentMock, - variant: variantMock, - iconImage: iconImageMock, - text: textMock, - attributedText: attributedTextMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // THEN - self.testProperties( - on: viewModel, - expectedIntent: intentMock, - expectedVariant: variantMock, - expectedIconImage: iconImageMock, - expectedText: textMock, - expectedAttributedText: attributedTextMock - ) - } - - func testProperties( - on givenViewModel: TagViewModel, - expectedIntent: TagIntent, - expectedVariant: TagVariant, - expectedIconImage: Image?, - expectedText: String?, - expectedAttributedText: AttributedString? - ) { - XCTAssertEqual(givenViewModel.colors, - self.tagColorsMock, - "Wrong colors value") - XCTAssertIdentical(givenViewModel.typography as? TypographyGeneratedMock, - self.themeTypographyMock, - "Wrong typography value") - XCTAssertIdentical(givenViewModel.spacing as? LayoutSpacingGeneratedMock, - self.themeSpacingMock, - "Wrong spacing value") - XCTAssertIdentical(givenViewModel.border as? BorderGeneratedMock, - self.themeBorderMock, - "Wrong border value") - XCTAssertEqual(givenViewModel.iconImage, - expectedIconImage, - "Wrong iconImage value") - XCTAssertEqual(givenViewModel.text, - expectedText, - "Wrong text value") - XCTAssertEqual(givenViewModel.attributedText, - expectedAttributedText, - "Wrong attributedText value") - - // ** - // GetColorsUseCase - let getColorsUseCaseArgs = self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantReceivedArguments - XCTAssertEqual(self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantCallsCount, - 1, - "Wrong call number on execute on getColorsUseCase") - - XCTAssertIdentical(getColorsUseCaseArgs?.theme as? ThemeGeneratedMock, - themeMock, - "Wrong theme parameter on execute on getColorsUseCase") - XCTAssertEqual(getColorsUseCaseArgs?.intent, - expectedIntent, - "Wrong intent parameter on execute on getColorsUseCase") - XCTAssertEqual(getColorsUseCaseArgs?.variant, - expectedVariant, - "Wrong variant parameter on execute on getColorsUseCase") - // ** - } - - // MARK: - Public Setter Tests - - func test_setIntent_should_update_colors() { - // GIVEN - let intentMock: TagIntent = .danger - - let newTagColorsMock = TagColors.mocked() - - self.getColorsUseCaseMock._executeWithThemeAndIntentAndVariant = { theme, intent, variant in - if self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantCallsCount == 0 { - return self.tagColorsMock - } else { - return newTagColorsMock - } - } - - let viewModel = TagViewModel( - theme: self.themeMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // WHEN - viewModel.setIntent(intentMock) - - // THEN - XCTAssertEqual(viewModel.colors, - newTagColorsMock, - "Wrong colors value") - - // ** - // GetColorsUseCase - let getColorsUseCaseArgs = self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantReceivedArguments - XCTAssertEqual(self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantCallsCount, - 2, - "Wrong call number on execute on getColorsUseCase") - - XCTAssertEqual(getColorsUseCaseArgs?.intent, - intentMock, - "Wrong intent parameter on execute on getColorsUseCase") - // ** - } - - func test_setVariant_should_update_colors() { - // GIVEN - let variantMock: TagVariant = .outlined - - let newTagColorsMock = TagColors.mocked() - - self.getColorsUseCaseMock._executeWithThemeAndIntentAndVariant = { theme, intent, variant in - if self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantCallsCount == 0 { - return self.tagColorsMock - } else { - return newTagColorsMock - } - } - - let viewModel = TagViewModel( - theme: self.themeMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // WHEN - viewModel.setVariant(variantMock) - - // THEN - XCTAssertEqual(viewModel.colors, - newTagColorsMock, - "Wrong colors value") - - // ** - // GetColorsUseCase - let getColorsUseCaseArgs = self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantReceivedArguments - XCTAssertEqual(self.getColorsUseCaseMock.executeWithThemeAndIntentAndVariantCallsCount, - 2, - "Wrong call number on execute on getColorsUseCase") - - XCTAssertEqual(getColorsUseCaseArgs?.variant, - variantMock, - "Wrong variant parameter on execute on getColorsUseCase") - // ** - } - - func test_setIconImage() { - // GIVEN - let newIconImage = Image(systemName: "square.and.arrow.up.fill") - - let viewModel = TagViewModel( - theme: self.themeMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // WHEN - viewModel.setIconImage(newIconImage) - - // THEN - XCTAssertEqual(viewModel.iconImage, - newIconImage, - "Wrong iconImage value") - } - - func test_setText() { - // GIVEN - let newText = "New text" - - let viewModel = TagViewModel( - theme: self.themeMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // WHEN - viewModel.setAttributedText(.init("AT Text")) - viewModel.setText(newText) - - // THEN - XCTAssertEqual(viewModel.text, - newText, - "Wrong text value") - XCTAssertNil(viewModel.attributedText, "Wrong attributedText value") - } - - func test_setAttributedText() { - // GIVEN - let newAttributedText = AttributedString("AT Text") - - let viewModel = TagViewModel( - theme: self.themeMock, - getColorsUseCase: self.getColorsUseCaseMock - ) - - // WHEN - viewModel.setText("My Text") - viewModel.setAttributedText(newAttributedText) - - // THEN - XCTAssertEqual(viewModel.attributedText, - newAttributedText, - "Wrong attributedText value") - XCTAssertNil(viewModel.text, "Wrong text value") - } -} diff --git a/core/Sources/Components/TextField/Addons/View/AccessibilityIdentifiier/TextFieldAddonsAccessibilityIdentifier.swift b/core/Sources/Components/TextField/Addons/View/AccessibilityIdentifiier/TextFieldAddonsAccessibilityIdentifier.swift deleted file mode 100644 index 7187bc6b5..000000000 --- a/core/Sources/Components/TextField/Addons/View/AccessibilityIdentifiier/TextFieldAddonsAccessibilityIdentifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TextFieldAddonsAccessibilityIdentifier.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the textfieldaddons. -public enum TextFieldAddonsAccessibilityIdentifier { - - /// The textfieldaddons accessibility identifier. - public static let view = "spark-textfieldaddons" -} diff --git a/core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddon.swift b/core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddon.swift deleted file mode 100644 index 5d9ad6b7e..000000000 --- a/core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddon.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// TextFieldAddon.swift -// SparkCore -// -// Created by louis.borlee on 21/03/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// Single TextFieldAddon embedding a Content View -public struct TextFieldAddon: View { - - let withPadding: Bool - let layoutPriority: Double - private let content: () -> Content - - /// TextFieldAddon initializer - /// - Parameters: - /// - withPadding: Add addon padding if `true`, default is `false` - /// - layoutPriority: Set addon .layoutPriority(), default is `1.0` - /// - content: Addon's content View - public init( - withPadding: Bool = false, - layoutPriority: Double = 1.0, - content: @escaping () -> Content) { - self.withPadding = withPadding - self.layoutPriority = layoutPriority - self.content = content - } - - public var body: Content { - content() - } -} diff --git a/core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddons.swift b/core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddons.swift deleted file mode 100644 index edbeeb170..000000000 --- a/core/Sources/Components/TextField/Addons/View/SwiftUI/TextFieldAddons.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// TextFieldAddons.swift -// SparkCore -// -// Created by louis.borlee on 21/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A Spark TextField that can be surrounded by left and/or right addons -public struct TextFieldAddons: View { - - @ScaledMetric private var scaleFactor: CGFloat = 1.0 - @ScaledMetric private var maxHeight: CGFloat = 44.0 - @ObservedObject private var viewModel: TextFieldAddonsViewModel - private let leftAddon: () -> TextFieldAddon - private let rightAddon: () -> TextFieldAddon - - private let titleKey: LocalizedStringKey - @Binding private var text: String - private var type: TextFieldViewType - private let leftView: () -> LeftView - private let rightView: () -> RightView - - /// TextFieldAddons initializer - /// - Parameters: - /// - titleKey: The textfield's current placeholder - /// - text: The textfield's text binding - /// - theme: The textfield's current theme - /// - intent: The textfield's current intent - /// - type: The type of field with its associated callback(s), default is `.standard()` - /// - isReadOnly: Set this to true if you want the textfield to be readOnly, default is `false` - /// - leftView: The TextField's left view, default is `EmptyView` - /// - rightView: The TextField's right view, default is `EmptyView` - /// - leftAddon: The TextField's left addon, default is `EmptyView` - /// - rightAddon: The TextField's right addon, default is `EmptyView` - public init( - _ titleKey: LocalizedStringKey, - text: Binding, - theme: Theme, - intent: TextFieldIntent, - type: TextFieldViewType = .standard(), - isReadOnly: Bool, - leftView: @escaping (() -> LeftView) = { EmptyView() }, - rightView: @escaping (() -> RightView) = { EmptyView() }, - leftAddon: @escaping (() -> TextFieldAddon) = { .init(withPadding: false) { EmptyView() } }, - rightAddon: @escaping (() -> TextFieldAddon) = { .init(withPadding: false) { EmptyView() } } - ) { - let viewModel = TextFieldAddonsViewModel( - theme: theme, - intent: intent - ) - self.viewModel = viewModel - - self.titleKey = titleKey - self._text = text - self.type = type - self.leftView = leftView - self.rightView = rightView - self.leftAddon = leftAddon - self.rightAddon = rightAddon - - self.viewModel.textFieldViewModel.isUserInteractionEnabled = isReadOnly != true - } - - private func getLeftAddonPadding(withPadding: Bool) -> EdgeInsets { - guard withPadding else { return .init(all: 0) } - return .init( - top: .zero, - leading: self.viewModel.leftSpacing, - bottom: .zero, - trailing: self.viewModel.leftSpacing - ) - } - - private func getRightAddonPadding(withPadding: Bool) -> EdgeInsets { - guard withPadding else { return .init(all: 0) } - return .init( - top: .zero, - leading: self.viewModel.rightSpacing, - bottom: .zero, - trailing: self.viewModel.rightSpacing - ) - } - - private func getTextFieldPadding() -> EdgeInsets { - return EdgeInsets( - top: .zero, - leading: self.viewModel.leftSpacing, - bottom: .zero, - trailing: self.viewModel.rightSpacing - ) - } - - public var body: some View { - ZStack { - self.viewModel.backgroundColor.color - HStack(spacing: 0) { - leftAddonIfNeeded() - textField() - .padding(getTextFieldPadding()) - rightAddonIfNeeded() - } - } - .frame(maxHeight: maxHeight) - .allowsHitTesting(self.viewModel.textFieldViewModel.isUserInteractionEnabled) - .border(width: self.viewModel.borderWidth * self.scaleFactor, radius: self.viewModel.borderRadius, colorToken: self.viewModel.textFieldViewModel.borderColor) - .opacity(self.viewModel.dim) - .accessibilityElement(children: .contain) - .accessibilityIdentifier(TextFieldAddonsAccessibilityIdentifier.view) - } - - @ViewBuilder - private func leftAddonIfNeeded() -> some View { - // If the content of leftAddon is EmptyView, it will show nothing - let leftAddon = self.leftAddon() - leftAddon - .padding(getLeftAddonPadding(withPadding: leftAddon.withPadding)) - .overlay(alignment: .trailing) { - separator() - } - .layoutPriority(leftAddon.layoutPriority) - } - - @ViewBuilder - private func rightAddonIfNeeded() -> some View { - // If the content of rightAddon is EmptyView, it will show nothing - let rightAddon = self.rightAddon() - rightAddon - .padding(getRightAddonPadding(withPadding: rightAddon.withPadding)) - .overlay(alignment: .leading) { - separator() - } - .layoutPriority(rightAddon.layoutPriority) - } - - @ViewBuilder - private func separator() -> some View { - self.viewModel.textFieldViewModel.borderColor.color - .frame(width: self.viewModel.borderWidth * self.scaleFactor, - height: maxHeight) - } - - @ViewBuilder - private func textField() -> TextFieldView { - TextFieldView(titleKey: titleKey, text: $text, viewModel: viewModel.textFieldViewModel, type: type, leftView: leftView, rightView: rightView) - } -} diff --git a/core/Sources/Components/TextField/Addons/View/UIKit/TextFieldAddonsUIView.swift b/core/Sources/Components/TextField/Addons/View/UIKit/TextFieldAddonsUIView.swift deleted file mode 100644 index b67ae4808..000000000 --- a/core/Sources/Components/TextField/Addons/View/UIKit/TextFieldAddonsUIView.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// TextFieldAddonsUIView.swift -// SparkCore -// -// Created by louis.borlee on 14/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -import Combine - -/// A Spark TextField that can be surrounded by left and/or right addons -public final class TextFieldAddonsUIView: UIControl { - - @ScaledUIMetric private var scaleFactor: CGFloat = 1.0 - - /// Embbeded textField - public let textField: TextFieldUIView - /// Current leftAddon, set using setLeftAddon(_:, _withPadding:) - private(set) public var leftAddon: UIView? - /// Current rightAddon, set using setRightAddon(_:, _withPadding:) - private(set) public var rightAddon: UIView? - - private var leftAddonContainer = UIView() - private var leftSeparatorView = UIView() - private var leftSeparatorWidthConstraint = NSLayoutConstraint() - private var rightAddonContainer = UIView() - private var rightSeparatorView = UIView() - private var rightSeparatorWidthConstraint = NSLayoutConstraint() - - private let viewModel: TextFieldAddonsViewModel - private var cancellables = Set() - - private var leadingConstraint: NSLayoutConstraint = NSLayoutConstraint() - private var trailingConstraint: NSLayoutConstraint = NSLayoutConstraint() - - private var borderWidth: CGFloat { - return self.viewModel.borderWidth * self.scaleFactor - } - - private lazy var stackView = UIStackView(arrangedSubviews: [ - self.leftAddonContainer, - self.textField, - self.rightAddonContainer - ]) - - public override var isEnabled: Bool { - didSet { - self.textField.isEnabled = self.isEnabled - } - } - - public override var isUserInteractionEnabled: Bool { - didSet { - self.textField.isUserInteractionEnabled = self.isUserInteractionEnabled - } - } - - /// TextFieldAddonsUIView initializer - /// - Parameters: - /// - theme: The textfield's current theme - /// - intent: The textfield's current intent - public init( - theme: Theme, - intent: TextFieldIntent - ) { - let viewModel = TextFieldAddonsViewModel( - theme: theme, - intent: intent - ) - self.viewModel = viewModel - self.textField = TextFieldUIView(viewModel: viewModel.textFieldViewModel) - self.leftAddon = nil - self.rightAddon = nil - super.init(frame: .init(origin: .zero, size: .init(width: 0, height: 44))) - self.textField.backgroundColor = ColorTokenDefault.clear.uiColor - self.setupViews() - self.subscribeToViewModel() - self.textField.backgroundColor = .clear - - self.accessibilityContainerType = .semanticGroup - self.accessibilityIdentifier = TextFieldAddonsAccessibilityIdentifier.view - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - self.clipsToBounds = true - self.addSubview(self.stackView) - - self.textField.setContentHuggingPriority(.defaultLow, for: .horizontal) - - self.stackView.translatesAutoresizingMaskIntoConstraints = false - // Adding constant padding to set borders outline instead of inline - self.leadingConstraint = self.stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: self.borderWidth) - self.trailingConstraint = self.stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.borderWidth) - NSLayoutConstraint.activate([ - self.leadingConstraint, - self.trailingConstraint, - self.stackView.topAnchor.constraint(equalTo: self.topAnchor), - self.stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor) - ]) - - self.setupSeparators() - self.setLeftAddon(nil) - self.setRightAddon(nil) - } - - private func setupSeparators() { - self.leftAddonContainer.addSubview(self.leftSeparatorView) - self.leftSeparatorView.translatesAutoresizingMaskIntoConstraints = false - self.leftSeparatorWidthConstraint = self.leftSeparatorView.widthAnchor.constraint(equalToConstant: self.borderWidth) - - self.rightAddonContainer.addSubview(self.rightSeparatorView) - self.rightSeparatorView.translatesAutoresizingMaskIntoConstraints = false - self.rightSeparatorWidthConstraint = self.rightSeparatorView.widthAnchor.constraint(equalToConstant: self.borderWidth) - - NSLayoutConstraint.activate([ - self.leftSeparatorWidthConstraint, - self.leftSeparatorView.topAnchor.constraint(equalTo: self.leftAddonContainer.topAnchor), - self.leftSeparatorView.bottomAnchor.constraint(equalTo: self.leftAddonContainer.bottomAnchor), - self.leftSeparatorView.trailingAnchor.constraint(equalTo: self.leftAddonContainer.trailingAnchor), - - self.rightSeparatorWidthConstraint, - self.rightSeparatorView.topAnchor.constraint(equalTo: self.rightAddonContainer.topAnchor), - self.rightSeparatorView.bottomAnchor.constraint(equalTo: self.rightAddonContainer.bottomAnchor), - self.rightSeparatorView.leadingAnchor.constraint(equalTo: self.rightAddonContainer.leadingAnchor) - ]) - } - - private func subscribeToViewModel() { - self.viewModel.$backgroundColor.removeDuplicates(by: { lhs, rhs in - lhs.equals(rhs) - }) - .subscribe(in: &self.cancellables) { [weak self] backgroundColor in - guard let self else { return } - self.backgroundColor = backgroundColor.uiColor - } - - self.viewModel.textFieldViewModel.$borderColor.removeDuplicates(by: { lhs, rhs in - lhs.equals(rhs) - }) - .subscribe(in: &self.cancellables) { [weak self] borderColor in - guard let self else { return } - self.setBorderColor(from: borderColor) - self.leftSeparatorView.backgroundColor = borderColor.uiColor - self.rightSeparatorView.backgroundColor = borderColor.uiColor - } - - self.viewModel.$borderWidth.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] borderWidth in - guard let self else { return } - let width = borderWidth * self.scaleFactor - self.setBorderWidth(width) - self.setLeftSpacing(self.viewModel.leftSpacing, borderWidth: width) - self.setRightSpacing(self.viewModel.rightSpacing, borderWidth: width) - self.leftSeparatorWidthConstraint.constant = width - self.rightSeparatorWidthConstraint.constant = width - } - - self.viewModel.$borderRadius.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] borderRadius in - guard let self else { return } - self.setCornerRadius(borderRadius) - } - - self.viewModel.$leftSpacing.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] leftSpacing in - guard let self else { return } - self.setLeftSpacing(leftSpacing, borderWidth: self.borderWidth) - self.setNeedsLayout() - } - - self.viewModel.$rightSpacing.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] rightSpacing in - guard let self else { return } - self.setRightSpacing(rightSpacing, borderWidth: self.borderWidth) - self.setNeedsLayout() - } - - self.viewModel.$contentSpacing.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] contentSpacing in - guard let self else { return } - self.stackView.spacing = contentSpacing - self.setNeedsLayout() - } - - self.viewModel.$dim.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] dim in - guard let self else { return } - self.alpha = dim - self.setNeedsLayout() - } - } - - private func setLeftSpacing(_ leftSpacing: CGFloat, borderWidth: CGFloat) { - self.leadingConstraint.constant = (self.leftAddonContainer.isHidden ? leftSpacing : .zero) + borderWidth - } - - private func setRightSpacing(_ rightSpacing: CGFloat, borderWidth: CGFloat) { - self.trailingConstraint.constant = (self.rightAddonContainer.isHidden ? -rightSpacing : .zero) - borderWidth - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.setBorderColor(from: self.viewModel.textFieldViewModel.borderColor) - } - - guard previousTraitCollection?.preferredContentSizeCategory != self.traitCollection.preferredContentSizeCategory else { return } - - self._scaleFactor.update(traitCollection: self.traitCollection) - self.setBorderWidth(self.borderWidth) - self.setLeftSpacing(self.viewModel.leftSpacing, borderWidth: self.borderWidth) - self.setRightSpacing(self.viewModel.rightSpacing, borderWidth: self.borderWidth) - self.leftSeparatorWidthConstraint.constant = self.borderWidth - self.rightSeparatorWidthConstraint.constant = self.borderWidth - self.invalidateIntrinsicContentSize() - } - - /// Set the textfield's left addon - /// - Parameters: - /// - leftAddon: the view to be set as leftAddon - /// - withPadding: adds a padding on the addon if `true`, default is `false` - public func setLeftAddon(_ leftAddon: UIView?, withPadding: Bool = false) { - if let oldValue = self.leftAddon, oldValue.isDescendant(of: self.leftAddonContainer) { - oldValue.removeFromSuperview() - } - if let leftAddon { - self.leftAddonContainer.addSubview(leftAddon) - leftAddon.setContentHuggingPriority(.defaultHigh, for: .horizontal) - leftAddon.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - leftAddon.trailingAnchor.constraint(equalTo: self.leftSeparatorView.leadingAnchor, constant: withPadding ? -self.viewModel.leftSpacing : 0), - leftAddon.centerXAnchor.constraint(equalTo: self.leftAddonContainer.centerXAnchor, constant: -self.borderWidth / 2.0), - leftAddon.centerYAnchor.constraint(equalTo: self.leftAddonContainer.centerYAnchor) - ]) - } - self.leftAddon = leftAddon - self.leftAddonContainer.isHidden = self.leftAddon == nil - self.setLeftSpacing(self.viewModel.leftSpacing, borderWidth: self.borderWidth) - } - - /// Set the textfield's right addon - /// - Parameters: - /// - rightAddon: the view to be set as rightAddon - /// - withPadding: adds a padding on the addon if `true`, default is `false` - public func setRightAddon(_ rightAddon: UIView? = nil, withPadding: Bool = false) { - if let oldValue = self.rightAddon, oldValue.isDescendant(of: self.rightAddonContainer) { - oldValue.removeFromSuperview() - } - if let rightAddon { - self.rightAddonContainer.addSubview(rightAddon) - rightAddon.setContentHuggingPriority(.defaultHigh, for: .horizontal) - rightAddon.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - rightAddon.leadingAnchor.constraint(equalTo: self.rightSeparatorView.trailingAnchor, constant: withPadding ? self.viewModel.rightSpacing : 0), - rightAddon.centerXAnchor.constraint(equalTo: self.rightAddonContainer.centerXAnchor, constant: self.borderWidth / 2.0), - rightAddon.centerYAnchor.constraint(equalTo: self.rightAddonContainer.centerYAnchor) - ]) - } - self.rightAddon = rightAddon - self.rightAddonContainer.isHidden = self.rightAddon == nil - self.setRightSpacing(self.viewModel.rightSpacing, borderWidth: self.borderWidth) - } -} diff --git a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModel.swift b/core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModel.swift deleted file mode 100644 index 8653d5e2d..000000000 --- a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModel.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// TextFieldAddonsViewModel.swift -// SparkCore -// -// Created by louis.borlee on 14/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -import Combine - -final class TextFieldAddonsViewModel: ObservableObject, Updateable { - - private var cancellables = Set() - - @Published private(set) var backgroundColor: any ColorToken - - // BorderLayout - @Published private(set) var borderRadius: CGFloat - @Published private(set) var borderWidth: CGFloat - - // Spacings - @Published private(set) var leftSpacing: CGFloat - @Published private(set) var contentSpacing: CGFloat - @Published private(set) var rightSpacing: CGFloat - - @Published private(set) var dim: CGFloat - - var textFieldViewModel: TextFieldViewModelForAddons - - init(theme: Theme, - intent: TextFieldIntent, - getColorsUseCase: TextFieldGetColorsUseCasable = TextFieldGetColorsUseCase(), - getBorderLayoutUseCase: TextFieldGetBorderLayoutUseCasable = TextFieldGetBorderLayoutUseCase(), - getSpacingsUseCase: TextFieldGetSpacingsUseCasable = TextFieldGetSpacingsUseCase()) { - let viewModel = TextFieldViewModelForAddons( - theme: theme, - intent: intent, - getColorsUseCase: getColorsUseCase, - getBorderLayoutUseCase: getBorderLayoutUseCase, - getSpacingsUseCase: getSpacingsUseCase - ) - self.backgroundColor = viewModel.addonsBackgroundColor - self.borderRadius = viewModel.addonsBorderWidth - self.borderWidth = viewModel.addonsBorderWidth - self.leftSpacing = viewModel.addonsLeftSpacing - self.contentSpacing = viewModel.addonsContentSpacing - self.rightSpacing = viewModel.addonsRightSpacing - self.dim = viewModel.addonsDim - - self.textFieldViewModel = viewModel - - self.subscribe() - } - - private func subscribe() { - self.textFieldViewModel.$addonsBackgroundColor.subscribe(in: &self.cancellables) { [weak self] backgroundColor in - guard let self else { return } - self.backgroundColor = backgroundColor - } - - self.textFieldViewModel.$addonsLeftSpacing.subscribe(in: &self.cancellables) { [weak self] leftSpacing in - guard let self else { return } - self.leftSpacing = leftSpacing - } - - self.textFieldViewModel.$addonsContentSpacing.subscribe(in: &self.cancellables) { [weak self] contentSpacing in - guard let self else { return } - self.contentSpacing = contentSpacing - } - - self.textFieldViewModel.$addonsRightSpacing.subscribe(in: &self.cancellables) { [weak self] rightSpacing in - guard let self else { return } - self.rightSpacing = rightSpacing - } - - self.textFieldViewModel.$addonsBorderWidth.subscribe(in: &self.cancellables) { [weak self] borderWidth in - guard let self else { return } - self.borderWidth = borderWidth - } - - self.textFieldViewModel.$addonsBorderRadius.subscribe(in: &self.cancellables) { [weak self] borderRadius in - guard let self else { return } - self.borderRadius = borderRadius - } - - self.textFieldViewModel.$addonsDim.subscribe(in: &self.cancellables) { [weak self] dim in - guard let self else { return } - self.dim = dim - } - } -} diff --git a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModelTests.swift b/core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModelTests.swift deleted file mode 100644 index a74a4182e..000000000 --- a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldAddonsViewModelTests.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// TextFieldAddonsViewModelTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 21/03/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import Combine -@testable import SparkCore - -final class TextFieldAddonsViewModelTests: XCTestCase { - private var theme: ThemeGeneratedMock! - private var publishers: TextFieldAddonsPublishers! - private var getColorsUseCase: TextFieldGetColorsUseCasableGeneratedMock! - private var getBorderLayoutUseCase: TextFieldGetBorderLayoutUseCasableGeneratedMock! - private var getSpacingsUseCase: TextFieldGetSpacingsUseCasableGeneratedMock! - private var viewModel: TextFieldAddonsViewModel! - - private let intent = TextFieldIntent.success - private let borderStyle = TextFieldBorderStyle.roundedRect - - private var expectedColors: TextFieldColors! - private var expectedBorderLayout: TextFieldBorderLayout! - private var expectedSpacings: TextFieldSpacings! - - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - - self.expectedColors = .mocked( - text: .blue(), - placeholder: .green(), - border: .yellow(), - background: .purple() - ) - self.expectedBorderLayout = .mocked(radius: 1, width: 2) - self.expectedSpacings = .mocked(left: 1, content: 2, right: 3) - - self.getColorsUseCase = .mocked(returnedColors: self.expectedColors) - self.getBorderLayoutUseCase = .mocked(returnedBorderLayout: self.expectedBorderLayout) - self.getSpacingsUseCase = .mocked(returnedSpacings: self.expectedSpacings) - self.viewModel = .init( - theme: self.theme, - intent: self.intent, - getColorsUseCase: self.getColorsUseCase, - getBorderLayoutUseCase: self.getBorderLayoutUseCase, - getSpacingsUseCase: self.getSpacingsUseCase - ) - - self.setupPublishers() - } - - func test_init() throws { - // GIVEN / WHEN - Inits from setUp() - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertIdentical(getColorsReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getColorsReceivedArguments.theme") - XCTAssertEqual(getColorsReceivedArguments.intent, self.intent, "Wrong getColorsReceivedArguments.intent") - XCTAssertFalse(getColorsReceivedArguments.isFocused, "Wrong getColorsReceivedArguments.isFocused") - XCTAssertTrue(getColorsReceivedArguments.isEnabled, "Wrong getColorsReceivedArguments.isEnabled") - XCTAssertTrue(getColorsReceivedArguments.isUserInteractionEnabled, "Wrong getColorsReceivedArguments.isUserInteractionEnabled") - XCTAssertTrue(self.viewModel.backgroundColor.equals(self.expectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertEqual(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCallsCount, 2, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should have been called twice (one for textfield, one for addons)") - let getBorderLayoutReceivedArguments = try XCTUnwrap(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReceivedArguments, "Couldn't unwrap getBorderLayoutReceivedArguments") - XCTAssertIdentical(getBorderLayoutReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getBorderLayoutReceivedArguments.theme") - XCTAssertEqual(getBorderLayoutReceivedArguments.borderStyle, .roundedRect, "Wrong getBorderLayoutReceivedArguments.borderStyle") - XCTAssertFalse(getBorderLayoutReceivedArguments.isFocused, "Wrong getBorderLayoutReceivedArguments.isFocused") - XCTAssertEqual(self.viewModel.borderWidth, self.expectedBorderLayout.width, "Wrong borderWidth") - XCTAssertEqual(self.viewModel.borderRadius, self.expectedBorderLayout.radius, "Wrong borderRadius") - - // THEN - Spacings - XCTAssertEqual(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCallsCount, 2, "getSpacingsUseCase.executeWithThemeAndBorderStyle should have been called twice (one for textfield, one for addons)") - let getSpacingsUseCaseReceivedArguments = try XCTUnwrap(self.getSpacingsUseCase.executeWithThemeAndBorderStyleReceivedArguments, "Couldn't unwrap getSpacingsUseCaseReceivedArguments") - XCTAssertIdentical(getSpacingsUseCaseReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getSpacingsUseCaseReceivedArguments.theme") - XCTAssertEqual(getSpacingsUseCaseReceivedArguments.borderStyle, .roundedRect, "Wrong getSpacingsUseCaseReceivedArguments.borderStyle") - XCTAssertEqual(self.viewModel.leftSpacing, self.expectedSpacings.left, "Wrong leftSpacing") - XCTAssertEqual(self.viewModel.contentSpacing, self.expectedSpacings.content, "Wrong contentSpacing") - XCTAssertEqual(self.viewModel.rightSpacing, self.expectedSpacings.right, "Wrong rightSpacing") - - XCTAssertEqual(self.viewModel.dim, self.theme.dims.none, "Wrong dim") - - // THEN - Publishers - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - - XCTAssertEqual(self.publishers.borderWidth.sinkCount, 1, "$borderWidth should have been called once") - XCTAssertEqual(self.publishers.borderRadius.sinkCount, 1, "$borderRadius should have been called once") - - XCTAssertEqual(self.publishers.leftSpacing.sinkCount, 1, "$leftSpacing should have been called once") - XCTAssertEqual(self.publishers.contentSpacing.sinkCount, 1, "$contentSpacing should have been called once") - XCTAssertEqual(self.publishers.rightSpacing.sinkCount, 1, "$rightSpacing should have been called once") - - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should have been called once") - } - - func test_set_backgroundColor() { - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // GIVEN - let newBackgroundColor = ColorTokenDefault.clear - - // WHEN - self.viewModel.textFieldViewModel.backgroundColor = newBackgroundColor - - // THEN - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "backgroundColor should have been called once") - XCTAssertTrue(self.viewModel.backgroundColor.equals(newBackgroundColor), "Wrong backgroundColor") - } - - func test_setBorderLayout() { - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // GIVEN - let newValue = TextFieldBorderLayout(radius: -1, width: -2) - self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReturnValue = newValue - - // WHEN - self.viewModel.textFieldViewModel.setBorderLayout() - - // THEN - XCTAssertEqual(self.publishers.borderWidth.sinkCount, 1, "borderWidth should have been called once") - XCTAssertEqual(self.viewModel.borderWidth, newValue.width, "Wrong borderWidth") - XCTAssertEqual(self.publishers.borderRadius.sinkCount, 1, "borderRadius should have been called once") - XCTAssertEqual(self.viewModel.borderRadius, newValue.radius, "Wrong borderRadius") - } - - func test_setSpacings() { - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // GIVEN - let newValue = TextFieldSpacings(left: -1, content: -2, right: -3) - self.getSpacingsUseCase.executeWithThemeAndBorderStyleReturnValue = newValue - - // WHEN - self.viewModel.textFieldViewModel.setSpacings() - - // THEN - XCTAssertEqual(self.publishers.leftSpacing.sinkCount, 1, "leftSpacing should have been called once") - XCTAssertEqual(self.viewModel.leftSpacing, newValue.left, "Wrong leftSpacing") - XCTAssertEqual(self.publishers.contentSpacing.sinkCount, 1, "contentSpacing should have been called once") - XCTAssertEqual(self.viewModel.contentSpacing, newValue.content, "Wrong contentSpacing") - XCTAssertEqual(self.publishers.rightSpacing.sinkCount, 1, "rightSpacing should have been called once") - XCTAssertEqual(self.viewModel.rightSpacing, newValue.right, "Wrong rightSpacing") - } - - func test_set_dim() { - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // GIVEN - let newDim = self.theme.dims.none + 1 - - // WHEN - self.viewModel.textFieldViewModel.dim = newDim - - // THEN - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "dim should have been called once") - XCTAssertEqual(self.viewModel.dim, newDim, "Wrong dim") - } - - // MARK: - Utils - private func setupPublishers() { - self.publishers = .init( - backgroundColor: PublisherMock(publisher: self.viewModel.$backgroundColor), - borderRadius: PublisherMock(publisher: self.viewModel.$borderRadius), - borderWidth: PublisherMock(publisher: self.viewModel.$borderWidth), - leftSpacing: PublisherMock(publisher: self.viewModel.$leftSpacing), - contentSpacing: PublisherMock(publisher: self.viewModel.$contentSpacing), - rightSpacing: PublisherMock(publisher: self.viewModel.$rightSpacing), - dim: PublisherMock(publisher: self.viewModel.$dim) - ) - self.publishers.load() - } - - private func resetUseCases() { - self.getColorsUseCase.reset() - self.getBorderLayoutUseCase.reset() - self.getSpacingsUseCase.reset() - } -} - -final class TextFieldAddonsPublishers { - var cancellables = Set() - - var backgroundColor: PublisherMock.Publisher> - - var borderRadius: PublisherMock.Publisher> - var borderWidth: PublisherMock.Publisher> - - var leftSpacing: PublisherMock.Publisher> - var contentSpacing: PublisherMock.Publisher> - var rightSpacing: PublisherMock.Publisher> - - var dim: PublisherMock.Publisher> - - init( - backgroundColor: PublisherMock.Publisher>, - borderRadius: PublisherMock.Publisher>, - borderWidth: PublisherMock.Publisher>, - leftSpacing: PublisherMock.Publisher>, - contentSpacing: PublisherMock.Publisher>, - rightSpacing: PublisherMock.Publisher>, - dim: PublisherMock.Publisher> - ) { - self.backgroundColor = backgroundColor - self.borderRadius = borderRadius - self.borderWidth = borderWidth - self.leftSpacing = leftSpacing - self.contentSpacing = contentSpacing - self.rightSpacing = rightSpacing - self.dim = dim - } - - func load() { - self.cancellables = Set() - - [self.backgroundColor].forEach { - $0.loadTesting(on: &self.cancellables) - } - - [self.borderWidth, self.borderRadius, self.leftSpacing, self.contentSpacing, self.rightSpacing, self.dim].forEach { - $0.loadTesting(on: &self.cancellables) - } - } - - func reset() { - [self.backgroundColor].forEach { - $0.reset() - } - - [self.borderWidth, self.borderRadius, self.leftSpacing, self.contentSpacing, self.rightSpacing, self.dim].forEach { - $0.reset() - } - } -} diff --git a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddons.swift b/core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddons.swift deleted file mode 100644 index 551bbda0f..000000000 --- a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddons.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// TextFieldViewModelForAddons.swift -// SparkCore -// -// Created by louis.borlee on 14/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -import Combine - -final class TextFieldViewModelForAddons: TextFieldViewModel { - - override var backgroundColor: any ColorToken { - get { - return ColorTokenDefault.clear - } - set { - self.addonsBackgroundColor = newValue - } - } - - override var dim: CGFloat { - get { - return 1.0 - } - set { - self.addonsDim = newValue - } - } - - @Published private(set) var addonsBackgroundColor: any ColorToken = ColorTokenDefault.clear - @Published private(set) var addonsBorderWidth: CGFloat = .zero - @Published private(set) var addonsBorderRadius: CGFloat = .zero - @Published private(set) var addonsLeftSpacing: CGFloat = .zero - @Published private(set) var addonsContentSpacing: CGFloat = .zero - @Published private(set) var addonsRightSpacing: CGFloat = .zero - @Published private(set) var addonsDim: CGFloat = 1.0 - - init( - theme: Theme, - intent: TextFieldIntent, - getColorsUseCase: TextFieldGetColorsUseCasable = TextFieldGetColorsUseCase(), - getBorderLayoutUseCase: TextFieldGetBorderLayoutUseCasable = TextFieldGetBorderLayoutUseCase(), - getSpacingsUseCase: TextFieldGetSpacingsUseCasable = TextFieldGetSpacingsUseCase() - ) { - super.init( - theme: theme, - intent: intent, - borderStyle: .none, - getColorsUseCase: getColorsUseCase, - getBorderLayoutUseCase: getBorderLayoutUseCase, - getSpacingsUseCase: getSpacingsUseCase) - - self.addonsBackgroundColor = super.backgroundColor - self.setBorderLayout() - self.setSpacings() - self.addonsDim = super.dim - } - - override func setBorderLayout() { - let borderLayout = self.getBorderLayoutUseCase.execute( - theme: self.theme, - borderStyle: .roundedRect, - isFocused: self.isFocused) - - self.addonsBorderWidth = borderLayout.width - self.addonsBorderRadius = borderLayout.radius - } - - override func setSpacings() { - let spacings = self.getSpacingsUseCase.execute( - theme: self.theme, - borderStyle: .roundedRect) - self.addonsLeftSpacing = spacings.left - self.addonsContentSpacing = spacings.content - self.addonsRightSpacing = spacings.right - } -} diff --git a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddonsTests.swift b/core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddonsTests.swift deleted file mode 100644 index 201b1d300..000000000 --- a/core/Sources/Components/TextField/Addons/ViewModel/TextFieldViewModelForAddonsTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// TextFieldViewModelTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import Combine -import UIKit -import SwiftUI -@testable import SparkCore - -final class TextFieldViewModelForAddonsTests: XCTestCase { - - private let superTests: TextFieldViewModelTests = .init() - private var viewModel: TextFieldViewModelForAddons! - - override func setUp() { - super.setUp() - self.superTests.setUp() - - self.viewModel = .init( - theme: self.superTests.theme, - intent: self.superTests.intent, - getColorsUseCase: self.superTests.getColorsUseCase, - getBorderLayoutUseCase: self.superTests.getBorderLayoutUseCase, - getSpacingsUseCase: self.superTests.getSpacingsUseCase - ) - } - - func test_init_borderStyle() { - XCTAssertEqual(self.viewModel.borderStyle, .none, "Wrong borderStyle") - XCTAssertTrue(self.viewModel.backgroundColor.equals(ColorTokenDefault.clear), "Wrong backgroundColor") - XCTAssertEqual(self.viewModel.dim, 1, "Wrong dim") - } - - func test_backgroundColor() { - self.superTests.publishers.reset() - self.superTests.resetUseCases() - - XCTAssertTrue(self.viewModel.backgroundColor.equals(ColorTokenDefault.clear), "Wrong viewModel.backgroundColor before set") - - let newColor = ColorTokenGeneratedMock(uiColor: .brown) - self.viewModel.backgroundColor = newColor - - XCTAssertTrue(self.viewModel.backgroundColor.equals(ColorTokenDefault.clear), "Wrong viewModel.backgroundColor after set") - XCTAssertTrue(self.viewModel.addonsBackgroundColor.equals(newColor), "Wrong delegate.backgroundColor") - - XCTAssertFalse(self.superTests.publishers.backgroundColor.sinkCalled, "backgroundColor should not have sinked") - } - - func test_dim() { - self.superTests.publishers.reset() - self.superTests.resetUseCases() - - XCTAssertEqual(self.viewModel.dim, 1.0, "Wrong viewModel.dim before set") - - let newDim = 0.2 - self.viewModel.dim = newDim - - XCTAssertEqual(self.viewModel.dim, 1.0, "Wrong viewModel.dim after set") - XCTAssertEqual(self.viewModel.addonsDim, newDim, "Wrong delegate.dim") - - XCTAssertFalse(self.superTests.publishers.dim.sinkCalled, "dim should not have sinked") - } - - func test_setBorderLayout() throws { - self.superTests.publishers.reset() - self.superTests.resetUseCases() - - let newExpectedBorderLayout: TextFieldBorderLayout = .mocked(radius: 40, width: 40) - self.superTests.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReturnValue = newExpectedBorderLayout - - // WHEN - self.viewModel.setBorderLayout() - - // THEN - XCTAssertEqual(self.viewModel.addonsBorderWidth, newExpectedBorderLayout.width, "Wrong delegate.boderWidth") - XCTAssertEqual(self.viewModel.addonsBorderRadius, newExpectedBorderLayout.radius, "Wrong delegate.boderRadius") - // Border with & Radius shouldn't change, the delegate takes charge - XCTAssertEqual(self.viewModel.borderWidth, self.superTests.expectedBorderLayout.width, "Wrong viewModel.borderRadius") - XCTAssertEqual(self.viewModel.borderRadius, self.superTests.expectedBorderLayout.radius, "Wrong viewModel.borderWidth") - - XCTAssertEqual(self.superTests.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCallsCount, 1, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should have been called once") - let getBorderLayoutReceivedArguments = try XCTUnwrap(self.superTests.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReceivedArguments, "Couldn't unwrap getBorderLayoutReceivedArguments") - XCTAssertIdentical(getBorderLayoutReceivedArguments.theme as? ThemeGeneratedMock, self.superTests.theme, "Wrong getBorderLayoutReceivedArguments.theme") - XCTAssertEqual(getBorderLayoutReceivedArguments.borderStyle, .roundedRect, "Wrong getBorderLayoutReceivedArguments.borderStyle") - XCTAssertFalse(getBorderLayoutReceivedArguments.isFocused, "Wrong getBorderLayoutReceivedArguments.isFocused") - - XCTAssertFalse(self.superTests.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.superTests.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - } - - func test_setSpacings() throws { - self.superTests.publishers.reset() - self.superTests.resetUseCases() - - let newExpectedSpacings = TextFieldSpacings.mocked(left: 2, content: 4, right: 3) - self.superTests.getSpacingsUseCase.executeWithThemeAndBorderStyleReturnValue = newExpectedSpacings - - // WHEN - self.viewModel.setSpacings() - - // THEN - XCTAssertEqual(self.viewModel.addonsLeftSpacing, newExpectedSpacings.left) - XCTAssertEqual(self.viewModel.addonsContentSpacing, newExpectedSpacings.content) - XCTAssertEqual(self.viewModel.addonsRightSpacing, newExpectedSpacings.right) - - // Spacings shouldn't change, the delegate takes charge - XCTAssertEqual(self.viewModel.leftSpacing, self.superTests.expectedSpacings.left) - XCTAssertEqual(self.viewModel.contentSpacing, self.superTests.expectedSpacings.content) - XCTAssertEqual(self.viewModel.rightSpacing, self.superTests.expectedSpacings.right) - - XCTAssertEqual(self.superTests.getSpacingsUseCase.executeWithThemeAndBorderStyleCallsCount, 1, "getSpacingsUseCase.executeWithThemeAndBorderStyle should have been called once") - let getSpacingsUseCaseReceivedArguments = try XCTUnwrap(self.superTests.getSpacingsUseCase.executeWithThemeAndBorderStyleReceivedArguments, "Couldn't unwrap getSpacingsUseCaseReceivedArguments") - XCTAssertIdentical(getSpacingsUseCaseReceivedArguments.theme as? ThemeGeneratedMock, self.superTests.theme, "Wrong getSpacingsUseCaseReceivedArguments.theme") - XCTAssertEqual(getSpacingsUseCaseReceivedArguments.borderStyle, .roundedRect, "Wrong getSpacingsUseCaseReceivedArguments.borderStyle") - - XCTAssertFalse(self.superTests.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.superTests.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.superTests.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - } -} diff --git a/core/Sources/Components/TextField/Enum/TextFieldBorderStyle.swift b/core/Sources/Components/TextField/Enum/TextFieldBorderStyle.swift deleted file mode 100644 index 02be2b640..000000000 --- a/core/Sources/Components/TextField/Enum/TextFieldBorderStyle.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// TextFieldBorderStyle.swift -// SparkCore -// -// Created by Jacklyn Situmorang on 18.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -enum TextFieldBorderStyle: CaseIterable { - case roundedRect - case none - - init(_ borderStyle: UITextField.BorderStyle) { - switch borderStyle { - case .roundedRect: - self = .roundedRect - default: - self = .none - } - } -} - -extension UITextField.BorderStyle { - init(_ borderStyle: TextFieldBorderStyle) { - switch borderStyle { - case .roundedRect: - self = .roundedRect - case .none: - self = .none - } - - } -} diff --git a/core/Sources/Components/TextField/Enum/TextFieldIntent.swift b/core/Sources/Components/TextField/Enum/TextFieldIntent.swift deleted file mode 100644 index a2b185362..000000000 --- a/core/Sources/Components/TextField/Enum/TextFieldIntent.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TextFieldIntent.swift -// Spark -// -// Created by Quentin.richard on 21/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public enum TextFieldIntent: CaseIterable { - case error - case alert - case neutral - case success -} diff --git a/core/Sources/Components/TextField/Model/TextFieldBorderLayout+ExtensionTests.swift b/core/Sources/Components/TextField/Model/TextFieldBorderLayout+ExtensionTests.swift deleted file mode 100644 index 83ac021bf..000000000 --- a/core/Sources/Components/TextField/Model/TextFieldBorderLayout+ExtensionTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TextFieldBorderLayout+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension TextFieldBorderLayout { - static func mocked(radius: CGFloat, width: CGFloat) -> TextFieldBorderLayout { - return .init(radius: radius, width: width) - } -} diff --git a/core/Sources/Components/TextField/Model/TextFieldBorderLayout.swift b/core/Sources/Components/TextField/Model/TextFieldBorderLayout.swift deleted file mode 100644 index 742306b1a..000000000 --- a/core/Sources/Components/TextField/Model/TextFieldBorderLayout.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// TextFieldBorderLayout.swift -// SparkCore -// -// Created by louis.borlee on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct TextFieldBorderLayout: Equatable { - let radius: CGFloat - let width: CGFloat -} diff --git a/core/Sources/Components/TextField/Model/TextFieldColors+ExtensionTests.swift b/core/Sources/Components/TextField/Model/TextFieldColors+ExtensionTests.swift deleted file mode 100644 index 3b92ec2b7..000000000 --- a/core/Sources/Components/TextField/Model/TextFieldColors+ExtensionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// TextFieldColors+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension TextFieldColors { - static func mocked( - text: ColorTokenGeneratedMock, - placeholder: ColorTokenGeneratedMock, - border: ColorTokenGeneratedMock, - background: ColorTokenGeneratedMock - ) -> TextFieldColors { - return .init( - text: text, - placeholder: placeholder, - border: border, - background: background - ) - } -} diff --git a/core/Sources/Components/TextField/Model/TextFieldColors.swift b/core/Sources/Components/TextField/Model/TextFieldColors.swift deleted file mode 100644 index f558eb223..000000000 --- a/core/Sources/Components/TextField/Model/TextFieldColors.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TextFieldColors.swift -// SparkCore -// -// Created by Quentin.richard on 22/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct TextFieldColors: Equatable { - let text: any ColorToken - let placeholder: any ColorToken - let border: any ColorToken - let background: any ColorToken - - static func == (lhs: TextFieldColors, rhs: TextFieldColors) -> Bool { - return lhs.text.equals(rhs.text) && - lhs.placeholder.equals(rhs.placeholder) && - lhs.border.equals(rhs.border) && - lhs.background.equals(rhs.background) - } -} diff --git a/core/Sources/Components/TextField/Model/TextFieldSpacings+ExtensionTests.swift b/core/Sources/Components/TextField/Model/TextFieldSpacings+ExtensionTests.swift deleted file mode 100644 index d82c28f77..000000000 --- a/core/Sources/Components/TextField/Model/TextFieldSpacings+ExtensionTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TextFieldSpacings+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension TextFieldSpacings { - static func mocked(left: CGFloat, content: CGFloat, right: CGFloat) -> TextFieldSpacings { - return .init(left: left, content: content, right: right) - } -} diff --git a/core/Sources/Components/TextField/Model/TextFieldSpacings.swift b/core/Sources/Components/TextField/Model/TextFieldSpacings.swift deleted file mode 100644 index a93a0684e..000000000 --- a/core/Sources/Components/TextField/Model/TextFieldSpacings.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// TextFieldSpacings.swift -// SparkCore -// -// Created by louis.borlee on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct TextFieldSpacings: Equatable { - let left: CGFloat - let content: CGFloat - let right: CGFloat -} diff --git a/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCasableGeneratedMock+ExtensionTests.swift b/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCasableGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 9a67e0e45..000000000 --- a/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCasableGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// TextFieldGetBorderLayoutUseCasableGeneratedMock+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension TextFieldGetBorderLayoutUseCasableGeneratedMock { - - static func mocked(returnedBorderLayout: TextFieldBorderLayout) -> TextFieldGetBorderLayoutUseCasableGeneratedMock { - let mock = TextFieldGetBorderLayoutUseCasableGeneratedMock() - mock.executeWithThemeAndBorderStyleAndIsFocusedReturnValue = returnedBorderLayout - return mock - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCase.swift b/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCase.swift deleted file mode 100644 index 0bb5bb2f7..000000000 --- a/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCase.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// TextFieldGetBorderLayoutUseCase.swift -// SparkCore -// -// Created by louis.borlee on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TextFieldGetBorderLayoutUseCasable { - func execute(theme: Theme, - borderStyle: TextFieldBorderStyle, - isFocused: Bool) -> TextFieldBorderLayout -} - -final class TextFieldGetBorderLayoutUseCase: TextFieldGetBorderLayoutUseCasable { - func execute(theme: Theme, - borderStyle: TextFieldBorderStyle, - isFocused: Bool) -> TextFieldBorderLayout { - switch borderStyle { - case .none: - return .init( - radius: theme.border.radius.none, - width: theme.border.width.none - ) - case .roundedRect: - return .init( - radius: theme.border.radius.large, - width: isFocused ? theme.border.width.medium : theme.border.width.small - ) - } - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCaseTests.swift b/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCaseTests.swift deleted file mode 100644 index aef94c537..000000000 --- a/core/Sources/Components/TextField/UseCase/GetBorderLayout/TextFieldGetBorderLayoutUseCaseTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// TextFieldGetBorderLayoutUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class TextFieldGetBorderLayoutUseCaseTests: XCTestCase { - - private let theme = ThemeGeneratedMock.mocked() - - func test_roundedRect_isFocused() { - // GIVEN - let useCase = TextFieldGetBorderLayoutUseCase() - let borderStyle = TextFieldBorderStyle.roundedRect - let isFocused = true - - // WHEN - let borderLayout = useCase.execute( - theme: self.theme, - borderStyle: borderStyle, - isFocused: isFocused - ) - - // THEN - XCTAssertEqual(borderLayout.radius, self.theme.border.radius.large, "Wrong radius") - XCTAssertEqual(borderLayout.width, self.theme.border.width.medium, "Wrong width") - } - - func test_roundedRect_isNotFocused() { - // GIVEN - let useCase = TextFieldGetBorderLayoutUseCase() - let borderStyle = TextFieldBorderStyle.roundedRect - let isFocused = false - - // WHEN - let borderLayout = useCase.execute( - theme: self.theme, - borderStyle: borderStyle, - isFocused: isFocused - ) - - // THEN - XCTAssertEqual(borderLayout.radius, self.theme.border.radius.large, "Wrong radius") - XCTAssertEqual(borderLayout.width, self.theme.border.width.small, "Wrong width") - } - - func test_none_isFocused() { - // GIVEN - let useCase = TextFieldGetBorderLayoutUseCase() - let borderStyle = TextFieldBorderStyle.none - let isFocused = true - - // WHEN - let borderLayout = useCase.execute( - theme: self.theme, - borderStyle: borderStyle, - isFocused: isFocused - ) - - // THEN - XCTAssertEqual(borderLayout.radius, self.theme.border.radius.none, "Wrong radius") - XCTAssertEqual(borderLayout.width, self.theme.border.width.none, "Wrong width") - } - - func test_none_isNotFocused() { - // GIVEN - let useCase = TextFieldGetBorderLayoutUseCase() - let borderStyle = TextFieldBorderStyle.none - let isFocused = false - - // WHEN - let borderLayout = useCase.execute( - theme: self.theme, - borderStyle: borderStyle, - isFocused: isFocused - ) - - // THEN - XCTAssertEqual(borderLayout.radius, self.theme.border.radius.none, "Wrong radius") - XCTAssertEqual(borderLayout.width, self.theme.border.width.none, "Wrong width") - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCasableGeneratedMock+ExtensionTests.swift b/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCasableGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 03066fefc..000000000 --- a/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCasableGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// TextFieldGetColorsUseCasableGeneratedMock+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension TextFieldGetColorsUseCasableGeneratedMock { - static func mocked(returnedColors: TextFieldColors) -> TextFieldGetColorsUseCasableGeneratedMock { - let mock = TextFieldGetColorsUseCasableGeneratedMock() - mock.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReturnValue = returnedColors - return mock - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCase.swift b/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCase.swift deleted file mode 100644 index b6e5ea644..000000000 --- a/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCase.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// TextFieldGetColorsUseCase.swift -// SparkCore -// -// Created by Quentin.richard on 21/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TextFieldGetColorsUseCasable { - func execute(theme: Theme, - intent: TextFieldIntent, - isFocused: Bool, - isEnabled: Bool, - isUserInteractionEnabled: Bool) -> TextFieldColors -} - -struct TextFieldGetColorsUseCase: TextFieldGetColorsUseCasable { - func execute(theme: Theme, - intent: TextFieldIntent, - isFocused: Bool, - isEnabled: Bool, - isUserInteractionEnabled: Bool) -> TextFieldColors { - let text = theme.colors.base.onSurface - let placeholder = theme.colors.base.onSurface.opacity(theme.dims.dim1) - - let border: any ColorToken - let background: any ColorToken - if isEnabled, isUserInteractionEnabled { - switch intent { - case .error: - border = theme.colors.feedback.error - case .alert: - border = theme.colors.feedback.alert - case .neutral: - border = isFocused ? theme.colors.base.outlineHigh : theme.colors.base.outline - case .success: - border = theme.colors.feedback.success - } - background = theme.colors.base.surface - } else { - border = theme.colors.base.outline - background = theme.colors.base.onSurface.opacity(theme.dims.dim5) - } - - return .init( - text: text, - placeholder: placeholder, - border: border, - background: background - ) - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCaseTests.swift b/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCaseTests.swift deleted file mode 100644 index 2dcb1cfd2..000000000 --- a/core/Sources/Components/TextField/UseCase/GetColors/TextFieldGetColorsUseCaseTests.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// TextFieldGetColorsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class TextFieldGetColorsUseCaseTests: XCTestCase { - - private let theme = ThemeGeneratedMock.mocked() - - func test_isFocused_isEnabled_isUserInteractionEnabled() { - let intentAndExpectedBorderColorArray: [(intent: TextFieldIntent, expectedBorderColor: any ColorToken)] = [ - (intent: .success, self.theme.colors.feedback.success), - (intent: .error, self.theme.colors.feedback.error), - (intent: .alert, self.theme.colors.feedback.alert), - (intent: .neutral, self.theme.colors.base.outlineHigh), - ] - XCTAssertEqual(intentAndExpectedBorderColorArray.count, TextFieldIntent.allCases.count, "Wrong intentAndExpectedBorderColorArray count") - - intentAndExpectedBorderColorArray.forEach { - self._test_isFocused_isEnabled_isUserInteractionEnabled(with: $0.intent, expectedBorderColor: $0.expectedBorderColor) - } - } - - private func _test_isFocused_isEnabled_isUserInteractionEnabled( - with intent: TextFieldIntent, - expectedBorderColor: any ColorToken - ) { - // GIVEN - let isFocused = true - let isEnabled = true - let isUserInteractionEnabled = true - let useCase = TextFieldGetColorsUseCase() - - // WHEN - let colors = useCase.execute( - theme: self.theme, - intent: intent, - isFocused: isFocused, - isEnabled: isEnabled, - isUserInteractionEnabled: isUserInteractionEnabled - ) - - // THEN - XCTAssertTrue(colors.text.equals(self.theme.colors.base.onSurface), "Wrong text color for intent: \(intent)") - XCTAssertTrue(colors.placeholder.equals(self.theme.colors.base.onSurface.opacity(self.theme.dims.dim1)), "Wrong placeholder color for intent: \(intent)") - XCTAssertTrue(colors.border.equals(expectedBorderColor), "Wrong border color for intent: \(intent)") - XCTAssertTrue(colors.background.equals(self.theme.colors.base.surface), "Wrong background color for intent: \(intent)") - } - - func test_isNotFocused_isEnabled_isUserInteractionEnabled() { - let intentAndExpectedBorderColorArray: [(intent: TextFieldIntent, expectedBorderColor: any ColorToken)] = [ - (intent: .success, self.theme.colors.feedback.success), - (intent: .error, self.theme.colors.feedback.error), - (intent: .alert, self.theme.colors.feedback.alert), - (intent: .neutral, self.theme.colors.base.outline), - ] - XCTAssertEqual(intentAndExpectedBorderColorArray.count, TextFieldIntent.allCases.count, "Wrong intentAndExpectedBorderColorArray count") - - intentAndExpectedBorderColorArray.forEach { - self._test_isNotFocused_isEnabled_isUserInteractionEnabled(with: $0.intent, expectedBorderColor: $0.expectedBorderColor) - } - } - - private func _test_isNotFocused_isEnabled_isUserInteractionEnabled( - with intent: TextFieldIntent, - expectedBorderColor: any ColorToken - ) { - // GIVEN - let isFocused = false - let isEnabled = true - let isUserInteractionEnabled = true - let useCase = TextFieldGetColorsUseCase() - - // WHEN - let colors = useCase.execute( - theme: self.theme, - intent: intent, - isFocused: isFocused, - isEnabled: isEnabled, - isUserInteractionEnabled: isUserInteractionEnabled - ) - - // THEN - XCTAssertTrue(colors.text.equals(self.theme.colors.base.onSurface), "Wrong text color for intent: \(intent)") - XCTAssertTrue(colors.placeholder.equals(self.theme.colors.base.onSurface.opacity(self.theme.dims.dim1)), "Wrong placeholder color for intent: \(intent)") - XCTAssertTrue(colors.border.equals(expectedBorderColor), "Wrong border color for intent: \(intent)") - XCTAssertTrue(colors.background.equals(self.theme.colors.base.surface), "Wrong background color for intent: \(intent)") - } - - func test_isNotFocused_isEnabled_isUserInteractionNotEnabled() { - let intentAndExpectedBorderColorArray: [(intent: TextFieldIntent, expectedBorderColor: any ColorToken)] = [ - (intent: .success, self.theme.colors.base.outline), - (intent: .error, self.theme.colors.base.outline), - (intent: .alert, self.theme.colors.base.outline), - (intent: .neutral, self.theme.colors.base.outline), - ] - XCTAssertEqual(intentAndExpectedBorderColorArray.count, TextFieldIntent.allCases.count, "Wrong intentAndExpectedBorderColorArray count") - - intentAndExpectedBorderColorArray.forEach { - self._test_isNotFocused_isEnabled_isUserInteractionNotEnabled(with: $0.intent, expectedBorderColor: $0.expectedBorderColor) - } - } - - private func _test_isNotFocused_isEnabled_isUserInteractionNotEnabled( - with intent: TextFieldIntent, - expectedBorderColor: any ColorToken - ) { - // GIVEN - let isFocused = false - let isEnabled = true - let isUserInteractionEnabled = false - let useCase = TextFieldGetColorsUseCase() - - // WHEN - let colors = useCase.execute( - theme: self.theme, - intent: intent, - isFocused: isFocused, - isEnabled: isEnabled, - isUserInteractionEnabled: isUserInteractionEnabled - ) - - // THEN - XCTAssertTrue(colors.text.equals(self.theme.colors.base.onSurface), "Wrong text color for intent: \(intent)") - XCTAssertTrue(colors.placeholder.equals(self.theme.colors.base.onSurface.opacity(self.theme.dims.dim1)), "Wrong placeholder color for intent: \(intent)") - XCTAssertTrue(colors.border.equals(expectedBorderColor), "Wrong border color for intent: \(intent)") - XCTAssertTrue(colors.background.equals(self.theme.colors.base.onSurface.opacity(theme.dims.dim5)), "Wrong background color for intent: \(intent)") - } - - func test_isFocused_isNotEnabled_isUserInteractionEnabled() { - let intentAndExpectedBorderColorArray: [(intent: TextFieldIntent, expectedBorderColor: any ColorToken)] = [ - (intent: .success, self.theme.colors.base.outline), - (intent: .error, self.theme.colors.base.outline), - (intent: .alert, self.theme.colors.base.outline), - (intent: .neutral, self.theme.colors.base.outline), - ] - XCTAssertEqual(intentAndExpectedBorderColorArray.count, TextFieldIntent.allCases.count, "Wrong intentAndExpectedBorderColorArray count") - - intentAndExpectedBorderColorArray.forEach { - self._test_isFocused_isNotEnabled_isUserInteractionEnabled(with: $0.intent, expectedBorderColor: $0.expectedBorderColor) - } - } - - private func _test_isFocused_isNotEnabled_isUserInteractionEnabled( - with intent: TextFieldIntent, - expectedBorderColor: any ColorToken - ) { - // GIVEN - let isFocused = true - let isEnabled = false - let isUserInteractionEnabled = true - let useCase = TextFieldGetColorsUseCase() - - // WHEN - let colors = useCase.execute( - theme: self.theme, - intent: intent, - isFocused: isFocused, - isEnabled: isEnabled, - isUserInteractionEnabled: isUserInteractionEnabled - ) - - // THEN - XCTAssertTrue(colors.text.equals(self.theme.colors.base.onSurface), "Wrong text color for intent: \(intent)") - XCTAssertTrue(colors.placeholder.equals(self.theme.colors.base.onSurface.opacity(self.theme.dims.dim1)), "Wrong placeholder color for intent: \(intent)") - XCTAssertTrue(colors.border.equals(expectedBorderColor), "Wrong border color for intent: \(intent)") - XCTAssertTrue(colors.background.equals(self.theme.colors.base.onSurface.opacity(theme.dims.dim5)), "Wrong background color for intent: \(intent)") - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCase.swift b/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCase.swift deleted file mode 100644 index 21a2a55d3..000000000 --- a/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCase.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// TextFieldGetSpacingsUseCase.swift -// SparkCore -// -// Created by louis.borlee on 25/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol TextFieldGetSpacingsUseCasable { - func execute(theme: Theme, - borderStyle: TextFieldBorderStyle) -> TextFieldSpacings -} - -final class TextFieldGetSpacingsUseCase: TextFieldGetSpacingsUseCasable { - func execute(theme: Theme, borderStyle: TextFieldBorderStyle) -> TextFieldSpacings { - switch borderStyle { - case .none: - return .init( - left: theme.layout.spacing.none, - content: theme.layout.spacing.medium, - right: theme.layout.spacing.none - ) - case .roundedRect: - return .init( - left: theme.layout.spacing.large, - content: theme.layout.spacing.medium, - right: theme.layout.spacing.large - ) - } - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseGeneratedMock+ExtensionTests.swift b/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 9908d9dd9..000000000 --- a/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// TextFieldGetSpacingsUseCaseGeneratedMock+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore - -extension TextFieldGetSpacingsUseCasableGeneratedMock { - static func mocked(returnedSpacings: TextFieldSpacings) -> TextFieldGetSpacingsUseCasableGeneratedMock { - let mock = TextFieldGetSpacingsUseCasableGeneratedMock() - mock.executeWithThemeAndBorderStyleReturnValue = returnedSpacings - return mock - } -} diff --git a/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseTests.swift b/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseTests.swift deleted file mode 100644 index dde56f2a6..000000000 --- a/core/Sources/Components/TextField/UseCase/GetSpacings/TextFieldGetSpacingsUseCaseTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// TextFieldGetSpacingsUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by Jacklyn Situmorang on 17.10.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -@testable import SparkCore - -final class TextFieldGetSpacingsUseCaseTests: XCTestCase { - - // MARK: - Properties - - private let themeMock = ThemeGeneratedMock.mocked() - - // MARK: - Tests - - func test_execute_for_none() { - self.testExecute( - givenBorderStyle: .none, - expectedSpacings: .init( - left: self.themeMock.layout.spacing.none, - content: self.themeMock.layout.spacing.medium, - right: self.themeMock.layout.spacing.none - ) - ) - } - - func text_execute_for_roundedRect() { - self.testExecute( - givenBorderStyle: .roundedRect, - expectedSpacings: .init( - left: self.themeMock.layout.spacing.large, - content: self.themeMock.layout.spacing.medium, - right: self.themeMock.layout.spacing.large - ) - ) - } -} - -private extension TextFieldGetSpacingsUseCaseTests { - func testExecute( - givenBorderStyle: TextFieldBorderStyle, - expectedSpacings: TextFieldSpacings - ) { - // GIVEN - let useCase = TextFieldGetSpacingsUseCase() - - // WHEN - let spacings = useCase.execute( - theme: self.themeMock, - borderStyle: givenBorderStyle - ) - - // THEN - XCTAssertEqual(spacings.content, expectedSpacings.content) - XCTAssertEqual(spacings.left, expectedSpacings.left) - XCTAssertEqual(spacings.right, expectedSpacings.right) - } -} diff --git a/core/Sources/Components/TextField/View/AccessibilityIdentifiier/TextFieldAccessibilityIdentifier.swift b/core/Sources/Components/TextField/View/AccessibilityIdentifiier/TextFieldAccessibilityIdentifier.swift deleted file mode 100644 index e8c38bb97..000000000 --- a/core/Sources/Components/TextField/View/AccessibilityIdentifiier/TextFieldAccessibilityIdentifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TextFieldAccessibilityIdentifier.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -/// The accessibility identifiers for the textfield. -public enum TextFieldAccessibilityIdentifier { - - /// The textfield accessibility identifier. - public static let view = "spark-textfield" -} diff --git a/core/Sources/Components/TextField/View/SwiftUI/TextFieldView.swift b/core/Sources/Components/TextField/View/SwiftUI/TextFieldView.swift deleted file mode 100644 index 93e527bc6..000000000 --- a/core/Sources/Components/TextField/View/SwiftUI/TextFieldView.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// TextFieldView.swift -// SparkCore -// -// Created by louis.borlee on 07/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -/// A TextField that can be surrounded by left and/or right views -public struct TextFieldView: View { - - private let titleKey: LocalizedStringKey - private let text: Binding - private let type: TextFieldViewType - private let viewModel: TextFieldViewModel - private let leftView: () -> LeftView - private let rightView: () -> RightView - - @Environment(\.isEnabled) private var isEnabled: Bool - @FocusState private var isFocused: Bool - - init(titleKey: LocalizedStringKey, - text: Binding, - viewModel: TextFieldViewModel, - type: TextFieldViewType, - leftView: @escaping (() -> LeftView), - rightView: @escaping (() -> RightView)) { - self.titleKey = titleKey - self.text = text - self.viewModel = viewModel - self.type = type - self.leftView = leftView - self.rightView = rightView - } - - init( - _ titleKey: LocalizedStringKey, - text: Binding, - theme: Theme, - intent: TextFieldIntent, - borderStyle: TextFieldBorderStyle, - type: TextFieldViewType, - isReadOnly: Bool, - leftView: @escaping (() -> LeftView), - rightView: @escaping (() -> RightView) - ) { - let viewModel = TextFieldViewModel( - theme: theme, - intent: intent, - borderStyle: borderStyle - ) - viewModel.isUserInteractionEnabled = isReadOnly != true - self.init( - titleKey: titleKey, - text: text, - viewModel: viewModel, - type: type, - leftView: leftView, - rightView: rightView - ) - } - - /// TextFieldView initializer - /// - Parameters: - /// - titleKey: The textfield's current placeholder - /// - text: The textfield's text binding - /// - theme: The textfield's current theme - /// - intent: The textfield's current intent - /// - type: The type of field with its associated callback(s), default is `.standard()` - /// - isReadOnly: Set this to true if you want the textfield to be readOnly, default is `false` - /// - leftView: The TextField's left view, default is `EmptyView` - /// - rightView: The TextField's right view, default is `EmptyView` - public init(_ titleKey: LocalizedStringKey, - text: Binding, - theme: Theme, - intent: TextFieldIntent, - type: TextFieldViewType = .standard(), - isReadOnly: Bool = false, - leftView: @escaping () -> LeftView = { EmptyView() }, - rightView: @escaping () -> RightView = { EmptyView() }) { - self.init( - titleKey, - text: text, - theme: theme, - intent: intent, - borderStyle: .roundedRect, - type: type, - isReadOnly: isReadOnly, - leftView: leftView, - rightView: rightView - ) - } - - public var body: some View { - TextFieldViewInternal( - titleKey: self.titleKey, - text: self.text, - viewModel: self.viewModel, - type: self.type, - leftView: self.leftView, - rightView: self.rightView) - .update( - isEnabled: self.isEnabled, - isFocused: self.isFocused - ) - .focused($isFocused) - } - -} diff --git a/core/Sources/Components/TextField/View/SwiftUI/TextFieldViewInternal.swift b/core/Sources/Components/TextField/View/SwiftUI/TextFieldViewInternal.swift deleted file mode 100644 index 66aef2fff..000000000 --- a/core/Sources/Components/TextField/View/SwiftUI/TextFieldViewInternal.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// TextFieldViewInternal.swift -// SparkCore -// -// Created by louis.borlee on 18/04/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import SwiftUI - -struct TextFieldViewInternal: View { - - @ScaledMetric private var height: CGFloat = 44 - @ScaledMetric private var scaleFactor: CGFloat = 1.0 - - @ObservedObject private var viewModel: TextFieldViewModel - @Binding private var text: String - - private let titleKey: LocalizedStringKey - private var type: TextFieldViewType - - private let leftView: () -> LeftView - private let rightView: () -> RightView - - init(titleKey: LocalizedStringKey, - text: Binding, - viewModel: TextFieldViewModel, - type: TextFieldViewType, - leftView: @escaping (() -> LeftView), - rightView: @escaping (() -> RightView)) { - self.titleKey = titleKey - self._text = text - self.viewModel = viewModel - self.type = type - self.leftView = leftView - self.rightView = rightView - } - - var body: some View { - ZStack { - self.viewModel.backgroundColor.color - contentView() - } - .tint(self.viewModel.textColor.color) - .allowsHitTesting(self.viewModel.isUserInteractionEnabled) - .border(width: self.viewModel.borderWidth * self.scaleFactor, radius: self.viewModel.borderRadius, colorToken: self.viewModel.borderColor) - .frame(height: self.height) - .opacity(self.viewModel.dim) - } - - // MARK: - Content - @ViewBuilder - private func contentView() -> some View { - HStack(spacing: self.viewModel.contentSpacing) { - leftView() - textField() - rightView() - } - .padding(EdgeInsets(top: .zero, leading: self.viewModel.leftSpacing, bottom: .zero, trailing: self.viewModel.rightSpacing)) - } - - // MARK: - TextField - @ViewBuilder - private func textField() -> some View { - Group { - switch type { - case .secure(let onCommit): - SecureField(titleKey, text: $text, onCommit: onCommit) - .font(self.viewModel.font.font) - case .standard(let onEditingChanged, let onCommit): - TextField(titleKey, text: $text, onEditingChanged: onEditingChanged, onCommit: onCommit) - .font(self.viewModel.font.font) - } - } - .textFieldStyle(.plain) - .foregroundStyle(self.viewModel.textColor.color) - .accessibilityIdentifier(TextFieldAccessibilityIdentifier.view) - } - - func update(isEnabled: Bool, isFocused: Bool) -> some View { - DispatchQueue.main.async { - self.viewModel.isEnabled = isEnabled - self.viewModel.isFocused = isFocused - } - return self - } -} diff --git a/core/Sources/Components/TextField/View/SwiftUI/TextFieldViewType.swift b/core/Sources/Components/TextField/View/SwiftUI/TextFieldViewType.swift deleted file mode 100644 index 46b28247a..000000000 --- a/core/Sources/Components/TextField/View/SwiftUI/TextFieldViewType.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// TextFieldViewType.swift -// SparkCore -// -// Created by louis.borlee on 02/04/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -/// A TextField type with its associated callback(s) -public enum TextFieldViewType { - case secure(onCommit: () -> Void = {}) - case standard(onEditingChanged: (Bool) -> Void = { _ in }, onCommit: () -> Void = {}) -} diff --git a/core/Sources/Components/TextField/View/TextFieldScenario+SnapshotTests.swift b/core/Sources/Components/TextField/View/TextFieldScenario+SnapshotTests.swift deleted file mode 100644 index c94ab10e1..000000000 --- a/core/Sources/Components/TextField/View/TextFieldScenario+SnapshotTests.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// TextFieldScenario+SnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by louis.borlee on 12/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation -@testable import SparkCore -import UIKit - -struct TextFieldScenario: CustomStringConvertible { - let description: String - let statesArray: [TextFieldScenarioStates] - let intents: [TextFieldIntent] - let texts: [TextFieldScenarioText] - let leftContents: [TextFieldScenarioSideContentOptionSet] - let rightContents: [TextFieldScenarioSideContentOptionSet] - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - static let test1: TextFieldScenario = .init( - description: "Test1", - statesArray: [ - .disabled, - .focused, - .readOnly, - .`default`, - .readyOnlyAndDisabled - ], - intents: [.neutral], - texts: [.normal], - leftContents: [.none], - rightContents: [.none], - modes: ComponentSnapshotTestConstants.Modes.all, - sizes: ComponentSnapshotTestConstants.Sizes.default - ) - - static let test2: TextFieldScenario = .init( - description: "Test2", - statesArray: [.`default`], - intents: [.neutral], - texts: [.empty, .placeholder, .normal, .long], - leftContents: [.none], - rightContents: [.none], - modes: [.light], - sizes: ComponentSnapshotTestConstants.Sizes.default - ) - - static let test3: TextFieldScenario = .init( - description: "Test3", - statesArray: [.focused], - intents: [.success], - texts: [.normal], - leftContents: [[.button], [.image]], - rightContents: [[.button], [.image]], - modes: [.light], - sizes: [.extraSmall, .medium] - ) - - static let test4: TextFieldScenario = .init( - description: "Test4", - statesArray: [.disabled], - intents: [.error], - texts: [.placeholder, .long], - leftContents: [[.button, .image]], - rightContents: [[.button, .image]], - modes: [.light], - sizes: ComponentSnapshotTestConstants.Sizes.default - ) - - static let test5: TextFieldScenario = .init( - description: "Test5", - statesArray: [.`default`], - intents: TextFieldIntent.allCases, - texts: [.normal], - leftContents: [.none], - rightContents: [.none], - modes: ComponentSnapshotTestConstants.Modes.all, - sizes: [.medium, .accessibilityExtraExtraExtraLarge] - ) - - func getTestName(intent: TextFieldIntent, states: TextFieldScenarioStates, text: TextFieldScenarioText, leftContent: TextFieldScenarioSideContentOptionSet, rightContent: TextFieldScenarioSideContentOptionSet) -> String { - var testName = "\(self)-\(intent)Intent-\(states.name)State-\(text.rawValue)Text" - if leftContent.isEmpty == false { - testName.append("-left\(leftContent.name)") - } - if rightContent.isEmpty == false { - testName.append("-right\(rightContent.name)") - } - return testName - } -} - -struct TextFieldScenarioStates { - let isEnabled: Bool - let isFocused: Bool - let isReadOnly: Bool - let name: String - - static let disabled: TextFieldScenarioStates = .init( - isEnabled: false, - isFocused: false, - isReadOnly: false, - name: "disabled" - ) - static let readOnly: TextFieldScenarioStates = .init( - isEnabled: true, - isFocused: false, - isReadOnly: true, - name: "readOnly" - ) - static let focused: TextFieldScenarioStates = .init( - isEnabled: true, - isFocused: true, - isReadOnly: false, - name: "focused" - ) - static let `default`: TextFieldScenarioStates = .init( - isEnabled: true, - isFocused: false, - isReadOnly: false, - name: "default" - ) - static let readyOnlyAndDisabled: TextFieldScenarioStates = .init( - isEnabled: false, - isFocused: false, - isReadOnly: true, - name: "readyOnlyAndDisabled" - ) -} - -enum TextFieldScenarioText: String { - case empty - case placeholder - case normal - case long - - var placeholder: String? { - switch self { - case .empty: - return nil - default: - return "Placeholder" - } - } - - var text: String? { - switch self { - case .empty, .placeholder: - return nil - case .normal: - return "Hello there" - case .long: - return """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ac faucibus metus. Praesent feugiat commodo nibh, at placerat enim pharetra ac. Integer sed dictum eros. - """ - } - } -} - -struct TextFieldScenarioSideContentOptionSet: OptionSet { - let rawValue: UInt - - static let none = TextFieldScenarioSideContentOptionSet(rawValue: 1 << 0) - static let button = TextFieldScenarioSideContentOptionSet(rawValue: 1 << 1) - static let image = TextFieldScenarioSideContentOptionSet(rawValue: 1 << 2) - - var name: String { - var contents = [String]() - if self.contains(.none) { - contents.append("None") - } - if self.contains(.button) { - contents.append("Button") - } - if self.contains(.image) { - contents.append("Image") - } - return contents.joined(separator: "_") - } -} diff --git a/core/Sources/Components/TextField/View/UIKit/TextFieldUIView.swift b/core/Sources/Components/TextField/View/UIKit/TextFieldUIView.swift deleted file mode 100644 index 12806808e..000000000 --- a/core/Sources/Components/TextField/View/UIKit/TextFieldUIView.swift +++ /dev/null @@ -1,289 +0,0 @@ -// -// TextFieldUIView.swift -// SparkCore -// -// Created by louis.borlee on 05/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -import Combine - -/// Spark TextField, subclasses UITextField -public final class TextFieldUIView: UITextField { - - private let viewModel: TextFieldViewModel - private var cancellables = Set() - - @ScaledUIMetric private var height: CGFloat = 44 - @ScaledUIMetric private var scaleFactor: CGFloat = 1.0 - - private let defaultClearButtonRightSpacing = 5.0 - - public override var placeholder: String? { - didSet { - self.setPlaceholder(self.placeholder, foregroundColor: self.viewModel.placeholderColor, font: self.viewModel.font) - } - } - - public override var isEnabled: Bool { - didSet { - self.viewModel.isEnabled = self.isEnabled - } - } - - public override var isUserInteractionEnabled: Bool { - didSet { - self.viewModel.isUserInteractionEnabled = self.isUserInteractionEnabled - } - } - - public override var borderStyle: UITextField.BorderStyle { - @available(*, unavailable) - set {} - get { return .init(self.viewModel.borderStyle) } - } - - /// The textfield's current theme. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - /// The textfield's current intent. - public var intent: TextFieldIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - internal init(viewModel: TextFieldViewModel) { - self.viewModel = viewModel - super.init(frame: .init(origin: .zero, size: .init(width: 0, height: 44))) - self.adjustsFontForContentSizeCategory = true - self.adjustsFontSizeToFitWidth = false - self.setupView() - } - - internal convenience init( - theme: Theme, - intent: TextFieldIntent, - borderStyle: TextFieldBorderStyle - ) { - self.init( - viewModel: .init( - theme: theme, - intent: intent, - borderStyle: borderStyle - ) - ) - } - - /// TextFieldUIView initializer - /// - Parameters: - /// - theme: The textfield's current theme - /// - intent: The textfield's current intent - public convenience init( - theme: Theme, - intent: TextFieldIntent - ) { - self.init( - theme: theme, - intent: intent, - borderStyle: .roundedRect - ) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - self.subscribeToViewModel() - self.setContentCompressionResistancePriority(.required, for: .vertical) - self.accessibilityIdentifier = TextFieldAccessibilityIdentifier.view - } - - private func subscribeToViewModel() { - self.viewModel.$textColor.removeDuplicates(by: { lhs, rhs in - lhs.equals(rhs) - }) - .subscribe(in: &self.cancellables) { [weak self] textColor in - guard let self else { return } - self.textColor = textColor.uiColor - self.tintColor = textColor.uiColor - } - - self.viewModel.$backgroundColor.removeDuplicates(by: { lhs, rhs in - lhs.equals(rhs) - }) - .subscribe(in: &self.cancellables) { [weak self] backgroundColor in - guard let self else { return } - self.backgroundColor = backgroundColor.uiColor - } - - self.viewModel.$borderColor.removeDuplicates(by: { lhs, rhs in - lhs.equals(rhs) - }) - .subscribe(in: &self.cancellables) { [weak self] borderColor in - guard let self else { return } - self.setBorderColor(from: borderColor) - } - - self.viewModel.$placeholderColor.removeDuplicates(by: { lhs, rhs in - lhs.equals(rhs) - }) - .subscribe(in: &self.cancellables) { [weak self] placeholderColor in - guard let self else { return } - self.setPlaceholder(self.placeholder, foregroundColor: placeholderColor, font: self.viewModel.font) - } - - self.viewModel.$borderWidth.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] borderWidth in - guard let self else { return } - self.setBorderWidth(borderWidth * self.scaleFactor) - } - - self.viewModel.$borderRadius.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] borderRadius in - guard let self else { return } - self.setCornerRadius(borderRadius) - } - - self.viewModel.$leftSpacing.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] dim in - guard let self else { return } - self.setNeedsLayout() - } - - self.viewModel.$rightSpacing.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] dim in - guard let self else { return } - self.setNeedsLayout() - } - - self.viewModel.$contentSpacing.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] dim in - guard let self else { return } - self.setNeedsLayout() - } - - self.viewModel.$dim.removeDuplicates().subscribe(in: &self.cancellables) { [weak self] dim in - guard let self else { return } - self.alpha = dim - } - - self.viewModel.$font.subscribe(in: &self.cancellables) { [weak self] font in - guard let self else { return } - self.font = font.uiFont - self.setPlaceholder(self.placeholder, foregroundColor: self.viewModel.placeholderColor, font: font) - } - } - - private func setAttributedPlaceholder(string: String, foregroundColor: UIColor, font: UIFont) { - self.attributedPlaceholder = NSAttributedString( - string: string, - attributes: [ - NSAttributedString.Key.foregroundColor: foregroundColor, - NSAttributedString.Key.font: font - ] - ) - } - - private func setPlaceholder(_ placeholder: String?, foregroundColor: any ColorToken, font: TypographyFontToken) { - if let placeholder { - self.setAttributedPlaceholder(string: placeholder, foregroundColor: foregroundColor.uiColor, font: font.uiFont) - } else { - self.attributedPlaceholder = nil - } - } - - public override func becomeFirstResponder() -> Bool { - let bool = super.becomeFirstResponder() - self.viewModel.isFocused = bool - return bool - } - - public override func resignFirstResponder() -> Bool { - super.resignFirstResponder() - self.viewModel.isFocused = false - return true - } - - // MARK: - Rects - private func setInsets(forBounds bounds: CGRect) -> CGRect { - var totalInsets = UIEdgeInsets( - top: 0, - left: self.viewModel.leftSpacing, - bottom: 0, - right: self.viewModel.rightSpacing - ) - let contentSpacing = self.viewModel.contentSpacing - if let leftView, - leftView.isDescendant(of: self) { - totalInsets.left += leftView.bounds.size.width + contentSpacing - } - if let rightView, - rightView.isDescendant(of: self) { - totalInsets.right += rightView.bounds.size.width + contentSpacing - } - if let clearButton = self.value(forKeyPath: "_clearButton") as? UIButton, - clearButton.isDescendant(of: self) { - totalInsets.right += clearButton.bounds.size.width + contentSpacing - } - return bounds.inset(by: totalInsets) - } - - public override func textRect(forBounds bounds: CGRect) -> CGRect { - return self.setInsets(forBounds: bounds) - } - - public override func placeholderRect(forBounds bounds: CGRect) -> CGRect { - return self.setInsets(forBounds: bounds) - } - public override func editingRect(forBounds bounds: CGRect) -> CGRect { - return self.setInsets(forBounds: bounds) - } - - private func getClearButtonXOffset() -> CGFloat { - return -self.viewModel.rightSpacing + self.defaultClearButtonRightSpacing - } - - public override func clearButtonRect(forBounds bounds: CGRect) -> CGRect { - return super.clearButtonRect(forBounds: bounds) - .offsetBy(dx: self.getClearButtonXOffset(), dy: 0) - } - - public override func leftViewRect(forBounds bounds: CGRect) -> CGRect { - return super.leftViewRect(forBounds: bounds) - .offsetBy(dx: self.viewModel.leftSpacing, dy: 0) - } - - public override func rightViewRect(forBounds bounds: CGRect) -> CGRect { - return super.rightViewRect(forBounds: bounds) - .offsetBy(dx: -self.viewModel.rightSpacing, dy: 0) - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.setBorderColor(from: self.viewModel.borderColor) - } - - guard previousTraitCollection?.preferredContentSizeCategory != self.traitCollection.preferredContentSizeCategory else { return } - - self._height.update(traitCollection: self.traitCollection) - self._scaleFactor.update(traitCollection: self.traitCollection) - self.setBorderWidth(self.viewModel.borderWidth * self.scaleFactor) - self.invalidateIntrinsicContentSize() - } - - public override var intrinsicContentSize: CGSize { - return .init( - width: super.intrinsicContentSize.width, - height: self.height - ) - } -} diff --git a/core/Sources/Components/TextField/View/UIKit/TextFieldUIViewSnapshotTests.swift b/core/Sources/Components/TextField/View/UIKit/TextFieldUIViewSnapshotTests.swift deleted file mode 100644 index 309fbb2a3..000000000 --- a/core/Sources/Components/TextField/View/UIKit/TextFieldUIViewSnapshotTests.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// TextFieldUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by louis.borlee on 12/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import UIKit -@testable import SparkCore - -final class TextFieldUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - private let theme = SparkTheme.shared - - private func _test(scenario: TextFieldScenario) { - let configurations = self.createConfigurations(from: scenario) - for configuration in configurations { - self.assertSnapshot(matching: configuration.view, modes: scenario.modes, sizes: scenario.sizes, testName: configuration.testName) - } - } - - func test1() { - self._test(scenario: TextFieldScenario.test1) - } - - func test2() { - self._test(scenario: TextFieldScenario.test2) - } - - func test3() { - self._test(scenario: TextFieldScenario.test3) - } - - func test4() { - self._test(scenario: TextFieldScenario.test4) - } - - func test5() { - self._test(scenario: TextFieldScenario.test5) - } - - private func createConfigurations(from scenario: TextFieldScenario) -> [(testName: String, view: UIView)] { - var configurations: [(testName: String, view: UIView)] = [] - for intent in scenario.intents { - for states in scenario.statesArray { - for text in scenario.texts { - for leftContent in scenario.leftContents { - for rightContent in scenario.rightContents { - let viewModel = TextFieldViewModel( - theme: self.theme, - intent: intent, - borderStyle: .roundedRect - ) - viewModel.isEnabled = states.isEnabled - viewModel.isUserInteractionEnabled = states.isReadOnly != true - viewModel.isFocused = states.isFocused - let textField = TextFieldUIView(viewModel: viewModel) - textField.text = text.text - textField.placeholder = text.placeholder - textField.clearButtonMode = states.isFocused ? .always : .never - textField.leftViewMode = .always - textField.rightViewMode = .always - textField.leftView = self.getContentViews(from: leftContent) - textField.rightView = self.getContentViews(from: rightContent) - - let backgroundView = UIView() - backgroundView.backgroundColor = .systemBackground - backgroundView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - backgroundView.widthAnchor.constraint(equalToConstant: 300) - ]) - - backgroundView.addSubview(textField) - textField.translatesAutoresizingMaskIntoConstraints = false - textField.invalidateIntrinsicContentSize() - textField.setNeedsLayout() - textField.layoutIfNeeded() - NSLayoutConstraint.stickEdges(from: textField, to: backgroundView, insets: .init(all: 12)) - - let testName = scenario.getTestName( - intent: intent, - states: states, - text: text, - leftContent: - leftContent, - rightContent: rightContent) - configurations.append((testName: testName, view: backgroundView)) - } - } - } - } - } - return configurations - } - - private func getContentViews(from optionSet: TextFieldScenarioSideContentOptionSet) -> UIView? { - var contentViews: [UIView] = [] - if optionSet.contains(.button) { - contentViews.append(self.createButton()) - } - if optionSet.contains(.image) { - contentViews.append(self.createImage()) - } - guard contentViews.isEmpty == false else { return nil } - if contentViews.count == 1 { - return contentViews.first - } - let stackView = UIStackView(arrangedSubviews: contentViews) - stackView.axis = .horizontal - stackView.spacing = 4 - return stackView - } - - private func createButton() -> UIButton { - let button = UIButton(configuration: .filled()) - button.setTitle("Button", for: .normal) - return button - } - - private func createImage() -> UIImageView { - let imageView = UIImageView(image: .init(systemName: "eject.circle.fill")) - imageView.adjustsImageSizeForAccessibilityContentSizeCategory = true - imageView.contentMode = .scaleAspectFit - return imageView - } -} diff --git a/core/Sources/Components/TextField/ViewModel/TextFieldViewModel.swift b/core/Sources/Components/TextField/ViewModel/TextFieldViewModel.swift deleted file mode 100644 index d2a6dff95..000000000 --- a/core/Sources/Components/TextField/ViewModel/TextFieldViewModel.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// TextFieldViewModel.swift -// Spark -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI -import Combine - -class TextFieldViewModel: ObservableObject { - - // Colors - @Published private(set) var textColor: any ColorToken - @Published private(set) var placeholderColor: any ColorToken - @Published var borderColor: any ColorToken - @Published var backgroundColor: any ColorToken - - // BorderLayout - @Published private(set) var borderRadius: CGFloat - @Published private(set) var borderWidth: CGFloat - - // Spacings - @Published private(set) var leftSpacing: CGFloat - @Published private(set) var contentSpacing: CGFloat - @Published private(set) var rightSpacing: CGFloat - - @Published var dim: CGFloat - - @Published private(set) var font: any TypographyFontToken - - let getColorsUseCase: any TextFieldGetColorsUseCasable - let getBorderLayoutUseCase: any TextFieldGetBorderLayoutUseCasable - let getSpacingsUseCase: any TextFieldGetSpacingsUseCasable - - var theme: Theme { - didSet { - self.setColors() - self.setBorderLayout() - self.setSpacings() - self.setDim() - self.setFont() - } - } - var intent: TextFieldIntent { - didSet { - guard oldValue != self.intent else { return } - self.setColors() - } - } - var borderStyle: TextFieldBorderStyle { - didSet { - guard oldValue != self.borderStyle else { return } - self.setBorderLayout() - self.setSpacings() - } - } - - var isFocused: Bool = false { - didSet { - guard oldValue != self.isFocused else { return } - self.setColors() - self.setBorderLayout() - } - } - - var isEnabled: Bool = true { - didSet { - guard oldValue != self.isEnabled else { return } - self.setColors() - self.setDim() - } - } - - var isUserInteractionEnabled: Bool = true { - didSet { - guard oldValue != self.isUserInteractionEnabled else { return } - self.setColors() - } - } - - init(theme: Theme, - intent: TextFieldIntent, - borderStyle: TextFieldBorderStyle, - getColorsUseCase: any TextFieldGetColorsUseCasable = TextFieldGetColorsUseCase(), - getBorderLayoutUseCase: any TextFieldGetBorderLayoutUseCasable = TextFieldGetBorderLayoutUseCase(), - getSpacingsUseCase: any TextFieldGetSpacingsUseCasable = TextFieldGetSpacingsUseCase()) { - self.theme = theme - self.intent = intent - self.borderStyle = borderStyle - - self.getColorsUseCase = getColorsUseCase - self.getBorderLayoutUseCase = getBorderLayoutUseCase - self.getSpacingsUseCase = getSpacingsUseCase - - // Colors - let colors = getColorsUseCase.execute( - theme: theme, - intent: intent, - isFocused: self.isFocused, - isEnabled: self.isEnabled, - isUserInteractionEnabled: self.isUserInteractionEnabled - ) - self.textColor = colors.text - self.placeholderColor = colors.placeholder - self.borderColor = colors.border - self.backgroundColor = colors.background - - // BorderLayout - let borderLayout = getBorderLayoutUseCase.execute( - theme: theme, - borderStyle: - borderStyle, - isFocused: self.isFocused) - self.borderWidth = borderLayout.width - self.borderRadius = borderLayout.radius - - // Spacings - let spacings = getSpacingsUseCase.execute(theme: theme, borderStyle: borderStyle) - self.leftSpacing = spacings.left - self.contentSpacing = spacings.content - self.rightSpacing = spacings.right - - self.dim = theme.dims.none - - self.font = theme.typography.body1 - } - - func setColors() { - // Colors - let colors = self.getColorsUseCase.execute( - theme: self.theme, - intent: self.intent, - isFocused: self.isFocused, - isEnabled: self.isEnabled, - isUserInteractionEnabled: self.isUserInteractionEnabled - ) - self.textColor = colors.text - self.placeholderColor = colors.placeholder - self.borderColor = colors.border - self.backgroundColor = colors.background - } - - func setBorderLayout() { - let borderLayout = self.getBorderLayoutUseCase.execute( - theme: self.theme, - borderStyle: self.borderStyle, // .none - isFocused: self.isFocused - ) - self.borderWidth = borderLayout.width - self.borderRadius = borderLayout.radius - } - - func setSpacings() { - let spacings = self.getSpacingsUseCase.execute(theme: self.theme, borderStyle: self.borderStyle) - self.leftSpacing = spacings.left - self.contentSpacing = spacings.content - self.rightSpacing = spacings.right - } - - func setDim() { - self.dim = self.isEnabled ? self.theme.dims.none : self.theme.dims.dim3 - } - - private func setFont() { - self.font = self.theme.typography.body1 - } -} diff --git a/core/Sources/Components/TextField/ViewModel/TextFieldViewModelTests.swift b/core/Sources/Components/TextField/ViewModel/TextFieldViewModelTests.swift deleted file mode 100644 index 9efd062e3..000000000 --- a/core/Sources/Components/TextField/ViewModel/TextFieldViewModelTests.swift +++ /dev/null @@ -1,717 +0,0 @@ -// -// TextFieldViewModelTests.swift -// SparkCoreUnitTests -// -// Created by louis.borlee on 01/02/2024. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import XCTest -import Combine -import UIKit -import SwiftUI -@testable import SparkCore - -final class TextFieldViewModelTests: XCTestCase { - - var theme: ThemeGeneratedMock! - var publishers: TextFieldPublishers! - var getColorsUseCase: TextFieldGetColorsUseCasableGeneratedMock! - var getBorderLayoutUseCase: TextFieldGetBorderLayoutUseCasableGeneratedMock! - var getSpacingsUseCase: TextFieldGetSpacingsUseCasableGeneratedMock! - private var viewModel: TextFieldViewModel! - - let intent = TextFieldIntent.success - let borderStyle = TextFieldBorderStyle.roundedRect - - var expectedColors: TextFieldColors! - var expectedBorderLayout: TextFieldBorderLayout! - var expectedSpacings: TextFieldSpacings! - - override func setUp() { - super.setUp() - self.theme = ThemeGeneratedMock.mocked() - - self.expectedColors = .mocked( - text: .blue(), - placeholder: .green(), - border: .yellow(), - background: .purple() - ) - self.expectedBorderLayout = .mocked(radius: 1, width: 2) - self.expectedSpacings = .mocked(left: 1, content: 2, right: 3) - - self.getColorsUseCase = .mocked(returnedColors: self.expectedColors) - self.getBorderLayoutUseCase = .mocked(returnedBorderLayout: self.expectedBorderLayout) - self.getSpacingsUseCase = .mocked(returnedSpacings: self.expectedSpacings) - self.viewModel = .init( - theme: self.theme, - intent: self.intent, - borderStyle: self.borderStyle, - getColorsUseCase: self.getColorsUseCase, - getBorderLayoutUseCase: self.getBorderLayoutUseCase, - getSpacingsUseCase: self.getSpacingsUseCase - ) - - self.setupPublishers() - } - - // MARK: - init - func test_init() throws { - // GIVEN / WHEN - Inits from setUp() - // THEN - Simple variables - XCTAssertIdentical(self.viewModel.theme as? ThemeGeneratedMock, self.theme, "Wrong theme") - XCTAssertEqual(self.viewModel.intent, self.intent, "Wrong intent") - XCTAssertEqual(self.viewModel.borderStyle, self.borderStyle, "Wrong borderStyle") - XCTAssertTrue(self.viewModel.isEnabled, "Wrong isEnabled") - XCTAssertTrue(self.viewModel.isUserInteractionEnabled, "Wrong isUserInteractionEnabled") - XCTAssertFalse(self.viewModel.isFocused, "Wrong isFocused") - XCTAssertEqual(self.viewModel.dim, self.theme.dims.none, "Wrong dim") - XCTAssertIdentical(self.viewModel.font as? TypographyFontTokenGeneratedMock, self.theme.typography.body1 as? TypographyFontTokenGeneratedMock, "Wrong font") - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertIdentical(getColorsReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getColorsReceivedArguments.theme") - XCTAssertEqual(getColorsReceivedArguments.intent, self.intent, "Wrong getColorsReceivedArguments.intent") - XCTAssertFalse(getColorsReceivedArguments.isFocused, "Wrong getColorsReceivedArguments.isFocused") - XCTAssertTrue(getColorsReceivedArguments.isEnabled, "Wrong getColorsReceivedArguments.isEnabled") - XCTAssertTrue(getColorsReceivedArguments.isUserInteractionEnabled, "Wrong getColorsReceivedArguments.isUserInteractionEnabled") - XCTAssertTrue(self.viewModel.textColor.equals(self.expectedColors.text), "Wrong textColor") - XCTAssertTrue(self.viewModel.placeholderColor.equals(self.expectedColors.placeholder), "Wrong placeholderColor") - XCTAssertTrue(self.viewModel.borderColor.equals(self.expectedColors.border), "Wrong borderColor") - XCTAssertTrue(self.viewModel.backgroundColor.equals(self.expectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertEqual(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCallsCount, 1, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should have been called once") - let getBorderLayoutReceivedArguments = try XCTUnwrap(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReceivedArguments, "Couldn't unwrap getBorderLayoutReceivedArguments") - XCTAssertIdentical(getBorderLayoutReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getBorderLayoutReceivedArguments.theme") - XCTAssertEqual(getBorderLayoutReceivedArguments.borderStyle, .roundedRect, "Wrong getBorderLayoutReceivedArguments.borderStyle") - XCTAssertFalse(getBorderLayoutReceivedArguments.isFocused, "Wrong getBorderLayoutReceivedArguments.isFocused") - XCTAssertEqual(self.viewModel.borderWidth, self.expectedBorderLayout.width, "Wrong borderWidth") - XCTAssertEqual(self.viewModel.borderRadius, self.expectedBorderLayout.radius, "Wrong borderRadius") - - // THEN - Spacings - XCTAssertEqual(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCallsCount, 1, "getSpacingsUseCase.executeWithThemeAndBorderStyle should have been called once") - let getSpacingsUseCaseReceivedArguments = try XCTUnwrap(self.getSpacingsUseCase.executeWithThemeAndBorderStyleReceivedArguments, "Couldn't unwrap getSpacingsUseCaseReceivedArguments") - XCTAssertIdentical(getSpacingsUseCaseReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getSpacingsUseCaseReceivedArguments.theme") - XCTAssertEqual(getSpacingsUseCaseReceivedArguments.borderStyle, .roundedRect, "Wrong getSpacingsUseCaseReceivedArguments.borderStyle") - XCTAssertEqual(self.viewModel.leftSpacing, self.expectedSpacings.left, "Wrong leftSpacing") - XCTAssertEqual(self.viewModel.contentSpacing, self.expectedSpacings.content, "Wrong contentSpacing") - XCTAssertEqual(self.viewModel.rightSpacing, self.expectedSpacings.right, "Wrong rightSpacing") - - // THEN - Publishers - XCTAssertEqual(self.publishers.textColor.sinkCount, 1, "$textColor should have been called once") - XCTAssertEqual(self.publishers.borderColor.sinkCount, 1, "$borderColorIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - XCTAssertEqual(self.publishers.placeholderColor.sinkCount, 1, "$placeholderColor should have been called once") - - XCTAssertEqual(self.publishers.borderWidth.sinkCount, 1, "$borderWidth should have been called once") - XCTAssertEqual(self.publishers.borderRadius.sinkCount, 1, "$borderRadius should have been called once") - - XCTAssertEqual(self.publishers.leftSpacing.sinkCount, 1, "$leftSpacing should have been called once") - XCTAssertEqual(self.publishers.contentSpacing.sinkCount, 1, "$contentSpacing should have been called once") - XCTAssertEqual(self.publishers.rightSpacing.sinkCount, 1, "$rightSpacing should have been called once") - - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should have been called once") - XCTAssertEqual(self.publishers.font.sinkCount, 1, "$font should have been called once") - } - - // MARK: Theme - func test_theme_didSet() throws { - // GIVEN - Inits from setUp() - let newTheme = ThemeGeneratedMock() - newTheme.typography = TypographyGeneratedMock.mocked() - newTheme.dims = DimsGeneratedMock.mocked() - - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - let newExpectedColors = TextFieldColors.mocked( - text: .red(), - placeholder: .blue(), - border: .green(), - background: .red() - ) - self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReturnValue = newExpectedColors - - let newExpectedBorderLayout = TextFieldBorderLayout.mocked(radius: 20.0, width: 100.0) - self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReturnValue = newExpectedBorderLayout - - let newExpectedSpacings = TextFieldSpacings.mocked(left: 10, content: 20, right: 30) - self.getSpacingsUseCase.executeWithThemeAndBorderStyleReturnValue = newExpectedSpacings - - // WHEN - self.viewModel.theme = newTheme - - // THEN - Theme - XCTAssertIdentical(self.viewModel.theme as? ThemeGeneratedMock, newTheme, "Wrong theme") - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertIdentical(getColorsReceivedArguments.theme as? ThemeGeneratedMock, newTheme, "Wrong getColorsReceivedArguments.theme") - XCTAssertTrue(self.viewModel.textColor.equals(newExpectedColors.text), "Wrong textColor") - XCTAssertTrue(self.viewModel.placeholderColor.equals(newExpectedColors.placeholder), "Wrong placeholderColor") - XCTAssertTrue(self.viewModel.borderColor.equals(newExpectedColors.border), "Wrong borderColor") - XCTAssertTrue(self.viewModel.backgroundColor.equals(newExpectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertEqual(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCallsCount, 1, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should have been called once") - let getBorderLayoutReceivedArguments = try XCTUnwrap(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReceivedArguments, "Couldn't unwrap getBorderLayoutReceivedArguments") - XCTAssertIdentical(getBorderLayoutReceivedArguments.theme as? ThemeGeneratedMock, newTheme, "Wrong getBorderLayoutReceivedArguments.theme") - XCTAssertEqual(self.viewModel.borderWidth, newExpectedBorderLayout.width, "Wrong borderWidth") - XCTAssertEqual(self.viewModel.borderRadius, newExpectedBorderLayout.radius, "Wrong borderRadius") - - // THEN - Spacings - XCTAssertEqual(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCallsCount, 1, "getSpacingsUseCase.executeWithThemeAndBorderStyle should have been called once") - let getSpacingsUseCaseReceivedArguments = try XCTUnwrap(self.getSpacingsUseCase.executeWithThemeAndBorderStyleReceivedArguments, "Couldn't unwrap getSpacingsUseCaseReceivedArguments") - XCTAssertIdentical(getSpacingsUseCaseReceivedArguments.theme as? ThemeGeneratedMock, newTheme, "Wrong getSpacingsUseCaseReceivedArguments.theme") - XCTAssertEqual(self.viewModel.leftSpacing, newExpectedSpacings.left, "Wrong leftSpacing") - XCTAssertEqual(self.viewModel.contentSpacing, newExpectedSpacings.content, "Wrong contentSpacing") - XCTAssertEqual(self.viewModel.rightSpacing, newExpectedSpacings.right, "Wrong rightSpacing") - - // THEN - Publishers - XCTAssertEqual(self.publishers.textColor.sinkCount, 1, "$textColor should have been called once") - XCTAssertEqual(self.publishers.borderColor.sinkCount, 1, "$borderColorIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - XCTAssertEqual(self.publishers.placeholderColor.sinkCount, 1, "$placeholderColor should have been called once") - - XCTAssertEqual(self.publishers.borderWidth.sinkCount, 1, "$borderWidth should have been called once") - XCTAssertEqual(self.publishers.borderRadius.sinkCount, 1, "$borderRadius should have been called once") - - XCTAssertEqual(self.publishers.leftSpacing.sinkCount, 1, "$leftSpacing should have been called once") - XCTAssertEqual(self.publishers.contentSpacing.sinkCount, 1, "$contentSpacing should have been called once") - XCTAssertEqual(self.publishers.rightSpacing.sinkCount, 1, "$rightSpacing should have been called once") - - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should have been called once") - XCTAssertEqual(self.publishers.font.sinkCount, 1, "$font should have been called once") - } - - // MARK: - Intent - func test_intent_didSet_equal() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.intent = self.intent - - // THEN - Colors - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCalled, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should not have been called") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.textColor.sinkCalled, "$textColor should not have been called") - XCTAssertFalse(self.publishers.borderColor.sinkCalled, "$borderColorIndicatorColor should not have been called") - XCTAssertFalse(self.publishers.backgroundColor.sinkCalled, "$backgroundColor should not have been called") - XCTAssertFalse(self.publishers.placeholderColor.sinkCalled, "$placeholderColor should not have been called") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - func test_intent_didSet_notEqual() throws { - // GIVEN - Inits from setUp() - self.viewModel.intent = .alert - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - let newExpectedColors = TextFieldColors.mocked( - text: .red(), - placeholder: .blue(), - border: .green(), - background: .red() - ) - self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReturnValue = newExpectedColors - - // WHEN - self.viewModel.intent = .neutral - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertEqual(getColorsReceivedArguments.intent, .neutral, "Wrong getColorsReceivedArguments.intent") - XCTAssertTrue(self.viewModel.textColor.equals(newExpectedColors.text), "Wrong textColor") - XCTAssertTrue(self.viewModel.placeholderColor.equals(newExpectedColors.placeholder), "Wrong placeholderColor") - XCTAssertTrue(self.viewModel.borderColor.equals(newExpectedColors.border), "Wrong borderColor") - XCTAssertTrue(self.viewModel.backgroundColor.equals(newExpectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertEqual(self.publishers.textColor.sinkCount, 1, "$textColor should have been called once") - XCTAssertEqual(self.publishers.borderColor.sinkCount, 1, "$borderColorIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - XCTAssertEqual(self.publishers.placeholderColor.sinkCount, 1, "$placeholderColor should have been called once") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - // MARK: - Border Style - func test_borderStyle_didSet_equal() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.borderStyle = self.borderStyle - - // THEN - Colors - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCalled, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should not have been called") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.textColor.sinkCalled, "$textColor should not have been called") - XCTAssertFalse(self.publishers.borderColor.sinkCalled, "$borderColorIndicatorColor should not have been called") - XCTAssertFalse(self.publishers.backgroundColor.sinkCalled, "$backgroundColor should not have been called") - XCTAssertFalse(self.publishers.placeholderColor.sinkCalled, "$placeholderColor should not have been called") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - func test_borderStyle_didSet_notEqual() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - let newExpectedBorderLayout = TextFieldBorderLayout.mocked(radius: 20.0, width: 100.0) - self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReturnValue = newExpectedBorderLayout - - let newExpectedSpacings = TextFieldSpacings.mocked(left: 10, content: 20, right: 30) - self.getSpacingsUseCase.executeWithThemeAndBorderStyleReturnValue = newExpectedSpacings - - // WHEN - self.viewModel.borderStyle = .none - - // THEN - Colors - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCalled, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should not have been called") - - // THEN - Border Layout - XCTAssertEqual(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCallsCount, 1, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should have been called once") - let getBorderLayoutReceivedArguments = try XCTUnwrap(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReceivedArguments, "Couldn't unwrap getBorderLayoutReceivedArguments") - XCTAssertEqual(getBorderLayoutReceivedArguments.borderStyle, .none, "Wrong getBorderLayoutReceivedArguments.borderStyle") - XCTAssertEqual(self.viewModel.borderWidth, newExpectedBorderLayout.width, "Wrong borderWidth") - XCTAssertEqual(self.viewModel.borderRadius, newExpectedBorderLayout.radius, "Wrong borderRadius") - - // THEN - Spacings - XCTAssertEqual(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCallsCount, 1, "getSpacingsUseCase.executeWithThemeAndBorderStyle should have been called once") - let getSpacingsUseCaseReceivedArguments = try XCTUnwrap(self.getSpacingsUseCase.executeWithThemeAndBorderStyleReceivedArguments, "Couldn't unwrap getSpacingsUseCaseReceivedArguments") - XCTAssertEqual(getSpacingsUseCaseReceivedArguments.borderStyle, .none, "Wrong getSpacingsUseCaseReceivedArguments.borderStyle") - XCTAssertEqual(self.viewModel.leftSpacing, newExpectedSpacings.left, "Wrong leftSpacing") - XCTAssertEqual(self.viewModel.contentSpacing, newExpectedSpacings.content, "Wrong contentSpacing") - XCTAssertEqual(self.viewModel.rightSpacing, newExpectedSpacings.right, "Wrong rightSpacing") - - // THEN - Publishers - XCTAssertFalse(self.publishers.textColor.sinkCalled, "$textColor should not have been called") - XCTAssertFalse(self.publishers.borderColor.sinkCalled, "$borderColorIndicatorColor should not have been called") - XCTAssertFalse(self.publishers.backgroundColor.sinkCalled, "$backgroundColor should not have been called") - XCTAssertFalse(self.publishers.placeholderColor.sinkCalled, "$placeholderColor should not have been called") - - XCTAssertEqual(self.publishers.borderWidth.sinkCount, 1, "$borderWidth should have been called once") - XCTAssertEqual(self.publishers.borderRadius.sinkCount, 1, "$borderRadius should have been called once") - - XCTAssertEqual(self.publishers.leftSpacing.sinkCount, 1, "$leftSpacing should have been called once") - XCTAssertEqual(self.publishers.contentSpacing.sinkCount, 1, "$contentSpacing should have been called once") - XCTAssertEqual(self.publishers.rightSpacing.sinkCount, 1, "$rightSpacing should have been called once") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - // MARK: - Is Focused - func test_isFocused_didSet_equal() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.isFocused = false - - // THEN - Colors - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCalled, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should not have been called") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.textColor.sinkCalled, "$textColor should not have been called") - XCTAssertFalse(self.publishers.borderColor.sinkCalled, "$borderColorIndicatorColor should not have been called") - XCTAssertFalse(self.publishers.backgroundColor.sinkCalled, "$backgroundColor should not have been called") - XCTAssertFalse(self.publishers.placeholderColor.sinkCalled, "$placeholderColor should not have been called") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - func test_isFocused_didSet_notEqual() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - let newExpectedColors = TextFieldColors.mocked( - text: .red(), - placeholder: .blue(), - border: .green(), - background: .red() - ) - self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReturnValue = newExpectedColors - - let newExpectedBorderLayout = TextFieldBorderLayout.mocked(radius: 20.0, width: 100.0) - self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReturnValue = newExpectedBorderLayout - - // WHEN - self.viewModel.isFocused = true - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertIdentical(getColorsReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getColorsReceivedArguments.theme") - XCTAssertEqual(getColorsReceivedArguments.intent, self.intent, "Wrong getColorsReceivedArguments.intent") - XCTAssertTrue(getColorsReceivedArguments.isFocused, "Wrong getColorsReceivedArguments.isFocused") - XCTAssertTrue(self.viewModel.textColor.equals(newExpectedColors.text), "Wrong textColor") - XCTAssertTrue(self.viewModel.placeholderColor.equals(newExpectedColors.placeholder), "Wrong placeholderColor") - XCTAssertTrue(self.viewModel.borderColor.equals(newExpectedColors.border), "Wrong borderColor") - XCTAssertTrue(self.viewModel.backgroundColor.equals(newExpectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertEqual(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCallsCount, 1, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should have been called once") - let getBorderLayoutReceivedArguments = try XCTUnwrap(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedReceivedArguments, "Couldn't unwrap getBorderLayoutReceivedArguments") - XCTAssertTrue(getBorderLayoutReceivedArguments.isFocused, "Wrong getBorderLayoutReceivedArguments.isFocused") - XCTAssertEqual(self.viewModel.borderWidth, newExpectedBorderLayout.width, "Wrong borderWidth") - XCTAssertEqual(self.viewModel.borderRadius, newExpectedBorderLayout.radius, "Wrong borderRadius") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertEqual(self.publishers.textColor.sinkCount, 1, "$textColor should have been called once") - XCTAssertEqual(self.publishers.borderColor.sinkCount, 1, "$borderColorIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - XCTAssertEqual(self.publishers.placeholderColor.sinkCount, 1, "$placeholderColor should have been called once") - - XCTAssertEqual(self.publishers.borderWidth.sinkCount, 1, "$borderWidth should have been called once") - XCTAssertEqual(self.publishers.borderRadius.sinkCount, 1, "$borderRadius should have been called once") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should have not been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should have not been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should have not been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - // MARK: - Is Enabled - func test_isEnabled_didSet_equal() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.isEnabled = true - - // THEN - Colors - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCalled, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should not have been called") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.textColor.sinkCalled, "$textColor should not have been called") - XCTAssertFalse(self.publishers.borderColor.sinkCalled, "$borderColorIndicatorColor should not have been called") - XCTAssertFalse(self.publishers.backgroundColor.sinkCalled, "$backgroundColor should not have been called") - XCTAssertFalse(self.publishers.placeholderColor.sinkCalled, "$placeholderColor should not have been called") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - func test_isEnabled_didSet_notEqual() throws { - // GIVEN - Inits from setUp() - self.viewModel.isEnabled = false - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - let newExpectedColors = TextFieldColors.mocked( - text: .red(), - placeholder: .blue(), - border: .green(), - background: .red() - ) - self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReturnValue = newExpectedColors - - // WHEN - self.viewModel.isEnabled = true - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertIdentical(getColorsReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getColorsReceivedArguments.theme") - XCTAssertEqual(getColorsReceivedArguments.intent, self.intent, "Wrong getColorsReceivedArguments.intent") - XCTAssertTrue(getColorsReceivedArguments.isEnabled, "Wrong getColorsReceivedArguments.isEnabledd") - XCTAssertTrue(self.viewModel.textColor.equals(newExpectedColors.text), "Wrong textColor") - XCTAssertTrue(self.viewModel.placeholderColor.equals(newExpectedColors.placeholder), "Wrong placeholderColor") - XCTAssertTrue(self.viewModel.borderColor.equals(newExpectedColors.border), "Wrong borderColor") - XCTAssertTrue(self.viewModel.backgroundColor.equals(newExpectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertEqual(self.publishers.textColor.sinkCount, 1, "$textColor should have been called once") - XCTAssertEqual(self.publishers.borderColor.sinkCount, 1, "$borderColorIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - XCTAssertEqual(self.publishers.placeholderColor.sinkCount, 1, "$placeholderColor should have been called once") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should have not been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should have not been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should have not been called") - - XCTAssertEqual(self.publishers.dim.sinkCount, 1, "$dim should have been called once") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - // MARK: - Is User Interaction Enabled - func test_isUserInteractionEnabled_didSet_equal() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - // WHEN - self.viewModel.isUserInteractionEnabled = true - - // THEN - Colors - XCTAssertFalse(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCalled, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should not have been called") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertFalse(self.publishers.textColor.sinkCalled, "$textColor should not have been called") - XCTAssertFalse(self.publishers.borderColor.sinkCalled, "$borderColorIndicatorColor should not have been called") - XCTAssertFalse(self.publishers.backgroundColor.sinkCalled, "$backgroundColor should not have been called") - XCTAssertFalse(self.publishers.placeholderColor.sinkCalled, "$placeholderColor should not have been called") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should not have been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should not have been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should not have been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - func test_isUserInteractionEnabled_didSet_notEqual() throws { - // GIVEN - Inits from setUp() - self.resetUseCases() // Removes execute from init - self.publishers.reset() // Removes publishes from init - - let newExpectedColors = TextFieldColors.mocked( - text: .red(), - placeholder: .blue(), - border: .green(), - background: .red() - ) - self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReturnValue = newExpectedColors - - // WHEN - self.viewModel.isUserInteractionEnabled = false - - // THEN - Colors - XCTAssertEqual(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledCallsCount, 1, "getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabled should have been called once") - let getColorsReceivedArguments = try XCTUnwrap(self.getColorsUseCase.executeWithThemeAndIntentAndIsFocusedAndIsEnabledAndIsUserInteractionEnabledReceivedArguments, "Couldn't unwrap getColorsReceivedArguments") - XCTAssertIdentical(getColorsReceivedArguments.theme as? ThemeGeneratedMock, self.theme, "Wrong getColorsReceivedArguments.theme") - XCTAssertEqual(getColorsReceivedArguments.intent, self.intent, "Wrong getColorsReceivedArguments.intent") - XCTAssertFalse(getColorsReceivedArguments.isUserInteractionEnabled, "Wrong getColorsReceivedArguments.isUserInteractionEnabled") - XCTAssertTrue(self.viewModel.textColor.equals(newExpectedColors.text), "Wrong textColor") - XCTAssertTrue(self.viewModel.placeholderColor.equals(newExpectedColors.placeholder), "Wrong placeholderColor") - XCTAssertTrue(self.viewModel.borderColor.equals(newExpectedColors.border), "Wrong borderColor") - XCTAssertTrue(self.viewModel.backgroundColor.equals(newExpectedColors.background), "Wrong backgroundColor") - - // THEN - Border Layout - XCTAssertFalse(self.getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocusedCalled, "getBorderLayoutUseCase.executeWithThemeAndBorderStyleAndIsFocused should not have been called") - - // THEN - Spacings - XCTAssertFalse(self.getSpacingsUseCase.executeWithThemeAndBorderStyleCalled, "getSpacingsUseCase.executeWithThemeAndBorderStyle should not have been called") - - // THEN - Publishers - XCTAssertEqual(self.publishers.textColor.sinkCount, 1, "$textColor should have been called once") - XCTAssertEqual(self.publishers.borderColor.sinkCount, 1, "$borderColorIndicatorColor should have been called once") - XCTAssertEqual(self.publishers.backgroundColor.sinkCount, 1, "$backgroundColor should have been called once") - XCTAssertEqual(self.publishers.placeholderColor.sinkCount, 1, "$placeholderColor should have been called once") - - XCTAssertFalse(self.publishers.borderWidth.sinkCalled, "$borderWidth should not have been called") - XCTAssertFalse(self.publishers.borderRadius.sinkCalled, "$borderRadius should not have been called") - - XCTAssertFalse(self.publishers.leftSpacing.sinkCalled, "$leftSpacing should have not been called") - XCTAssertFalse(self.publishers.contentSpacing.sinkCalled, "$contentSpacing should have not been called") - XCTAssertFalse(self.publishers.rightSpacing.sinkCalled, "$rightSpacing should have not been called") - - XCTAssertFalse(self.publishers.dim.sinkCalled, "$dim should not have been called") - XCTAssertFalse(self.publishers.font.sinkCalled, "$font should not have been called") - } - - // MARK: - Utils - func setupPublishers() { - self.publishers = .init( - textColor: PublisherMock(publisher: self.viewModel.$textColor), - placeholderColor: PublisherMock(publisher: self.viewModel.$placeholderColor), - borderColor: PublisherMock(publisher: self.viewModel.$borderColor), - backgroundColor: PublisherMock(publisher: self.viewModel.$backgroundColor), - borderRadius: PublisherMock(publisher: self.viewModel.$borderRadius), - borderWidth: PublisherMock(publisher: self.viewModel.$borderWidth), - leftSpacing: PublisherMock(publisher: self.viewModel.$leftSpacing), - contentSpacing: PublisherMock(publisher: self.viewModel.$contentSpacing), - rightSpacing: PublisherMock(publisher: self.viewModel.$rightSpacing), - dim: PublisherMock(publisher: self.viewModel.$dim), - font: PublisherMock(publisher: self.viewModel.$font) - ) - self.publishers.load() - } - - func resetUseCases() { - self.getColorsUseCase.reset() - self.getBorderLayoutUseCase.reset() - self.getSpacingsUseCase.reset() - } -} - -final class TextFieldPublishers { - var cancellables = Set() - - var textColor: PublisherMock.Publisher> - var placeholderColor: PublisherMock.Publisher> - var borderColor: PublisherMock.Publisher> - var backgroundColor: PublisherMock.Publisher> - - var borderRadius: PublisherMock.Publisher> - var borderWidth: PublisherMock.Publisher> - - var leftSpacing: PublisherMock.Publisher> - var contentSpacing: PublisherMock.Publisher> - var rightSpacing: PublisherMock.Publisher> - - var dim: PublisherMock.Publisher> - - var font: PublisherMock.Publisher> - - init( - textColor: PublisherMock.Publisher>, - placeholderColor: PublisherMock.Publisher>, - borderColor: PublisherMock.Publisher>, - backgroundColor: PublisherMock.Publisher>, - borderRadius: PublisherMock.Publisher>, - borderWidth: PublisherMock.Publisher>, - leftSpacing: PublisherMock.Publisher>, - contentSpacing: PublisherMock.Publisher>, - rightSpacing: PublisherMock.Publisher>, - dim: PublisherMock.Publisher>, - font: PublisherMock.Publisher> - ) { - self.textColor = textColor - self.placeholderColor = placeholderColor - self.borderColor = borderColor - self.backgroundColor = backgroundColor - self.borderRadius = borderRadius - self.borderWidth = borderWidth - self.leftSpacing = leftSpacing - self.contentSpacing = contentSpacing - self.rightSpacing = rightSpacing - self.dim = dim - self.font = font - } - - func load() { - self.cancellables = Set() - - [self.textColor, self.placeholderColor, self.borderColor, self.backgroundColor].forEach { - $0.loadTesting(on: &self.cancellables) - } - - [self.borderWidth, self.borderRadius, self.leftSpacing, self.contentSpacing, self.rightSpacing, self.dim].forEach { - $0.loadTesting(on: &self.cancellables) - } - - self.font.loadTesting(on: &self.cancellables) - } - - func reset() { - [self.textColor, self.placeholderColor, self.borderColor, self.backgroundColor].forEach { - $0.reset() - } - - [self.borderWidth, self.borderRadius, self.leftSpacing, self.contentSpacing, self.rightSpacing, self.dim].forEach { - $0.reset() - } - - self.font.reset() - } -} diff --git a/core/Sources/Components/TextLink/AccessibilityIdentifier/TextLinkAccessibilityIdentifier.swift b/core/Sources/Components/TextLink/AccessibilityIdentifier/TextLinkAccessibilityIdentifier.swift deleted file mode 100644 index 0ce6cddfe..000000000 --- a/core/Sources/Components/TextLink/AccessibilityIdentifier/TextLinkAccessibilityIdentifier.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TextLinkAccessibilityIdentifier.swift -// SparkCore -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The accessibility identifiers for the textLink. -public enum TextLinkAccessibilityIdentifier { - - // MARK: - Properties - - /// The view accessibility identifier. - public static let view = "spark-textLink" - /// The default content stackView accessibility identifier. - static let contentStackView = "spark-textLink-content-stackView" - /// The text accessibility identifier. - public static let text = "spark-textLink-text" - /// The icon view accessibility identifier. - public static let imageContentStackView = "spark-textLink-image-content-stackView" - /// The image accessibility identifier. - public static let image = "spark-textLink-image" -} diff --git a/core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignment.swift b/core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignment.swift deleted file mode 100644 index 65e10e548..000000000 --- a/core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignment.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// TextLinkAlignment.swift -// SparkCore -// -// Created by robin.lemaire on 08/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The alignment of the switch. -public enum TextLinkAlignment: CaseIterable { - /// Image on the leading edge of the textlink. - /// Text on the trailing edge of the textlink. - /// Not interpreted if textlink contains only text. - case leadingImage - /// Image on the trailing edge of the textlink. - /// Text on the leading edge of the textlink - /// Not interpreted if textlink contains only text. - case trailingImage - - // MARK: - Properties - - var isTrailingImage: Bool { - return self == .trailingImage - } -} diff --git a/core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignmentTests.swift b/core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignmentTests.swift deleted file mode 100644 index 12c2ef8d5..000000000 --- a/core/Sources/Components/TextLink/Enum/Public/Alignment/TextLinkAlignmentTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// TextLinkAlignmentTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 08/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class TextLinkAlignmentTests: XCTestCase { - - // MARK: - Tests - - func test_isTrailingImage_for_all_cases() { - // GIVEN - let items: [(givenAlignment: TextLinkAlignment, expectedIsTrailingImage: Bool)] = [ - (givenAlignment: .leadingImage, expectedIsTrailingImage: false), - (givenAlignment: .trailingImage, expectedIsTrailingImage: true) - ] - - for item in items { - // WHEN - let isTrailingImage = item.givenAlignment.isTrailingImage - - // THEN - XCTAssertEqual( - isTrailingImage, - item.expectedIsTrailingImage, - "Wrong isTrailingImage for .\(item.givenAlignment) cases" - ) - } - } -} diff --git a/core/Sources/Components/TextLink/Enum/Public/TextLinkIntent.swift b/core/Sources/Components/TextLink/Enum/Public/TextLinkIntent.swift deleted file mode 100644 index 7048c75d8..000000000 --- a/core/Sources/Components/TextLink/Enum/Public/TextLinkIntent.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// TextLinkIntent.swift -// SparkCore -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The intent of the text link. -public enum TextLinkIntent: CaseIterable { - case accent - case alert - case basic - case danger - case info - case main - case neutral - case onSurface - case success - case support -} diff --git a/core/Sources/Components/TextLink/Enum/Public/TextLinkTypography.swift b/core/Sources/Components/TextLink/Enum/Public/TextLinkTypography.swift deleted file mode 100644 index fe5268342..000000000 --- a/core/Sources/Components/TextLink/Enum/Public/TextLinkTypography.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TextLinkTypography.swift -// SparkCore -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -/// The typography of the text link. -public enum TextLinkTypography: CaseIterable { - /// Use the **display1** typography - case display1 - /// Use the **display2** typography - case display2 - /// Use the **display3** typography - case display3 - - /// Use the **headline1** typography - case headline1 - /// Use the **headline2** typography - case headline2 - - /// Use the **subhead** typography - case subhead - - /// Use the **body1** and **body1Highlight** typographies - case body1 - /// Use the **body2** and **body2Highlight** typographies - case body2 - - /// Use the **caption** and **captionHighlight** typographies - case caption - - /// Use the **small** and **smallHighlight** typographies - case small - - /// Use the **callout** typography - case callout -} diff --git a/core/Sources/Components/TextLink/Enum/Public/TextLinkVariant.swift b/core/Sources/Components/TextLink/Enum/Public/TextLinkVariant.swift deleted file mode 100644 index 6a6f941d7..000000000 --- a/core/Sources/Components/TextLink/Enum/Public/TextLinkVariant.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// TextLinkVariant.swift -// SparkCore -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// A text link variant is used to distinguish between different design and appearance options. -public enum TextLinkVariant: CaseIterable { - /// A text link with an underline. - case underline - - /// A text link without any variant (underline). - /// *Not recommended, please use it carefully.* - case none -} diff --git a/core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSize.swift b/core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSize.swift deleted file mode 100644 index 660cf105d..000000000 --- a/core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSize.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TextLinkImageSize.swift -// SparkCore -// -// Created by robin.lemaire on 14/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct TextLinkImageSize: Equatable { - - // MARK: - Properties - - let size: CGFloat - let padding: CGFloat -} diff --git a/core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSizeMock+ExtensionTests.swift b/core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSizeMock+ExtensionTests.swift deleted file mode 100644 index 5c8ae1921..000000000 --- a/core/Sources/Components/TextLink/Properties/Internal/ImageSize/TextLinkImageSizeMock+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// TextLinkImageSizeMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 18/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import Foundation - -extension TextLinkImageSize { - - // MARK: - Methods - - static func mocked( - size: CGFloat = 10, - padding: CGFloat = 11 - ) -> Self { - return .init( - size: size, - padding: padding - ) - } -} diff --git a/core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographies.swift b/core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographies.swift deleted file mode 100644 index 731f1d8fe..000000000 --- a/core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographies.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TextLinkTypographies.swift -// SparkCore -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -struct TextLinkTypographies: Equatable { - - // MARK: - Properties - - let normal: any TypographyFontToken - let highlight: any TypographyFontToken - - // MARK: - Equatable - - static func == (lhs: TextLinkTypographies, rhs: TextLinkTypographies) -> Bool { - return lhs.normal.font == rhs.normal.font && - lhs.normal.uiFont == rhs.normal.uiFont && - lhs.highlight.font == rhs.highlight.font && - lhs.highlight.uiFont == rhs.highlight.uiFont - } -} diff --git a/core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographiesMock+ExtensionsTests.swift b/core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographiesMock+ExtensionsTests.swift deleted file mode 100644 index d818757af..000000000 --- a/core/Sources/Components/TextLink/Properties/Internal/Typographies/TextLinkTypographiesMock+ExtensionsTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// TextLinkTypographiesMock+ExtensionTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 14/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension TextLinkTypographies { - - // MARK: - Methods - - static func mocked() -> Self { - let normalTypographyFontTokenMock = TypographyFontTokenGeneratedMock.mocked( - uiFont: .systemFont(ofSize: 12), - font: .title - ) - let highlightTypographyFontTokenMock = TypographyFontTokenGeneratedMock.mocked( - uiFont: .boldSystemFont(ofSize: 12), - font: .title2 - ) - - return .init( - normal: normalTypographyFontTokenMock, - highlight: highlightTypographyFontTokenMock - ) - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCase.swift b/core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCase.swift deleted file mode 100644 index 66bd76baa..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCase.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// TextLinkGetAttributedStringUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -// sourcery: AutoMockable, AutoMockTest -protocol TextLinkGetAttributedStringUseCaseable { - - // sourcery: textColorToken = "Identical" - func execute(frameworkType: FrameworkType, - text: String, - textColorToken: any ColorToken, - textHighlightRange: NSRange?, - isHighlighted: Bool, - variant: TextLinkVariant, - typographies: TextLinkTypographies) -> AttributedStringEither -} - -struct TextLinkGetAttributedStringUseCase: TextLinkGetAttributedStringUseCaseable { - - // MARK: - Properties - - private let getUnderlineUseCaseable: TextLinkGetUnderlineUseCaseable - - // MARK: - Initialization - - init(getUnderlineUseCaseable: TextLinkGetUnderlineUseCaseable = TextLinkGetUnderlineUseCase()) { - self.getUnderlineUseCaseable = getUnderlineUseCaseable - } - - // MARK: - Methods - - func execute( - frameworkType: FrameworkType, - text: String, - textColorToken: any ColorToken, - textHighlightRange: NSRange?, - isHighlighted: Bool, - variant: TextLinkVariant, - typographies: TextLinkTypographies - ) -> AttributedStringEither { - let underlineStyle = self.getUnderlineUseCaseable.execute( - variant: variant, - isHighlighted: isHighlighted - ) - - // Two possibilities: - // - Without range: Add highlight font and add underline from variant for all text - // - With range: Add highlight font and add underline from variant for range text, and normal for other. - switch frameworkType { - case .uiKit: - let attributedString = self.makeNSAttributedString( - text: text, - textColorToken: textColorToken, - textHighlightRange: textHighlightRange, - underlineStyle: underlineStyle, - typographies: typographies - ) - return .left(attributedString) - - case .swiftUI: - let attributedString = self.makerAttributedString( - text: text, - textColorToken: textColorToken, - textHighlightRange: textHighlightRange, - underlineStyle: underlineStyle, - typographies: typographies - ) - - return .right(attributedString) - } - } - - // MARK: - Maker - - private func makeNSAttributedString( - text: String, - textColorToken: any ColorToken, - textHighlightRange: NSRange?, - underlineStyle: NSUnderlineStyle?, - typographies: TextLinkTypographies - ) -> NSAttributedString { - var attributedString: NSMutableAttributedString - - var highlightAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: textColorToken.uiColor, - .font: typographies.highlight.uiFont - ] - - if let underlineStyle { - highlightAttributes[.underlineStyle] = underlineStyle.rawValue - highlightAttributes[.underlineColor] = textColorToken.uiColor - } - - if let textHighlightRange, text.count > textHighlightRange.upperBound { - let normalAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: textColorToken.uiColor, - .font: typographies.normal.uiFont - ] - - attributedString = NSMutableAttributedString( - string: text, - attributes: normalAttributes - ) - - attributedString.addAttributes( - highlightAttributes, - range: textHighlightRange - ) - - } else { - attributedString = NSMutableAttributedString( - string: text, - attributes: highlightAttributes - ) - } - - return attributedString - } - - private func makerAttributedString( - text: String, - textColorToken: any ColorToken, - textHighlightRange: NSRange?, - underlineStyle: NSUnderlineStyle?, - typographies: TextLinkTypographies - ) -> AttributedString { - var attributedString = AttributedString(text) - attributedString.foregroundColor = textColorToken.color - - if let textHighlightRangeTemp = textHighlightRange, - let textHighlightRange = Range(textHighlightRangeTemp, in: attributedString) { - attributedString.font = typographies.normal.font - - attributedString[textHighlightRange].font = typographies.highlight.font - attributedString[textHighlightRange].underlineStyle = underlineStyle - - } else { - attributedString.font = typographies.highlight.font - attributedString.underlineStyle = underlineStyle - } - - return attributedString - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCaseTests.swift b/core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCaseTests.swift deleted file mode 100644 index 2f5398855..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetAttributedString/TextLinkGetAttributedStringUseCaseTests.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// TextLinkGetAttributedStringTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TextLinkGetAttributedStringTests: XCTestCase { - - // MARK: - Properties - - private var mocks = Mocks() - private var useCase = TextLinkGetAttributedStringUseCase() - - // MARK: - Setupt - - override func setUp() { - super.setUp() - - self.mocks = Mocks() - self.useCase = TextLinkGetAttributedStringUseCase( - getUnderlineUseCaseable: self.mocks.getUnderlineUseCaseMock - ) - } - - // MARK: - UIKit Tests - - func test_execute_for_UIKit_without_range() { - // GIVEN - let expectedAttributedString = NSMutableAttributedString( - mocks: self.mocks, - isRange: false - ) - - // WHEN - let attributedString = self.useCase.execute( - frameworkType: .uiKit, - text: self.mocks.textMock, - textColorToken: self.mocks.colorTokenMock, - textHighlightRange: nil, - isHighlighted: self.mocks.isHighlightedMock, - variant: self.mocks.variantMock, - typographies: self.mocks.typographiesMock - ) - - // THEN - XCTAssertEqual( - attributedString.leftValue, - expectedAttributedString, - "Wrong attributed string" - ) - - // Use Case - self.testUseCase(from: self.mocks) - } - - func test_execute_for_UIKit_with_range() { - // GIVEN - let expectedAttributedString = NSMutableAttributedString( - mocks: self.mocks, - isRange: true - ) - - // WHEN - let attributedString = self.useCase.execute( - frameworkType: .uiKit, - text: self.mocks.textMock, - textColorToken: self.mocks.colorTokenMock, - textHighlightRange: self.mocks.textHighlightRangeMock, - isHighlighted: self.mocks.isHighlightedMock, - variant: self.mocks.variantMock, - typographies: self.mocks.typographiesMock - ) - - // THEN - XCTAssertEqual( - attributedString.leftValue, - expectedAttributedString, - "Wrong attributed string" - ) - - // Use Case - self.testUseCase(from: self.mocks) - } - - // MARK: - SwiftUI Test - - func test_execute_for_SwiftUI_without_range() { - // GIVEN - var expectedAttributedString = AttributedString( - mocks: self.mocks, - isRange: false - ) - - // WHEN - let attributedString = self.useCase.execute( - frameworkType: .swiftUI, - text: self.mocks.textMock, - textColorToken: self.mocks.colorTokenMock, - textHighlightRange: nil, - isHighlighted: self.mocks.isHighlightedMock, - variant: self.mocks.variantMock, - typographies: self.mocks.typographiesMock - ) - - // THEN - XCTAssertEqual( - attributedString.rightValue, - expectedAttributedString, - "Wrong attributed string" - ) - - // Use Case - self.testUseCase(from: self.mocks) - } - - func test_execute_for_SwiftUI_with_range() throws { - // GIVEN - var expectedAttributedString = AttributedString( - mocks: self.mocks, - isRange: true - ) - - let textHighlightRange = try XCTUnwrap( - Range(self.mocks.textHighlightRangeMock, in: expectedAttributedString), - "Range should not be nil" - ) - expectedAttributedString[textHighlightRange].font = self.mocks.typographiesMock.highlight.font - expectedAttributedString[textHighlightRange].underlineStyle = self.mocks.underlineStyleMock - - // WHEN - let attributedString = self.useCase.execute( - frameworkType: .swiftUI, - text: self.mocks.textMock, - textColorToken: self.mocks.colorTokenMock, - textHighlightRange: self.mocks.textHighlightRangeMock, - isHighlighted: self.mocks.isHighlightedMock, - variant: self.mocks.variantMock, - typographies: self.mocks.typographiesMock - ) - - // THEN - XCTAssertEqual( - attributedString.rightValue, - expectedAttributedString, - "Wrong attributed string" - ) - - // Use Case - self.testUseCase(from: self.mocks) - } -} - -// MARK: - Use Case Testing - -private extension TextLinkGetAttributedStringTests { - - func testUseCase(from mocks: Mocks) { - TextLinkGetUnderlineUseCaseableMockTest.XCTAssert( - mocks.getUnderlineUseCaseMock, - expectedNumberOfCalls: 1, - givenVariant: mocks.variantMock, - givenIsHighlighted: mocks.isHighlightedMock, - expectedReturnValue: mocks.underlineStyleMock - ) - } -} - -// MARK: - Mocks - -private final class Mocks { - - let textMock = "My Text" - let textHighlightRangeMock = NSRange(location: 0, length: 2) - let variantMock: TextLinkVariant = .underline - let typographiesMock: TextLinkTypographies = .mocked() - let isHighlightedMock: Bool = true - let colorTokenMock = ColorTokenGeneratedMock() - - let underlineStyleMock: NSUnderlineStyle = .double - - lazy var getUnderlineUseCaseMock: TextLinkGetUnderlineUseCaseableGeneratedMock = { - let mock = TextLinkGetUnderlineUseCaseableGeneratedMock() - mock.executeWithVariantAndIsHighlightedReturnValue = self.underlineStyleMock - return mock - }() -} - -// MARK: - Extension - -private extension NSMutableAttributedString { - - convenience init(mocks: Mocks, isRange: Bool) { - let highlightAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: mocks.colorTokenMock.uiColor, - .font: mocks.typographiesMock.highlight.uiFont, - .underlineStyle: mocks.underlineStyleMock.rawValue, - .underlineColor: mocks.colorTokenMock.uiColor, - ] - - let attributes: [NSAttributedString.Key: Any] - if isRange { - attributes = [ - .foregroundColor: mocks.colorTokenMock.uiColor, - .font: mocks.typographiesMock.normal.uiFont - ] - } else { - attributes = highlightAttributes - } - - self.init( - string: mocks.textMock, - attributes: attributes - ) - - if isRange { - self.addAttributes( - highlightAttributes, - range: mocks.textHighlightRangeMock - ) - } - } -} - -private extension AttributedString { - - init(mocks: Mocks, isRange: Bool) { - self.init(mocks.textMock) - - if isRange { - self.foregroundColor = mocks.colorTokenMock.color - self.font = mocks.typographiesMock.normal.font - } else { - self.foregroundColor = mocks.colorTokenMock.color - self.font = mocks.typographiesMock.highlight.font - self.underlineStyle = mocks.underlineStyleMock - } - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCase.swift b/core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCase.swift deleted file mode 100644 index 2dc37556f..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCase.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// TextLinkGetColorUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable, AutoMockTest -protocol TextLinkGetColorUseCaseable { - - // sourcery: colors = "Identical", return = "Identical" - func execute(intent: TextLinkIntent, - isHighlighted: Bool, - colors: Colors) -> any ColorToken -} - -struct TextLinkGetColorUseCase: TextLinkGetColorUseCaseable { - - // MARK: - Methods - - func execute( - intent: TextLinkIntent, - isHighlighted: Bool, - colors: Colors - ) -> any ColorToken { - switch intent { - case .accent: - return isHighlighted ? colors.states.accentPressed : colors.accent.accent - case .alert: - return isHighlighted ? colors.states.alertPressed : colors.feedback.alert - case .basic: - return isHighlighted ? colors.states.basicPressed : colors.basic.basic - case .danger: - return isHighlighted ? colors.states.errorPressed : colors.feedback.error - case .info: - return isHighlighted ? colors.states.infoPressed : colors.feedback.info - case .main: - return isHighlighted ? colors.states.mainPressed : colors.main.main - case .neutral: - return isHighlighted ? colors.states.neutralPressed : colors.feedback.neutral - case .onSurface: - return colors.base.onSurface - case .success: - return isHighlighted ? colors.states.successPressed : colors.feedback.success - case .support: - return isHighlighted ? colors.states.supportPressed : colors.support.support - } - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCaseTests.swift b/core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCaseTests.swift deleted file mode 100644 index 47d85ccbb..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetColor/TextLinkGetColorUseCaseTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// TextLinkGetColorUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 05/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TextLinkGetColorUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_for_all_intents_when_isHighlighted_is_false() { - // GIVEN - let useCase = TextLinkGetColorUseCase() - let colorsMock = ColorsGeneratedMock.mocked() - - let givenIntents = TextLinkIntent.allCases - - // WHEN - for givenIntent in givenIntents { - let colorToken = useCase.execute( - intent: givenIntent, - isHighlighted: false, - colors: colorsMock - ) - - let expectedColorToken = givenIntent.expectedColorTokenWithoutHighlighted( - from: colorsMock - ) - - // THEN - XCTAssertIdentical( - colorToken as? ColorTokenGeneratedMock, - expectedColorToken as? ColorTokenGeneratedMock, - "Wrong color for .\(givenIntent) case" - ) - } - } - - func test_execute_for_all_intents_when_isHighlighted_is_true() { - // GIVEN - let useCase = TextLinkGetColorUseCase() - let colorsMock = ColorsGeneratedMock.mocked() - - let givenIntents = TextLinkIntent.allCases - - // WHEN - for givenIntent in givenIntents { - let colorToken = useCase.execute( - intent: givenIntent, - isHighlighted: true, - colors: colorsMock - ) - - let expectedColorToken = givenIntent.expectedColorTokenWithHighlighted( - from: colorsMock - ) - - // THEN - XCTAssertIdentical( - colorToken as? ColorTokenGeneratedMock, - expectedColorToken as? ColorTokenGeneratedMock, - "Wrong color for .\(givenIntent) case" - ) - } - } -} - -// MARK: - Extension - -private extension TextLinkIntent { - - func expectedColorTokenWithHighlighted(from colorsMock: ColorsGeneratedMock) -> any ColorToken { - switch self { - case .accent: - return colorsMock.states.accentPressed - case .alert: - return colorsMock.states.alertPressed - case .basic: - return colorsMock.states.basicPressed - case .danger: - return colorsMock.states.errorPressed - case .info: - return colorsMock.states.infoPressed - case .main: - return colorsMock.states.mainPressed - case .neutral: - return colorsMock.states.neutralPressed - case .onSurface: - return colorsMock.base.onSurface - case .success: - return colorsMock.states.successPressed - case .support: - return colorsMock.states.supportPressed - } - } - - func expectedColorTokenWithoutHighlighted(from colorsMock: ColorsGeneratedMock) -> any ColorToken { - switch self { - case .accent: - return colorsMock.accent.accent - case .alert: - return colorsMock.feedback.alert - case .basic: - return colorsMock.basic.basic - case .danger: - return colorsMock.feedback.error - case .info: - return colorsMock.feedback.info - case .main: - return colorsMock.main.main - case .neutral: - return colorsMock.feedback.neutral - case .onSurface: - return colorsMock.base.onSurface - case .success: - return colorsMock.feedback.success - case .support: - return colorsMock.support.support - } - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCase.swift b/core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCase.swift deleted file mode 100644 index f820b5776..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCase.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// TextLinkGetImageSizeUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 14/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable, AutoMockTest -protocol TextLinkGetImageSizeUseCaseable { - func execute(typographies: TextLinkTypographies) -> TextLinkImageSize -} - -struct TextLinkGetImageSizeUseCase: TextLinkGetImageSizeUseCaseable { - - // MARK: - Methods - - func execute( - typographies: TextLinkTypographies - ) -> TextLinkImageSize { - let lineHeight = typographies.highlight.uiFont.lineHeight - let pointSize = typographies.highlight.uiFont.pointSize - - return .init( - size: pointSize, - padding: (abs(lineHeight - pointSize)) / 2 - ) - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCaseTests.swift b/core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCaseTests.swift deleted file mode 100644 index 36af13b0a..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetImageSize/TextLinkGetImageSizeUseCaseTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TextLinkGetImageSizeUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 14/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TextLinkGetImageSizeUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute() { - // GIVEN - let typograhiesMock = TextLinkTypographies.mocked() - let highlighFontMock = typograhiesMock.highlight.uiFont - - let useCase = TextLinkGetImageSizeUseCase() - - // WHEN - let imageSize = useCase.execute(typographies: typograhiesMock) - - // THEN - XCTAssertEqual( - imageSize.size, - highlighFontMock.pointSize, - "Wrong size" - ) - XCTAssertEqual( - imageSize.padding, - (abs(highlighFontMock.lineHeight - highlighFontMock.pointSize)) / 2, - "Wrong padding" - ) - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCase.swift b/core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCase.swift deleted file mode 100644 index d7447a60a..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCase.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// TextLinkGetTypographiesUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable, AutoMockTest -protocol TextLinkGetTypographiesUseCaseable { - - // sourcery: typography = "Identical" - func execute(textLinkTypography: TextLinkTypography, - typography: any Typography) -> TextLinkTypographies -} - -struct TextLinkGetTypographiesUseCase: TextLinkGetTypographiesUseCaseable { - - // MARK: - Methods - - func execute( - textLinkTypography: TextLinkTypography, - typography: any Typography - ) -> TextLinkTypographies { - switch textLinkTypography { - case .display1: - return .init( - normal: typography.display1, - highlight: typography.display1 - ) - case .display2: - return .init( - normal: typography.display2, - highlight: typography.display2 - ) - case .display3: - return .init( - normal: typography.display3, - highlight: typography.display3 - ) - - case .headline1: - return .init( - normal: typography.headline1, - highlight: typography.headline1 - ) - case .headline2: - return .init( - normal: typography.headline2, - highlight: typography.headline2 - ) - - case .subhead: - return .init( - normal: typography.subhead, - highlight: typography.subhead - ) - - case .body1: - return .init( - normal: typography.body1, - highlight: typography.body1Highlight - ) - case .body2: - return .init( - normal: typography.body2, - highlight: typography.body2Highlight - ) - - case .caption: - return .init( - normal: typography.caption, - highlight: typography.captionHighlight - ) - - case .small: - return .init( - normal: typography.small, - highlight: typography.smallHighlight - ) - - case .callout: - return .init( - normal: typography.callout, - highlight: typography.callout - ) - } - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCaseTests.swift b/core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCaseTests.swift deleted file mode 100644 index d12e03305..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetTypographies/TextLinkGetTypographiesUseCaseTests.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// TextLinkGetTypographiesUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TextLinkGetTypographiesUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_for_all_intents() { - // GIVEN - let useCase = TextLinkGetTypographiesUseCase() - let typographyMock = TypographyGeneratedMock.mocked() - - let givenTypographies = TextLinkTypography.allCases - - // WHEN - for givenTypography in givenTypographies { - let typographies = useCase.execute( - textLinkTypography: givenTypography, - typography: typographyMock - ) - - let expectedTypographies = givenTypography.expectedTypographies( - from: typographyMock - ) - - // THEN - XCTAssertEqual( - typographies, - expectedTypographies, - "Wrong typographies for .\(givenTypography) case" - ) - } - } -} - -// MARK: - Extension - -private extension TextLinkTypography { - - func expectedTypographies(from typographyMock: TypographyGeneratedMock) -> TextLinkTypographies { - switch self { - case .display1: - return .init( - normal: typographyMock.display1, - highlight: typographyMock.display1 - ) - case .display2: - return .init( - normal: typographyMock.display2, - highlight: typographyMock.display2 - ) - case .display3: - return .init( - normal: typographyMock.display3, - highlight: typographyMock.display3 - ) - - case .headline1: - return .init( - normal: typographyMock.headline1, - highlight: typographyMock.headline1 - ) - case .headline2: - return .init( - normal: typographyMock.headline2, - highlight: typographyMock.headline2 - ) - - case .subhead: - return .init( - normal: typographyMock.subhead, - highlight: typographyMock.subhead - ) - - case .body1: - return .init( - normal: typographyMock.body1, - highlight: typographyMock.body1Highlight - ) - case .body2: - return .init( - normal: typographyMock.body2, - highlight: typographyMock.body2Highlight - ) - - case .caption: - return .init( - normal: typographyMock.caption, - highlight: typographyMock.captionHighlight - ) - - case .small: - return .init( - normal: typographyMock.small, - highlight: typographyMock.smallHighlight - ) - - case .callout: - return .init( - normal: typographyMock.callout, - highlight: typographyMock.callout - ) - } - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCase.swift b/core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCase.swift deleted file mode 100644 index fa54c89af..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCase.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TextLinkGetUnderlineUseCase.swift -// SparkCore -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import UIKit - -// sourcery: AutoMockable, AutoMockTest -protocol TextLinkGetUnderlineUseCaseable { - - func execute(variant: TextLinkVariant, - isHighlighted: Bool) -> NSUnderlineStyle? -} - -struct TextLinkGetUnderlineUseCase: TextLinkGetUnderlineUseCaseable { - - // MARK: - Methods - - func execute( - variant: TextLinkVariant, - isHighlighted: Bool - ) -> NSUnderlineStyle? { - // Always single line when the textlink is highlighted - guard !isHighlighted else { - return .single - } - - switch variant { - case .underline: - return .single - case .none: - return nil - } - } -} diff --git a/core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCaseTests.swift b/core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCaseTests.swift deleted file mode 100644 index d43659a83..000000000 --- a/core/Sources/Components/TextLink/UseCase/GetUnderline/TextLinkGetUnderlineUseCaseTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// TextLinkGetUnderlineUseCaseTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class TextLinkGetUnderlineUseCaseTests: XCTestCase { - - // MARK: - Tests - - func test_execute_for_all_intents_and_isHighlighted_at_true() { - // GIVEN - let useCase = TextLinkGetUnderlineUseCase() - - let textLinkVariants = TextLinkVariant.allCases - - // WHEN - for textLinkVariant in textLinkVariants { - let underline = useCase.execute( - variant: textLinkVariant, - isHighlighted: true - ) - - // THEN - XCTAssertEqual( - underline, - .single, - "Wrong underline for .\(textLinkVariant) case" - ) - } - } - - func test_execute_for_all_intents_and_isHighlighted_at_false() { - // GIVEN - let useCase = TextLinkGetUnderlineUseCase() - - let textLinkVariants = TextLinkVariant.allCases - - // WHEN - for textLinkVariant in textLinkVariants { - let underline = useCase.execute( - variant: textLinkVariant, - isHighlighted: false - ) - - // THEN - XCTAssertEqual( - underline, - textLinkVariant.expectedUnderlineStyle, - "Wrong underline for .\(textLinkVariant) case" - ) - } - } -} - -// MARK: - Extension - -private extension TextLinkVariant { - - var expectedUnderlineStyle: NSUnderlineStyle? { - switch self { - case .underline: - return .single - case .none: - return nil - } - } -} diff --git a/core/Sources/Components/TextLink/View/Common/TextLinkConfigurationSnapshotTests.swift b/core/Sources/Components/TextLink/View/Common/TextLinkConfigurationSnapshotTests.swift deleted file mode 100644 index 65ef02a03..000000000 --- a/core/Sources/Components/TextLink/View/Common/TextLinkConfigurationSnapshotTests.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// TextLinkConfigurationSnapshotTests.swift -// SparkCore -// -// Created by robin.lemaire on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import XCTest - -struct TextLinkConfigurationSnapshotTests { - - // MARK: - Type Alias - - private typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Properties - - let scenario: TextLinkScenarioSnapshotTests - - let type: TextLinkType - let variant: TextLinkVariant - let image: ImageEither? - let alignment: TextLinkAlignment - let intent: TextLinkIntent - let size: TextLinkSize - let modes: [ComponentSnapshotTestMode] - let sizes: [UIContentSizeCategory] - - // MARK: - Initialization - - init( - scenario: TextLinkScenarioSnapshotTests, - type: TextLinkType = .text, - variant: TextLinkVariant = .underline, - image: ImageEither? = nil, - alignment: TextLinkAlignment = .leadingImage, - intent: TextLinkIntent = .main, - size: TextLinkSize = .size1, - modes: [ComponentSnapshotTestMode] = Constants.Modes.default, - sizes: [UIContentSizeCategory] = Constants.Sizes.default - ) { - self.scenario = scenario - self.type = type - self.variant = variant - self.image = image - self.alignment = alignment - self.intent = intent - self.size = size - self.modes = modes - self.sizes = sizes - } - - // MARK: - Getter - - func testName() -> String { - return [ - self.scenario.rawValue, - self.type.rawValue, - "\(self.intent)" + "Intent", - "\(self.variant)" + "Variant", - self.image != nil ? "\(self.alignment)" : nil, - self.size.rawValue, - ].compactMap { $0 }.joined(separator: "-") - } -} - -// MARK: - Enum - -enum TextLinkType: String, CaseIterable { - case text - case paragraph - - // MARK: - All Cases - - var text: String { - switch self { - case .text: - return "My Text" - case .paragraph: - return "My paragraphe\nwith many lines\nmany many lines" - } - } - - var textHighlightRange: NSRange? { - switch self { - case .text: - return nil - case .paragraph: - return .init(location: 0, length: 13) - } - } -} - -enum TextLinkSize: String, CaseIterable { - case size1 - case size2 - - // MARK: - Properties - - var typography: TextLinkTypography { - switch self { - case .size1: - return .body1 - case .size2: - return .headline1 - } - } -} diff --git a/core/Sources/Components/TextLink/View/Common/TextLinkScenarioSnapshotTests.swift b/core/Sources/Components/TextLink/View/Common/TextLinkScenarioSnapshotTests.swift deleted file mode 100644 index c23ceb743..000000000 --- a/core/Sources/Components/TextLink/View/Common/TextLinkScenarioSnapshotTests.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// TextLinkScenarioSnapshotTests.swift -// SparkCore -// -// Created by robin.lemaire on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import UIKit -import SwiftUI - -enum TextLinkScenarioSnapshotTests: String, CaseIterable { - case test1 - case test2 - case test3 - case test4 - case test5 - - // MARK: - Type Alias - - typealias Constants = ComponentSnapshotTestConstants - - // MARK: - Configurations - - func configuration(isSwiftUIComponent: Bool) -> [TextLinkConfigurationSnapshotTests] { - switch self { - case .test1: - return self.test1() - case .test2: - return self.test2(isSwiftUIComponent: isSwiftUIComponent) - case .test3: - return self.test3() - case .test4: - return self.test4(isSwiftUIComponent: isSwiftUIComponent) - case .test5: - return self.test5(isSwiftUIComponent: isSwiftUIComponent) - } - } - - // MARK: - Scenarios - - /// Test 1 - /// - /// Description: To test all types of link - /// - /// Content: - /// - type: all - /// - underline: default - /// - with icon: default - /// - icon aligment: default - /// - intent: default - /// - size: default - /// - a11y size: default - /// - mode: all - private func test1() -> [TextLinkConfigurationSnapshotTests] { - let typePossibilities = TextLinkType.allCases - - return typePossibilities.map { type in - .init( - scenario: self, - type: type, - modes: Constants.Modes.all - ) - } - } - - /// Test 2 - /// - /// Description: To test inheritance - /// - /// Content: - /// - type: default - /// - underline: default - /// - with icon: true - /// - icon aligment: default - /// - intent: all - /// - size: all - /// - a11y size: default - /// - mode: default - private func test2(isSwiftUIComponent: Bool) -> [TextLinkConfigurationSnapshotTests] { - let intentsPossibilities = TextLinkIntent.allCases - let sizesPossibilities = TextLinkSize.allCases - - return intentsPossibilities.flatMap { intent in - sizesPossibilities.map { size in - return .init( - scenario: self, - image: .mock(isSwiftUIComponent: isSwiftUIComponent), - intent: intent, - size: size - ) - } - } - } - - /// Test 3 - /// - /// Description: To test underline - /// - /// Content: - /// - type: default - /// - underline: all - /// - with icon: default - /// - icon aligment: default - /// - intent: default - /// - size: default - /// - a11y size: default - /// - mode: default - private func test3() -> [TextLinkConfigurationSnapshotTests] { - let variantPossibilities = TextLinkVariant.allCases - - return variantPossibilities.map { variant in - .init( - scenario: self, - variant: variant - ) - } - } - - /// Test 4 - /// - /// Description: To test with icons - /// - /// Content: - /// - type: all - /// - underline: default - /// - with icon: true - /// - icon aligment: all - /// - intent: default - /// - size: default - /// - a11y size: default - /// - mode: default - private func test4(isSwiftUIComponent: Bool) -> [TextLinkConfigurationSnapshotTests] { - let typePossibilities = TextLinkType.allCases - let alignmentPossibilities = TextLinkAlignment.allCases - - return typePossibilities.flatMap { type in - alignmentPossibilities.map { alignment in - .init( - scenario: self, - type: type, - image: .mock(isSwiftUIComponent: isSwiftUIComponent), - alignment: alignment - ) - } - } - } - - /// Test 5 - /// - /// Description: To test a11y sizes - /// - /// Content: - /// - type: default - /// - underline: default - /// - with icon: default - /// - icon aligment: default - /// - intent: default - /// - size: default - /// - a11y size: all - /// - mode: default - private func test5(isSwiftUIComponent: Bool) -> [TextLinkConfigurationSnapshotTests] { - return [ - .init( - scenario: self, - sizes: Constants.Sizes.all - ) - ] - } -} diff --git a/core/Sources/Components/TextLink/View/SwiftUI/TextLinkView.swift b/core/Sources/Components/TextLink/View/SwiftUI/TextLinkView.swift deleted file mode 100644 index aa833288e..000000000 --- a/core/Sources/Components/TextLink/View/SwiftUI/TextLinkView.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// TextLinkView.swift -// SparkCore -// -// Created by robin.lemaire on 06/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public struct TextLinkView: View { - - // MARK: - Component - - private let image: Image? - - // MARK: - Properties - - @ObservedObject private var viewModel: TextLinkViewModel - @ObservedObject private var style = TextLinkStyle() - - private var action: () -> Void - - @Environment(\.sizeCategory) var sizeCategory - - @ScaledMetric private var spacing: CGFloat - - // MARK: - Initialization - - /// Initialize a new text link view. - /// - Parameters: - /// - theme: The spark theme of the text link. - /// - text: The text of the text link. - /// - textHighlightRange: The optional range to specify the highlighted part of the text link. - /// - intent: The intent of the text link. - /// - typography: The typography of the text link. - /// - variant: The variant of the text link. - /// - image: The optional image of the text link. - /// - alignment: The alignment image of the text link. - /// - action: The action of the text link when the user tap on the component. - public init( - theme: any Theme, - text: String, - textHighlightRange: NSRange? = nil, - intent: TextLinkIntent, - typography: TextLinkTypography, - variant: TextLinkVariant, - image: Image?, - alignment: TextLinkAlignment = .leadingImage, - action: @escaping () -> Void - ) { - let viewModel = TextLinkViewModel( - for: .swiftUI, - theme: theme, - text: text, - textHighlightRange: textHighlightRange, - intent: intent, - typography: typography, - variant: variant, - alignment: alignment - ) - self.viewModel = viewModel - - self.image = image - - self._spacing = .init(wrappedValue: viewModel.spacing) - - self.action = action - } - - // MARK: - View - - public var body: some View { - if self.image == nil { - self.buttonContent() - } else { - self.buttonContent() - .accessibilityAddTraits(.isImage) - } - } - - @ViewBuilder - private func buttonContent() -> some View { - Button(action: self.action) { - self.content() - } - .buttonStyle(PressedButtonStyle(isPressed: self.$viewModel.isHighlighted)) - .accessibilityIdentifier(TextLinkAccessibilityIdentifier.view) - .accessibilityRemoveTraits(.isButton) - .accessibilityAddTraits(.isLink) - } - - // MARK: - View Builder - - @ViewBuilder - private func content() -> some View { - HStack( - alignment: .top, - spacing: self.spacing - ) { - if self.viewModel.isTrailingImage { - self.text() - self.imageView() - } else { - self.imageView() - self.text() - } - } - } - - @ViewBuilder - private func imageView() -> some View { - self.image? - .resizable() - .aspectRatio(contentMode: .fit) - .frame( - width: self.viewModel.imageSize?.size, - height: self.viewModel.imageSize?.size, - alignment: .center - ) - .padding(.init( - vertical: self.viewModel.imageSize?.padding ?? .zero, - horizontal: .zero - )) - .foregroundStyle(self.viewModel.imageTintColor.color) - .accessibilityIdentifier(TextLinkAccessibilityIdentifier.image) - } - - @ViewBuilder - private func text() -> some View { - Text(self.viewModel.attributedText?.rightValue ?? "") - .multilineTextAlignment(self.style.multilineTextAlignment) - .lineLimit(self.style.lineLimit) - .accessibilityIdentifier(TextLinkAccessibilityIdentifier.text) - .onChange(of: self.sizeCategory) { _ in - self.viewModel.contentSizeCategoryDidUpdate() - } - } - - // MARK: - Modifier - - /// Sets the alignment of a text view that contains multiple lines of text. - /// - Parameters: - /// - alignment: A value that you use to align multiple lines of text within a view. - /// - Returns: Current TextLink View. - public func multilineTextAlignment(_ alignment: TextAlignment) -> Self { - self.style.multilineTextAlignment = alignment - return self - } - - /// Sets the maximum number of lines that text can occupy in this view. - /// - Parameters: - /// - number: The line limit. If `nil`, no line limit applies. - /// - Returns: Current TextLink View. - public func lineLimit(_ lineLimit: Int?) -> Self { - self.style.lineLimit = lineLimit - return self - } -} - -// MARK: - Observable Style - -private final class TextLinkStyle: ObservableObject { - - // MARK: - Properties - - @Published var multilineTextAlignment: TextAlignment = .leading - @Published var lineLimit: Int? -} diff --git a/core/Sources/Components/TextLink/View/SwiftUI/TextLinkViewSnapshotTests.swift b/core/Sources/Components/TextLink/View/SwiftUI/TextLinkViewSnapshotTests.swift deleted file mode 100644 index 041eee12d..000000000 --- a/core/Sources/Components/TextLink/View/SwiftUI/TextLinkViewSnapshotTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// TextLinkViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore -import SwiftUI - -final class TextLinkViewSnapshotTests: SwiftUIComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = TextLinkScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [TextLinkConfigurationSnapshotTests] = scenario.configuration( - isSwiftUIComponent: true - ) - - for configuration in configurations { - let view = TextLinkView( - theme: self.theme, - text: configuration.type.text, - textHighlightRange: configuration.type.textHighlightRange, - intent: configuration.intent, - typography: configuration.size.typography, - variant: configuration.variant, - image: configuration.image?.rightValue, - alignment: configuration.alignment, - action: {} - ) - .fixedSize() - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/TextLink/View/UIKit/TextLinkUIView.swift b/core/Sources/Components/TextLink/View/UIKit/TextLinkUIView.swift deleted file mode 100644 index 4bd9bf610..000000000 --- a/core/Sources/Components/TextLink/View/UIKit/TextLinkUIView.swift +++ /dev/null @@ -1,428 +0,0 @@ -// -// TextLinkUIView.swift -// SparkCore -// -// Created by robin.lemaire on 07/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import Combine -import SwiftUI - -/// The UIKit version for the text link. -public final class TextLinkUIView: UIControl { - - // MARK: - Components - - private lazy var contentStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - self.imageContentStackView, - self.textLabel - ] - ) - stackView.axis = .horizontal - stackView.alignment = .top - stackView.accessibilityIdentifier = TextLinkAccessibilityIdentifier.contentStackView - stackView.isUserInteractionEnabled = false - return stackView - }() - - private lazy var imageContentStackView: UIStackView = { - let stackView = UIStackView( - arrangedSubviews: - [ - self.imageTopSpaceView, - self.imageView, - self.imageBottomSpaceView, - ] - ) - stackView.axis = .vertical - stackView.accessibilityIdentifier = TextLinkAccessibilityIdentifier.imageContentStackView - return stackView - }() - - private let imageTopSpaceView = UIView() - - private var imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.tintAdjustmentMode = .normal - imageView.accessibilityIdentifier = TextLinkAccessibilityIdentifier.image - return imageView - }() - - private let imageBottomSpaceView = UIView() - - private lazy var textLabel: UILabel = { - let label = UILabel() - label.numberOfLines = self.numberOfLines - label.lineBreakMode = self.lineBreakMode - label.textAlignment = self.textAlignment - label.adjustsFontForContentSizeCategory = true - label.accessibilityIdentifier = TextLinkAccessibilityIdentifier.text - return label - }() - - /// The tap publisher. Alternatively, you can use the native **action** (addAction) or **target** (addTarget). - public var tapPublisher: UIControl.EventPublisher { - return self.publisher(for: .touchUpInside) - } - - // MARK: - Public Properties - - /// The spark theme of the text link. - public var theme: Theme { - get { - return self.viewModel.theme - } - set { - self.viewModel.theme = newValue - } - } - - /// The text of the text link. - public var text: String { - get { - return self.viewModel.text - } - set { - self.viewModel.text = newValue - self.accessibilityLabelManager.internalValue = newValue - } - } - - /// The intent of the text link. - public var intent: TextLinkIntent { - get { - return self.viewModel.intent - } - set { - self.viewModel.intent = newValue - } - } - - /// The optional range to specify the highlighted part of the text link. - public var textHighlightRange: NSRange? { - get { - return self.viewModel.textHighlightRange - } - set { - self.viewModel.textHighlightRange = newValue - } - } - - /// The typography of the text link. - public var typography: TextLinkTypography { - get { - return self.viewModel.typography - } - set { - self.viewModel.typography = newValue - } - } - - /// The variant of the text link. - public var variant: TextLinkVariant { - get { - return self.viewModel.variant - } - set { - self.viewModel.variant = newValue - } - } - - /// The optional image of the text link. - public var image: UIImage? { - didSet { - self.updateImage() - } - } - - /// The alignment of the text link. - public var alignment: TextLinkAlignment { - get { - return self.viewModel.alignment - } - set { - self.viewModel.alignment = newValue - } - } - - /// The text alignment of the textlink. Default is **.natural**. - public var textAlignment: NSTextAlignment = .natural { - didSet { - self.textLabel.textAlignment = self.textAlignment - } - } - - /// The line break mode of the textlink. Default is **.byTruncatingTail**. - public var lineBreakMode: NSLineBreakMode = .byTruncatingTail { - didSet { - self.textLabel.lineBreakMode = self.lineBreakMode - } - } - - /// The number of lines of the textlink. Default is **1**. - public var numberOfLines: Int = 1 { - didSet { - self.textLabel.numberOfLines = self.numberOfLines - } - } - - /// A Boolean value indicating whether the text link draws a highlight. - public override var isHighlighted: Bool { - get { - return super.isHighlighted - } - set { - super.isHighlighted = newValue - self.viewModel.isHighlighted = newValue - } - } - - public override var accessibilityLabel: String? { - get { - return self.accessibilityLabelManager.value - } - set { - self.accessibilityLabelManager.value = newValue - } - } - - // MARK: - Private Properties - - private let viewModel: TextLinkViewModel - - private var imageTopSpaceViewConstraint: NSLayoutConstraint? - private var imageViewHeightConstraint: NSLayoutConstraint? - - @ScaledUIMetric private var contentStackViewSpacing: CGFloat = 0 - - private var accessibilityLabelManager = AccessibilityLabelManager() - - private var subscriptions = Set() - - // MARK: - Initialization - - /// Initialize a new text link view. - /// - Parameters: - /// - theme: The spark theme of the text link. - /// - text: The text of the text link. - /// - textHighlightRange: The optional range to specify the highlighted part of the text link. - /// - intent: The intent of the text link. - /// - typography: The typography of the text link. - /// - variant: The variant of the text link. - /// - image: The optional image of the text link.. - /// - alignment: The alignment of the content of the textlink: image on left or right of the text. - public init( - theme: any Theme, - text: String, - textHighlightRange: NSRange? = nil, - intent: TextLinkIntent, - typography: TextLinkTypography, - variant: TextLinkVariant, - image: UIImage? = nil, - alignment: TextLinkAlignment = .leadingImage - ) { - self.viewModel = .init( - for: .uiKit, - theme: theme, - text: text, - textHighlightRange: textHighlightRange, - intent: intent, - typography: typography, - variant: variant, - alignment: alignment - ) - - self.image = image - - super.init(frame: .zero) - - // Setup - self.setupView() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - View setup - - func setupView() { - // Add subviews - self.addSubview(self.contentStackView) - - // Accessibility - self.accessibilityIdentifier = TextLinkAccessibilityIdentifier.view - - // View properties - self.backgroundColor = .clear - self.updateImage() - - // Setup constraints - self.setupConstraints() - - // Setup gesture - self.enableTouch() - - // Setup accessibility - self.setupAccessibility() - - // Setup subscriptions - self.setupSubscriptions() - - // Load view model - self.viewModel.load() - } - - // MARK: - Constraints - - private func setupConstraints() { - self.setupViewConstraints() - self.setupContentStackViewConstraints() - self.setupImageSpaceViewsConstraints() - self.setupImageViewConstraints() - } - - private func setupViewConstraints() { - self.translatesAutoresizingMaskIntoConstraints = false - } - - private func setupContentStackViewConstraints() { - self.contentStackView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.stickEdges( - from: self.contentStackView, - to: self, - insets: .zero - ) - } - - private func setupImageSpaceViewsConstraints() { - self.imageTopSpaceView.translatesAutoresizingMaskIntoConstraints = false - self.imageBottomSpaceView.translatesAutoresizingMaskIntoConstraints = false - - self.imageTopSpaceViewConstraint = self.imageTopSpaceView.heightAnchor.constraint(equalToConstant: .zero) - self.imageTopSpaceViewConstraint?.isActive = true - - self.imageBottomSpaceView.heightAnchor.constraint(equalTo: self.imageTopSpaceView.heightAnchor).isActive = true - } - - private func setupImageViewConstraints() { - self.imageView.translatesAutoresizingMaskIntoConstraints = false - - self.imageViewHeightConstraint = self.imageView.widthAnchor.constraint(equalToConstant: .zero) - self.imageViewHeightConstraint?.isActive = true - - self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor).isActive = true - } - - // MARK: - Accessibility - private func setupAccessibility() { - self.isAccessibilityElement = true - self.accessibilityLabel = self.viewModel.text - self.accessibilityTraits.insert(.link) - if self.image != nil { - self.accessibilityContainerType = .semanticGroup - self.accessibilityTraits.insert(.image) - } - } - - // MARK: - Update UI - - private func updateImage() { - self.imageContentStackView.isHidden = self.image == nil - self.imageView.image = self.image - if self.image == nil { - self.accessibilityTraits.remove(.image) - self.accessibilityContainerType = .none - } else { - self.accessibilityTraits.insert(.image) - self.accessibilityContainerType = .semanticGroup - } - } - - private func updateContentStackViewSpacing() { - // Reload spacing only if value changed and constraint is active - if self.contentStackViewSpacing != self.contentStackView.spacing { - self.contentStackView.spacing = contentStackViewSpacing - self.invalidateIntrinsicContentSize() - } - } - - // MARK: - Subscribe - - private func setupSubscriptions() { - // Attributed Text - self.viewModel.$attributedText.subscribe(in: &self.subscriptions) { [weak self] attributedText in - guard let self, let attributedText else { return } - - self.textLabel.attributedText = attributedText.leftValue - } - - // Spacing - self.viewModel.$spacing.subscribe(in: &self.subscriptions) { [weak self] spacing in - guard let self else { return } - - self.contentStackViewSpacing = spacing - self._contentStackViewSpacing.update(traitCollection: self.traitCollection) - - self.updateContentStackViewSpacing() - } - - // Image Size - self.viewModel.$imageSize.subscribe(in: &self.subscriptions) { [weak self] imageSize in - guard let self, let imageSize else { return } - - self.imageViewHeightConstraint?.constant = imageSize.size - self.imageView.updateConstraintsIfNeeded() - - self.imageTopSpaceViewConstraint?.constant = imageSize.padding - self.imageTopSpaceView.updateConstraintsIfNeeded() - } - - // Image Tint Color - self.viewModel.$imageTintColor.subscribe(in: &self.subscriptions) { [weak self] imageTintColor in - guard let self else { return } - - self.imageView.tintColor = imageTintColor.uiColor - } - - // Image Position - self.viewModel.$isTrailingImage.subscribe(in: &self.subscriptions) { [weak self] isTrailingImage in - guard let self else { return } - - self.contentStackView.semanticContentAttribute = isTrailingImage ? .forceRightToLeft : .forceLeftToRight - } - } - - // MARK: - Label priorities - - public func setLabelContentCompressionResistancePriority( - _ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis - ) { - self.textLabel.setContentCompressionResistancePriority(priority, for: axis) - } - - public func setLabelContentHuggingPriority( - _ priority: UILayoutPriority, - for axis: NSLayoutConstraint.Axis - ) { - self.textLabel.setContentHuggingPriority(priority, for: axis) - } - - // MARK: - Trait Collection - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // Update spacings - self._contentStackViewSpacing.update(traitCollection: self.traitCollection) - self.updateContentStackViewSpacing() - - self.viewModel.contentSizeCategoryDidUpdate() - } -} diff --git a/core/Sources/Components/TextLink/View/UIKit/TextLinkUIViewSnapshotTests.swift b/core/Sources/Components/TextLink/View/UIKit/TextLinkUIViewSnapshotTests.swift deleted file mode 100644 index f910838ff..000000000 --- a/core/Sources/Components/TextLink/View/UIKit/TextLinkUIViewSnapshotTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// TextLinkUIViewSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 19/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -@testable import SparkCore - -final class TextLinkUIViewSnapshotTests: UIKitComponentSnapshotTestCase { - - // MARK: - Properties - - private let theme: Theme = SparkTheme.shared - - // MARK: - Tests - - func test() { - let scenarios = TextLinkScenarioSnapshotTests.allCases - - for scenario in scenarios { - let configurations: [TextLinkConfigurationSnapshotTests] = scenario.configuration( - isSwiftUIComponent: false - ) - for configuration in configurations { - - let view: TextLinkUIView = .init( - theme: self.theme, - text: configuration.type.text, - textHighlightRange: configuration.type.textHighlightRange, - intent: configuration.intent, - typography: configuration.size.typography, - variant: configuration.variant, - image: configuration.image?.leftValue, - alignment: configuration.alignment - ) - view.textAlignment = .left - view.numberOfLines = 0 - - self.assertSnapshot( - matching: view, - modes: configuration.modes, - sizes: configuration.sizes, - testName: configuration.testName() - ) - } - } - } -} diff --git a/core/Sources/Components/TextLink/ViewModel/TextLinkViewModel.swift b/core/Sources/Components/TextLink/ViewModel/TextLinkViewModel.swift deleted file mode 100644 index 4e0787ca1..000000000 --- a/core/Sources/Components/TextLink/ViewModel/TextLinkViewModel.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// TextLinkViewModel.swift -// SparkCore -// -// Created by robin.lemaire on 08/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation - -// sourcery: AutoPublisherTest, AutoViewModelStub -// sourcery: imageTintColor = "Identical" -class TextLinkViewModel: ObservableObject { - - // MARK: - State Properties - - var theme: any Theme { - didSet { - self.updateContentAndImageSize() - } - } - - var text: String { - didSet { - guard self.text != oldValue else { return } - self.contentDidUpdate() - } - } - - var textHighlightRange: NSRange? { - didSet { - guard self.textHighlightRange != oldValue else { return } - self.contentDidUpdate() - } - } - - var intent: TextLinkIntent { - didSet { - guard self.intent != oldValue else { return } - self.contentDidUpdate() - } - } - - var isHighlighted: Bool = false { - didSet { - guard self.isHighlighted != oldValue else { return } - self.contentDidUpdate() - } - } - - var variant: TextLinkVariant { - didSet { - guard self.variant != oldValue else { return } - self.contentDidUpdate() - } - } - - var typography: TextLinkTypography { - didSet { - guard self.typography != oldValue else { return } - self.updateAll() - } - } - - var alignment: TextLinkAlignment { - didSet { - guard self.alignment != oldValue else { return } - self.aligmentDidUpdate() - } - } - - // MARK: - Published Properties - - @Published private(set) var attributedText: AttributedStringEither? - @Published private(set) var spacing: CGFloat = .zero - @Published private(set) var imageSize: TextLinkImageSize? - @Published private(set) var imageTintColor: any ColorToken = ColorTokenDefault.clear - @Published private(set) var isTrailingImage: Bool = false - - // MARK: - Private Properties - - private let frameworkType: FrameworkType - - private var typographies: TextLinkTypographies? - - // MARK: - UseCases - - let getColorUseCase: TextLinkGetColorUseCaseable - let getTypographiesUseCase: TextLinkGetTypographiesUseCaseable - let getAttributedStringUseCase: TextLinkGetAttributedStringUseCaseable - let getImageSizeUseCase: TextLinkGetImageSizeUseCaseable - - // MARK: - Initialization - - init( - for frameworkType: FrameworkType, - theme: any Theme, - text: String, - textHighlightRange: NSRange?, - intent: TextLinkIntent, - typography: TextLinkTypography, - variant: TextLinkVariant, - alignment: TextLinkAlignment, - getColorUseCase: TextLinkGetColorUseCaseable = TextLinkGetColorUseCase(), - getTypographiesUseCase: TextLinkGetTypographiesUseCaseable = TextLinkGetTypographiesUseCase(), - getAttributedStringUseCase: TextLinkGetAttributedStringUseCaseable = TextLinkGetAttributedStringUseCase(), - getImageSizeUseCase: TextLinkGetImageSizeUseCaseable = TextLinkGetImageSizeUseCase() - ) { - self.frameworkType = frameworkType - - self.theme = theme - self.text = text - self.textHighlightRange = textHighlightRange - self.intent = intent - self.typography = typography - self.variant = variant - self.alignment = alignment - - self.getColorUseCase = getColorUseCase - self.getTypographiesUseCase = getTypographiesUseCase - self.getAttributedStringUseCase = getAttributedStringUseCase - self.getImageSizeUseCase = getImageSizeUseCase - - // Load the values directly on init just for SwiftUI - if frameworkType == .swiftUI { - self.updateAll() - } - } - - // MARK: - Load - - func load() { - // Update all values when UIKit view is ready to receive published values - if self.frameworkType == .uiKit { - self.updateAll() - } - } - - // MARK: - Internal Did Update - - func contentSizeCategoryDidUpdate() { - /// The image size depend of the size of the font. - /// So each time the content size category changed - /// We must get the new value from the current dynanic font size - self.imageSizeDidUpdate() - } - - // MARK: - Private Did Update - - private func updateAll() { - self.updateContentAndImageSize() - self.aligmentDidUpdate() - } - - private func updateContentAndImageSize() { - self.contentDidUpdate(forceToReload: true) - self.imageSizeDidUpdate() - } - - private func contentDidUpdate(forceToReload: Bool = false) { - let color = self.getColorUseCase.execute( - intent: self.intent, - isHighlighted: self.isHighlighted, - colors: self.theme.colors - ) - - self.spacing = self.theme.layout.spacing.medium - - self.imageTintColor = color - - self.attributedText = self.getAttributedStringUseCase.execute( - frameworkType: self.frameworkType, - text: self.text, - textColorToken: color, - textHighlightRange: self.textHighlightRange, - isHighlighted: self.isHighlighted, - variant: self.variant, - typographies: self.getTypographies(forceToReload: true) - ) - } - - private func aligmentDidUpdate() { - self.isTrailingImage = self.alignment.isTrailingImage - } - - private func imageSizeDidUpdate() { - self.imageSize = self.getImageSizeUseCase.execute( - typographies: self.getTypographies() - ) - } - - // MARK: - Private Getter - - private func getTypographies(forceToReload: Bool = false) -> TextLinkTypographies { - if let typographies = self.typographies, !forceToReload { - return typographies - } - - let typographies = self.getTypographiesUseCase.execute( - textLinkTypography: self.typography, - typography: self.theme.typography - ) - self.typographies = typographies - - return typographies - } -} diff --git a/core/Sources/Components/TextLink/ViewModel/TextLinkViewModelTests.swift b/core/Sources/Components/TextLink/ViewModel/TextLinkViewModelTests.swift deleted file mode 100644 index aafabd731..000000000 --- a/core/Sources/Components/TextLink/ViewModel/TextLinkViewModelTests.swift +++ /dev/null @@ -1,801 +0,0 @@ -// -// TextLinkCommonViewModelTests.swift -// SparkCoreUnitTests -// -// Created by robin.lemaire on 08/12/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore -import Combine - -final class TextLinkViewModelTests: XCTestCase { - - // MARK: - Properties - - private var subscriptions = Set() - - // MARK: - Setup - - override func tearDown() { - super.tearDown() - - // Clear publishers - self.subscriptions.removeAll() - } - - // MARK: - Init & Load Tests - - func test_properties_on_init_when_frameworkType_is_UIKit() throws { - try self.testAllOnInitOrLoad( - givenFrameworkType: .uiKit, - givenIsInit: true, - expectedIsNumberOfSinksCalled: true, - expectedIsSinksValues: false, - expectedIsNumberOfCallsCalled: false - ) - } - - func test_properties_on_init_when_frameworkType_is_SwiftUI() throws { - try self.testAllOnInitOrLoad( - givenFrameworkType: .swiftUI, - givenIsInit: true, - expectedIsNumberOfSinksCalled: true, - expectedIsSinksValues: true, - expectedIsNumberOfCallsCalled: true - ) - } - - func test_published_properties_on_load_when_frameworkType_is_UIKit() throws { - try self.testAllOnInitOrLoad( - givenFrameworkType: .uiKit, - givenIsInit: false, - expectedIsNumberOfSinksCalled: true, - expectedIsSinksValues: true, - expectedIsNumberOfCallsCalled: true - ) - } - - func test_published_properties_on_load_when_frameworkType_is_SwiftUI() throws { - try self.testAllOnInitOrLoad( - givenFrameworkType: .swiftUI, - givenIsInit: false, - expectedIsNumberOfSinksCalled: false, - expectedIsSinksValues: false, - expectedIsNumberOfCallsCalled: false - ) - } - - private func testAllOnInitOrLoad( - givenFrameworkType: FrameworkType, - givenIsInit: Bool, - expectedIsNumberOfSinksCalled: Bool, - expectedIsSinksValues: Bool, - expectedIsNumberOfCallsCalled: Bool - ) throws { - // GIVEN - let textMock = "My Text" - let textHighlightRangeMock = NSRange() - let intentMock: TextLinkIntent = .accent - let typographyMock: TextLinkTypography = .body1 - let variantMock: TextLinkVariant = .underline - let alignmentMock: TextLinkAlignment = .leadingImage - - // WHEN - let stub = Stub( - frameworkType: givenFrameworkType, - text: textMock, - textHighlightRange: textHighlightRangeMock, - intent: intentMock, - typography: typographyMock, - variant: variantMock, - alignment: alignmentMock - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - if !givenIsInit { - // Reset all UseCase mock - stub.resetMockedData() - - viewModel.load() - } - - // THEN - XCTAssertIdentical( - viewModel.theme as? ThemeGeneratedMock, - stub.themeMock, - "Wrong theme value" - ) - XCTAssertEqual( - viewModel.text, - textMock, - "Wrong text value" - ) - XCTAssertEqual( - viewModel.intent, - intentMock, - "Wrong intent value" - ) - XCTAssertFalse( - viewModel.isHighlighted, - "Wrong isHighlighted value" - ) - XCTAssertEqual( - viewModel.textHighlightRange, - textHighlightRangeMock, - "Wrong range value" - ) - XCTAssertEqual( - viewModel.typography, - typographyMock, - "Wrong typography value" - ) - XCTAssertEqual( - viewModel.variant, - variantMock, - "Wrong variant value" - ) - XCTAssertEqual( - viewModel.alignment, - alignmentMock, - "Wrong alignment value" - ) - - // ** - // Published count (the properties are already test on load and init tests) - let expectedNumberOfSinks = expectedIsNumberOfSinksCalled ? 1 : 0 - TextLinkViewModelPublisherTest.XCTAssert( - attributedText: stub.attributedTextPublisherMock, - expectedNumberOfSinks: expectedNumberOfSinks, - expectedValue: expectedIsSinksValues ? stub.attributedStringMock : nil - ) - TextLinkViewModelPublisherTest.XCTAssert( - spacing: stub.spacingPublisherMock, - expectedNumberOfSinks: expectedNumberOfSinks, - expectedValue: expectedIsSinksValues ? stub.spacingMock : .zero - ) - TextLinkViewModelPublisherTest.XCTAssert( - imageSize: stub.imageSizePublisherMock, - expectedNumberOfSinks: expectedNumberOfSinks, - expectedValue: expectedIsSinksValues ? stub.imageSizeMock : nil - ) - if expectedIsSinksValues { - TextLinkViewModelPublisherTest.XCTAssert( - imageTintColor: stub.imageTintColorPublisherMock, - expectedNumberOfSinks: expectedNumberOfSinks, - expectedValue: stub.colorMock - ) - } else { - TextLinkViewModelPublisherTest.XCTSinksCount( - imageTintColor: stub.imageTintColorPublisherMock, - expectedNumberOfSinks: expectedNumberOfSinks - ) - } - - TextLinkViewModelPublisherTest.XCTAssert( - isTrailingImage: stub.isTrailingImagePublisherMock, - expectedNumberOfSinks: expectedNumberOfSinks, - expectedValue: expectedIsSinksValues ? stub.isTrailingImageMock : false - ) - // ** - - // Use Cases count (the parameters and returns are already test on load and init tests) - let expectedNumberOfCalls = expectedIsNumberOfCallsCalled ? 1 : 0 - TextLinkGetTypographiesUseCaseableMockTest.XCTAssert( - stub.getTypographiesUseCaseMock, - expectedNumberOfCalls: expectedNumberOfCalls, - givenTextLinkTypography: typographyMock, - givenTypography: stub.themeMock.typography as? TypographyGeneratedMock, - expectedReturnValue: stub.typographiesMock - ) - TextLinkGetAttributedStringUseCaseableMockTest.XCTAssert( - stub.getAttributedStringUseCaseMock, - expectedNumberOfCalls: expectedNumberOfCalls, - givenFrameworkType: givenFrameworkType, - givenText: textMock, - givenTextColorToken: stub.colorMock, - givenTextHighlightRange: textHighlightRangeMock, - givenIsHighlighted: false, - givenVariant: variantMock, - givenTypographies: stub.typographiesMock, - expectedReturnValue: stub.attributedStringMock - ) - TextLinkGetImageSizeUseCaseableMockTest.XCTAssert( - stub.getImageSizeUseCaseMock, - expectedNumberOfCalls: expectedNumberOfCalls, - givenTypographies: stub.typographiesMock, - expectedReturnValue: stub.imageSizeMock - ) - } - - // MARK: - Update State Properties Tests - - func test_set_themes() { - // GIVEN - let newTheme = ThemeGeneratedMock.mocked() - - let stub = Stub(frameworkType: .swiftUI) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.theme = newTheme - - // THEN - XCTAssertIdentical( - viewModel.theme as? ThemeGeneratedMock, - newTheme, - "Wrong theme value" - ) - - // ** - // Published count (the properties are already test on load and init tests) - TextLinkViewModelPublisherTest.XCTSinksCount( - attributedText: stub.attributedTextPublisherMock, - expectedNumberOfSinks: 1 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - spacing: stub.spacingPublisherMock, - expectedNumberOfSinks: 1 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageSize: stub.imageSizePublisherMock, - expectedNumberOfSinks: 1 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageTintColor: stub.imageTintColorPublisherMock, - expectedNumberOfSinks: 1 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - isTrailingImage: stub.isTrailingImagePublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // Use Cases count (the parameters and returns are already test on load and init tests) - TextLinkGetTypographiesUseCaseableMockTest.XCTCallsCount( - stub.getTypographiesUseCaseMock, - executeWithTextLinkTypographyAndTypographyNumberOfCalls: 1 - ) - TextLinkGetAttributedStringUseCaseableMockTest.XCTCallsCount( - stub.getAttributedStringUseCaseMock, - executeWithFrameworkTypeAndTextAndTextColorTokenAndTextHighlightRangeAndIsHighlightedAndVariantAndTypographiesNumberOfCalls: 1 - ) - TextLinkGetImageSizeUseCaseableMockTest.XCTCallsCount( - stub.getImageSizeUseCaseMock, - executeWithTypographiesNumberOfCalls: 1 - ) - } - - func test_set_text_with_different_new_value() { - self.testSetText( - givenIsDifferentNewValue: true - ) - } - - func test_set_text_with_same_new_value() { - self.testSetText( - givenIsDifferentNewValue: false - ) - } - - private func testSetText( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue = "Text" - let newValue = givenIsDifferentNewValue ? "New Text" : defaultValue - - let stub = Stub( - frameworkType: .swiftUI, - text: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.text = newValue - - // THEN - XCTAssertEqual( - viewModel.text, - newValue, - "Wrong text value" - ) - - self.testPublishersAndUseCasesWhenContentChanged( - stub: stub, - givenIsContentDidUpdate: givenIsDifferentNewValue - ) - } - - func test_set_textHighlightRange_with_different_new_value() { - self.testSetTextHighlightRange( - givenIsDifferentNewValue: true - ) - } - - func test_set_textHighlightRange_with_same_new_value() { - self.testSetTextHighlightRange( - givenIsDifferentNewValue: false - ) - } - - private func testSetTextHighlightRange( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue = NSRange(location: 0, length: 1) - let newValue = givenIsDifferentNewValue ? .init(location: 1, length: 2) : defaultValue - - let stub = Stub( - frameworkType: .swiftUI, - textHighlightRange: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.textHighlightRange = newValue - - // THEN - XCTAssertEqual( - viewModel.textHighlightRange, - newValue, - "Wrong range value" - ) - - self.testPublishersAndUseCasesWhenContentChanged( - stub: stub, - givenIsContentDidUpdate: givenIsDifferentNewValue - ) - } - - func test_set_intent_with_different_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: true - ) - } - - func test_set_intent_with_same_new_value() { - self.testSetIntent( - givenIsDifferentNewValue: false - ) - } - - func testSetIntent( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: TextLinkIntent = .main - let newValue = givenIsDifferentNewValue ? .accent : defaultValue - - let stub = Stub( - frameworkType: .swiftUI, - intent: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.intent = newValue - - // THEN - XCTAssertEqual( - viewModel.intent, - newValue, - "Wrong intent value" - ) - - self.testPublishersAndUseCasesWhenContentChanged( - stub: stub, - givenIsContentDidUpdate: givenIsDifferentNewValue - ) - } - - func test_set_isHighlighted_with_different_new_value() { - self.testSetIsHighlighted( - givenIsDifferentNewValue: true - ) - } - - func test_set_isHighlighted_with_same_new_value() { - self.testSetIsHighlighted( - givenIsDifferentNewValue: false - ) - } - - private func testSetIsHighlighted( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let newValue = givenIsDifferentNewValue ? true : false // On init, the value is false - - let stub = Stub( - frameworkType: .swiftUI - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.isHighlighted = newValue - - // THEN - self.testPublishersAndUseCasesWhenContentChanged( - stub: stub, - givenIsContentDidUpdate: givenIsDifferentNewValue - ) - } - - func test_set_typography_with_different_new_value() { - self.testSetTypography( - givenIsDifferentNewValue: true - ) - } - - func test_set_typography_with_same_new_value() { - self.testSetTypography( - givenIsDifferentNewValue: false - ) - } - - private func testSetTypography( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: TextLinkTypography = .body1 - let newValue = givenIsDifferentNewValue ? .body2 : defaultValue - - let stub = Stub( - frameworkType: .swiftUI, - typography: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.typography = newValue - - // THEN - XCTAssertEqual( - viewModel.typography, - newValue, - "Wrong typography value" - ) - - self.testPublishersAndUseCasesWhenContentChanged( - stub: stub, - givenIsContentDidUpdate: givenIsDifferentNewValue, - givenIsImageSizeDidUpdate: givenIsDifferentNewValue - ) - } - - func test_set_variant_with_different_new_value() { - self.testSetVariant( - givenIsDifferentNewValue: true - ) - } - - func test_set_variant_with_same_new_value() { - self.testSetVariant( - givenIsDifferentNewValue: false - ) - } - - private func testSetVariant( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: TextLinkVariant = .underline - let newValue = givenIsDifferentNewValue ? .none : defaultValue - - let stub = Stub( - frameworkType: .swiftUI, - variant: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.variant = newValue - - // THEN - XCTAssertEqual( - viewModel.variant, - newValue, - "Wrong variant value" - ) - - self.testPublishersAndUseCasesWhenContentChanged( - stub: stub, - givenIsContentDidUpdate: givenIsDifferentNewValue - ) - } - - func test_set_alignment_with_different_new_value() { - self.testSetAlignment( - givenIsDifferentNewValue: true - ) - } - - func test_set_alignment_with_same_new_value() { - self.testSetAlignment( - givenIsDifferentNewValue: false - ) - } - - private func testSetAlignment( - givenIsDifferentNewValue: Bool - ) { - // GIVEN - let defaultValue: TextLinkAlignment = .leadingImage - let newValue = givenIsDifferentNewValue ? .trailingImage : defaultValue - - let stub = Stub( - frameworkType: .swiftUI, - alignment: defaultValue - ) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.alignment = newValue - - // THEN - XCTAssertEqual( - viewModel.alignment, - newValue, - "Wrong alignment value" - ) - - // ** - // Published count (the properties are already test on load and init tests) - TextLinkViewModelPublisherTest.XCTSinksCount( - attributedText: stub.attributedTextPublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - spacing: stub.spacingPublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageSize: stub.imageSizePublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageTintColor: stub.imageTintColorPublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - isTrailingImage: stub.isTrailingImagePublisherMock, - expectedNumberOfSinks: givenIsDifferentNewValue ? 1 : 0 - ) - // ** - - // Use Cases count (the parameters and returns are already test on load and init tests) - TextLinkGetTypographiesUseCaseableMockTest.XCTCallsCount( - stub.getTypographiesUseCaseMock, - executeWithTextLinkTypographyAndTypographyNumberOfCalls: 0 - ) - TextLinkGetAttributedStringUseCaseableMockTest.XCTCallsCount( - stub.getAttributedStringUseCaseMock, - executeWithFrameworkTypeAndTextAndTextColorTokenAndTextHighlightRangeAndIsHighlightedAndVariantAndTypographiesNumberOfCalls: 0 - ) - TextLinkGetImageSizeUseCaseableMockTest.XCTCallsCount( - stub.getImageSizeUseCaseMock, - executeWithTypographiesNumberOfCalls: 0 - ) - } - - private func testPublishersAndUseCasesWhenContentChanged( - stub: Stub, - givenIsContentDidUpdate: Bool, - givenIsImageSizeDidUpdate: Bool = false - ) { - // ** - // Published count (the properties are already test on load and init tests) - TextLinkViewModelPublisherTest.XCTSinksCount( - attributedText: stub.attributedTextPublisherMock, - expectedNumberOfSinks: givenIsContentDidUpdate ? 1 : 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - spacing: stub.spacingPublisherMock, - expectedNumberOfSinks: givenIsContentDidUpdate ? 1 : 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageSize: stub.imageSizePublisherMock, - expectedNumberOfSinks: givenIsImageSizeDidUpdate ? 1 : 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageTintColor: stub.imageTintColorPublisherMock, - expectedNumberOfSinks: givenIsContentDidUpdate ? 1 : 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - isTrailingImage: stub.isTrailingImagePublisherMock, - expectedNumberOfSinks: givenIsImageSizeDidUpdate ? 1 : 0 - ) - // ** - - // Use Cases count (the parameters and returns are already test on load and init tests) - TextLinkGetTypographiesUseCaseableMockTest.XCTCallsCount( - stub.getTypographiesUseCaseMock, - executeWithTextLinkTypographyAndTypographyNumberOfCalls: givenIsContentDidUpdate ? 1 : 0 - ) - TextLinkGetAttributedStringUseCaseableMockTest.XCTCallsCount( - stub.getAttributedStringUseCaseMock, - executeWithFrameworkTypeAndTextAndTextColorTokenAndTextHighlightRangeAndIsHighlightedAndVariantAndTypographiesNumberOfCalls: givenIsContentDidUpdate ? 1 : 0 - ) - TextLinkGetImageSizeUseCaseableMockTest.XCTCallsCount( - stub.getImageSizeUseCaseMock, - executeWithTypographiesNumberOfCalls: givenIsImageSizeDidUpdate ? 1 : 0 - ) - } - - // MARK: - DidUpdate Tests - - func test_contentSizeCategoryDidUpdate() { - // GIVEN - let stub = Stub(frameworkType: .swiftUI) - let viewModel = stub.viewModel - - stub.subscribePublishers(on: &self.subscriptions) - - // Reset all UseCase mock - stub.resetMockedData() - - // WHEN - viewModel.contentSizeCategoryDidUpdate() - - // THEN - - // ** - // Published properties - TextLinkViewModelPublisherTest.XCTSinksCount( - attributedText: stub.attributedTextPublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - spacing: stub.spacingPublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageSize: stub.imageSizePublisherMock, - expectedNumberOfSinks: 1 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - imageTintColor: stub.imageTintColorPublisherMock, - expectedNumberOfSinks: 0 - ) - TextLinkViewModelPublisherTest.XCTSinksCount( - isTrailingImage: stub.isTrailingImagePublisherMock, - expectedNumberOfSinks: 0 - ) - // ** - - // Use Cases - TextLinkGetTypographiesUseCaseableMockTest.XCTCallsCount( - stub.getTypographiesUseCaseMock, - executeWithTextLinkTypographyAndTypographyNumberOfCalls: 0 - ) - TextLinkGetAttributedStringUseCaseableMockTest.XCTCallsCount( - stub.getAttributedStringUseCaseMock, - executeWithFrameworkTypeAndTextAndTextColorTokenAndTextHighlightRangeAndIsHighlightedAndVariantAndTypographiesNumberOfCalls: 0 - ) - TextLinkGetImageSizeUseCaseableMockTest.XCTCallsCount( - stub.getImageSizeUseCaseMock, - executeWithTypographiesNumberOfCalls: 1 - ) - } -} - -// MARK: - Stub - -private final class Stub: TextLinkViewModelStub { - - // MARK: - Data Properties - - let frameworkType: FrameworkType - - let themeMock = ThemeGeneratedMock.mocked() - - let colorMock = ColorTokenGeneratedMock.random() - - let typographiesMock = TextLinkTypographies.mocked() - let attributedStringMock: AttributedStringEither = .left(.init(string: "AS")) - var spacingMock: CGFloat { - return self.themeMock.layout.spacing.medium - } - let imageSizeMock = TextLinkImageSize.mocked() - var isTrailingImageMock: Bool { - return self.viewModel.alignment.isTrailingImage - } - - // MARK: - Initialization - - init( - frameworkType: FrameworkType, - text: String = "My Text", - textHighlightRange: NSRange? = nil, - intent: TextLinkIntent = .main, - typography: TextLinkTypography = .body1, - variant: TextLinkVariant = .underline, - alignment: TextLinkAlignment = .leadingImage - ) { - // Data properties - self.frameworkType = frameworkType - - // ** - // Use Cases - let getColorUseCaseMock = TextLinkGetColorUseCaseableGeneratedMock() - getColorUseCaseMock.executeWithIntentAndIsHighlightedAndColorsReturnValue = self.colorMock - - let getTypographiesUseCaseMock = TextLinkGetTypographiesUseCaseableGeneratedMock() - getTypographiesUseCaseMock.executeWithTextLinkTypographyAndTypographyReturnValue = self.typographiesMock - - let getAttributedStringUseCaseMock = TextLinkGetAttributedStringUseCaseableGeneratedMock() - getAttributedStringUseCaseMock.executeWithFrameworkTypeAndTextAndTextColorTokenAndTextHighlightRangeAndIsHighlightedAndVariantAndTypographiesReturnValue = self.attributedStringMock - - let getImageSizeUseCaseMock = TextLinkGetImageSizeUseCaseableGeneratedMock() - getImageSizeUseCaseMock.executeWithTypographiesReturnValue = self.imageSizeMock - // ** - - // View Model - let viewModel = TextLinkViewModel( - for: frameworkType, - theme: self.themeMock, - text: text, - textHighlightRange: textHighlightRange, - intent: intent, - typography: typography, - variant: variant, - alignment: alignment, - getColorUseCase: getColorUseCaseMock, - getTypographiesUseCase: getTypographiesUseCaseMock, - getAttributedStringUseCase: getAttributedStringUseCaseMock, - getImageSizeUseCase: getImageSizeUseCaseMock - ) - - super.init( - viewModel: viewModel, - getColorUseCaseMock: getColorUseCaseMock, - getTypographiesUseCaseMock: getTypographiesUseCaseMock, - getAttributedStringUseCaseMock: getAttributedStringUseCaseMock, - getImageSizeUseCaseMock: getImageSizeUseCaseMock - ) - } -} diff --git a/core/Sources/Extension/Font/FontTextStyle+Extension.swift b/core/Sources/Extension/Font/FontTextStyle+Extension.swift deleted file mode 100644 index 155001238..000000000 --- a/core/Sources/Extension/Font/FontTextStyle+Extension.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// FontTextStyle+Extension.swift -// SparkCore -// -// Created by robin.lemaire on 18/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -extension Font.TextStyle { - - // MARK: - Initialization - - init(from textStyle: TextStyle) { - switch textStyle { - case .largeTitle: - self = .largeTitle - case .title: - self = .title - case .title2: - self = .title2 - case .title3: - self = .title3 - case .headline: - self = .headline - case .subheadline: - self = .subheadline - case .body: - self = .body - case .callout: - self = .callout - case .footnote: - self = .footnote - case .caption: - self = .caption - case .caption2: - self = .caption2 - } - } -} diff --git a/core/Sources/Extension/Font/FontTextStyle+ExtensionTests.swift b/core/Sources/Extension/Font/FontTextStyle+ExtensionTests.swift deleted file mode 100644 index 1675ac930..000000000 --- a/core/Sources/Extension/Font/FontTextStyle+ExtensionTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// FontTextStyle+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 18/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -@testable import SparkCore - -final class FontTextStyleExtensionTests: XCTestCase { - - // MARK: - Tests - - func test_init_for_all_TextStyle_cases() { - // GIVEN - let items: [(givenTextStyle: TextStyle, expectedFontTextStyle: Font.TextStyle)] = [ - (.largeTitle, .largeTitle), - (.title, .title), - (.title2, .title2), - (.title3, .title3), - (.headline, .headline), - (.subheadline, .subheadline), - (.body, .body), - (.callout, .callout), - (.footnote, .footnote), - (.caption, .caption), - (.caption2, .caption2) - ] - - for item in items { - // WHEN - let textStyle = Font.TextStyle(from: item.givenTextStyle) - - // THEN - XCTAssertEqual(textStyle, - item.expectedFontTextStyle, - "Wrong Font.TextStyle value for .\(item.givenTextStyle) case") - } - } -} diff --git a/core/Sources/Extension/PropertyWrapper/ScaledUIMetric.swift b/core/Sources/Extension/PropertyWrapper/ScaledUIMetric.swift deleted file mode 100644 index 6c01d6c5c..000000000 --- a/core/Sources/Extension/PropertyWrapper/ScaledUIMetric.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// ScaledUIMetric.swift -// Spark -// -// Created by janniklas.freundt.ext on 05.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// ScaledUIMetric is a property wrapper for UIKit. It scales values according to the current trait collection content size and behaves similar to the @`ScaledMetric`-property wrapper for SwiftUI. -@propertyWrapper struct ScaledUIMetric where Value: BinaryFloatingPoint { - // MARK: - Properties - - /// Returns the scaled value for the `baseValue` according to the trait collection. When setting this property a new baseValue is set. - var wrappedValue: Value { - get { - return self.scaledValue(for: self.traitCollection) - } - set { - self.baseValue = newValue - } - } - - /// The base value for the calculation. - private var baseValue: Value - - /// The font metrics the scaling is based on. The default value is `.body`-text-style. - private let metrics: UIFontMetrics - - /// The trait collection used for the scaling operation. The default value is nil, which means the current trait collection will be used (`UITraitCollection.current`). - private var traitCollection: UITraitCollection? - - // MARK: - Initialization - - /// Initialize a new property wrapper. - /// - Parameters: - /// - baseValue: The base value used in the calculation. - /// - metrics: The font metrics the scaling is based on. - /// - traitCollection: The trait collection used for the scaling operation. The default value is nil, which means the current trait collection will be used (`UITraitCollection.current`). - init( - wrappedValue baseValue: Value, - relativeTo metrics: UIFontMetrics, - traitCollection: UITraitCollection? = nil - ) { - self.baseValue = baseValue - self.metrics = metrics - self.traitCollection = traitCollection - } - - /// Initialize a new property wrapper. - /// - Parameters: - /// - baseValue: The base value used in the calculation. - /// - metrics: The text style the scaling is based on. The default value is `.body`-text-style. - /// - traitCollection: The trait collection used for the scaling operation. The default value is nil, which means the current trait collection will be used (`UITraitCollection.current`). - init( - wrappedValue baseValue: Value, - relativeTo textStyle: UIFont.TextStyle = .body, - traitCollection: UITraitCollection? = nil - ) { - self.init( - wrappedValue: baseValue, - relativeTo: UIFontMetrics(forTextStyle: textStyle), - traitCollection: traitCollection - ) - } - - // MARK: - Methods - - /// Update the trait collection. - /// - Parameter traitCollection: a new trait collection. - mutating func update(traitCollection: UITraitCollection?) { - self.traitCollection = traitCollection - } - - /// Returns a scaled base value for the given trait colletion. - /// - Parameter traitCollection: the trait collection - /// - Returns: a scaled value - func scaledValue(for traitCollection: UITraitCollection?) -> Value { - Value(self.metrics.scaledValue(for: CGFloat(self.baseValue), compatibleWith: traitCollection ?? UITraitCollection.current)) - } -} diff --git a/core/Sources/Extension/PropertyWrapper/ScaledUIMetricTests.swift b/core/Sources/Extension/PropertyWrapper/ScaledUIMetricTests.swift deleted file mode 100644 index 5393c9c6e..000000000 --- a/core/Sources/Extension/PropertyWrapper/ScaledUIMetricTests.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// ScaledUIMetricTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 05.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -@testable import SparkCore -import SwiftUI -import XCTest - -// swiftlint:disable force_unwrapping -final class ScaledUIMetricTests: XCTestCase { - - // MARK: - Properties - - @ScaledUIMetric(traitCollection: UITraitCollection(preferredContentSizeCategory: .medium)) var basicMetric: CGFloat = 11 - @ScaledUIMetric(relativeTo: .body) var bodyMetric: CGFloat = 22 - @ScaledUIMetric(relativeTo: .largeTitle) var titleMetric: CGFloat = 0 - @ScaledUIMetric var zeroMetric: CGFloat = 0 - @ScaledUIMetric var comparisonMetric: CGFloat = 10 - @ScaledUIMetric var comparisonMetricExtraLarge: CGFloat = 10 - - // MARK: - Tests - func test_property_wrapper_basic() throws { - // Then - XCTAssertEqual(self.basicMetric, 10.666, accuracy: 0.1, "wrong scaled value") - - // Given - self.basicMetric = 110 - - // Then - XCTAssertEqual(self.basicMetric, 105, accuracy: 0.1, "wrong scaled value") - } - - func test_property_wrapper_body() throws { - // Given - self._bodyMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)) - - // Then - XCTAssertEqual(self.bodyMetric, 62.0, accuracy: 0.1, "wrong scaled value") - - // Given - self._bodyMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: .extraSmall)) - - // Then - XCTAssertEqual(self.bodyMetric, 19.0, accuracy: 0.1, "wrong scaled value") - } - - func test_property_wrapper_large_title() throws { - // Given - titleMetric = 22 - self._titleMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)) - - // Then - XCTAssertEqual(self.titleMetric, 37.66, accuracy: 0.1, "wrong scaled value") - - // Given - self._titleMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: .extraSmall)) - - // Then - XCTAssertEqual(self.titleMetric, 20.333, accuracy: 0.1, "wrong scaled value") - } - - func test_property_wrapper_zero() throws { - // Given - self._zeroMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)) - - // Then - XCTAssertEqual(self.zeroMetric, 0, "wrong scaled value") - - // Given - self._zeroMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: .extraSmall)) - - // Then - XCTAssertEqual(self.zeroMetric, 0, "wrong scaled value") - } - - /// Compares the result of SwiftUI `ScaledMetric` with the result of `ScaledUIMetric`. - func test_property_wrapper_behaves_like_swiftui_scaled_metric() throws { - // Given - let size = UIContentSizeCategory.extraLarge - self._comparisonMetric.update(traitCollection: UITraitCollection(preferredContentSizeCategory: size)) - - // Then - let expectation = expectation(description: "swiftui view did appear") - - self.givenSwiftUIView(size: ContentSizeCategory(size)!, didAppearHandler: { swiftUISize in - XCTAssertEqual(self.comparisonMetric, swiftUISize, accuracy: 0.1, "ScaledMetric and ScaledUIMetric produce different values") - - expectation.fulfill() - }) - // Wait for the async request to complete - waitForExpectations(timeout: 5, handler: nil) - } - - /// Compares the result of SwiftUI `ScaledMetric` with the result of `ScaledUIMetric`. - func test_property_wrapper_behaves_like_swiftui_scaled_metric_extra_large() throws { - // Given - let size = UIContentSizeCategory.accessibilityExtraExtraExtraLarge - self._comparisonMetricExtraLarge.update(traitCollection: UITraitCollection(preferredContentSizeCategory: size)) - - // Then - let expectation = expectation(description: "swiftui view did appear") - - self.givenSwiftUIView(size: ContentSizeCategory(size)!, didAppearHandler: { swiftUISize in - XCTAssertEqual(self.comparisonMetricExtraLarge, swiftUISize, accuracy: 0.1, "ScaledMetric and ScaledUIMetric produce different values") - - expectation.fulfill() - }) - // Wait for the async request to complete - waitForExpectations(timeout: 5, handler: nil) - } - - // MARK: - Helper - - private func givenSwiftUIView(size: ContentSizeCategory, didAppearHandler: @escaping ((_ value: CGFloat) -> Void)) { - - let window = UIWindow() - - let view = - TestView(didAppearHandler: { swiftUIValue in - didAppearHandler(swiftUIValue) - window.removeFromSuperview() - }) - .environment(\.sizeCategory, size) - - let viewController = UIHostingController(rootView: view) - - // Simulate the view appearance - window.rootViewController = viewController - window.makeKeyAndVisible() - } -} - -// MARK: - Test view to compare with SwiftUI ScaledMetric -private struct TestView: View { - // MARK: - Properties - - /// We have to wrap the `ScaledMetric`inside a view. Other configurations are not supported. - @ScaledMetric var value: CGFloat = 10 - - var didAppearHandler: ((_ value: CGFloat) -> Void)? - - // MARK: - Body - var body: some View { - EmptyView() - .onAppear { - self.didAppearHandler?(value) - } - } -} diff --git a/core/Sources/Extension/SparkAttributedString/SparkAttributedString.swift b/core/Sources/Extension/SparkAttributedString/SparkAttributedString.swift deleted file mode 100644 index 88f18757a..000000000 --- a/core/Sources/Extension/SparkAttributedString/SparkAttributedString.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// SparkAttributedString.swift -// SparkCore -// -// Created by alican.aycil on 02.04.24. -// Copyright © 2024 Adevinta. All rights reserved. -// - -import Foundation - -protocol SparkAttributedString {} -extension NSAttributedString: SparkAttributedString {} -extension AttributedString: SparkAttributedString {} diff --git a/core/Sources/Extension/UIFont/UIFontTextStyle+Extension.swift b/core/Sources/Extension/UIFont/UIFontTextStyle+Extension.swift deleted file mode 100644 index b1fd805bb..000000000 --- a/core/Sources/Extension/UIFont/UIFontTextStyle+Extension.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// UIFontTextStyle+Extension.swift -// SparkCore -// -// Created by robin.lemaire on 18/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension UIFont.TextStyle { - - // MARK: - Initialization - - init(from textStyle: TextStyle) { - switch textStyle { - case .largeTitle: - self = .largeTitle - case .title: - self = .title1 - case .title2: - self = .title2 - case .title3: - self = .title3 - case .headline: - self = .headline - case .subheadline: - self = .subheadline - case .body: - self = .body - case .callout: - self = .callout - case .footnote: - self = .footnote - case .caption: - self = .caption1 - case .caption2: - self = .caption2 - } - } -} diff --git a/core/Sources/Extension/UIFont/UIFontTextStyle+ExtensionTests.swift b/core/Sources/Extension/UIFont/UIFontTextStyle+ExtensionTests.swift deleted file mode 100644 index 48b91d9d1..000000000 --- a/core/Sources/Extension/UIFont/UIFontTextStyle+ExtensionTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// UIFontTextStyle+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 18/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import UIKit -@testable import SparkCore - -final class UIFontTextStyleExtensionTests: XCTestCase { - - // MARK: - Tests - - func test_init_for_all_TextStyle_cases() { - // GIVEN / WHEN - let items: [(givenTextStyle: TextStyle, expectedUIFontTextStyle: UIFont.TextStyle)] = [ - (.largeTitle, .largeTitle), - (.title, .title1), - (.title2, .title2), - (.title3, .title3), - (.headline, .headline), - (.subheadline, .subheadline), - (.body, .body), - (.callout, .callout), - (.footnote, .footnote), - (.caption, .caption1), - (.caption2, .caption2) - ] - - for item in items { - // WHEN - let textStyle = UIFont.TextStyle(from: item.givenTextStyle) - - // THEN - XCTAssertEqual(textStyle, - item.expectedUIFontTextStyle, - "Wrong UIFont.TextStyle value for .\(item.givenTextStyle) case") - } - } -} diff --git a/core/Sources/Theming/Content/Border/Border.swift b/core/Sources/Theming/Content/Border/Border.swift deleted file mode 100644 index 732b4a6ec..000000000 --- a/core/Sources/Theming/Content/Border/Border.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Border.swift -// SparkCore -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -public protocol Border { - var width: BorderWidth { get } - var radius: BorderRadius { get } -} - -// MARK: - Width - -// sourcery: AutoMockable -public protocol BorderWidth { - var none: CGFloat { get } - var small: CGFloat { get } - var medium: CGFloat { get } -} - -public extension BorderWidth { - var none: CGFloat { 0 } -} - -// MARK: - Radius - -// sourcery: AutoMockable -public protocol BorderRadius { - var none: CGFloat { get } - var small: CGFloat { get } - var medium: CGFloat { get } - var large: CGFloat { get } - var xLarge: CGFloat { get } - var full: CGFloat { get } -} - -public extension BorderRadius { - var none: CGFloat { 0 } - var full: CGFloat { .infinity } -} diff --git a/core/Sources/Theming/Content/Border/BorderDefault.swift b/core/Sources/Theming/Content/Border/BorderDefault.swift deleted file mode 100644 index f7da877a2..000000000 --- a/core/Sources/Theming/Content/Border/BorderDefault.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// BorderDefault.swift -// SparkCore -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public struct BorderDefault: Border { - - // MARK: - Properties - - public let width: BorderWidth - public let radius: BorderRadius - - // MARK: - Initialization - - public init(width: BorderWidth, radius: BorderRadius) { - self.width = width - self.radius = radius - } -} - -// MARK: - Width - -public struct BorderWidthDefault: BorderWidth { - - // MARK: - Properties - - public let small: CGFloat - public let medium: CGFloat - - // MARK: - Initialization - - public init(small: CGFloat, - medium: CGFloat) { - self.small = small - self.medium = medium - } -} - -// MARK: - Radius - -public struct BorderRadiusDefault: BorderRadius { - - // MARK: - Properties - - public let small: CGFloat - public let medium: CGFloat - public let large: CGFloat - public let xLarge: CGFloat - - // MARK: - Initialization - - public init(small: CGFloat, - medium: CGFloat, - large: CGFloat, - xLarge: CGFloat) { - self.small = small - self.medium = medium - self.large = large - self.xLarge = xLarge - } -} diff --git a/core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 38b5f982c..000000000 --- a/core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// BorderGeneratedMock+ExtensionTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension BorderGeneratedMock { - static func mocked() -> BorderGeneratedMock { - let border = BorderGeneratedMock() - border.radius = BorderRadiusGeneratedMock.mocked() - border.width = BorderWidthGeneratedMock.mocked() - - return border - } -} - -extension BorderRadiusGeneratedMock { - static func mocked() -> BorderRadiusGeneratedMock { - let radius = BorderRadiusGeneratedMock() - radius.small = 4 - radius.medium = 8 - radius.large = 16 - radius.xLarge = 24 - return radius - } -} - -extension BorderWidthGeneratedMock { - static func mocked() -> BorderWidthGeneratedMock { - let width = BorderWidthGeneratedMock() - width.small = 1 - width.medium = 2 - return width - } -} diff --git a/core/Sources/Theming/Content/Colors/ColorToken+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/ColorToken+ExtensionTests.swift deleted file mode 100644 index 3bccd06a5..000000000 --- a/core/Sources/Theming/Content/Colors/ColorToken+ExtensionTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ColorToken+ExtensionTests.swift -// SparkCoreTests -// -// Created by louis.borlee on 13/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SparkCore - -extension ColorToken { - var isClear: Bool { - return self.color == .clear && self.uiColor == .clear - } -} diff --git a/core/Sources/Theming/Content/Colors/ColorTokenGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/ColorTokenGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 979eda375..000000000 --- a/core/Sources/Theming/Content/Colors/ColorTokenGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// ColorTokenGeneratedMock.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit - -extension ColorTokenGeneratedMock { - - // MARK: - Methods - - static func random() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - let random = UIColor.random() - color.underlyingUiColor = random - color.underlyingColor = Color(random) - return color - } - - static func red() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - color.underlyingColor = .red - color.underlyingUiColor = .red - return color - } - - static func blue() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - color.underlyingColor = .blue - color.underlyingUiColor = .blue - return color - } - - static func green() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - color.underlyingColor = .green - color.underlyingUiColor = .green - return color - } - - static func orange() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - color.underlyingColor = .orange - color.underlyingUiColor = .orange - return color - } - - static func yellow() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - color.underlyingColor = .yellow - color.underlyingUiColor = .yellow - return color - } - - static func purple() -> ColorTokenGeneratedMock { - let color = ColorTokenGeneratedMock() - color.underlyingColor = .purple - color.underlyingUiColor = .purple - return color - } -} - -// MARK: - Private extension - -private extension UIColor { - static func random( - hue: CGFloat = CGFloat.random(in: 0...1), - saturation: CGFloat = CGFloat.random(in: 0.5...1), // from 0.5 to 1.0 to stay away from white - brightness: CGFloat = CGFloat.random(in: 0.5...1), // from 0.5 to 1.0 to stay away from black - alpha: CGFloat = 1 - ) -> UIColor { - return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) - } -} diff --git a/core/Sources/Theming/Content/Colors/ColorTokenTests.swift b/core/Sources/Theming/Content/Colors/ColorTokenTests.swift deleted file mode 100644 index 9c0de2d19..000000000 --- a/core/Sources/Theming/Content/Colors/ColorTokenTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ColorTokenTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 21.07.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -final class ColorTokenTests: XCTestCase { - - // MARK: - Tests - - func test_colorTokens_equal() { - XCTAssertTrue(SparkTheme.shared.colors.base.surface.equals(SparkTheme.shared.colors.base.surface)) - } - - func test_colorTokens_not_equal() { - XCTAssertFalse(SparkTheme.shared.colors.base.surface.equals(SparkTheme.shared.colors.base.onSurface)) - } -} diff --git a/core/Sources/Theming/Content/Colors/Colors.swift b/core/Sources/Theming/Content/Colors/Colors.swift deleted file mode 100644 index 4766727e1..000000000 --- a/core/Sources/Theming/Content/Colors/Colors.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// Colors.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -// MARK: - Colors - -// sourcery: AutoMockable -public protocol Colors { - var main: ColorsMain { get } - var support: ColorsSupport { get } - var accent: ColorsAccent { get } - var basic: ColorsBasic { get } - var base: ColorsBase { get } - var feedback: ColorsFeedback { get } - var states: ColorsStates { get } -} - -// MARK: - Token - -// sourcery: AutoMockable -public protocol ColorToken: Hashable, Equatable { - var uiColor: UIColor { get } - var color: Color { get } -} - -// Hashable & Equatable -public extension ColorToken { - func hash(into hasher: inout Hasher) { - hasher.combine(self.color) - hasher.combine(self.uiColor) - } - - func equals(_ other: any ColorToken) -> Bool { - return self.color == other.color && self.uiColor == other.uiColor - } - - static func == (lhs: any ColorToken, rhs: any ColorToken) -> Bool { - return lhs.equals(rhs) - } -} - -public extension Optional where Wrapped == any ColorToken { - - func equals(_ other: (any ColorToken)?) -> Bool { - return self?.color == other?.color && self?.uiColor == other?.uiColor - } -} - -public extension ColorToken { - static var clear: any ColorToken { - return ColorTokenClear() - } -} - -fileprivate struct ColorTokenClear: ColorToken { - var uiColor: UIColor { .clear } - var color: Color { .clear } -} - -public extension ColorToken { - - func opacity(_ opacity: CGFloat) -> any ColorToken { - return OpacityColorToken(colorToken: self, - opacity: opacity) - } -} - -fileprivate struct OpacityColorToken: ColorToken { - static func == (lhs: OpacityColorToken, rhs: OpacityColorToken) -> Bool { - return lhs.colorToken.equals(rhs.colorToken) && - lhs.opacity == rhs.opacity - } - - let colorToken: any ColorToken - let opacity: CGFloat - - var uiColor: UIColor { - return self.colorToken.uiColor.withAlphaComponent(self.opacity) - } - var color: Color { - return self.colorToken.color.opacity(self.opacity) - } -} diff --git a/core/Sources/Theming/Content/Colors/ColorsDefault.swift b/core/Sources/Theming/Content/Colors/ColorsDefault.swift deleted file mode 100644 index 31a67b394..000000000 --- a/core/Sources/Theming/Content/Colors/ColorsDefault.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// ColorsDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit - -public struct ColorsDefault: Colors { - - // MARK: - Properties - - public let main: ColorsMain - public let support: ColorsSupport - public let accent: ColorsAccent - public let basic: ColorsBasic - public let base: ColorsBase - public let feedback: ColorsFeedback - public let states: ColorsStates - - // MARK: - Initialization - - public init(main: ColorsMain, - support: ColorsSupport, - accent: ColorsAccent, - basic: ColorsBasic, - base: ColorsBase, - feedback: ColorsFeedback, - states: ColorsStates) { - self.main = main - self.support = support - self.accent = accent - self.basic = basic - self.base = base - self.feedback = feedback - self.states = states - } -} - -// MARK: - Token - -public struct ColorTokenDefault: ColorToken { - - // MARK: - Properties - - private let colorName: String - private let bundle: Bundle - - public var uiColor: UIColor { - guard let uiColor = UIColor(named: self.colorName, in: self.bundle, compatibleWith: nil) else { - fatalError("Missing color asset named \(self.colorName) in bundle \(self.bundle.bundleIdentifier ?? self.bundle.description)") - } - return uiColor - } - public var color: Color { - return Color(self.colorName, bundle: self.bundle) - } - - // MARK: - Initialization - - public init(named colorName: String, in bundle: Bundle) { - self.colorName = colorName - self.bundle = bundle - } -} diff --git a/core/Sources/Theming/Content/Colors/ColorsGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/ColorsGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 08efeccd7..000000000 --- a/core/Sources/Theming/Content/Colors/ColorsGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ColorsGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsGeneratedMock { - let mock = ColorsGeneratedMock() - - mock.main = ColorsMainGeneratedMock.mocked() - mock.support = ColorsSupportGeneratedMock.mocked() - mock.accent = ColorsAccentGeneratedMock.mocked() - mock.basic = ColorsBasicGeneratedMock.mocked() - mock.base = ColorsBaseGeneratedMock.mocked() - mock.feedback = ColorsFeedbackGeneratedMock.mocked() - mock.states = ColorsStatesGeneratedMock.mocked() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccent.swift b/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccent.swift deleted file mode 100644 index b1cf1146a..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccent.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ColorsAccent.swift -// Spark -// -// Created by louis.borlee on 01/08/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsAccent { - var accent: any ColorToken { get } - var onAccent: any ColorToken { get } - var accentVariant: any ColorToken { get } - var onAccentVariant: any ColorToken { get } - var accentContainer: any ColorToken { get } - var onAccentContainer: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentDefault.swift b/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentDefault.swift deleted file mode 100644 index 915947230..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentDefault.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ColorsAccentDefault.swift -// SparkCore -// -// Created by louis.borlee on 01/08/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsAccentDefault: ColorsAccent { - - // MARK: - Properties - - public let accent: any ColorToken - public let onAccent: any ColorToken - public let accentVariant: any ColorToken - public let onAccentVariant: any ColorToken - public let accentContainer: any ColorToken - public let onAccentContainer: any ColorToken - - // MARK: - Init - - public init(accent: any ColorToken, - onAccent: any ColorToken, - accentVariant: any ColorToken, - onAccentVariant: any ColorToken, - accentContainer: any ColorToken, - onAccentContainer: any ColorToken) { - self.accent = accent - self.onAccent = onAccent - self.accentVariant = accentVariant - self.onAccentVariant = onAccentVariant - self.accentContainer = accentContainer - self.onAccentContainer = onAccentContainer - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 217c470b7..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Accent/ColorsAccentGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ColorsAccentGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsAccentGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsAccentGeneratedMock { - let mock = ColorsAccentGeneratedMock() - - mock.underlyingAccent = ColorTokenGeneratedMock.random() - mock.underlyingOnAccent = ColorTokenGeneratedMock.random() - - mock.underlyingAccentVariant = ColorTokenGeneratedMock.random() - mock.underlyingOnAccentVariant = ColorTokenGeneratedMock.random() - - mock.underlyingAccentContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnAccentContainer = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Base/ColorsBase.swift b/core/Sources/Theming/Content/Colors/Content/Base/ColorsBase.swift deleted file mode 100644 index 330f68826..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Base/ColorsBase.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ColorsBase.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsBase { - - // MARK: - Background - - var background: any ColorToken { get } - var onBackground: any ColorToken { get } - var backgroundVariant: any ColorToken { get } - var onBackgroundVariant: any ColorToken { get } - - // MARK: - Surface - - var surface: any ColorToken { get } - var onSurface: any ColorToken { get } - var surfaceInverse: any ColorToken { get } - var onSurfaceInverse: any ColorToken { get } - - // MARK: - Outline - - var outline: any ColorToken { get } - var outlineHigh: any ColorToken { get } - - // MARK: - Overlay - - var overlay: any ColorToken { get } - var onOverlay: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseDefault.swift b/core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseDefault.swift deleted file mode 100644 index 35991d3a8..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseDefault.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// ColorsBaseDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsBaseDefault: ColorsBase { - - // MARK: - Properties - - public let background: any ColorToken - public let onBackground: any ColorToken - public let backgroundVariant: any ColorToken - public let onBackgroundVariant: any ColorToken - public let surface: any ColorToken - public let onSurface: any ColorToken - public let surfaceInverse: any ColorToken - public let onSurfaceInverse: any ColorToken - public let outline: any ColorToken - public let outlineHigh: any ColorToken - public let overlay: any ColorToken - public let onOverlay: any ColorToken - - // MARK: - Init - - public init(background: any ColorToken, - onBackground: any ColorToken, - backgroundVariant: any ColorToken, - onBackgroundVariant: any ColorToken, - surface: any ColorToken, - onSurface: any ColorToken, - surfaceInverse: any ColorToken, - onSurfaceInverse: any ColorToken, - outline: any ColorToken, - outlineHigh: any ColorToken, - overlay: any ColorToken, - onOverlay: any ColorToken) { - self.background = background - self.onBackground = onBackground - self.backgroundVariant = backgroundVariant - self.onBackgroundVariant = onBackgroundVariant - self.surface = surface - self.onSurface = onSurface - self.surfaceInverse = surfaceInverse - self.onSurfaceInverse = onSurfaceInverse - self.outline = outline - self.outlineHigh = outlineHigh - self.overlay = overlay - self.onOverlay = onOverlay - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 9a6648eb0..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Base/ColorsBaseGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ColorsBaseGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsBaseGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsBaseGeneratedMock { - let mock = ColorsBaseGeneratedMock() - - mock.underlyingBackground = ColorTokenGeneratedMock.random() - mock.underlyingOnBackground = ColorTokenGeneratedMock.random() - mock.underlyingBackgroundVariant = ColorTokenGeneratedMock.random() - mock.underlyingOnBackgroundVariant = ColorTokenGeneratedMock.random() - - mock.underlyingSurface = ColorTokenGeneratedMock.random() - mock.underlyingOnSurface = ColorTokenGeneratedMock.random() - mock.underlyingSurfaceInverse = ColorTokenGeneratedMock.random() - mock.underlyingOnSurfaceInverse = ColorTokenGeneratedMock.random() - - mock.underlyingOutline = ColorTokenGeneratedMock.random() - mock.underlyingOutlineHigh = ColorTokenGeneratedMock.random() - - mock.underlyingOverlay = ColorTokenGeneratedMock.random() - mock.underlyingOnOverlay = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasic.swift b/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasic.swift deleted file mode 100644 index 961b5e2ed..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasic.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ColorsBasic.swift -// Spark -// -// Created by louis.borlee on 01/08/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsBasic { - var basic: any ColorToken { get } - var onBasic: any ColorToken { get } - var basicContainer: any ColorToken { get } - var onBasicContainer: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicDefault.swift b/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicDefault.swift deleted file mode 100644 index 3071156b4..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicDefault.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ColorsBasicDefault.swift -// SparkCore -// -// Created by louis.borlee on 01/08/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsBasicDefault: ColorsBasic { - - // MARK: - Properties - - public let basic: any ColorToken - public let onBasic: any ColorToken - public let basicContainer: any ColorToken - public let onBasicContainer: any ColorToken - - // MARK: - Init - - public init(basic: any ColorToken, - onBasic: any ColorToken, - basicContainer: any ColorToken, - onBasicContainer: any ColorToken) { - self.basic = basic - self.onBasic = onBasic - self.basicContainer = basicContainer - self.onBasicContainer = onBasicContainer - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicGeneratedMock+ExtensionTests.swift deleted file mode 100644 index c9c818b6a..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Basic/ColorsBasicGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ColorsBasicGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsBasicGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsBasicGeneratedMock { - let mock = ColorsBasicGeneratedMock() - - mock.underlyingBasic = ColorTokenGeneratedMock.random() - mock.underlyingOnBasic = ColorTokenGeneratedMock.random() - - mock.underlyingBasicContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnBasicContainer = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedback.swift b/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedback.swift deleted file mode 100644 index f35d7c1fd..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedback.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ColorsFeedback.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsFeedback { - - // MARK: - Success - - var success: any ColorToken { get } - var onSuccess: any ColorToken { get } - var successContainer: any ColorToken { get } - var onSuccessContainer: any ColorToken { get } - - // MARK: - Alert - - var alert: any ColorToken { get } - var onAlert: any ColorToken { get } - var alertContainer: any ColorToken { get } - var onAlertContainer: any ColorToken { get } - - // MARK: - Error - - var error: any ColorToken { get } - var onError: any ColorToken { get } - var errorContainer: any ColorToken { get } - var onErrorContainer: any ColorToken { get } - - // MARK: - Info - - var info: any ColorToken { get } - var onInfo: any ColorToken { get } - var infoContainer: any ColorToken { get } - var onInfoContainer: any ColorToken { get } - - // MARK: - Neutral - - var neutral: any ColorToken { get } - var onNeutral: any ColorToken { get } - var neutralContainer: any ColorToken { get } - var onNeutralContainer: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackDefault.swift b/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackDefault.swift deleted file mode 100644 index 1e761d6ca..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackDefault.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// ColorsFeedbackDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsFeedbackDefault: ColorsFeedback { - - // MARK: - Properties - - public let success: any ColorToken - public let onSuccess: any ColorToken - public let successContainer: any ColorToken - public let onSuccessContainer: any ColorToken - public let alert: any ColorToken - public let onAlert: any ColorToken - public let alertContainer: any ColorToken - public let onAlertContainer: any ColorToken - public let error: any ColorToken - public let onError: any ColorToken - public let errorContainer: any ColorToken - public let onErrorContainer: any ColorToken - public let info: any ColorToken - public let onInfo: any ColorToken - public let infoContainer: any ColorToken - public let onInfoContainer: any ColorToken - public let neutral: any ColorToken - public let onNeutral: any ColorToken - public let neutralContainer: any ColorToken - public let onNeutralContainer: any ColorToken - - // MARK: - Init - - public init(success: any ColorToken, - onSuccess: any ColorToken, - successContainer: any ColorToken, - onSuccessContainer: any ColorToken, - alert: any ColorToken, - onAlert: any ColorToken, - alertContainer: any ColorToken, - onAlertContainer: any ColorToken, - error: any ColorToken, - onError: any ColorToken, - errorContainer: any ColorToken, - onErrorContainer: any ColorToken, - info: any ColorToken, - onInfo: any ColorToken, - infoContainer: any ColorToken, - onInfoContainer: any ColorToken, - neutral: any ColorToken, - onNeutral: any ColorToken, - neutralContainer: any ColorToken, - onNeutralContainer: any ColorToken) { - self.success = success - self.onSuccess = onSuccess - self.successContainer = successContainer - self.onSuccessContainer = onSuccessContainer - self.alert = alert - self.onAlert = onAlert - self.alertContainer = alertContainer - self.onAlertContainer = onAlertContainer - self.error = error - self.onError = onError - self.errorContainer = errorContainer - self.onErrorContainer = onErrorContainer - self.info = info - self.onInfo = onInfo - self.infoContainer = infoContainer - self.onInfoContainer = onInfoContainer - self.neutral = neutral - self.onNeutral = onNeutral - self.neutralContainer = neutralContainer - self.onNeutralContainer = onNeutralContainer - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 28193ae8b..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Feedback/ColorsFeedbackGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ColorsFeedbackGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsFeedbackGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsFeedbackGeneratedMock { - let mock = ColorsFeedbackGeneratedMock() - - mock.underlyingSuccess = ColorTokenGeneratedMock.random() - mock.underlyingOnSuccess = ColorTokenGeneratedMock.random() - mock.underlyingSuccessContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnSuccessContainer = ColorTokenGeneratedMock.random() - - mock.underlyingAlert = ColorTokenGeneratedMock.random() - mock.underlyingOnAlert = ColorTokenGeneratedMock.random() - mock.underlyingAlertContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnAlertContainer = ColorTokenGeneratedMock.random() - - mock.underlyingError = ColorTokenGeneratedMock.random() - mock.underlyingOnError = ColorTokenGeneratedMock.random() - mock.underlyingErrorContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnErrorContainer = ColorTokenGeneratedMock.random() - - mock.underlyingInfo = ColorTokenGeneratedMock.random() - mock.underlyingOnInfo = ColorTokenGeneratedMock.random() - mock.underlyingInfoContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnInfoContainer = ColorTokenGeneratedMock.random() - - mock.underlyingNeutral = ColorTokenGeneratedMock.random() - mock.underlyingOnNeutral = ColorTokenGeneratedMock.random() - mock.underlyingNeutralContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnNeutralContainer = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Main/ColorsMain.swift b/core/Sources/Theming/Content/Colors/Content/Main/ColorsMain.swift deleted file mode 100644 index 20ee864c1..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Main/ColorsMain.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ColorsMain.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsMain { - var main: any ColorToken { get } - var onMain: any ColorToken { get } - var mainVariant: any ColorToken { get } - var onMainVariant: any ColorToken { get } - var mainContainer: any ColorToken { get } - var onMainContainer: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Main/ColorsMainDefault.swift b/core/Sources/Theming/Content/Colors/Content/Main/ColorsMainDefault.swift deleted file mode 100644 index d05e0c431..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Main/ColorsMainDefault.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ColorsMainDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsMainDefault: ColorsMain { - - // MARK: - Properties - - public let main: any ColorToken - public let onMain: any ColorToken - public let mainVariant: any ColorToken - public let onMainVariant: any ColorToken - public let mainContainer: any ColorToken - public let onMainContainer: any ColorToken - - // MARK: - Init - - public init(main: any ColorToken, - onMain: any ColorToken, - mainVariant: any ColorToken, - onMainVariant: any ColorToken, - mainContainer: any ColorToken, - onMainContainer: any ColorToken) { - self.main = main - self.onMain = onMain - self.mainVariant = mainVariant - self.onMainVariant = onMainVariant - self.mainContainer = mainContainer - self.onMainContainer = onMainContainer - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Main/ColorsMainGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/Main/ColorsMainGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 63abe9665..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Main/ColorsMainGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ColorsMainGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsMainGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsMainGeneratedMock { - let mock = ColorsMainGeneratedMock() - - mock.underlyingMain = ColorTokenGeneratedMock.random() - mock.underlyingOnMain = ColorTokenGeneratedMock.random() - - mock.underlyingMainVariant = ColorTokenGeneratedMock.random() - mock.underlyingOnMainVariant = ColorTokenGeneratedMock.random() - - mock.underlyingMainContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnMainContainer = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/States/ColorsStates.swift b/core/Sources/Theming/Content/Colors/Content/States/ColorsStates.swift deleted file mode 100644 index da5a675cb..000000000 --- a/core/Sources/Theming/Content/Colors/Content/States/ColorsStates.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ColorsStates.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsStates { - - // MARK: - Main - - var mainPressed: any ColorToken { get } - var mainVariantPressed: any ColorToken { get } - var mainContainerPressed: any ColorToken { get } - - // MARK: - Support - - var supportPressed: any ColorToken { get } - var supportVariantPressed: any ColorToken { get } - var supportContainerPressed: any ColorToken { get } - - // MARK: - Accent - var accentPressed: any ColorToken { get } - var accentVariantPressed: any ColorToken { get } - var accentContainerPressed: any ColorToken { get } - - // MARK: - Basic - var basicPressed: any ColorToken { get } - var basicContainerPressed: any ColorToken { get } - - // MARK: - Base - - var surfacePressed: any ColorToken { get } - var surfaceInversePressed: any ColorToken { get } - - // MARK: - Feedback - - var successPressed: any ColorToken { get } - var successContainerPressed: any ColorToken { get } - var alertPressed: any ColorToken { get } - var alertContainerPressed: any ColorToken { get } - var errorPressed: any ColorToken { get } - var errorContainerPressed: any ColorToken { get } - var infoPressed: any ColorToken { get } - var infoContainerPressed: any ColorToken { get } - var neutralPressed: any ColorToken { get } - var neutralContainerPressed: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/States/ColorsStatesDefault.swift b/core/Sources/Theming/Content/Colors/Content/States/ColorsStatesDefault.swift deleted file mode 100644 index f55026bb5..000000000 --- a/core/Sources/Theming/Content/Colors/Content/States/ColorsStatesDefault.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ColorsStatesDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsStatesDefault: ColorsStates { - - // MARK: - Properties - - public let mainPressed: any ColorToken - public let mainVariantPressed: any ColorToken - public let mainContainerPressed: any ColorToken - public let supportPressed: any ColorToken - public let supportVariantPressed: any ColorToken - public let supportContainerPressed: any ColorToken - public let accentPressed: any ColorToken - public let accentVariantPressed: any ColorToken - public let accentContainerPressed: any ColorToken - public let basicPressed: any ColorToken - public let basicContainerPressed: any ColorToken - public let surfacePressed: any ColorToken - public let surfaceInversePressed: any ColorToken - public let successPressed: any ColorToken - public let successContainerPressed: any ColorToken - public let alertPressed: any ColorToken - public let alertContainerPressed: any ColorToken - public let errorPressed: any ColorToken - public let errorContainerPressed: any ColorToken - public let infoPressed: any ColorToken - public let infoContainerPressed: any ColorToken - public let neutralPressed: any ColorToken - public let neutralContainerPressed: any ColorToken - - // MARK: - Init - - public init(mainPressed: any ColorToken, - mainVariantPressed: any ColorToken, - mainContainerPressed: any ColorToken, - supportPressed: any ColorToken, - supportVariantPressed: any ColorToken, - supportContainerPressed: any ColorToken, - accentPressed: any ColorToken, - accentVariantPressed: any ColorToken, - accentContainerPressed: any ColorToken, - basicPressed: any ColorToken, - basicContainerPressed: any ColorToken, - surfacePressed: any ColorToken, - surfaceInversePressed: any ColorToken, - successPressed: any ColorToken, - successContainerPressed: any ColorToken, - alertPressed: any ColorToken, - alertContainerPressed: any ColorToken, - errorPressed: any ColorToken, - errorContainerPressed: any ColorToken, - infoPressed: any ColorToken, - infoContainerPressed: any ColorToken, - neutralPressed: any ColorToken, - neutralContainerPressed: any ColorToken) { - self.mainPressed = mainPressed - self.mainVariantPressed = mainVariantPressed - self.mainContainerPressed = mainContainerPressed - self.supportPressed = supportPressed - self.supportVariantPressed = supportVariantPressed - self.supportContainerPressed = supportContainerPressed - self.accentPressed = accentPressed - self.accentVariantPressed = accentVariantPressed - self.accentContainerPressed = accentContainerPressed - self.basicPressed = basicPressed - self.basicContainerPressed = basicContainerPressed - self.surfacePressed = surfacePressed - self.surfaceInversePressed = surfaceInversePressed - self.successPressed = successPressed - self.successContainerPressed = successContainerPressed - self.alertPressed = alertPressed - self.alertContainerPressed = alertContainerPressed - self.errorPressed = errorPressed - self.errorContainerPressed = errorContainerPressed - self.infoPressed = infoPressed - self.infoContainerPressed = infoContainerPressed - self.neutralPressed = neutralPressed - self.neutralContainerPressed = neutralContainerPressed - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/States/ColorsStatesGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/States/ColorsStatesGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 93ae990a4..000000000 --- a/core/Sources/Theming/Content/Colors/Content/States/ColorsStatesGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// ColorsStatesGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsStatesGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsStatesGeneratedMock { - let mock = ColorsStatesGeneratedMock() - - mock.underlyingMainPressed = ColorTokenGeneratedMock.random() - mock.underlyingMainVariantPressed = ColorTokenGeneratedMock.random() - mock.underlyingMainContainerPressed = ColorTokenGeneratedMock.random() - - mock.underlyingSupportPressed = ColorTokenGeneratedMock.random() - mock.underlyingSupportVariantPressed = ColorTokenGeneratedMock.random() - mock.underlyingSupportContainerPressed = ColorTokenGeneratedMock.random() - - mock.underlyingAccentPressed = ColorTokenGeneratedMock.random() - mock.underlyingAccentVariantPressed = ColorTokenGeneratedMock.random() - mock.underlyingAccentContainerPressed = ColorTokenGeneratedMock.random() - - mock.underlyingBasicPressed = ColorTokenGeneratedMock.random() - mock.underlyingBasicContainerPressed = ColorTokenGeneratedMock.random() - - mock.underlyingSurfacePressed = ColorTokenGeneratedMock.random() - mock.underlyingSurfaceInversePressed = ColorTokenGeneratedMock.random() - - mock.underlyingSuccessPressed = ColorTokenGeneratedMock.random() - mock.underlyingSuccessContainerPressed = ColorTokenGeneratedMock.random() - mock.underlyingAlertPressed = ColorTokenGeneratedMock.random() - mock.underlyingAlertContainerPressed = ColorTokenGeneratedMock.random() - mock.underlyingErrorPressed = ColorTokenGeneratedMock.random() - mock.underlyingErrorContainerPressed = ColorTokenGeneratedMock.random() - mock.underlyingInfoPressed = ColorTokenGeneratedMock.random() - mock.underlyingInfoContainerPressed = ColorTokenGeneratedMock.random() - mock.underlyingNeutralPressed = ColorTokenGeneratedMock.random() - mock.underlyingNeutralContainerPressed = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupport.swift b/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupport.swift deleted file mode 100644 index 64e064ee3..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupport.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ColorsSupport.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ColorsSupport { - var support: any ColorToken { get } - var onSupport: any ColorToken { get } - var supportVariant: any ColorToken { get } - var onSupportVariant: any ColorToken { get } - var supportContainer: any ColorToken { get } - var onSupportContainer: any ColorToken { get } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportDefault.swift b/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportDefault.swift deleted file mode 100644 index bdac76dec..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportDefault.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ColorsSupportDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ColorsSupportDefault: ColorsSupport { - - // MARK: - Properties - - public let support: any ColorToken - public let onSupport: any ColorToken - public let supportVariant: any ColorToken - public let onSupportVariant: any ColorToken - public let supportContainer: any ColorToken - public let onSupportContainer: any ColorToken - - // MARK: - Init - - public init(support: any ColorToken, - onSupport: any ColorToken, - supportVariant: any ColorToken, - onSupportVariant: any ColorToken, - supportContainer: any ColorToken, - onSupportContainer: any ColorToken) { - self.support = support - self.onSupport = onSupport - self.supportVariant = supportVariant - self.onSupportVariant = onSupportVariant - self.supportContainer = supportContainer - self.onSupportContainer = onSupportContainer - } -} diff --git a/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 7898835af..000000000 --- a/core/Sources/Theming/Content/Colors/Content/Support/ColorsSupportGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ColorsSupportGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by robin.lemaire on 11/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension ColorsSupportGeneratedMock { - - // MARK: - Methods - - static func mocked() -> ColorsSupportGeneratedMock { - let mock = ColorsSupportGeneratedMock() - - mock.underlyingSupport = ColorTokenGeneratedMock.random() - mock.underlyingOnSupport = ColorTokenGeneratedMock.random() - - mock.underlyingSupportVariant = ColorTokenGeneratedMock.random() - mock.underlyingOnSupportVariant = ColorTokenGeneratedMock.random() - - mock.underlyingSupportContainer = ColorTokenGeneratedMock.random() - mock.underlyingOnSupportContainer = ColorTokenGeneratedMock.random() - - return mock - } -} diff --git a/core/Sources/Theming/Content/Dims/Dims.swift b/core/Sources/Theming/Content/Dims/Dims.swift deleted file mode 100644 index 1b9b49d91..000000000 --- a/core/Sources/Theming/Content/Dims/Dims.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Dims.swift -// Spark -// -// Created by louis.borlee on 22/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -public protocol Dims { - var dim1: CGFloat { get } - var dim2: CGFloat { get } - var dim3: CGFloat { get } - var dim4: CGFloat { get } - var dim5: CGFloat { get } -} - -public extension Dims { - /// None corresponding to 1.0 value - var none: CGFloat { - return 1.0 - } -} diff --git a/core/Sources/Theming/Content/Dims/DimsDefault.swift b/core/Sources/Theming/Content/Dims/DimsDefault.swift deleted file mode 100644 index d66a54225..000000000 --- a/core/Sources/Theming/Content/Dims/DimsDefault.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// DimsDefault.swift -// Spark -// -// Created by louis.borlee on 22/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public struct DimsDefault: Dims { - - // MARK: - Properties - - public let dim1: CGFloat - public let dim2: CGFloat - public let dim3: CGFloat - public let dim4: CGFloat - public let dim5: CGFloat - - // MARK: - Init - - public init(dim1: CGFloat, - dim2: CGFloat, - dim3: CGFloat, - dim4: CGFloat, - dim5: CGFloat) { - self.dim1 = dim1 - self.dim2 = dim2 - self.dim3 = dim3 - self.dim4 = dim4 - self.dim5 = dim5 - } -} diff --git a/core/Sources/Theming/Content/Dims/DimsGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Dims/DimsGeneratedMock+ExtensionTests.swift deleted file mode 100644 index db844f21b..000000000 --- a/core/Sources/Theming/Content/Dims/DimsGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// DimsGeneratedMock+ExtensionTests.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension DimsGeneratedMock { - - // MARK: - Methods - - static func mocked() -> DimsGeneratedMock { - let mock = DimsGeneratedMock() - mock.dim1 = 0.2 - mock.dim2 = 0.3 - mock.dim3 = 0.4 - mock.dim4 = 0.5 - mock.dim5 = 0.6 - - return mock - } -} diff --git a/core/Sources/Theming/Content/Elevation/Elevation.swift b/core/Sources/Theming/Content/Elevation/Elevation.swift deleted file mode 100644 index 0e14e5064..000000000 --- a/core/Sources/Theming/Content/Elevation/Elevation.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Elevation.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public protocol Elevation { - var dropShadow: ElevationShadow & ElevationDropShadows { get } -} diff --git a/core/Sources/Theming/Content/Elevation/ElevationDefault.swift b/core/Sources/Theming/Content/Elevation/ElevationDefault.swift deleted file mode 100644 index 9d96ff468..000000000 --- a/core/Sources/Theming/Content/Elevation/ElevationDefault.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ElevationDefault.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ElevationDefault: Elevation { - - // MARK: - Properties - - public let dropShadow: ElevationDropShadows & ElevationShadow - - // MARK: - Init - - public init(dropShadow: ElevationDropShadows & ElevationShadow) { - self.dropShadow = dropShadow - } -} diff --git a/core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadows.swift b/core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadows.swift deleted file mode 100644 index a0536ba41..000000000 --- a/core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadows.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ElevationDropShadows.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -// sourcery: AutoMockable -public protocol ElevationDropShadows { - var small: ElevationShadow { get } - var medium: ElevationShadow { get } - var large: ElevationShadow { get } - var extraLarge: ElevationShadow { get } - var none: ElevationShadow { get } -} - -public extension ElevationDropShadows { - var none: ElevationShadow { - ElevationShadowDefault(offset: .zero, - blur: 0, - colorToken: ColorTokenDefault.clear, - opacity: 0) - } -} diff --git a/core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadowsDefault.swift b/core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadowsDefault.swift deleted file mode 100644 index fc84fdf24..000000000 --- a/core/Sources/Theming/Content/Elevation/Shadow/Drop/ElevationDropShadowsDefault.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ElevationDropShadowsDefault.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ElevationDropShadowsDefault: ElevationDropShadows { - - // MARK: - Properties - - public let small: ElevationShadow - public let medium: ElevationShadow - public let large: ElevationShadow - public let extraLarge: ElevationShadow - - // MARK: - Init - - public init(small: ElevationShadow, - medium: ElevationShadow, - large: ElevationShadow, - extraLarge: ElevationShadow) { - self.small = small - self.medium = medium - self.large = large - self.extraLarge = extraLarge - } -} diff --git a/core/Sources/Theming/Content/Elevation/Shadow/ElevationShadow.swift b/core/Sources/Theming/Content/Elevation/Shadow/ElevationShadow.swift deleted file mode 100644 index 3d7fa9300..000000000 --- a/core/Sources/Theming/Content/Elevation/Shadow/ElevationShadow.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ElevationShadow.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -public protocol ElevationShadow { - var offset: CGPoint { get } - var blur: CGFloat { get } - var colorToken: any ColorToken { get } - var opacity: Float { get } -} diff --git a/core/Sources/Theming/Content/Elevation/Shadow/ElevationShadowDefault.swift b/core/Sources/Theming/Content/Elevation/Shadow/ElevationShadowDefault.swift deleted file mode 100644 index 076de7c08..000000000 --- a/core/Sources/Theming/Content/Elevation/Shadow/ElevationShadowDefault.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ElevationShadowDefault.swift -// SparkCore -// -// Created by louis.borlee on 27/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public struct ElevationShadowDefault: ElevationShadow { - - // MARK: - Properties - - public let offset: CGPoint - public let blur: CGFloat - public let colorToken: any ColorToken - public let opacity: Float - - // MARK: - Init - - public init(offset: CGPoint, - blur: CGFloat, - colorToken: any ColorToken, - opacity: Float) { - self.offset = offset - self.blur = blur - self.colorToken = colorToken - self.opacity = opacity - } -} - diff --git a/core/Sources/Theming/Content/Elevation/Shadow/UIView+ElevationShadow.swift b/core/Sources/Theming/Content/Elevation/Shadow/UIView+ElevationShadow.swift deleted file mode 100644 index 5248d53d5..000000000 --- a/core/Sources/Theming/Content/Elevation/Shadow/UIView+ElevationShadow.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UIView+ElevationShadow.swift -// SparkCore -// -// Created by louis.borlee on 30/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -public extension UIView { - - /// Apply a shadow to the view - /// Note: This will need to be reapplied when switching from dark to light theme as CGColors do not refresh automatically - /// Note: You could watch changes in traitCollectionDidChange using traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) - /// - Parameter shadow: An ElevationShadow - func applyShadow(_ shadow: ElevationShadow) { - self.layer.masksToBounds = false - self.layer.shadowColor = shadow.colorToken.uiColor.cgColor - self.layer.shadowOpacity = shadow.opacity - self.layer.shadowOffset = CGSize(width: shadow.offset.x, height: shadow.offset.y) - self.layer.shadowRadius = shadow.blur - self.layer.shouldRasterize = true - self.layer.rasterizationScale = UIScreen.main.scale - } -} diff --git a/core/Sources/Theming/Content/Elevation/Shadow/View+ElevationShadow.swift b/core/Sources/Theming/Content/Elevation/Shadow/View+ElevationShadow.swift deleted file mode 100644 index ae3aef56e..000000000 --- a/core/Sources/Theming/Content/Elevation/Shadow/View+ElevationShadow.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// View+ElevationShadow.swift -// SparkCore -// -// Created by louis.borlee on 30/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI - -public extension View { - func shadow(_ shadow: ElevationShadow) -> some View { - return self.shadow(color: shadow.colorToken.color.opacity(Double(shadow.opacity)), - radius: shadow.blur, - x: shadow.offset.x, - y: shadow.offset.y) - } -} diff --git a/core/Sources/Theming/Content/Layout/Layout.swift b/core/Sources/Theming/Content/Layout/Layout.swift deleted file mode 100644 index 4208e60a0..000000000 --- a/core/Sources/Theming/Content/Layout/Layout.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Layout.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -public protocol Layout { - var spacing: LayoutSpacing { get } -} - -// MARK: - Spacing - -// sourcery: AutoMockable -public protocol LayoutSpacing { - var none: CGFloat { get } - var small: CGFloat { get } - var medium: CGFloat { get } - var large: CGFloat { get } - var xLarge: CGFloat { get } - var xxLarge: CGFloat { get } - var xxxLarge: CGFloat { get } -} - -public extension LayoutSpacing { - var none: CGFloat { 0 } -} diff --git a/core/Sources/Theming/Content/Layout/LayoutDefault.swift b/core/Sources/Theming/Content/Layout/LayoutDefault.swift deleted file mode 100644 index 3428e945e..000000000 --- a/core/Sources/Theming/Content/Layout/LayoutDefault.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// LayoutDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public struct LayoutDefault: Layout { - - // MARK: - Properties - - public let spacing: LayoutSpacing - - // MARK: - Initialization - - public init(spacing: LayoutSpacing) { - self.spacing = spacing - } -} - -// MARK: - Spacing - -public struct LayoutSpacingDefault: LayoutSpacing { - - // MARK: - Properties - - public let small: CGFloat - public let medium: CGFloat - public let large: CGFloat - public let xLarge: CGFloat - public let xxLarge: CGFloat - public let xxxLarge: CGFloat - - // MARK: - Initialization - - public init(small: CGFloat, - medium: CGFloat, - large: CGFloat, - xLarge: CGFloat, - xxLarge: CGFloat, - xxxLarge: CGFloat) { - self.small = small - self.medium = medium - self.large = large - self.xLarge = xLarge - self.xxLarge = xxLarge - self.xxxLarge = xxxLarge - } -} diff --git a/core/Sources/Theming/Content/Layout/LayoutGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Layout/LayoutGeneratedMock+ExtensionTests.swift deleted file mode 100644 index cb5073ef1..000000000 --- a/core/Sources/Theming/Content/Layout/LayoutGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// LayoutGeneratedMock.swift -// SparkCore -// -// Created by janniklas.freundt.ext on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore - -extension LayoutGeneratedMock { - - // MARK: - Methods - - static func mocked() -> LayoutGeneratedMock { - let mock = LayoutGeneratedMock() - mock.spacing = LayoutSpacingGeneratedMock.mocked() - - return mock - } -} - -extension LayoutSpacingGeneratedMock { - - // MARK: - Methods - - static func mocked() -> LayoutSpacingGeneratedMock { - let mock = LayoutSpacingGeneratedMock() - mock.none = 1 - mock.small = 3 - mock.medium = 5 - mock.large = 7 - mock.xLarge = 9 - mock.xxLarge = 11 - mock.xxxLarge = 13 - - return mock - } -} diff --git a/core/Sources/Theming/Content/Typography/Typography.swift b/core/Sources/Theming/Content/Typography/Typography.swift deleted file mode 100644 index 08679b4b5..000000000 --- a/core/Sources/Theming/Content/Typography/Typography.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Typography.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI - -// sourcery: AutoMockable -public protocol Typography { - var display1: TypographyFontToken { get } - var display2: TypographyFontToken { get } - var display3: TypographyFontToken { get } - - var headline1: TypographyFontToken { get } - var headline2: TypographyFontToken { get } - - var subhead: TypographyFontToken { get } - - var body1: TypographyFontToken { get } - var body1Highlight: TypographyFontToken { get } - - var body2: TypographyFontToken { get } - var body2Highlight: TypographyFontToken { get } - - var caption: TypographyFontToken { get } - var captionHighlight: TypographyFontToken { get } - - var small: TypographyFontToken { get } - var smallHighlight: TypographyFontToken { get } - - var callout: TypographyFontToken { get } -} - -// MARK: - Font - -// sourcery: AutoMockable -public protocol TypographyFontToken { - var uiFont: UIFont { get } - var font: Font { get } -} diff --git a/core/Sources/Theming/Content/Typography/TypographyDefault.swift b/core/Sources/Theming/Content/Typography/TypographyDefault.swift deleted file mode 100644 index bd6d2b405..000000000 --- a/core/Sources/Theming/Content/Typography/TypographyDefault.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// TypographyDefault.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit - -public struct TypographyDefault: Typography { - - // MARK: - Properties - - public let display1: TypographyFontToken - public let display2: TypographyFontToken - public let display3: TypographyFontToken - - public let headline1: TypographyFontToken - public let headline2: TypographyFontToken - - public let subhead: TypographyFontToken - - public let body1: TypographyFontToken - public let body1Highlight: TypographyFontToken - - public let body2: TypographyFontToken - public let body2Highlight: TypographyFontToken - - public let caption: TypographyFontToken - public let captionHighlight: TypographyFontToken - - public let small: TypographyFontToken - public let smallHighlight: TypographyFontToken - - public let callout: TypographyFontToken - - // MARK: - Initialization - - public init(display1: TypographyFontToken, - display2: TypographyFontToken, - display3: TypographyFontToken, - headline1: TypographyFontToken, - headline2: TypographyFontToken, - subhead: TypographyFontToken, - body1: TypographyFontToken, - body1Highlight: TypographyFontToken, - body2: TypographyFontToken, - body2Highlight: TypographyFontToken, - caption: TypographyFontToken, - captionHighlight: TypographyFontToken, - small: TypographyFontToken, - smallHighlight: TypographyFontToken, - callout: TypographyFontToken) { - self.display1 = display1 - self.display2 = display2 - self.display3 = display3 - self.headline1 = headline1 - self.headline2 = headline2 - self.subhead = subhead - self.body1 = body1 - self.body1Highlight = body1Highlight - self.body2 = body2 - self.body2Highlight = body2Highlight - self.caption = caption - self.captionHighlight = captionHighlight - self.small = small - self.smallHighlight = smallHighlight - self.callout = callout - } -} - -// MARK: - Font - -public struct TypographyFontTokenDefault: TypographyFontToken { - - // MARK: - Properties - - private let fontName: String - private let fontSize: CGFloat - private let fontTextStyle: TextStyle - - public var uiFont: UIFont { - guard let font = UIFont(name: self.fontName, size: self.fontSize) else { - fatalError("Missing font named \(self.fontName)") - } - let textStyle = UIFont.TextStyle(from: self.fontTextStyle) - return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: font) - } - - public var font: Font { - let textStyle = Font.TextStyle(from: self.fontTextStyle) - return Font.custom(self.fontName, - size: self.fontSize, - relativeTo: textStyle) - } - - // MARK: - Initialization - - public init(named fontName: String, - size: CGFloat, - textStyle: TextStyle) { - self.fontName = fontName - self.fontSize = size - self.fontTextStyle = textStyle - } -} diff --git a/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift deleted file mode 100644 index e8efeeb6f..000000000 --- a/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// TypographyGeneratedMock+ExtensionTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI -import UIKit - -extension TypographyGeneratedMock { - - // MARK: - Methods - - static func mocked() -> TypographyGeneratedMock { - let typography = TypographyGeneratedMock() - - typography.display1 = TypographyFontTokenGeneratedMock.mocked(.title) - typography.display2 = TypographyFontTokenGeneratedMock.mocked(.title2) - typography.display3 = TypographyFontTokenGeneratedMock.mocked(.title3) - - typography.headline1 = TypographyFontTokenGeneratedMock.mocked(.headline) - typography.headline2 = TypographyFontTokenGeneratedMock.mocked(.headline) - - typography.subhead = TypographyFontTokenGeneratedMock.mocked(.subheadline) - - typography.body1 = TypographyFontTokenGeneratedMock.mocked(.body) - typography.body1Highlight = TypographyFontTokenGeneratedMock.mocked(.body.bold()) - typography.body2 = TypographyFontTokenGeneratedMock.mocked(.body) - typography.body2Highlight = TypographyFontTokenGeneratedMock.mocked(.body.bold()) - - typography.caption = TypographyFontTokenGeneratedMock.mocked(.caption) - typography.captionHighlight = TypographyFontTokenGeneratedMock.mocked(.caption.bold()) - - typography.small = TypographyFontTokenGeneratedMock.mocked(.caption2) - typography.smallHighlight = TypographyFontTokenGeneratedMock.mocked(.caption2.bold()) - typography.subhead = TypographyFontTokenGeneratedMock.mocked(.subheadline) - - typography.callout = TypographyFontTokenGeneratedMock.mocked(.callout) - - return typography - } -} - -extension TypographyFontTokenGeneratedMock { - // MARK: - Methods - - static func mocked(_ font: Font) -> TypographyFontTokenGeneratedMock { - let fontToken = TypographyFontTokenGeneratedMock() - fontToken.font = font - return fontToken - } - - static func mocked(_ font: UIFont) -> TypographyFontTokenGeneratedMock { - let fontToken = TypographyFontTokenGeneratedMock() - fontToken.uiFont = font - return fontToken - } - - static func mocked(uiFont: UIFont, font: Font) -> TypographyFontTokenGeneratedMock { - let fontToken = TypographyFontTokenGeneratedMock() - fontToken.uiFont = uiFont - fontToken.font = font - return fontToken - } -} diff --git a/core/Sources/Theming/Theme/Theme.swift b/core/Sources/Theming/Theme/Theme.swift deleted file mode 100644 index b3eab51ce..000000000 --- a/core/Sources/Theming/Theme/Theme.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Theme.swift -// SparkCore -// -// Created by louis.borlee on 23/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation -import SwiftUI - -// sourcery: AutoMockable -public protocol Theme { - var border: SparkCore.Border { get } - var colors: SparkCore.Colors { get } - var elevation: SparkCore.Elevation { get } - var layout: SparkCore.Layout { get } - var typography: SparkCore.Typography { get } - var dims: SparkCore.Dims { get } -} diff --git a/core/Sources/Theming/Theme/ThemeDefault.swift b/core/Sources/Theming/Theme/ThemeDefault.swift deleted file mode 100644 index 0be975d2f..000000000 --- a/core/Sources/Theming/Theme/ThemeDefault.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ThemeDefault.swift -// SparkCore -// -// Created by robin.lemaire on 06/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -public struct ThemeDefault: Theme { - - // MARK: - Properties - - public let border: Border - public let colors: Colors - public let elevation: Elevation - public let layout: Layout - public let typography: Typography - public let dims: Dims - - // MARK: - Initialization - - public init(border: Border, - colors: Colors, - elevation: Elevation, - layout: Layout, - typography: Typography, - dims: Dims) { - self.border = border - self.colors = colors - self.elevation = elevation - self.layout = layout - self.typography = typography - self.dims = dims - } -} diff --git a/core/Sources/Theming/Theme/ThemeGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Theme/ThemeGeneratedMock+ExtensionTests.swift deleted file mode 100644 index 3cf52bedd..000000000 --- a/core/Sources/Theming/Theme/ThemeGeneratedMock+ExtensionTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ThemeGeneratedMock+ExtensionTests.swift -// SparkCoreTests -// -// Created by michael.zimmermann on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -extension ThemeGeneratedMock { - static func mocked() -> ThemeGeneratedMock { - let theme = ThemeGeneratedMock() - - theme.colors = ColorsGeneratedMock.mocked() - theme.layout = LayoutGeneratedMock.mocked() - theme.typography = TypographyGeneratedMock.mocked() - - theme.dims = DimsGeneratedMock.mocked() - theme.border = BorderGeneratedMock.mocked() - - return theme - } -} diff --git a/core/Sources/UseCaseDemo.swift b/core/Sources/UseCaseDemo.swift deleted file mode 100644 index a9cba40c9..000000000 --- a/core/Sources/UseCaseDemo.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// UseCaseDemo.swift -// SparkCore -// -// Created by louis.borlee on 22/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct UseCaseDemo { - let id = 42 -} diff --git a/core/Sources/UseCaseDemoTests.swift b/core/Sources/UseCaseDemoTests.swift deleted file mode 100644 index d4db5e3f5..000000000 --- a/core/Sources/UseCaseDemoTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// UseCaseDemoTests.swift -// SparkCoreTests -// -// Created by louis.borlee on 22/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import SparkCore - -final class UseCaseDemoTests: XCTestCase { - - func testUseCaseDemo() { - let demo = UseCaseDemo() - XCTAssertEqual(demo.id, 42) - } - -} diff --git a/core/Unit-tests/Classes/Publisher/PublisherMock.swift b/core/Unit-tests/Classes/Publisher/PublisherMock.swift deleted file mode 100644 index 91faa2d5a..000000000 --- a/core/Unit-tests/Classes/Publisher/PublisherMock.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// PublisherMock.swift -// Spark -// -// Created by robin.lemaire on 31/08/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import Foundation - -final class PublisherMock { - - // MARK: - Properties - - private let publisher: T - let name: String - var sinkValue: T.Output? - var sinkValues = [T.Output]() - var sinkCount = 0 - var sinkCalled: Bool { - return self.sinkCount > 0 - } - - // MARK: - Initialization - - init( - publisher: T, - name: String = String(describing: T.Output.self) - ) { - self.publisher = publisher - self.name = name - } - - // MARK: - Methods - - func reset() { - self.sinkValue = nil - self.sinkValues.removeAll() - self.sinkCount = 0 - } -} - -// MARK: - Tests Management - -extension PublisherMock where T.Failure == Never { - - func loadTesting(on subscriptions: inout Set) { - self.publisher.sink(receiveValue: { [weak self] value in - guard let self = self else { return } - self.sinkCount += 1 - - // T.Output is optional and nil ? We don't set the value - if !((value as AnyObject) is NSNull) { - self.sinkValue = value - self.sinkValues.append(value) - } - }) - .store(in: &subscriptions) - } -} diff --git a/core/Unit-tests/Classes/Publisher/XCTest+PublisherMock.swift b/core/Unit-tests/Classes/Publisher/XCTest+PublisherMock.swift deleted file mode 100644 index dcbc3ae86..000000000 --- a/core/Unit-tests/Classes/Publisher/XCTest+PublisherMock.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// XCTest+PublisherMock.swift -// SparkCoreTests -// -// Created by robin.lemaire on 04/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import Combine - -func XCTAssertPublisherSinkCountEqual( - on mock: PublisherMock, - _ expression: Int, - function: String = #function, - file: StaticString = #filePath, - line: UInt = #line -) { - XCTAssertEqual( - mock.sinkCount, - expression, - XCTest.errorMessage( - on: mock, - for: .count, - function: function, - file: file, - line: line - ) - ) -} - -func XCTAssertPublisherSinkIsCalled( - on mock: PublisherMock, - _ expression: Bool, - function: String = #function, - file: StaticString = #filePath, - line: UInt = #line -) { - XCTAssertEqual( - mock.sinkCalled, - expression, - XCTest.errorMessage( - on: mock, - for: .isCalled, - function: function, - file: file, - line: line - ) - ) -} - -func XCTAssertPublisherSinkValueEqual( - on mock: PublisherMock, - _ expression: T.Output, - function: String = #function, - file: StaticString = #filePath, - line: UInt = #line -) where T.Output: Equatable { - XCTAssertEqual( - mock.sinkValue, - expression, - XCTest.errorMessage( - on: mock, - for: .value, - function: function, - file: file, - line: line - ) - ) -} - -func XCTAssertPublisherSinkValueIdentical( - on mock: PublisherMock, - _ expression: Z?, - expressionShouldBeSet: Bool = true, - function: String = #function, - file: StaticString = #filePath, - line: UInt = #line -) { - guard (expressionShouldBeSet && expression != nil) || !expressionShouldBeSet else { - XCTFail("\(Z.self) expression should be set") - return - } - - XCTAssertIdentical( - mock.sinkValue as? Z, - expression, - XCTest.errorMessage( - on: mock, - for: .value, - function: function, - file: file, - line: line - ) - ) -} - -func XCTAssertPublisherSinkValueNil( - on mock: PublisherMock, - function: String = #function, - file: StaticString = #filePath, - line: UInt = #line -) { - XCTAssertNil( - mock.sinkValue, - XCTest.errorMessage( - on: mock, - for: .valueNil, - function: function, - file: file, - line: line - ) - ) -} - -func XCTAssertPublisherSinkValuesEqual( - on mock: PublisherMock, - _ expression: [T.Output], - function: String = #function, - file: StaticString = #filePath, - line: UInt = #line -) where T.Output: Equatable { - XCTAssertEqual( - mock.sinkValues, - expression, - XCTest.errorMessage( - on: mock, - for: .values, - function: function, - file: file, - line: line - ) - ) -} - -private extension XCTest { - - // MARK: - Test Type - - enum TestingSinkType: String { - case count - case isCalled - case value - case valueNil - case values - } - - // MARK: - Message - - static func errorMessage( - on mock: PublisherMock, - for type: TestingSinkType, - function: String, - file: StaticString, - line: UInt - ) -> String { - return "Wrong \(mock.name) sink \(type.rawValue) value on \(function) function, \(file) file, line \(line)" - } -} diff --git a/core/Unit-tests/ColorSnapshotTests.swift b/core/Unit-tests/ColorSnapshotTests.swift deleted file mode 100644 index b8c31782f..000000000 --- a/core/Unit-tests/ColorSnapshotTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// SparkColorSnapshotTests.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 13.04.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import XCTest - -@testable import SparkCore - -final class ColorSnapshotTests: SnapshotTestCase { - let colors = SparkTheme.shared.colors - - func test_base_colors() throws { - let mirror = Mirror(reflecting: self.colors.base) - self.testAllColors(colors: self.getColors(for: mirror)) - } - - func test_feedback_colors() throws { - let mirror = Mirror(reflecting: self.colors.feedback) - self.testAllColors(colors: self.getColors(for: mirror)) - } - - func test_main_colors() throws { - let mirror = Mirror(reflecting: self.colors.main) - self.testAllColors(colors: self.getColors(for: mirror)) - } - - func test_support_colors() throws { - let mirror = Mirror(reflecting: self.colors.support) - self.testAllColors(colors: self.getColors(for: mirror)) - } - - func test_state_colors() throws { - let mirror = Mirror(reflecting: self.colors.states) - self.testAllColors(colors: self.getColors(for: mirror)) - } - - private func testAllColors(colors: [String: any ColorToken], testName: String = #function) { - for value in colors { - let view = Spacer().background(value.value.color) - let vc = UIHostingController(rootView: view) - vc.view.frame = CGRect(x: 0, y: 0, width: 10, height: 10) - vc.overrideUserInterfaceStyle = .light - sparkAssertSnapshot( - matching: vc.view, - as: .image, - named: value.key, - testName: testName - ) - - vc.overrideUserInterfaceStyle = .dark - sparkAssertSnapshot( - matching: vc.view, - as: .image, - named: value.key + "-dark", - testName: testName - ) - } - } - - private func getColors(for mirror: Mirror) -> [String: any ColorToken] { - var dictionary: [String: any ColorToken] = [:] - for child in mirror.children { - guard let label = child.label, let color = child.value as? (any ColorToken) else { continue } - - dictionary[label] = color - } - - return dictionary - } -} diff --git a/core/Unit-tests/Either/AttributedStringEither+ExtensionSnapshotTests.swift b/core/Unit-tests/Either/AttributedStringEither+ExtensionSnapshotTests.swift deleted file mode 100644 index 7f4cbe67d..000000000 --- a/core/Unit-tests/Either/AttributedStringEither+ExtensionSnapshotTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// AttributedStringEither+ExtensionSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import SwiftUI -import UIKit - -extension AttributedStringEither { - - static func mock( - isSwiftUIComponent: Bool, - text: String = "My AT Title", - fontSize: CGFloat = 20 - ) -> Self { - return isSwiftUIComponent ? .right( - .mock(text: text, fontSize: fontSize) - ) : .left( - .mock(text: text, fontSize: fontSize) - ) - } -} - -private extension NSAttributedString { - static func mock( - text: String, - fontSize: CGFloat - ) -> NSAttributedString { - return .init(string: text, - attributes: [ - .foregroundColor: UIColor.purple, - .font: UIFont.italicSystemFont(ofSize: fontSize), - .underlineStyle: NSUnderlineStyle.single.rawValue - ] - ) - } -} - -private extension AttributedString { - static func mock( - text: String, - fontSize: CGFloat - ) -> AttributedString { - var attributedString = AttributedString(text) - attributedString.font = .italicSystemFont(ofSize: fontSize) - attributedString.underlineStyle = .single - attributedString.underlineColor = .purple - attributedString.foregroundColor = .purple - return attributedString - } -} diff --git a/core/Unit-tests/Either/ImageEither+ExtensionSnapshotTests.swift b/core/Unit-tests/Either/ImageEither+ExtensionSnapshotTests.swift deleted file mode 100644 index 5ecb8833c..000000000 --- a/core/Unit-tests/Either/ImageEither+ExtensionSnapshotTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ImageEither+ExtensionSnapshotTests.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 30/11/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import SwiftUI -import UIKit - -extension ImageEither { - - static func mock(isSwiftUIComponent: Bool) -> Self { - return isSwiftUIComponent ? .right(.mock) : .left(.mock) - } -} - -private extension Image { - static let mock = Image(systemName: "person.2.circle.fill") -} - -private extension UIImage { - static var mock = UIImage(systemName: "person.2.circle.fill") ?? UIImage() -} diff --git a/core/Unit-tests/Extensions/Bool+ExtensionsTests.swift b/core/Unit-tests/Extensions/Bool+ExtensionsTests.swift deleted file mode 100644 index 1a95cec5b..000000000 --- a/core/Unit-tests/Extensions/Bool+ExtensionsTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Bool+ExtensionsTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 03/07/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -extension Bool { - - // MARK: - Properties - - static let allCases: [Self] = [true, false] -} diff --git a/core/Unit-tests/Extensions/Color+ExtensionTests.swift b/core/Unit-tests/Extensions/Color+ExtensionTests.swift deleted file mode 100644 index 5a7ab5d5f..000000000 --- a/core/Unit-tests/Extensions/Color+ExtensionTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Color+ExtensionTests.swift -// Spark -// -// Created by janniklas.freundt.ext on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -@testable import SparkCore -import SwiftUI - -extension ColorToken where Self == ColorTokenGeneratedMock { - - // MARK: - Methods - - static func mock(_ color: Color) -> Self { - let mock = ColorTokenGeneratedMock() - mock.color = color - return mock - } -} diff --git a/core/Unit-tests/Extensions/UITraitCollection+ExtensionTests.swift b/core/Unit-tests/Extensions/UITraitCollection+ExtensionTests.swift deleted file mode 100644 index bf76c2747..000000000 --- a/core/Unit-tests/Extensions/UITraitCollection+ExtensionTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// UITraitCollection+ExtensionTests.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension UITraitCollection { - - // MARK: - Properties - - static let darkMode: UITraitCollection = .init( - traitsFrom: [ - .init(userInterfaceStyle: .dark) - ] - ) -} diff --git a/core/Unit-tests/Resources/IconographyTests.swift b/core/Unit-tests/Resources/IconographyTests.swift deleted file mode 100644 index 2b57ae122..000000000 --- a/core/Unit-tests/Resources/IconographyTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// IconographyTests.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 16.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import SparkCore -import XCTest - -struct IconographyTests { - - // MARK: - Shared - - static var shared = IconographyTests() - - // MARK: - Initialize - - private init() {} - - // MARK: - Icons - - lazy var arrow: UIImage = { - return self.getImage(name: "arrow") - }() - - lazy var checkmark: UIImage = { - return self.getImage(name: "checkbox-selected") - }() - - lazy var switchOff: UIImage = { - return self.getImage(name: "switchOff") - }() - - lazy var switchOn: UIImage = { - return self.getImage(name: "switchOn") - }() - - // MARK: - Helper - - private func getImage(name: String) -> UIImage { - guard let image = UIImage(named: name, in: Bundle(for: ClassForBundle.self), with: nil) else { - fatalError("no image found for \(name)") - } - return image - } - - private class ClassForBundle {} -} diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/arrow.imageset/icon.svg b/core/Unit-tests/Resources/IconographyTests.xcassets/arrow.imageset/icon.svg deleted file mode 100644 index d0653a080..000000000 --- a/core/Unit-tests/Resources/IconographyTests.xcassets/arrow.imageset/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset/close.svg b/core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset/close.svg deleted file mode 100644 index 269a121ed..000000000 --- a/core/Unit-tests/Resources/IconographyTests.xcassets/switchOff.imageset/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset/check.svg b/core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset/check.svg deleted file mode 100644 index dd277df6e..000000000 --- a/core/Unit-tests/Resources/IconographyTests.xcassets/switchOn.imageset/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/core/Unit-tests/Sourcery/ColorTokenGeneratedMock+Extensions.swift b/core/Unit-tests/Sourcery/ColorTokenGeneratedMock+Extensions.swift deleted file mode 100644 index 55a5c9a98..000000000 --- a/core/Unit-tests/Sourcery/ColorTokenGeneratedMock+Extensions.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// SparkCoreTests -// -// Created by louis.borlee on 29/06/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -extension ColorTokenGeneratedMock { - static func == (lhs: ColorTokenGeneratedMock, rhs: ColorTokenGeneratedMock) -> Bool { - return lhs === rhs - } -} - -extension ColorTokenGeneratedMock { - convenience init(uiColor: UIColor) { - self.init() - self.uiColor = uiColor - } -} diff --git a/core/Unit-tests/SparkCoreSnapshotTests.swift b/core/Unit-tests/SparkCoreSnapshotTests.swift deleted file mode 100644 index c0696bea8..000000000 --- a/core/Unit-tests/SparkCoreSnapshotTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// SparkCoreSnapshotTests.swift -// SparkCoreTests -// -// Created by luis.figueiredo-ext on 08/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting -import SwiftUI - -// MARK: - UIKit snapshot example -final class SparkCoreUISnapshotTests: UIKitComponentSnapshotTestCase { - func testExample() throws { - let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 300)) - view.backgroundColor = .red - sparkAssertSnapshot(matching: view, as: .image) - } - - func testDynamicContentSize() throws { - let view = UILabel() - view.text = "Hello world" - view.font = .preferredFont(forTextStyle: .body) - view.adjustsFontForContentSizeCategory = true - view.translatesAutoresizingMaskIntoConstraints = false - assertSnapshotInDarkAndLight(matching: view) - } -} - -// MARK: - SwiftUI snapshot example -final class SparkCoreSnapshotTests: SwiftUIComponentSnapshotTestCase { - func testDynamicContentSize() throws { - let view = Text("Hello world").fixedSize() - assertSnapshotInDarkAndLight(matching: view) - } -} diff --git a/core/Unit-tests/SparkCoreSnapshotTestsUtils.swift b/core/Unit-tests/SparkCoreSnapshotTestsUtils.swift deleted file mode 100644 index da32b074a..000000000 --- a/core/Unit-tests/SparkCoreSnapshotTestsUtils.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// SparkCoreSnapshotTestsUtils.swift -// SparkCoreTests -// -// Created by luis.figueiredo-ext on 08/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SnapshotTesting - -public func sparkAssertSnapshot( - matching value: @autoclosure () throws -> Value, - as snapshotting: Snapshotting, - named name: String? = nil, - record recording: Bool = false, - timeout: TimeInterval = 5, - file: StaticString = #file, - testName: String = #function, - line: UInt = #line -) { - let failure = verifySnapshot( - matching: try value(), - as: snapshotting, - named: name, - record: recording, - snapshotDirectory: SnapshotTestCaseTracker.shared.snapshotDirectory(for: file), - timeout: timeout, - file: file, - testName: testName - ) - guard let message = failure else { return } - XCTFail("\(message): \(testName)", file: file, line: line) -} diff --git a/core/Unit-tests/TestCase/Component/Constants/ComponentSnapshotTestConstants.swift b/core/Unit-tests/TestCase/Component/Constants/ComponentSnapshotTestConstants.swift deleted file mode 100644 index a76ce72cd..000000000 --- a/core/Unit-tests/TestCase/Component/Constants/ComponentSnapshotTestConstants.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ComponentSnapshotTestConstants.swift -// SparkCoreTests -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -enum ComponentSnapshotTestConstants { - static let record = false - static let timeout: TimeInterval = 5 - - static let imagePrecision: Float = 0.98 - static let imagePerceptualPrecision: Float = 0.98 - - enum Modes { - static let all: [ComponentSnapshotTestMode] = [.light, .dark] - static let `default`: [ComponentSnapshotTestMode] = [.light] - } - - enum Sizes { - static let all: [UIContentSizeCategory] = [.extraSmall, .medium, .accessibilityExtraExtraExtraLarge] - static let `default`: [UIContentSizeCategory] = [.medium] - } -} diff --git a/core/Unit-tests/TestCase/Component/Enum/ComponentSnapshotTestMode.swift b/core/Unit-tests/TestCase/Component/Enum/ComponentSnapshotTestMode.swift deleted file mode 100644 index c9d0d7b57..000000000 --- a/core/Unit-tests/TestCase/Component/Enum/ComponentSnapshotTestMode.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ComponentSnapshotTestMode.swift -// SparkCoreTests -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -enum ComponentSnapshotTestMode: String { - case dark - case light - - // MARK: - Properties - - private var interfaceStyle: UIUserInterfaceStyle { - switch self { - case .dark: - return .dark - case .light: - return .light - } - } - - var traitCollection: UITraitCollection { - return .init( - traitsFrom: [ - .init(userInterfaceStyle: self.interfaceStyle) - ] - ) - } - - var suffix: String { - return self.rawValue - } -} diff --git a/core/Unit-tests/TestCase/Component/Helpers/ComponentSnapshotTestHelpers.swift b/core/Unit-tests/TestCase/Component/Helpers/ComponentSnapshotTestHelpers.swift deleted file mode 100644 index c2edf91fb..000000000 --- a/core/Unit-tests/TestCase/Component/Helpers/ComponentSnapshotTestHelpers.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ComponentSnapshotTestHelpers.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -enum ComponentSnapshotTestHelpers { - - // MARK: - Helpers - - static func testName( - _ testName: String, - mode: ComponentSnapshotTestMode, - size: UIContentSizeCategory - ) -> String { - return [testName, mode.suffix, size.identifier] - .joined(separator: "-") - } - - static func traitCollection( - mode: ComponentSnapshotTestMode, - size: UIContentSizeCategory - ) -> UITraitCollection { - return UITraitCollection(traitsFrom: [ - mode.traitCollection, - UITraitCollection(preferredContentSizeCategory: size) - ]) - } -} - -// MARK: - Private extension - -private extension UIContentSizeCategory { - /// Returns the identifier used in the filename - var identifier: String { - switch self { - case .unspecified: - return "unspecified" - case .extraSmall: - return "extraSmall" - case .small: - return "small" - case .medium: - return "medium" - case .large: - return "large" - case .extraLarge: - return "extraLarge" - case .extraExtraLarge: - return "extraExtraLarge" - case .extraExtraExtraLarge: - return "extraExtraExtraLarge" - case .accessibilityMedium: - return "accessibilityMedium" - case .accessibilityLarge: - return "accessibilityLarge" - case .accessibilityExtraLarge: - return "accessibilityExtraLarge" - case .accessibilityExtraExtraLarge: - return "accessibilityExtraExtraLarge" - case .accessibilityExtraExtraExtraLarge: - return "accessibilityExtraExtraExtraLarge" - default: - return "unknown" - } - } -} - diff --git a/core/Unit-tests/TestCase/Component/TestCase/SwiftUIComponentSnapshotTestCase.swift b/core/Unit-tests/TestCase/Component/TestCase/SwiftUIComponentSnapshotTestCase.swift deleted file mode 100644 index 3473b622a..000000000 --- a/core/Unit-tests/TestCase/Component/TestCase/SwiftUIComponentSnapshotTestCase.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// SwiftUIComponentSnapshotTestCase.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit -import SnapshotTesting -@testable import SparkCore - -open class SwiftUIComponentSnapshotTestCase: SnapshotTestCase { - - // MARK: - Type Alias - - private typealias Constants = ComponentSnapshotTestConstants - private typealias Helpers = ComponentSnapshotTestHelpers - - // MARK: - Snapshot Testing - - func assertSnapshot( - matching view: @autoclosure () -> some View, - named name: String? = nil, - modes: [ComponentSnapshotTestMode], - sizes: [UIContentSizeCategory], - record recording: Bool = Constants.record, - timeout: TimeInterval = Constants.timeout, - file: StaticString = #file, - testName: String = #function, - line: UInt = #line - ) { - for mode in modes { - for size in sizes { - let sizeCategory = ContentSizeCategory(size) ?? .extraSmall - let view = view().environment(\.sizeCategory, sizeCategory) - .background(Color.gray) - sparkAssertSnapshot( - matching: view, - as: .image( - precision: Constants.imagePrecision, - perceptualPrecision: Constants.imagePerceptualPrecision, - traits: Helpers.traitCollection( - mode: mode, - size: size - ) - ), - named: name, - record: recording, - timeout: timeout, - file: file, - testName: Helpers.testName( - testName, - mode: mode, - size: size - ), - line: line - ) - } - } - } -} diff --git a/core/Unit-tests/TestCase/Component/TestCase/UIKitComponentSnapshotTestCase.swift b/core/Unit-tests/TestCase/Component/TestCase/UIKitComponentSnapshotTestCase.swift deleted file mode 100644 index 1420b4e2e..000000000 --- a/core/Unit-tests/TestCase/Component/TestCase/UIKitComponentSnapshotTestCase.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// UIKitComponentSnapshotTestCase.swift -// SparkCoreSnapshotTests -// -// Created by robin.lemaire on 06/10/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import UIKit -import SnapshotTesting -@testable import SparkCore - -open class UIKitComponentSnapshotTestCase: SnapshotTestCase { - - // MARK: - Type Alias - - private typealias Constants = ComponentSnapshotTestConstants - private typealias Helpers = ComponentSnapshotTestHelpers - - // MARK: - Snapshot Testing - - func assertSnapshot( - matching view: @autoclosure () -> some UIView, - named name: String? = nil, - modes: [ComponentSnapshotTestMode], - sizes: [UIContentSizeCategory], - record recording: Bool = Constants.record, - delay: TimeInterval = 0, - timeout: TimeInterval = Constants.timeout, - file: StaticString = #file, - testName: String = #function, - line: UInt = #line - ) { - for mode in modes { - for size in sizes { - sparkAssertSnapshot( - matching: view(), - as: .wait( - for: delay, - on: .image( - precision: Constants.imagePrecision, - perceptualPrecision: Constants.imagePerceptualPrecision, - traits: Helpers.traitCollection( - mode: mode, - size: size - ) - ) - ), - named: name, - record: recording, - timeout: timeout, - file: file, - testName: Helpers.testName( - testName, - mode: mode, - size: size - ), - line: line - ) - } - } - } -} diff --git a/core/Unit-tests/TestCase/Deprecated/ComponentSnapshotTestCase.swift b/core/Unit-tests/TestCase/Deprecated/ComponentSnapshotTestCase.swift deleted file mode 100644 index fb6ba0399..000000000 --- a/core/Unit-tests/TestCase/Deprecated/ComponentSnapshotTestCase.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// ComponentSnapshotTestCase.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import UIKit - -@testable import SparkCore - -// MARK: - Constants - -fileprivate enum Constants { - static let record = false - static let timeout: TimeInterval = 5 - - static let namedSuffixForLight = "light" - static let namedSuffixForDark = "dark" - - static let imagePrecision: Float = 0.98 - static let imagePerceptualPrecision: Float = 0.98 - - static let sizes: [UIContentSizeCategory] = [.extraSmall, .medium, .accessibilityExtraExtraExtraLarge] - - static let separator = "-" -} - -// MARK: - SwiftUI - -extension SwiftUIComponentSnapshotTestCase { - - // MARK: - Snapshot Testing - - @available(*, deprecated, message: "Use assertSnapshot instead !") - func assertSnapshotInDarkAndLight( - matching view: @autoclosure () -> some View, - named name: String? = nil, - sizes: [UIContentSizeCategory] = Constants.sizes, - record recording: Bool = Constants.record, - timeout: TimeInterval = Constants.timeout, - file: StaticString = #file, - testName: String = #function, - line: UInt = #line - ) { - // Dark mode testing - for size in sizes { - let traits = UITraitCollection(traitsFrom: [.darkMode]) - let filename = [testName, Constants.namedSuffixForDark, size.identifier] - .joined(separator: Constants.separator) - sparkAssertSnapshot( - matching: view().environment(\.sizeCategory, ContentSizeCategory(size) ?? .extraSmall), - as: .image(precision: Constants.imagePrecision, - perceptualPrecision: Constants.imagePerceptualPrecision, - traits: traits), - named: name, - record: recording, - timeout: timeout, - file: file, - testName: filename, - line: line - ) - } - - // Light mode testing - for size in sizes { - let filename = [testName, Constants.namedSuffixForLight, size.identifier] - .joined(separator: Constants.separator) - sparkAssertSnapshot( - matching: view().environment(\.sizeCategory, ContentSizeCategory(size) ?? .extraSmall), - as: .image(precision: Constants.imagePrecision, - perceptualPrecision: Constants.imagePerceptualPrecision), - named: name, - record: recording, - timeout: timeout, - file: file, - testName: filename, - line: line - ) - } - } -} - -// MARK: - UIKit - -extension UIKitComponentSnapshotTestCase { - - // MARK: - Snapshot Testing - - @available(*, deprecated, message: "Use assertSnapshot instead !") - func assertSnapshotInDarkAndLight( - matching view: @autoclosure () -> some UIView, - named name: String? = nil, - sizes: [UIContentSizeCategory] = Constants.sizes, - record recording: Bool = Constants.record, - delay: TimeInterval = 0, - timeout: TimeInterval = Constants.timeout, - file: StaticString = #file, - testName: String = #function, - line: UInt = #line - ) { - - // Dark mode testing - for size in sizes { - let traits = UITraitCollection(traitsFrom: [.darkMode, UITraitCollection(preferredContentSizeCategory: size)]) - let filename = [testName, Constants.namedSuffixForDark, size.identifier] - .joined(separator: Constants.separator) - sparkAssertSnapshot( - matching: view(), - as: .wait( - for: delay, - on: .image( - precision: Constants.imagePrecision, - perceptualPrecision: Constants.imagePerceptualPrecision, - traits: traits - ) - ), - named: name, - record: recording, - timeout: timeout, - file: file, - testName: filename, - line: line - ) - } - - // Light mode testing - for size in sizes { - let traits = UITraitCollection(preferredContentSizeCategory: size) - let filename = [testName, Constants.namedSuffixForLight, size.identifier] - .joined(separator: Constants.separator) - - sparkAssertSnapshot( - matching: view(), - as: .wait( - for: delay, - on: .image( - precision: Constants.imagePrecision, - perceptualPrecision: Constants.imagePerceptualPrecision, - traits: traits - ) - ), - named: name, - record: recording, - timeout: timeout, - file: file, - testName: filename, - line: line - ) - } - } -} - -// MARK: - UIContentSizeCategory identifier extension - -private extension UIContentSizeCategory { - /// Returns the identifier used in the filename - var identifier: String { - switch self { - case .unspecified: - return "unspecified" - case .extraSmall: - return "extraSmall" - case .small: - return "small" - case .medium: - return "medium" - case .large: - return "large" - case .extraLarge: - return "extraLarge" - case .extraExtraLarge: - return "extraExtraLarge" - case .extraExtraExtraLarge: - return "extraExtraExtraLarge" - case .accessibilityMedium: - return "accessibilityMedium" - case .accessibilityLarge: - return "accessibilityLarge" - case .accessibilityExtraLarge: - return "accessibilityExtraLarge" - case .accessibilityExtraExtraLarge: - return "accessibilityExtraExtraLarge" - case .accessibilityExtraExtraExtraLarge: - return "accessibilityExtraExtraExtraLarge" - default: - return "unknown" - } - } -} diff --git a/core/Unit-tests/TestCase/SnapshotTestCase.swift b/core/Unit-tests/TestCase/SnapshotTestCase.swift deleted file mode 100644 index 4377c0313..000000000 --- a/core/Unit-tests/TestCase/SnapshotTestCase.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SnapshotTestCase.swift -// SparkCoreTests -// -// Created by robin.lemaire on 05/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SwiftUI -import UIKit - -open class SnapshotTestCase: XCTestCase { - - // MARK: - Set up - - override open class func setUp() { - super.setUp() - - SparkConfiguration.load() - SnapshotTestCaseTracker.shared.subscribe() - } -} diff --git a/core/Unit-tests/TestCase/SnapshotTestCaseTracker.swift b/core/Unit-tests/TestCase/SnapshotTestCaseTracker.swift deleted file mode 100644 index 6e65c77d9..000000000 --- a/core/Unit-tests/TestCase/SnapshotTestCaseTracker.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// SnapshotTestCaseTracker.swift -// SparkCoreTests -// -// Created by janniklas.freundt.ext on 13.04.23. -// Copyright © 2023 Adevi nta. All rights reserved. -// - -import XCTest - -/// `TestCaseTracker` is used to keep the track of the current test suite. It creates a sub-directory for the current test case classname for snapshot-images. -// swiftlint:disable force_unwrapping -final class SnapshotTestCaseTracker: NSObject, XCTestObservation { - // MARK: - Shared instance - - static let shared = SnapshotTestCaseTracker() - - // MARK: - Properties - - private(set) var currentTestCase: XCTestCase! - private var didSubscribe = false - - // MARK: - Methods - - func snapshotDirectory(for file: StaticString) -> String { - let snapshotDirectory = ProcessInfo.processInfo.environment["SNAPSHOT_REFERENCE_DIR"]! + "/" - guard let url = URL(string: snapshotDirectory) else { return "" } - - return url.appendingPathComponent(self.currentTestCase.testClassName).path - } - - func testName(_ identifier: String? = nil) -> String { - [self.currentTestCase.testMethodName, identifier] - .compactMap { $0 } - .filter { !$0.isEmpty } - .joined(separator: "_") - } - - // MARK: - Subscription - - func subscribe() { - guard !self.didSubscribe else { - return - } - - defer { self.didSubscribe = true } - - XCTestObservationCenter.shared.addTestObserver(self) - } - - @objc func testCaseWillStart(_ testCase: XCTestCase) { - self.currentTestCase = testCase - } - - @objc func testCaseDidFinish(_ testCase: XCTestCase) { - self.currentTestCase = nil - } -} - -// MARK: - Private extensions - -private extension String { - // MARK: - Initialize - - init(_ staticString: StaticString) { - self = staticString.withUTF8Buffer { - String(decoding: $0, as: UTF8.self) - } - } -} - -private extension XCTestCase { - // MARK: - Methods - - var testClassName: String { - String(self.sanitizedName.split(separator: " ").first!) - } - - var testMethodName: String { - String(self.sanitizedName.split(separator: " ").last!) - } - - // MARK: - Private - - private var sanitizedName: String { - let fullName = name - let characterSet = CharacterSet(charactersIn: "[]+-") - let name = fullName.components(separatedBy: characterSet).joined() - - if let quickClass = NSClassFromString("QuickSpec"), isKind(of: quickClass) { - let className = String(describing: type(of: self)) - if let range = name.range(of: className), range.lowerBound == name.startIndex { - return name - .replacingCharacters(in: range, with: "") - .trimmingCharacters(in: .whitespacesAndNewlines) - } - } - - return name - } -} diff --git a/core/Unit-tests/TestCase/TestCase.swift b/core/Unit-tests/TestCase/TestCase.swift deleted file mode 100644 index 928d772bf..000000000 --- a/core/Unit-tests/TestCase/TestCase.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// TestCase.swift -// SparkCoreTests -// -// Created by louis.borlee on 22/09/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -open class TestCase: XCTestCase { - - // MARK: - Set up - - override open class func setUp() { - super.setUp() - - SparkConfiguration.load() - } -} diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 1ac60d345..000000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,47 +0,0 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - -default_platform(:ios) - -platform :ios do - desc "Lane to generate xcodeproj with xcodegen" - lane :generate_xcodeproj do |options| - root_folder = options[:ROOT_FOLDER] || "." - templateFile = options[:TEMPLATE_FILE] || "" - sh("scripts/xcodeproj/generate.sh", root_folder, templateFile) - end - - desc "Lane for build framework" - lane :build_framework do |options| - gym( - clean: true, - scheme: options[:TARGET_NAME], - project: "Spark.xcodeproj", - skip_package_ipa: true, - ) - end - - desc "Lane for unit testing" - lane :unit_tests do |options| - scan( - clean: true, - scheme: options[:TARGET_NAME], - project: "Spark.xcodeproj", - output_directory: "out", - result_bundle: true, - device: "iPhone 14 Pro", - ) - end -end diff --git a/fastlane/scripts/xcodeproj/generate.sh b/fastlane/scripts/xcodeproj/generate.sh deleted file mode 100755 index cd6c0056b..000000000 --- a/fastlane/scripts/xcodeproj/generate.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -ROOT_FOLDER=$1 -TEMPLATE_FILE=$2 - -#Go to home -cd .. - -#Go to root folder -cd $ROOT_FOLDER - -# Generate xcodeproj -rm -rf ./*.xcodeproj # Keep a single .xcodeproj - -if [[ -z "$TEMPLATE_FILE" ]] -then - xcodegen -else - xcodegen --spec $TEMPLATE_FILE -fi - diff --git a/project-ci.yml b/project-ci.yml deleted file mode 100644 index e189848ce..000000000 --- a/project-ci.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Spark -include: - - xcodegen/spark-shared.yml - - xcodegen/spark-core.yml - - xcodegen/spark-core-unit-tests.yml - - xcodegen/spark-core-snapshot-tests.yml - - xcodegen/spark.yml - -targetTemplates: - SparkCoreSchemeTemplate: - templates: - - SparkCoreTemplate - scheme: - testTargets: - - name: SparkCoreUnitTests - gatherCoverageData: true - SparkDemoSchemeTemplate: - templates: - - SparkDemoTemplate - scheme: - testTargets: - - name: SparkCoreSnapshotTests - environmentVariables: - - variable: SNAPSHOT_REFERENCE_DIR - value: "$(SRCROOT)/spark-ios-snapshots" - isEnabled: true - gatherCoverageData: true - -targets: - SparkCore: - templates: - - SparkCoreSchemeTemplate - SparkDemo: - templates: - - SparkDemoSchemeTemplate diff --git a/project.yml b/project.yml index a47a3262a..cf6d144ec 100644 --- a/project.yml +++ b/project.yml @@ -1,26 +1,52 @@ name: Spark -include: - - xcodegen/spark-shared.yml - - xcodegen/spark-core.yml - - xcodegen/spark-core-unit-tests.yml - - xcodegen/spark-core-snapshot-tests.yml - - xcodegen/spark.yml + +configs: + Debug: debug + Release: release + +options: + createIntermediateGroups: true + defaultConfig: Release + groupSortPosition: top + deploymentTarget: + iOS: 15.0 + useBaseInternationalization: false + postGenCommand: sh .postGenCommand.sh + +packages: + SparkCore: + path: .Demo/.. targetTemplates: - SparkCoreSchemeTemplate: - templates: - - SparkCoreTemplate - scheme: - testTargets: - - name: SparkCoreUnitTests - - name: SparkCoreSnapshotTests - environmentVariables: - - variable: SNAPSHOT_REFERENCE_DIR - value: "$(SRCROOT)/spark-ios-snapshots" - isEnabled: true - gatherCoverageData: true + SparkDemoTemplate: + type: application + platform: iOS + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark.demo" + SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" + SUPPORTS_MACCATALYST: NO + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO + info: + path: .Demo/Info.plist + properties: + UILaunchScreen: [] + + UIApplicationSceneManifest: + UIApplicationSupportsMultipleScenes: false + UISceneConfigurations: + UIWindowSceneSessionRoleApplication: + - UISceneConfigurationName: Default Configuration + UISceneDelegateClassName: $(PRODUCT_MODULE_NAME).SceneDelegate + sources: + - path: .Demo + dependencies: + - package: SparkCore + product: SparkCore + - package: SparkCore + product: SparkCoreTesting targets: - SparkCore: + SparkDemo: templates: - - SparkCoreSchemeTemplate + - SparkDemoTemplate diff --git a/scripts/swiftgen.sh b/scripts/swiftgen.sh deleted file mode 100755 index f3ad8ce25..000000000 --- a/scripts/swiftgen.sh +++ /dev/null @@ -1,6 +0,0 @@ -export PATH="$PATH:/opt/homebrew/bin" -if which swiftgen >/dev/null; then - swiftgen -else - echo "warning: SwiftGen not installed, download it from https://github.com/SwiftGen/SwiftGen" -fi diff --git a/scripts/swiftlint.sh b/scripts/swiftlint.sh deleted file mode 100755 index 278c95de9..000000000 --- a/scripts/swiftlint.sh +++ /dev/null @@ -1,5 +0,0 @@ -if which swiftlint >/dev/null; then - swiftlint -else - echo "warning: swiftling not installed." -fi diff --git a/spark/Demo/Assets.xcassets/arrow.imageset/Contents.json b/spark/Demo/Assets.xcassets/arrow.imageset/Contents.json deleted file mode 100644 index 5adb9f4b2..000000000 --- a/spark/Demo/Assets.xcassets/arrow.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/spark/Demo/Assets.xcassets/check.imageset/Contents.json b/spark/Demo/Assets.xcassets/check.imageset/Contents.json deleted file mode 100644 index 14f9a4074..000000000 --- a/spark/Demo/Assets.xcassets/check.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "check.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/spark/Demo/Assets.xcassets/checkbox-selected.imageset/Contents.json b/spark/Demo/Assets.xcassets/checkbox-selected.imageset/Contents.json deleted file mode 100644 index 084b8dde6..000000000 --- a/spark/Demo/Assets.xcassets/checkbox-selected.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "check-fill.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/spark/Demo/Assets.xcassets/checkbox-selected.imageset/check-fill.svg b/spark/Demo/Assets.xcassets/checkbox-selected.imageset/check-fill.svg deleted file mode 100644 index 4dd43814c..000000000 --- a/spark/Demo/Assets.xcassets/checkbox-selected.imageset/check-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/spark/Demo/Assets.xcassets/close.imageset/Contents.json b/spark/Demo/Assets.xcassets/close.imageset/Contents.json deleted file mode 100644 index 1bc027f6d..000000000 --- a/spark/Demo/Assets.xcassets/close.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "close.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/accent-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/accent-container.colorset/Contents.json deleted file mode 100644 index 82ee83e74..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/accent-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xD5", - "red" : "0xEA" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xBB", - "green" : "0x64", - "red" : "0x8D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/accent-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/accent-variant.colorset/Contents.json deleted file mode 100644 index e0994b19f..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/accent-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x77", - "green" : "0x38", - "red" : "0x51" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xB7", - "red" : "0xDB" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/accent.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/accent.colorset/Contents.json deleted file mode 100644 index 1545cb377..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/accent.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x99", - "red" : "0xCC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x99", - "red" : "0xCC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/on-accent-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/on-accent-container.colorset/Contents.json deleted file mode 100644 index cfb6aeb9d..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/on-accent-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x55", - "green" : "0x25", - "red" : "0x36" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/on-accent-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/on-accent-variant.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/on-accent-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Accent/on-accent.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Accent/on-accent.colorset/Contents.json deleted file mode 100644 index 736042141..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Accent/on-accent.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/background-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/background-variant.colorset/Contents.json deleted file mode 100644 index c8062477c..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/background-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x00", - "green" : "0x00", - "red" : "0x00" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/background.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/background.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/on-background-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/on-background-variant.colorset/Contents.json deleted file mode 100644 index fa7ccff20..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/on-background-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/on-background.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/on-background.colorset/Contents.json deleted file mode 100644 index 7d6602f59..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/on-background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/on-overlay.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/on-overlay.colorset/Contents.json deleted file mode 100644 index 2536dc2d1..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/on-overlay.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/on-surface-inverse.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/on-surface-inverse.colorset/Contents.json deleted file mode 100644 index 32ce9af10..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/on-surface-inverse.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x41", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/on-surface.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/on-surface.colorset/Contents.json deleted file mode 100644 index 7d6602f59..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/on-surface.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/outline-high.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/outline-high.colorset/Contents.json deleted file mode 100644 index 7d6602f59..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/outline-high.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/outline.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/outline.colorset/Contents.json deleted file mode 100644 index 84cd7cd80..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/outline.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xDE", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x57", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/overlay.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/overlay.colorset/Contents.json deleted file mode 100644 index b41721e95..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/overlay.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.720", - "blue" : "0x36", - "green" : "0x30", - "red" : "0x31" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.720", - "blue" : "0x36", - "green" : "0x30", - "red" : "0x31" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/surface-inverse.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/surface-inverse.colorset/Contents.json deleted file mode 100644 index 66ed6800a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/surface-inverse.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x41", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Base/surface.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Base/surface.colorset/Contents.json deleted file mode 100644 index f92d76550..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Base/surface.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Basic/Contents.json b/spark/Sources/Resources/Colors.xcassets/Basic/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Basic/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Basic/basic-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Basic/basic-container.colorset/Contents.json deleted file mode 100644 index 29f04c2af..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Basic/basic-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7B", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Basic/basic.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Basic/basic.colorset/Contents.json deleted file mode 100644 index 6b7bc37cd..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Basic/basic.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6A", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Basic/on-basic-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Basic/on-basic-container.colorset/Contents.json deleted file mode 100644 index 1162e6227..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Basic/on-basic-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x58", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Basic/on-basic.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Basic/on-basic.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Basic/on-basic.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Contents.json b/spark/Sources/Resources/Colors.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/alert-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/alert-container.colorset/Contents.json deleted file mode 100644 index acd91f0a1..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/alert-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE4", - "green" : "0xF5", - "red" : "0xFD" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x1D", - "green" : "0x4C", - "red" : "0x62" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/alert.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/alert.colorset/Contents.json deleted file mode 100644 index fa6fc2c41..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/alert.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x48", - "green" : "0xBF", - "red" : "0xF4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x76", - "green" : "0xCF", - "red" : "0xF7" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/error-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/error-container.colorset/Contents.json deleted file mode 100644 index 4205397d1..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/error-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xED", - "red" : "0xFD" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x20", - "green" : "0x22", - "red" : "0x62" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/error.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/error.colorset/Contents.json deleted file mode 100644 index 0825b9f58..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/error.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x51", - "green" : "0x56", - "red" : "0xF6" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7D", - "green" : "0x80", - "red" : "0xF8" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/info-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/info-container.colorset/Contents.json deleted file mode 100644 index 1f2682275..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/info-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF1", - "red" : "0xDA" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x4A", - "green" : "0x40", - "red" : "0x03" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/info.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/info.colorset/Contents.json deleted file mode 100644 index b813ae004..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/info.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xB8", - "green" : "0xA0", - "red" : "0x07" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xCA", - "green" : "0xB8", - "red" : "0x45" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/neutral-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/neutral-container.colorset/Contents.json deleted file mode 100644 index 4283ecb4d..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/neutral-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xDE", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x41", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/neutral.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/neutral.colorset/Contents.json deleted file mode 100644 index a0695d1ff..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/neutral.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x41", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xB1", - "green" : "0xAD", - "red" : "0xAC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-alert-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-alert-container.colorset/Contents.json deleted file mode 100644 index ea9d84ff7..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-alert-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2B", - "green" : "0x73", - "red" : "0x92" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-alert.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-alert.colorset/Contents.json deleted file mode 100644 index 736042141..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-alert.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-error-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-error-container.colorset/Contents.json deleted file mode 100644 index bffd95ca9..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-error-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x31", - "green" : "0x34", - "red" : "0x94" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-error.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-error.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-error.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-info-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-info-container.colorset/Contents.json deleted file mode 100644 index 20364f9d3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-info-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6E", - "green" : "0x60", - "red" : "0x04" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-info.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-info.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-info.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral-container.colorset/Contents.json deleted file mode 100644 index 857f2001d..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x41", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-neutral.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-success-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-success-container.colorset/Contents.json deleted file mode 100644 index d80ca8ad3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-success-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x40", - "green" : "0x63", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/on-success.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/on-success.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/on-success.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/success-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/success-container.colorset/Contents.json deleted file mode 100644 index 761f68ad4..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/success-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE9", - "green" : "0xF2", - "red" : "0xE0" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2B", - "green" : "0x42", - "red" : "0x14" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Feedback/success.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Feedback/success.colorset/Contents.json deleted file mode 100644 index 575a8c44d..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Feedback/success.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6B", - "green" : "0xA5", - "red" : "0x31" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x90", - "green" : "0xBC", - "red" : "0x64" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/main-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/main-container.colorset/Contents.json deleted file mode 100644 index 92fd031ca..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/main-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xD6", - "red" : "0xC2" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x66", - "green" : "0x17", - "red" : "0x00" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/main-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/main-variant.colorset/Contents.json deleted file mode 100644 index 2aff007c5..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/main-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x99", - "green" : "0x27", - "red" : "0x00" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xB5", - "red" : "0x91" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/main.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/main.colorset/Contents.json deleted file mode 100644 index 5ca50c747..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/main.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x52", - "red" : "0x00" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x73", - "red" : "0x31" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/on-main-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/on-main-container.colorset/Contents.json deleted file mode 100644 index 094c2d0dc..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/on-main-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x99", - "green" : "0x27", - "red" : "0x00" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/on-main-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/on-main-variant.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/on-main-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Main/on-main.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Main/on-main.colorset/Contents.json deleted file mode 100644 index 22c4bb0a8..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Main/on-main.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/accent-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/accent-container-pressed.colorset/Contents.json deleted file mode 100644 index 743d0953f..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/accent-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xF3", - "red" : "0xF9" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x99", - "green" : "0x4D", - "red" : "0x6E" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/accent-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/accent-pressed.colorset/Contents.json deleted file mode 100644 index 15e5f4e76..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/accent-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xB7", - "red" : "0xDB" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xDD", - "green" : "0x7D", - "red" : "0xAC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/accent-variant-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/accent-variant-pressed.colorset/Contents.json deleted file mode 100644 index b9f1a1942..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/accent-variant-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x99", - "green" : "0x4D", - "red" : "0x6E" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x99", - "red" : "0xCC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/alert-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/alert-container-pressed.colorset/Contents.json deleted file mode 100644 index a8a9f8283..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/alert-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF6", - "green" : "0xFC", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x12", - "green" : "0x30", - "red" : "0x3D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/alert-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/alert-pressed.colorset/Contents.json deleted file mode 100644 index 3352d650e..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/alert-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x76", - "green" : "0xCF", - "red" : "0xF7" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x48", - "green" : "0xBF", - "red" : "0xF4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/basic-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/basic-container-pressed.colorset/Contents.json deleted file mode 100644 index 153b4b364..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/basic-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6A", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/basic-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/basic-pressed.colorset/Contents.json deleted file mode 100644 index 01cda98cd..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/basic-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7B", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xD3", - "green" : "0xC5", - "red" : "0xC4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/error-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/error-container-pressed.colorset/Contents.json deleted file mode 100644 index 3502b968d..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/error-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF6", - "green" : "0xF7", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x14", - "green" : "0x15", - "red" : "0x3E" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/error-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/error-pressed.colorset/Contents.json deleted file mode 100644 index 6a4f36a7a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/error-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7D", - "green" : "0x80", - "red" : "0xF8" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x51", - "green" : "0x56", - "red" : "0xF6" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/info-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/info-container-pressed.colorset/Contents.json deleted file mode 100644 index 93e23d9f2..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/info-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFC", - "green" : "0xFA", - "red" : "0xF3" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x28", - "red" : "0x02" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/info-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/info-pressed.colorset/Contents.json deleted file mode 100644 index d49466870..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/info-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xCA", - "green" : "0xB8", - "red" : "0x45" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xB8", - "green" : "0xA0", - "red" : "0x07" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/main-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/main-container-pressed.colorset/Contents.json deleted file mode 100644 index 9c3773c29..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/main-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xF6", - "red" : "0xF2" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x33", - "green" : "0x0A", - "red" : "0x00" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/main-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/main-pressed.colorset/Contents.json deleted file mode 100644 index 05ca6b2aa..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/main-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x78", - "red" : "0x38" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x52", - "red" : "0x00" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/main-variant-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/main-variant-pressed.colorset/Contents.json deleted file mode 100644 index fa35632c6..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/main-variant-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xCC", - "green" : "0x3B", - "red" : "0x00" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x94", - "red" : "0x61" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/neutral-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/neutral-container-pressed.colorset/Contents.json deleted file mode 100644 index 62df98722..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/neutral-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/neutral-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/neutral-pressed.colorset/Contents.json deleted file mode 100644 index d04cd5300..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/neutral-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x57", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x9A", - "green" : "0x95", - "red" : "0x94" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/success-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/success-container-pressed.colorset/Contents.json deleted file mode 100644 index a08cf29a1..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/success-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0xFB", - "red" : "0xF5" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2B", - "green" : "0x42", - "red" : "0x14" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/success-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/success-pressed.colorset/Contents.json deleted file mode 100644 index bc1ae2b45..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/success-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x90", - "green" : "0xBC", - "red" : "0x64" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6B", - "green" : "0xA5", - "red" : "0x31" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/support-container-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/support-container-pressed.colorset/Contents.json deleted file mode 100644 index 153b4b364..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/support-container-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6A", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/support-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/support-pressed.colorset/Contents.json deleted file mode 100644 index 01cda98cd..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/support-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7B", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xD3", - "green" : "0xC5", - "red" : "0xC4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/support-variant-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/support-variant-pressed.colorset/Contents.json deleted file mode 100644 index 71902cb68..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/support-variant-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x8D", - "green" : "0x66", - "red" : "0x65" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/surface-inverse-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/surface-inverse-pressed.colorset/Contents.json deleted file mode 100644 index 66ed6800a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/surface-inverse-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x41", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/States/surface-pressed.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/States/surface-pressed.colorset/Contents.json deleted file mode 100644 index 31d8377ed..000000000 --- a/spark/Sources/Resources/Colors.xcassets/States/surface-pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xEA", - "red" : "0xE0" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x00", - "green" : "0x00", - "red" : "0x00" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/on-support-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/on-support-container.colorset/Contents.json deleted file mode 100644 index 1162e6227..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/on-support-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x58", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/on-support-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/on-support-variant.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/on-support-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/on-support.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/on-support.colorset/Contents.json deleted file mode 100644 index 0082f30a3..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/on-support.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1F", - "red" : "0x1D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/support-container.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/support-container.colorset/Contents.json deleted file mode 100644 index 29f04c2af..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/support-container.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7B", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/support-variant.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/support-variant.colorset/Contents.json deleted file mode 100644 index 5d6725e87..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/support-variant.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7B", - "green" : "0x4E", - "red" : "0x4D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF4", - "red" : "0xF4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Colors.xcassets/Support/support.colorset/Contents.json b/spark/Sources/Resources/Colors.xcassets/Support/support.colorset/Contents.json deleted file mode 100644 index 6b7bc37cd..000000000 --- a/spark/Sources/Resources/Colors.xcassets/Support/support.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6A", - "green" : "0x37", - "red" : "0x35" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xDC", - "red" : "0xDC" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/spark/Sources/Resources/Font/NunitoSans-Bold.ttf b/spark/Sources/Resources/Font/NunitoSans-Bold.ttf deleted file mode 100644 index fb01ae356954a2cc215541eae2b51981ba716d3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141236 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 - CTFontManagerRegisterGraphicsFont(font, &error) - } - } -} diff --git a/spark/Sources/Theming/Configuration/SparkConfiguration.swift b/spark/Sources/Theming/Configuration/SparkConfiguration.swift deleted file mode 100644 index 3b9aca138..000000000 --- a/spark/Sources/Theming/Configuration/SparkConfiguration.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SparkConfiguration.swift -// Spark -// -// Created by robin.lemaire on 02/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SparkCore -import Foundation -import UIKit - -public struct SparkConfiguration { - - // MARK: - Subclass - - private class Class {} - - private static var didLoad = false - - // MARK: - static func - - public static func load() { - guard !self.didLoad else { return } - self.didLoad = true - - Bundle(for: Class.self).registerAllFonts() - } -} diff --git a/spark/Sources/Theming/Content/SparkBorder.swift b/spark/Sources/Theming/Content/SparkBorder.swift deleted file mode 100644 index cde0d284a..000000000 --- a/spark/Sources/Theming/Content/SparkBorder.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// SparkTheme.swift -// Spark -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SparkCore - -struct SparkBorder: Border { - - // MARK: - Properties - - let width: BorderWidth = BorderWidthDefault(small: 1, - medium: 2) - let radius: BorderRadius = BorderRadiusDefault(small: 4, - medium: 8, - large: 16, - xLarge: 24) -} diff --git a/spark/Sources/Theming/Content/SparkColors.swift b/spark/Sources/Theming/Content/SparkColors.swift deleted file mode 100644 index 47bdd6a18..000000000 --- a/spark/Sources/Theming/Content/SparkColors.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// SparkColors.swift -// Spark -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SparkCore -import UIKit -import SwiftUI - -struct SparkColors: Colors { - - private class ClassForBundle {} - - let main: ColorsMain = ColorsMainDefault( - main: ColorTokenDefault(named: "main", in: Bundle(for: ClassForBundle.self)), - onMain: ColorTokenDefault(named: "on-main", in: Bundle(for: ClassForBundle.self)), - mainVariant: ColorTokenDefault(named: "main-variant", in: Bundle(for: ClassForBundle.self)), - onMainVariant: ColorTokenDefault(named: "on-main-variant", in: Bundle(for: ClassForBundle.self)), - mainContainer: ColorTokenDefault(named: "main-container", in: Bundle(for: ClassForBundle.self)), - onMainContainer: ColorTokenDefault(named: "on-main-container", in: Bundle(for: ClassForBundle.self))) - - let support: ColorsSupport = ColorsSupportDefault( - support: ColorTokenDefault(named: "support", in: Bundle(for: ClassForBundle.self)), - onSupport: ColorTokenDefault(named: "on-support", in: Bundle(for: ClassForBundle.self)), - supportVariant: ColorTokenDefault(named: "support-variant", in: Bundle(for: ClassForBundle.self)), - onSupportVariant: ColorTokenDefault(named: "on-support-variant", in: Bundle(for: ClassForBundle.self)), - supportContainer: ColorTokenDefault(named: "support-container", in: Bundle(for: ClassForBundle.self)), - onSupportContainer: ColorTokenDefault(named: "on-support-container", in: Bundle(for: ClassForBundle.self))) - - let accent: ColorsAccent = ColorsAccentDefault( - accent: ColorTokenDefault(named: "accent", in: Bundle(for: ClassForBundle.self)), - onAccent: ColorTokenDefault(named: "on-accent", in: Bundle(for: ClassForBundle.self)), - accentVariant: ColorTokenDefault(named: "accent-variant", in: Bundle(for: ClassForBundle.self)), - onAccentVariant: ColorTokenDefault(named: "on-accent-variant", in: Bundle(for: ClassForBundle.self)), - accentContainer: ColorTokenDefault(named: "accent-container", in: Bundle(for: ClassForBundle.self)), - onAccentContainer: ColorTokenDefault(named: "on-accent-container", in: Bundle(for: ClassForBundle.self))) - - let basic: ColorsBasic = ColorsBasicDefault( - basic: ColorTokenDefault(named: "basic", in: Bundle(for: ClassForBundle.self)), - onBasic: ColorTokenDefault(named: "on-basic", in: Bundle(for: ClassForBundle.self)), - basicContainer: ColorTokenDefault(named: "basic-container", in: Bundle(for: ClassForBundle.self)), - onBasicContainer: ColorTokenDefault(named: "on-basic-container", in: Bundle(for: ClassForBundle.self))) - - let base: ColorsBase = ColorsBaseDefault( - background: ColorTokenDefault(named: "background", in: Bundle(for: ClassForBundle.self)), - onBackground: ColorTokenDefault(named: "on-background", in: Bundle(for: ClassForBundle.self)), - backgroundVariant: ColorTokenDefault(named: "background-variant", in: Bundle(for: ClassForBundle.self)), - onBackgroundVariant: ColorTokenDefault(named: "on-background-variant", in: Bundle(for: ClassForBundle.self)), - surface: ColorTokenDefault(named: "surface", in: Bundle(for: ClassForBundle.self)), - onSurface: ColorTokenDefault(named: "on-surface", in: Bundle(for: ClassForBundle.self)), - surfaceInverse: ColorTokenDefault(named: "surface-inverse", in: Bundle(for: ClassForBundle.self)), - onSurfaceInverse: ColorTokenDefault(named: "on-surface-inverse", in: Bundle(for: ClassForBundle.self)), - outline: ColorTokenDefault(named: "outline", in: Bundle(for: ClassForBundle.self)), - outlineHigh: ColorTokenDefault(named: "outline-high", in: Bundle(for: ClassForBundle.self)), - overlay: ColorTokenDefault(named: "overlay", in: Bundle(for: ClassForBundle.self)), - onOverlay: ColorTokenDefault(named: "on-overlay", in: Bundle(for: ClassForBundle.self))) - - let feedback: ColorsFeedback = ColorsFeedbackDefault( - success: ColorTokenDefault(named: "success", in: Bundle(for: ClassForBundle.self)), - onSuccess: ColorTokenDefault(named: "on-success", in: Bundle(for: ClassForBundle.self)), - successContainer: ColorTokenDefault(named: "success-container", in: Bundle(for: ClassForBundle.self)), - onSuccessContainer: ColorTokenDefault(named: "on-success-container", in: Bundle(for: ClassForBundle.self)), - alert: ColorTokenDefault(named: "alert", in: Bundle(for: ClassForBundle.self)), - onAlert: ColorTokenDefault(named: "on-alert", in: Bundle(for: ClassForBundle.self)), - alertContainer: ColorTokenDefault(named: "alert-container", in: Bundle(for: ClassForBundle.self)), - onAlertContainer: ColorTokenDefault(named: "on-alert-container", in: Bundle(for: ClassForBundle.self)), - error: ColorTokenDefault(named: "error", in: Bundle(for: ClassForBundle.self)), - onError: ColorTokenDefault(named: "on-error", in: Bundle(for: ClassForBundle.self)), - errorContainer: ColorTokenDefault(named: "error-container", in: Bundle(for: ClassForBundle.self)), - onErrorContainer: ColorTokenDefault(named: "on-error-container", in: Bundle(for: ClassForBundle.self)), - info: ColorTokenDefault(named: "info", in: Bundle(for: ClassForBundle.self)), - onInfo: ColorTokenDefault(named: "on-info", in: Bundle(for: ClassForBundle.self)), - infoContainer: ColorTokenDefault(named: "info-container", in: Bundle(for: ClassForBundle.self)), - onInfoContainer: ColorTokenDefault(named: "on-info-container", in: Bundle(for: ClassForBundle.self)), - neutral: ColorTokenDefault(named: "neutral", in: Bundle(for: ClassForBundle.self)), - onNeutral: ColorTokenDefault(named: "on-neutral", in: Bundle(for: ClassForBundle.self)), - neutralContainer: ColorTokenDefault(named: "neutral-container", in: Bundle(for: ClassForBundle.self)), - onNeutralContainer: ColorTokenDefault(named: "on-neutral-container", in: Bundle(for: ClassForBundle.self)) - ) - - let states: ColorsStates = ColorsStatesDefault( - mainPressed: ColorTokenDefault(named: "main-pressed", in: Bundle(for: ClassForBundle.self)), - mainVariantPressed: ColorTokenDefault(named: "main-variant-pressed", in: Bundle(for: ClassForBundle.self)), - mainContainerPressed: ColorTokenDefault(named: "main-container-pressed", in: Bundle(for: ClassForBundle.self)), - supportPressed: ColorTokenDefault(named: "support-pressed", in: Bundle(for: ClassForBundle.self)), - supportVariantPressed: ColorTokenDefault(named: "support-variant-pressed", in: Bundle(for: ClassForBundle.self)), - supportContainerPressed: ColorTokenDefault(named: "support-container-pressed", in: Bundle(for: ClassForBundle.self)), - accentPressed: ColorTokenDefault(named: "accent-pressed", in: Bundle(for: ClassForBundle.self)), - accentVariantPressed: ColorTokenDefault(named: "accent-variant-pressed", in: Bundle(for: ClassForBundle.self)), - accentContainerPressed: ColorTokenDefault(named: "accent-container-pressed", in: Bundle(for: ClassForBundle.self)), - basicPressed: ColorTokenDefault(named: "basic-pressed", in: Bundle(for: ClassForBundle.self)), - basicContainerPressed: ColorTokenDefault(named: "basic-container-pressed", in: Bundle(for: ClassForBundle.self)), - surfacePressed: ColorTokenDefault(named: "surface-pressed", in: Bundle(for: ClassForBundle.self)), - surfaceInversePressed: ColorTokenDefault(named: "surface-inverse-pressed", in: Bundle(for: ClassForBundle.self)), - successPressed: ColorTokenDefault(named: "success-pressed", in: Bundle(for: ClassForBundle.self)), - successContainerPressed: ColorTokenDefault(named: "success-container-pressed", in: Bundle(for: ClassForBundle.self)), - alertPressed: ColorTokenDefault(named: "alert-pressed", in: Bundle(for: ClassForBundle.self)), - alertContainerPressed: ColorTokenDefault(named: "alert-container-pressed", in: Bundle(for: ClassForBundle.self)), - errorPressed: ColorTokenDefault(named: "error-pressed", in: Bundle(for: ClassForBundle.self)), - errorContainerPressed: ColorTokenDefault(named: "error-container-pressed", in: Bundle(for: ClassForBundle.self)), - infoPressed: ColorTokenDefault(named: "info-pressed", in: Bundle(for: ClassForBundle.self)), - infoContainerPressed: ColorTokenDefault(named: "info-container-pressed", in: Bundle(for: ClassForBundle.self)), - neutralPressed: ColorTokenDefault(named: "neutral-pressed", in: Bundle(for: ClassForBundle.self)), - neutralContainerPressed: ColorTokenDefault(named: "neutral-container-pressed", in: Bundle(for: ClassForBundle.self))) -} diff --git a/spark/Sources/Theming/Content/SparkColorsTests.swift b/spark/Sources/Theming/Content/SparkColorsTests.swift deleted file mode 100644 index 0851eecd7..000000000 --- a/spark/Sources/Theming/Content/SparkColorsTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SparkColorsTests.swift -// SparkTests -// -// Created by louis.borlee on 19/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SparkCore -@testable import Spark - -final class SparkColorsTests: XCTestCase { - - private lazy var tokens: [any ColorToken] = self.getAllColorTokens() - - // MARK: - Tests - func testUIColors() { - self.tokens.forEach { - XCTAssertNotEqual( - $0.uiColor, - .clear, - "Wrong uiColor for token \($0)" - ) - } - } - - func testSwiftUIColors() { - self.tokens.forEach { - XCTAssertNotEqual( - $0.color, - .clear, - "Wrong color for token \($0)" - ) - } - } - - // MARK: - Get Colors - private func getAllColorTokens() -> [any ColorToken] { - let mirror = Mirror(reflecting: SparkColors()) - return mirror.children.flatMap { (_, value: Any) in - return self.getColorTokens(from: value) - } - } - - private func getColorTokens(from object: Any) -> [any ColorToken] { - let mirror = Mirror(reflecting: object) - return mirror.children.compactMap { (_, value: Any) in - return value as? any ColorToken - } - } -} diff --git a/spark/Sources/Theming/Content/SparkElevation.swift b/spark/Sources/Theming/Content/SparkElevation.swift deleted file mode 100644 index 1ecc40a37..000000000 --- a/spark/Sources/Theming/Content/SparkElevation.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SparkElevation.swift -// Spark -// -// Created by louis.borlee on 30/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit -import SwiftUI -import SparkCore - -struct SparkElevation: Elevation { - let dropShadow: ElevationDropShadows & ElevationShadow = SparkDropShadow() -} - -struct SparkDropShadow: ElevationDropShadows & ElevationShadow { - - let small: ElevationShadow = ElevationShadowDefault( - offset: .init(x: 0, y: 1), - blur: 2, - colorToken: SparkColorTokenShadow(), - opacity: 0.20) - let medium: ElevationShadow = ElevationShadowDefault( - offset: .init(x: 0, y: 6), - blur: 12, - colorToken: SparkColorTokenShadow(), - opacity: 0.20) - let large: ElevationShadow = ElevationShadowDefault( - offset: .init(x: 0, y: 8), - blur: 16, - colorToken: SparkColorTokenShadow(), - opacity: 0.20) - let extraLarge: ElevationShadow = ElevationShadowDefault( - offset: .init(x: 0, y: 12), - blur: 24, - colorToken: SparkColorTokenShadow(), - opacity: 0.20) - - let offset: CGPoint = .init(x: 0, y: 4) - let blur: CGFloat = 8 - let colorToken: any ColorToken = SparkColorTokenShadow() - let opacity: Float = 0.20 -} - -fileprivate struct SparkColorTokenShadow: ColorToken { - var uiColor: UIColor { .black } - var color: Color { .black } -} diff --git a/spark/Sources/Theming/Content/SparkLayout.swift b/spark/Sources/Theming/Content/SparkLayout.swift deleted file mode 100644 index b46364b74..000000000 --- a/spark/Sources/Theming/Content/SparkLayout.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// SparkLayout.swift -// Spark -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SparkCore - -struct SparkLayout: Layout { - - // MARK: - Properties - - let spacing: LayoutSpacing = LayoutSpacingDefault(small: 4, - medium: 8, - large: 16, - xLarge: 24, - xxLarge: 32, - xxxLarge: 40) -} diff --git a/spark/Sources/Theming/Content/SparkTypography.swift b/spark/Sources/Theming/Content/SparkTypography.swift deleted file mode 100644 index cb0059705..000000000 --- a/spark/Sources/Theming/Content/SparkTypography.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// SparkTypography.swift -// Spark -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SparkCore -import SwiftUI -import UIKit - -struct SparkTypography: Typography { - - // MARK: - Properties - - let display1: TypographyFontToken = TypographyFontTokenDefault(size: 40, - isHighlighted: true, - textStyle: .largeTitle) - let display2: TypographyFontToken = TypographyFontTokenDefault(size: 32, - isHighlighted: true, - textStyle: .largeTitle) - let display3: TypographyFontToken = TypographyFontTokenDefault(size: 24, - isHighlighted: true, - textStyle: .largeTitle) - - let headline1: TypographyFontToken = TypographyFontTokenDefault(size: 20, - isHighlighted: true, - textStyle: .headline) - let headline2: TypographyFontToken = TypographyFontTokenDefault(size: 18, - isHighlighted: true, - textStyle: .headline) - - let subhead: TypographyFontToken = TypographyFontTokenDefault(size: 16, - isHighlighted: true, - textStyle: .subheadline) - - let body1: TypographyFontToken = TypographyFontTokenDefault(size: 16, - isHighlighted: false, - textStyle: .body) - let body1Highlight: TypographyFontToken = TypographyFontTokenDefault(size: 16, - isHighlighted: true, - textStyle: .body) - - let body2: TypographyFontToken = TypographyFontTokenDefault(size: 14, - isHighlighted: false, - textStyle: .body) - let body2Highlight: TypographyFontToken = TypographyFontTokenDefault(size: 14, - isHighlighted: true, - textStyle: .body) - - let caption: TypographyFontToken = TypographyFontTokenDefault(size: 12, - isHighlighted: false, - textStyle: .caption) - let captionHighlight: TypographyFontToken = TypographyFontTokenDefault(size: 12, - isHighlighted: true, - textStyle: .caption) - - let small: TypographyFontToken = TypographyFontTokenDefault(size: 10, - isHighlighted: false, - textStyle: .footnote) - let smallHighlight: TypographyFontToken = TypographyFontTokenDefault(size: 10, - isHighlighted: true, - textStyle: .footnote) - - let callout: TypographyFontToken = TypographyFontTokenDefault(size: 16, - isHighlighted: true, - textStyle: .callout) -} - -// MARK: - TypographyFont Extension - -private extension TypographyFontTokenDefault { - - // MARK: - Constants - - private enum Constants { - static let boldFontName = "NunitoSans-Bold" - static let regularFontName = "NunitoSans-Regular" - } - - // MARK: - Initialization - - init(size: CGFloat, - isHighlighted: Bool, - textStyle: TextStyle) { - // Properties - let fontName = isHighlighted ? Constants.boldFontName : Constants.regularFontName - self.init(named: fontName, - size: size, - textStyle: textStyle) - } -} diff --git a/spark/Sources/Theming/Content/SparkTypographyTests.swift b/spark/Sources/Theming/Content/SparkTypographyTests.swift deleted file mode 100644 index c007e9b63..000000000 --- a/spark/Sources/Theming/Content/SparkTypographyTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// SparkTypographyTests.swift -// SparkTests -// -// Created by louis.borlee on 20/04/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -import SparkCore -@testable import Spark - -final class SparkTypographyTests: XCTestCase { - - private lazy var tokens: [SparkCore.TypographyFontToken] = self.getAllTypographyFontTokens() - - func testUIFonts() { - self.tokens.forEach { - // Will trigger a fatalError if font hasn't been found - _ = $0.uiFont - } - } - - func testSwiftUIFonts() { - self.tokens.forEach { - // Haven't found another solution yet to test if font hasn't been found - XCTAssertFalse("\($0)".contains("unknown context")) - } - } - - // MARK: - Get Fonts - private func getAllTypographyFontTokens() -> [SparkCore.TypographyFontToken] { - let mirror = Mirror(reflecting: SparkTypography()) - return mirror.children.flatMap { (_, value: Any) in - return self.getTypographyFontTokens(from: value) - } - } - - private func getTypographyFontTokens(from object: Any) -> [SparkCore.TypographyFontToken] { - let mirror = Mirror(reflecting: object) - return mirror.children.compactMap { (label: String?, value: Any) in - return value as? SparkCore.TypographyFontToken - } - } -} diff --git a/spark/Sources/Theming/SparkTheme.swift b/spark/Sources/Theming/SparkTheme.swift deleted file mode 100644 index 2d54f4e8e..000000000 --- a/spark/Sources/Theming/SparkTheme.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SparkTheme.swift -// Spark -// -// Created by robin.lemaire on 28/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Combine -import SparkCore -import Foundation - -public struct SparkTheme: Theme { - - // MARK: - Properties - - public static var shared = Self() - - public init() {} - - public let border: Border = SparkBorder() - public var colors: Colors = SparkColors() - public let elevation: Elevation = SparkElevation() - public let layout: Layout = SparkLayout() - public let typography: Typography = SparkTypography() - public let dims: Dims = DimsDefault(dim1: 0.72, - dim2: 0.56, - dim3: 0.40, - dim4: 0.16, - dim5: 0.08) -} diff --git a/spark/Sources/UseCaseDemo.swift b/spark/Sources/UseCaseDemo.swift deleted file mode 100644 index a9cba40c9..000000000 --- a/spark/Sources/UseCaseDemo.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// UseCaseDemo.swift -// SparkCore -// -// Created by louis.borlee on 22/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -struct UseCaseDemo { - let id = 42 -} diff --git a/spark/Sources/UseCaseDemoTests.swift b/spark/Sources/UseCaseDemoTests.swift deleted file mode 100644 index 5434f3851..000000000 --- a/spark/Sources/UseCaseDemoTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// UseCaseDemoTests.swift -// SparkCoreTests -// -// Created by louis.borlee on 22/02/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest -@testable import Spark - -final class UseCaseDemoTests: XCTestCase { - - func testUseCaseDemo() { - let demo = UseCaseDemo() - XCTAssertEqual(demo.id, 42) - } - -} diff --git a/spark/Unit-tests/SparkTests.swift b/spark/Unit-tests/SparkTests.swift deleted file mode 100644 index 980aee5a8..000000000 --- a/spark/Unit-tests/SparkTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// SparkTests.swift -// SparkCoreTests -// -// Created by louis.borlee on 08/03/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import XCTest - -final class SparkTests: XCTestCase { - - func test() { - XCTAssertTrue(true) - } -} diff --git a/stencil/sourcery-template/SparkCoreAutoMockTest.stencil b/stencil/sourcery-template/SparkCoreAutoMockTest.stencil deleted file mode 100644 index 46fbd3da5..000000000 --- a/stencil/sourcery-template/SparkCoreAutoMockTest.stencil +++ /dev/null @@ -1,144 +0,0 @@ -// swiftlint:disable all - -import Foundation -import UIKit -import SwiftUI -import XCTest - -{% for import in argument.autoMockableImports %} -import {{ import }} -{% endfor %} -{% for import in argument.autoMockableTestableImports %} -@testable import {{ import }} -{% endfor %} - -{% macro removeGenericContentInMethodName name %}{% if method.shortName| hasSuffix:">" %}{% for element in name|split:"<" %}{% if forloop.first %}{{ element }}{% endif %}{% endfor %}{% else %}{{name}}{% endif %}{% endmacro %} -{% macro swiftifyMethodNameV2 method %}{% if method.parameters.count > 0 %}{% call removeGenericContentInMethodName method.shortName %}With{% for param in method.parameters %}{{ param.name | snakeToCamelCase }}{% if not forloop.last %}And{% endif %}{% endfor %}{% else %}{{method.shortName}}{% endif %}{% endmacro %} -{% macro mockName protocol %}{{ protocol.name }}GeneratedMock{% endmacro %} -{% macro dynamicTypeName name %}_{{ name | upperFirstLetter }}{% endmacro %} -{% macro givenParameter name %}given{{ param.name | snakeToCamelCase }}{% endmacro %} - -{% for protocol in types.protocols where protocol|annotated:"AutoMockTest" %}{% if protocol.name != "AutoMockTest" %} -final class {{ protocol.name }}MockTest { - - // MARK: - Initialization - - private init(){ - } - - // MARK: - Tests - - {% for method in protocol.allMethods|!definedInExtension %} - - static func XCTCallsCount( - _ mock: {% call mockName protocol %}, - {% call swiftifyMethodNameV2 method %}NumberOfCalls numberOfCalls: Int - ) { - XCTAssertEqual( - mock.{% call swiftifyMethodNameV2 method %}CallsCount, - numberOfCalls, - "Wrong {{ method.name }} number of called on {{ protocol.name }}" - ) - } - - static func XCTAssert{% for key, value in method.annotations where value == "Identical" %}{% if forloop.first %}< - {% for param in method.parameters where method.annotations[param.name] == "Identical" %} - {% call dynamicTypeName param.name %}: AnyObject{% if not forloop.last or method.annotations["return"] == "Identical" %},{% endif %} - {% endfor %} - {% if method.annotations["return"] == "Identical" %} - {% call dynamicTypeName "return" %}: AnyObject - {% endif %} - >{% endif %}{% endfor %}( - _ mock: {% call mockName protocol %}, - expectedNumberOfCalls: Int, - {% for param in method.parameters %} - {% call givenParameter param %}: {% if method.annotations[param.name] == "Identical" %}{% call dynamicTypeName param.name %}{% elif protocol.associatedTypes[param.typeName] != nil %}{% call mockName protocol %}.{{ protocol.associatedTypes[param.typeName].name }}Mock{% else %}{{ param.typeName | replace:"?","" }}{% endif %}? = nil{% if not forloop.last or not method.returnTypeName.isVoid %}, {% endif %} - {% endfor %} - {% if not method.returnTypeName.isVoid %}expectedReturnValue: {% if method.annotations["return"] == "Identical" %}{% call dynamicTypeName "return" %}{% elif protocol.associatedTypes["Return"] != nil %}{% call mockName protocol %}.ReturnMock{% if method.isOptionalReturnType %}?{% endif %}{% else %}{{ method.returnTypeName }}{% endif %}{% endif %} - ) { - // Count - XCTAssertEqual( - mock.{% call swiftifyMethodNameV2 method %}CallsCount, - expectedNumberOfCalls, - "Wrong {{ method.name }} number of called on {{ protocol.name }}" - ) - - // Parameters - if expectedNumberOfCalls > 0 { - {% if method.parameters.count > 1 %} - let args = mock.{% call swiftifyMethodNameV2 method %}ReceivedArguments - - {% endif %} - {% for param in method.parameters %} - // {{ param.name|upperFirstLetter }} - if let {% call givenParameter param %} { - - {% if method.annotations[param.name] == "Identical" %} - {% if method.parameters.count == 1 %} - XCTAssertIdentical( - mock.{% call swiftifyMethodNameV2 method %}Received{{ param.name|upperFirstLetter }} as? _{{ param.name|upperFirstLetter }}, - given{{ param.name | snakeToCamelCase }}, - "Wrong {{ method.name }} {{ param.name }} parameter on {{ protocol.name }}" - ) - {% else %} - XCTAssertIdentical( - args?.{{ param.name }} as? _{{ param.name|upperFirstLetter }}, - given{{ param.name | snakeToCamelCase }}, - "Wrong {{ method.name }} {{ param.name }} parameter on {{ protocol.name }}" - ) - {% endif %} - {% else %} - {% if method.parameters.count == 1 %} - XCTAssertEqual( - mock.{% call swiftifyMethodNameV2 method %}Received{{ param.name|upperFirstLetter }}, - given{{ param.name | snakeToCamelCase }}, - "Wrong {{ method.name }} {{ param.name }} parameter on {{ protocol.name }}" - ) - {% else %} - XCTAssertEqual( - args?.{{ param.name }}, - given{{ param.name | snakeToCamelCase }}, - "Wrong {{ method.name }} {{ param.name }} parameter on {{ protocol.name }}" - ) - {% endif %} - {% endif %} - } else { - XCTAssertNil( - {% if method.parameters.count == 1 %}mock.{% call swiftifyMethodNameV2 method %}Received{{ param.name|upperFirstLetter }}{% else %}args?.{{ param.name }}{% endif %}, - "Wrong {{ method.name }} {{ param.name }} parameter value on {{ protocol.name }}. Should be nil" - ) - } - - {% endfor %} - } - - {% if not method.returnTypeName.isVoid %} - // Return - {% if method.isOptionalReturnType %}if let expectedReturnValue { {% endif %} - {% if method.annotations["return"] == "Identical" %} - XCTAssertIdentical( - mock.{% call swiftifyMethodNameV2 method %}ReturnValue as? _Return, - expectedReturnValue, - "Wrong {{ method.name }} return value on {{ protocol.name }}" - ) - {% else %} - XCTAssertEqual( - mock.{% call swiftifyMethodNameV2 method %}ReturnValue, - expectedReturnValue, - "Wrong {{ method.name }} return value on {{ protocol.name }}" - ) - {% endif %} - {% endif %} - {% if method.isOptionalReturnType %} } else { - XCTAssertNil( - mock.{% call swiftifyMethodNameV2 method %}ReturnValue, - "Wrong {{ method.name }} return value on {{ protocol.name }}. Should be nil" - ) - } - {% endif %} - } - - {% endfor %} -} - -{% endif %}{% endfor %} \ No newline at end of file diff --git a/stencil/sourcery-template/SparkCoreAutoMockable.stencil b/stencil/sourcery-template/SparkCoreAutoMockable.stencil deleted file mode 100644 index 8db757f24..000000000 --- a/stencil/sourcery-template/SparkCoreAutoMockable.stencil +++ /dev/null @@ -1,239 +0,0 @@ -// swiftlint:disable all - -import Foundation -import UIKit -import SwiftUI - -{% for import in argument.autoMockableImports %} -import {{ import }} -{% endfor %} -{% for import in argument.autoMockableTestableImports %} -@testable import {{ import }} - -{% macro existentialVariableTypeName typeName %}{% if typeName|contains:"any" and typeName|contains:"!" %}{{ typeName | replace:"any","(any" | replace:"!",")!" }}{% elif typeName|contains:"any" and typeName.isOptional %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{% elif typeName|contains:"any" and typeName.isClosure %}({{ typeName | replace:"any","(any" | replace:"?",")?" }}){%else%}{{ typeName }}{%endif%}{% endmacro %} -{% macro existentialClosureVariableTypeName typeName %}{% if typeName|contains:"any" and typeName|contains:"!" %}{{ typeName | replace:"any","(any" | replace:"!",")?" }}{% elif typeName|contains:"any" and typeName.isClosure and typeName|contains:"?" %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{% elif typeName|contains:"any" and typeName|contains:"?" %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{%else%}{{ typeName }}{%endif%}{% endmacro %} -{% macro existentialParameterTypeName typeName %}{% if typeName|contains:"any" and typeName|contains:"!" %}{{ typeName | replace:"any","(any" | replace:"!",")!" }}{% elif typeName|contains:"any" and typeName.isClosure and typeName|contains:"?" %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{% elif typeName|contains:"any" and typeName.isOptional %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{%else%}{{ typeName }}{%endif%}{% endmacro %} -{% macro methodName method %}func {{ method.shortName}}({%- for param in method.parameters %}{% if param.argumentLabel == nil %}_ {{ param.name }}{%elif param.argumentLabel == param.name%}{{ param.name }}{%else%}{{ param.argumentLabel }} {{ param.name }}{% endif %}: {% call existentialParameterTypeName param.typeName %}{% if not forloop.last %}, {% endif %}{% endfor -%}){% endmacro %} - -{% macro genericTestedParam GenericList paramName %}{% set paramV2 %}"{{paramName}}"{% endset %}{% if GenericList | contains: paramV2 %}Any{% else %}{{paramName}}{%endif%}{% endmacro %} -{% macro removeGenericContentInMethodName name %}{% if method.shortName| hasSuffix:">" %}{% for element in name|split:"<" %}{% if forloop.first %}{{ element }}{% endif %}{% endfor %}{% else %}{{name}}{% endif %}{% endmacro %} -{% macro swiftifyMethodNameV2 method %}{%if method.parameters.count > 0 %}{% call removeGenericContentInMethodName method.shortName %}With{% for param in method.parameters %}{{ param.name | snakeToCamelCase }}{% if not forloop.last %}And{% endif %}{% endfor %}{% else %}{{method.shortName}}{% endif %}{% endmacro %} -{% macro methodThrowableErrorDeclaration method %} - var {% call swiftifyMethodNameV2 method %}ThrowableError: Error? -{% endmacro %} -{% macro methodThrowableErrorUsage method %} - if let error = {% call swiftifyMethodNameV2 method %}ThrowableError { - throw error - } -{% endmacro %} -{% macro methodReceivedParameters method %} -{% if method.shortName| hasSuffix:">" %} -{% for element in method.shortName|replace:">",""|replace:" ",""|split:"<" %} - {% if forloop.last %} - {% set GenericParameters %}{{element|split:","}}{% endset %} - {%if method.parameters.count == 1 %} - {% call swiftifyMethodNameV2 method %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %} - {% call swiftifyMethodNameV2 method %}ReceivedInvocations.append({% for param in method.parameters %}{{ param.name }}){% endfor %} - {% else %} - {% if not method.parameters.count == 0 %} - {% call swiftifyMethodNameV2 method %}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}) - {% call swiftifyMethodNameV2 method %}ReceivedInvocations.append(({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %})) - {% endif %} - {% endif %} - {% endif %} -{% endfor %} -{% else %} - {%if method.parameters.count == 1 %} - {% call swiftifyMethodNameV2 method %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %} - {% call swiftifyMethodNameV2 method %}ReceivedInvocations.append({% for param in method.parameters %}{{ param.name }}){% endfor %} - {% else %} - {% if not method.parameters.count == 0 %} - {% call swiftifyMethodNameV2 method %}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}) - {% call swiftifyMethodNameV2 method %}ReceivedInvocations.append(({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %})) - {% endif %} - {% endif %} -{% endif %} -{% endmacro %} -{% macro methodClosureName method %}_{% call swiftifyMethodNameV2 method %}{% endmacro %} -{% macro closureReturnTypeNameV2 method GenericParameters %}{% if method.returnTypeName.isVoid%} Void {% else %}Any?{% endif %}{% endmacro %} -{% macro closureReturnTypeName method %}{% if method.isOptionalReturnType %}({{ method.unwrappedReturnTypeName }})?{% else %}{{ method.returnTypeName }}{% endif %}{% endmacro %} - -{% macro methodClosureDeclaration method %} - var {% call methodClosureName method %}: (({% for param in method.parameters %}{% call existentialVariableTypeName param.typeName %}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.isAsync %}async {% endif %}{% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{% call closureReturnTypeName method %}{% endif %})? -{% endmacro %} - -{% macro methodClosureDeclarationV2 method GenericParameters %} - var {% call methodClosureName method %}: (({% for param in method.parameters %}{% call genericTestedParam GenericParameters param.typeName.unwrappedTypeName %}{%if param.typeName.isOptional%}?{%endif%}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{% call closureReturnTypeNameV2 method GenericParameters %}{% endif %})? -{% endmacro %} -{% macro methodClosureCallParameters method %}{% for param in method.parameters %}{% if param.typeName.name| hasPrefix:"inout" %}&{% endif %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %} -{% macro methodReturnValueDefaultValueMacro typeName %}{% if typeName.name == "Int" or typeName.name == "UInt" or typeName.name == "Double" or typeName.name == "Float" or typeName.name == "CGFloat" %} = 0{% elif typeName.name == "Bool" %} = false{% elif typeName.name == "String" %} = ""{% elif typeName.name == "UIViewController" or typeName.name == "UINavigationController" or typeName.name == "UIView" or typeName.name|hasPrefix:"PassthroughSubject<" or typeName.name|hasPrefix:"PassthroughRelay<" %} = .init(){% elif typeName.name == "URL" %} = .init(fileURLWithPath: ""){% elif typeName.name == "URLRequest" %} = .init(url: .init(fileURLWithPath: "")){% elif typeName.name|hasPrefix:"AnyPublisher<" %} = Empty().eraseToAnyPublisher(){% elif typeName.isArray %} = []{% elif typeName.isDictionary %} = [:]{% else %}!{% endif %} -{% endmacro %} -{% macro mockMethod method %} - // MARK: - {{ method.shortName }} - -{% if method.shortName| hasSuffix:">" %} - {% for element in method.shortName|replace:">",""|replace:" ",""|split:"<" %} - {% if forloop.last %} - {% set GenericParameters %}{{element|split:","}}{% endset %} - - {% if method.throws %} - {% call methodThrowableErrorDeclaration method %} - {% endif %} - {% if not method.isInitializer %} - var {% call swiftifyMethodNameV2 method %}CallsCount = 0 - var {% call swiftifyMethodNameV2 method %}Called: Bool { - return {% call swiftifyMethodNameV2 method %}CallsCount > 0 - } - {% endif %} - {% if method.parameters.count == 1 %} - var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: ({{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }})?{% endfor %} - var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations{% for param in method.parameters %}: [{%if param.typeName.isOptional%}({%endif%}{{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}{%if param.typeName.isOptional%})?{%endif%}]{% endfor %} = [] - - {% elif not method.parameters.count == 0 %} - var {% call swiftifyMethodNameV2 method %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% call genericTestedParam GenericParameters param.unwrappedTypeName %}{% if param.typeName.isOptional %}{% endif%}{{ ', ' if not forloop.last }}{% endfor %})? - var {% call swiftifyMethodNameV2 method %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {% call genericTestedParam GenericParameters param.unwrappedTypeName %}{% if param.typeName.isOptional %}{% endif%}{{ ', ' if not forloop.last }}{% endfor %})] = [] - {% endif %} - {% if not method.returnTypeName.isVoid and not method.isInitializer %} - var {% call swiftifyMethodNameV2 method %}ReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '(' if method.returnTypeName|contains:"any" }}{% call genericTestedParam GenericParameters method.returnTypeName.unwrappedTypeName %}{% if method.returnTypeName.isOptional %}?{% endif%}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '!' if not method.isOptionalReturnType }} - {% endif %} - {% call methodClosureDeclarationV2 method GenericParameters %} - - {% endif %} - {% endfor %} -{% else %} - {% if method.throws %} - {% call methodThrowableErrorDeclaration method %} - {% endif %} - {% if not method.isInitializer %} - var {% call swiftifyMethodNameV2 method %}CallsCount = 0 - var {% call swiftifyMethodNameV2 method %}Called: Bool { - return {% call swiftifyMethodNameV2 method %}CallsCount > 0 - } - {% endif %} - {% if method.parameters.count == 1 %} - var {% call swiftifyMethodNameV2 method %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}?{% endfor %} - var {% call swiftifyMethodNameV2 method %}ReceivedInvocations{% for param in method.parameters %}: [{{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}{%if param.typeName.isOptional%}?{%endif%}]{% endfor %} = [] - {% elif not method.parameters.count == 0 %} - var {% call swiftifyMethodNameV2 method %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{{ ', ' if not forloop.last }}{% endfor %})? - var {% call swiftifyMethodNameV2 method %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{{ ', ' if not forloop.last }}{% endfor %})] = [] - {% endif %} - {% if not method.returnTypeName.isVoid and not method.isInitializer %} - var {% call swiftifyMethodNameV2 method %}ReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType or method.returnTypeName|contains:"any"}}{{ method.returnTypeName }}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType or method.returnTypeName|contains:"any"}}{% if not method.returnTypeName.isClosure and not method.isOptionalReturnType %}{% call methodReturnValueDefaultValueMacro method.actualReturnTypeName %}{% endif %} - {% endif %} - {% call methodClosureDeclaration method %} -{% endif %} -{% if method.isInitializer %} - required {{ method.name }} { - {% call methodReceivedParameters method %} - {% call methodClosureName method %}?({% call methodClosureCallParameters method %}) - } -{% else %} - {% call methodName method %}{{ ' async' if method.isAsync }}{{ ' throws' if method.throws }}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { - {% if not method.shortName| hasSuffix:">" %} - {% if method.throws %} - {% call methodThrowableErrorUsage method %} - {% endif %} - {% call swiftifyMethodNameV2 method %}CallsCount += 1 - {% call methodReceivedParameters method %} - {% if method.returnTypeName.isVoid %} - {% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParameters method %}) - {% else %} - return {{ 'try ' if method.throws }}{% call methodClosureName method %}.map({ {{ 'try ' if method.throws }}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodNameV2 method %}ReturnValue - {% endif %} - {% else %} - {% call swiftifyMethodNameV2 method %}CallsCount += 1 - {% call methodReceivedParameters method %} - {% if not method.returnTypeName.isVoid %} - return ({{ 'try ' if method.throws }}{% call methodClosureName method %}.map({ {{ 'try' if method.throws }} $0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodNameV2 method %}ReturnValue) as! {{method.returnTypeName}} - {% else %} - {{ 'try ' if method.throws }}{% call methodClosureName method %}?({% call methodClosureCallParameters method %}) - {% endif %} - {% endif %} - } - -{% endif %} -{% endmacro %} -{% macro mockOptionalVariable variable %} - var {% call mockedVariableName variable %}: {{ variable.typeName }} -{% endmacro %} -{% macro mockNonOptionalArrayOrDictionaryVariable variable %} - var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %} -{% endmacro %} -{% macro initEmptyClosureForClosureWithVoidReturnType variable %}{% if variable.typeName.closure.returnTypeName.isVoid%} = { {% for param in variable.typeName.closure.parameters %} _{% if not forloop.last %},{% endif %}{% endfor %} in }{% else %}!{% endif %} -{% endmacro %} -{% macro underlyingVariableDefaultValueMacro typeName %}{% if typeName.name == "Int" or typeName.name == "UInt" or typeName.name == "Double" or typeName.name == "Float" or typeName.name == "CGFloat" %} = 0{% elif typeName.name == "Bool" %} = false{% elif typeName.name == "String" %} = ""{% elif typeName.name == "UIViewController" or typeName.name == "UINavigationController" or typeName.name == "UIView" or typeName.name|hasPrefix:"PassthroughSubject<" or typeName.name|hasPrefix:"PassthroughRelay<" %} = .init(){% elif typeName.name == "URL" %} = .init(fileURLWithPath: ""){% elif typeName.name == "URLRequest" %} = .init(url: .init(fileURLWithPath: "")){% elif typeName.name|hasPrefix:"AnyPublisher<" %} = Empty().eraseToAnyPublisher(){% elif typeName.isArray %} = []{% elif typeName.isDictionary %} = [:]{% elif typeName.name == "UIColor" %} = .clear{% elif typeName.name == "Color" %} = .clear{% elif typeName.name == "UIFont" %} = .init(){% elif typeName.name == "Font" %} = .body{% else %}!{% endif %} -{% endmacro %} -{% macro underlyingVariableValue variable %} - var {% call underlyingMockedVariableName variable %}: {% if variable.typeName|contains:"any" %}{{ variable.typeName | replace:"any","(any" }}){% else %}{{ variable.typeName }}{% endif %}{% if variable.typeName.closure.returnTypeName.isVoid%} = { {% for param in variable.typeName.closure.parameters %}_{% if not forloop.last %}, {% endif %}{% endfor %}{% if not variable.typeName.closure.parameters.count == 0%} in{% endif %} }{% else %}{% call underlyingVariableDefaultValueMacro variable.actualTypeName %}{% endif %} -{% endmacro %} -{% macro mockNonOptionalVariable variable %} - {% call underlyingVariableValue variable %} - var {% call mockedVariableName variable %}: {{ variable.typeName }} { - get { return {% call underlyingMockedVariableName variable %} } - set(value) { {% call underlyingMockedVariableName variable %} = value } - } -{% endmacro %} -{% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %} -{% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %} -{% for type in types.protocols where type.based.AutoMockable or type|annotated:"AutoMockable" %}{% if type.name != "AutoMockable" %} - -final class {{ type.name }}GeneratedMock: {{ import }}.{{ type.name }}{% if type.allMethods|!definedInExtension|count > 0 %}, ResetGeneratedMock{% endif %} { - -{% for key, value in type.associatedTypes %} - {% if value.typeName | contains:"Equatable" %} - {% if forloop.first %} - // MARK: - Type Alias - - {% endif%} - typealias {{ key }} = {{ key }}Mock - {% else %} - #error ("The protocol with associatedtype should work only with Equatable.") - {% endif%} -{% endfor %} -{% for key, value in type.associatedTypes %} - {% if value.typeName | contains:"Equatable" %} - {% if forloop.first %} - - // MARK: - Associated Type - - {% endif%} - struct {{ key }}Mock: {{ value.typeName }} { - let id = UUID().uuidString - } - {% endif%} -{% endfor %} -{% for variable in type.allVariables|!definedInExtension %} - {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %} -{% endfor %} - - // MARK: - Initialization - - init() {} - -{% for method in type.allMethods|!definedInExtension %} - {% call mockMethod method %} -{% endfor %} -{% if type.allMethods|!definedInExtension|count > 0 %} - // MARK: Reset - - func reset() { - {% for method in type.allMethods|!definedInExtension %} - {% call swiftifyMethodNameV2 method %}CallsCount = 0 - {% if method.parameters.count == 1 %} - {% call swiftifyMethodNameV2 method %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = nil {% endfor %} - {% elif not method.parameters.count == 0 %} - {% call swiftifyMethodNameV2 method %}ReceivedArguments = nil - {% endif %} - {% call swiftifyMethodNameV2 method %}ReceivedInvocations = [] - {% endfor %} - } -{% endif %} -} -{% endif %}{% endfor %} -{% endfor %} - -// MARK: - Reset - -protocol ResetGeneratedMock { - func reset() -} \ No newline at end of file diff --git a/stencil/sourcery-template/SparkCoreAutoPublisherTest.stencil b/stencil/sourcery-template/SparkCoreAutoPublisherTest.stencil deleted file mode 100644 index fb47fd62b..000000000 --- a/stencil/sourcery-template/SparkCoreAutoPublisherTest.stencil +++ /dev/null @@ -1,86 +0,0 @@ -// swiftlint:disable all - -import Foundation -import UIKit -import SwiftUI -import XCTest - -{% for import in argument.autoMockableImports %} -import {{ import }} -{% endfor %} -{% for import in argument.autoMockableTestableImports %} -@testable import {{ import }} -{% endfor %} - -{% macro expectedSinkCount variable %}expected{{ variable.name | upperFirstLetter }}SinkCount{% endmacro %} -{% macro existentialVariableTypeName typeName %}{% if typeName|contains:"any" and typeName|contains:"!" %}{{ typeName | replace:"any","(any" | replace:"!",")!" }}{% elif typeName|contains:"any" and typeName.isOptional %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{% elif typeName|contains:"any" and typeName.isClosure %}({{ typeName | replace:"any","(any" | replace:"?",")?" }}){%else%}{{ typeName }}{%endif%}{% endmacro %} - -{% for class in types.classes where class|annotated:"AutoPublisherTest" %}{% if class.name != "AutoPublisherTest" %} -final class {{ class.name }}PublisherTest { - - {% for key, value in class.annotations where key|contains: "<" and key|contains: ">" %}{% if forloop.first %}// MARK: - Type Alias - - {% endif %} - typealias {{ key|replace:"<", ""|replace:">", "" }} = {{ value }} - {% endfor %} - - // MARK: - Initialization - - private init(){ - } - - // MARK: - Tests - - {% for variable in class.allVariables|!definedInExtension where variable.readAccess != "private" and variable.definedInTypeName|contains: class.name and variable.attributes["Published"] != nil %} - - static func XCTSinksCount( - {{ variable.name }} mock: PublisherMock.Publisher>, - expectedNumberOfSinks: Int - ) { - XCTAssertPublisherSinkCountEqual( - on: mock, - expectedNumberOfSinks - ) - } - - static func XCTAssert{% if class.annotations[variable.name] == "Identical" %}< - Value: AnyObject - >{% endif %}( - {{ variable.name }} mock: PublisherMock.Publisher>, - expectedNumberOfSinks: Int, - expectedValue: {% if class.annotations[variable.name] == "Identical" %}Value{% if variable.isOptional %}?{% endif %}{% else %}{{ variable.typeName }}{% endif %}{% if variable.isOptional %} = nil{% endif %} - ) { - // Count - XCTAssertPublisherSinkCountEqual( - on: mock, - expectedNumberOfSinks - ) - - // Value - if expectedNumberOfSinks > 0 { - {% if variable.isOptional %}if let expectedValue { {% endif %} - {% if class.annotations[variable.name] == "Identical" %} - XCTAssertPublisherSinkValueIdentical( - on: mock, - expectedValue - ) - {% else %} - XCTAssertPublisherSinkValueEqual( - on: mock, - expectedValue - ) - {% endif %} - {% if variable.isOptional %} - } else { - XCTAssertPublisherSinkValueNil( - on: mock - ) - } - {% endif %} - } - } - - {% endfor %} -} - -{% endif %}{% endfor %} \ No newline at end of file diff --git a/stencil/sourcery-template/SparkCoreAutoViewModelStub.stencil b/stencil/sourcery-template/SparkCoreAutoViewModelStub.stencil deleted file mode 100644 index 5e87bcbf2..000000000 --- a/stencil/sourcery-template/SparkCoreAutoViewModelStub.stencil +++ /dev/null @@ -1,89 +0,0 @@ -// swiftlint:disable all - -import Combine -import UIKit -import SwiftUI -import XCTest - -{% for import in argument.autoMockableImports %} -import {{ import }} -{% endfor %} -{% for import in argument.autoMockableTestableImports %} -@testable import {{ import }} -{% endfor %} - -{% macro publisherName variable %}{{ variable.name }}PublisherMock{% endmacro %} -{% macro viewModelType class %}{{ class.name }}{% for key in class.annotations where key|contains: "<" and key|contains: ">" %}{% if forloop.first %}<{% endif %}{{ key|replace:"<", "" | replace:">", "" | upperFirstLetter }}{% if forloop.last %}>{% else %},{% endif %}{% endfor %}{% endmacro %} -{% macro useCaseName variable %}{{ variable.name }}Mock{% endmacro %} -{% macro useCaseMock class variable %}{{ variable.typeName }}GeneratedMock{% endmacro %} -{% macro genericTypeName class variable %}{% if class.annotations[variable.name] %}{{ class.annotations[variable.name] }}{% else %}{% call useCaseMock class variable %}{% endif %}{% endmacro %} -{% macro genericTypeFromAnnotation key %}{{ key|replace:"<", ""|replace:">", "" }}{% endmacro %} -{% macro existentialVariableTypeName typeName %}{% if typeName|contains:"any" and typeName|contains:"!" %}{{ typeName | replace:"any","(any" | replace:"!",")!" }}{% elif typeName|contains:"any" and typeName.isOptional %}{{ typeName | replace:"any","(any" | replace:"?",")?" }}{% elif typeName|contains:"any" and typeName.isClosure %}({{ typeName | replace:"any","(any" | replace:"?",")?" }}){%else%}{{ typeName }}{%endif%}{% endmacro %} - -{% for class in types.classes where class|annotated:"AutoViewModelStub" %}{% if class.name != "AutoViewModelStub" %} -class {{ class.name }}Stub { - - {% for key, value in class.annotations where key|contains: "<" and key|contains: ">" %}{% if forloop.first %}// MARK: - Type Alias - - {% endif %} - typealias {% call genericTypeFromAnnotation key %} = {{ value }} - typealias {% call genericTypeFromAnnotation key %}GeneratedMock = {% call genericTypeFromAnnotation key %} - {% endfor %} - - // MARK: - Properties - - let viewModel: {% call viewModelType class %} - - // MARK: - Published Properties - - {% for variable in class.allVariables|!definedInExtension where variable.readAccess != "private" and variable.definedInTypeName|contains: class.name and variable.attributes["Published"] != nil %} - let {% call publisherName variable %}: PublisherMock.Publisher> - {% endfor %} - - // MARK: Dependencies - - {% for variable in class.allVariables|!definedInExtension where variable.definedInTypeName|contains: class.name and variable.name|contains: "UseCase" %} - let {% call useCaseName variable %}: {% call genericTypeName class variable %} - {% endfor %} - - // MARK: - Initialization - - init( - viewModel: {% call viewModelType class %}{% for variable in class.allVariables|!definedInExtension where variable.definedInTypeName|contains: class.name and variable.name|contains: "UseCase" %}{% if forloop.first %},{% endif %} - {% call useCaseName variable %}: {% call genericTypeName class variable %}{% if not forloop.last %},{% endif %}{% endfor %} - ) { - self.viewModel = viewModel - - // UseCases - {% for variable in class.allVariables|!definedInExtension where variable.definedInTypeName|contains: class.name and variable.name|contains: "UseCase" %} - self.{% call useCaseName variable %} = {% call useCaseName variable %} - {% endfor %} - - // Publishers - {% for variable in class.allVariables|!definedInExtension where variable.readAccess != "private" and variable.definedInTypeName|contains: class.name and variable.attributes["Published"] != nil %} - self.{% call publisherName variable %} = .init(publisher: viewModel.${{ variable.name }}) - {% endfor %} - } - - // MARK: - Subscription - - func subscribePublishers(on subscriptions: inout Set) { - {% for variable in class.allVariables|!definedInExtension where variable.readAccess != "private" and variable.definedInTypeName|contains: class.name and variable.attributes["Published"] != nil %} - self.{% call publisherName variable %}.loadTesting(on: &subscriptions) - {% endfor %} - } - - // MARK: - Reset - - func resetMockedData() { - {% for variable in class.allVariables|!definedInExtension where variable.definedInTypeName|contains: class.name and variable.name|contains: "UseCase" %}{% if forloop.first %}// Clear UseCases Mock{% endif %} - self.{% call useCaseName variable %}.reset() - {% endfor %} - - {% for variable in class.allVariables|!definedInExtension where variable.readAccess != "private" and variable.definedInTypeName|contains: class.name and variable.attributes["Published"] != nil %}{% if forloop.first %}// Reset published sink counter{% endif %} - self.{% call publisherName variable %}.reset() - {% endfor %} - } -} - -{% endif %}{% endfor %} \ No newline at end of file diff --git a/swiftgen.yml b/swiftgen.yml deleted file mode 100644 index 234605224..000000000 --- a/swiftgen.yml +++ /dev/null @@ -1,11 +0,0 @@ -input_dir: ${PROJECT_DIR}/spark/Sources/Resources -output_dir: ${PROJECT_DIR}/spark/Sources/Resources/Generated - -xcassets: - - inputs: Colors.xcassets - outputs: - - templateName: swift5 - output: Colors+Generated.swift - params: - enumName: SparkColorAsset - publicAccess: true diff --git a/wiki_assets/badge_anatomy.png b/wiki_assets/badge_anatomy.png deleted file mode 100644 index 16f95d145451721bbbb9b63143db3f75f165aa42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45509 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 diff --git a/wiki_assets/checkbox_anatomy.png b/wiki_assets/checkbox_anatomy.png deleted file mode 100644 index f24de66df7ace1d11568d636160a6fdeee47308c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42564 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 ebNILXWY)QL2qzElBTGsvu2- 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%0AoIA9QJiOrbC^o7fx+?_X@i{MufJX$73u;u zbTV|96Ld8D?HZR73#8K?zrD5F6r{!?UVSNS;F;% z&G7jfd4m2lHmc`F(tWogPu9t4r?{iPHvHlf1`W=WXoZTmD1ZOQV`Dx(X$e&iB6d_S zQD|fMwx;<1&Un}Sr|{ptrrG(>fdm>z-Uk&i+AP7_xWjQbr-V?yt_oQi7GacNZaLVJ zl}ItrABgQG&VS$=8?HTQ*L2|Ll{JuyH@@S`P)C2Z<$vS%pg`&Yz)^7t0?ttQA5} zhGuXm`9PG~xZ$TC`?{++hu(KMuUPT(McNG6-mO`|{Rb{@~E=v%`p5^ zd4Bf?@t5LJ%bCEIzq%AW)~;$9@AC*1ICo17zjg)4GZW`~E=w&oJtcXGp8+5vKW=_h zOA-jp+Cpn?g)$k)$}Tc$posMDlb_Qdm99Ql;`rg61*q_6Fmdzn*lBJk7A5IifV~)% z30t){@)*8&`Orc)E?DC-E9h(?)Mzh~EyUUuVRs4yEJjV6S|sn@-;sd*F7(P_4y5qj z@^XzyUoYmfTKy6Am8rx->FMs%Q3gfLW7+06o}*;8Si2pR+q!!nDwLMlCSbu1+2X4H zRl{VxaV{;)^3?p;NV1jVTB~XITNJT?bjw~G93fwH9D=YMwIR7J(vj_av_fag< zNng9vCZD5>o9|2(Bw_A?d{<=_%rZRBwfRjY^s=0%LlX`t$)u^#bu*or0@qu*?s1QH zS2t*WsWTFg%#YnfMzG2lsE*Bse_tm1yQ(+u_Wu-3#?MIbfd++f|0s0xt&}5Hpv6wJ z1K3PIR#jmssj2F<+ixDDY#->%-2Tuj>RAo-6`ksICmqJEdpQOB(wnVQJ5qGWuIL9! zgu7#(Pis&VL-adNn-5Wn-+vC@tUJl76fivOn!y%c&18U+TKBB*nkbP9aLGcSVTQ8K z%;85ZD%0N zUWN|fhJc-`RrKCdAi({15rHw@Dtix})2T2V0lN%IjZl zy8{fDtzD}1^>>a-TNgOoW$B%&daxzxqcK^F1)CcZH`R1sp+w}j-9f!@-#68@ydd~P zm5B?Y)y~Ig@S@$b{?{|vdqZMA$~HaO=J483TF=hBG*LDq!tPyDy|hffoyb%NAlHO| z1`A`xTTCPu2HcS_sHcNZ<+M?Phb;>PMRd7|pN>iHwS*KFYA|{31~&=qJZkA;Dr{+} z*L*43s9|zn^IDtURNU~^8yIR5!PH5Sx5(Haji@0HZh_PBp*&tUdTm53b2n~CScgtMPn`*de^frFqKxJCnnPo{eg*(4{0H#!Y0DPNB7jo1^ZKh35P7SZiNI;aAbJOPyM8I(o^{MZ?4yLB5yipXu_P)>{r9zV|n^dyzE4G zT&}NoS-u(t32<8&r_JJMM|wdTOgqF= zAw(2Anx`+j*)#&r5)RI$dxrY9S}~w=}#+J}FynpuZEwe&MkzdgicG{$QS)GN2!+3dUxjLs0l4MlamVT zw#!)pWbg`W@ubefH81T9-4lC5R-q!vgwc{EiJ2LzfQ+vhEk9f57rT;5qGt@UC+>N7 z`XA8Tk`4U%4kTobXm>@BcR%Ku>@qPGX$`CJeQ_d^`B-*|)p_QFX0Fl;zbYhtI*20FpAZj{p!0}--7kl-o%1xt>vZ;D981+fml)&!ZMHOb(m$k;? zMV_9RWf#b5a+pA$fPLd5bTw*av(N#4 z``_%1_gI?aH0K&4&uwz4H@>gf_I84HpZM6PE&P<#cQuc5p>}$A^SdoiEMX#HBhm6hF?-}Du0~wqPhH_s{U-SM9 z)$zySSmjrr${6A}2@dE`=e4ZUZv}PTY)UamTYgw0{!1?3 z(OE0YX+-?JiR{hUQ3~W*-|L~7^54J7xWo+(|cffwlneSe@Qi7ApLec5U>+13jlqR%P{O1=H>wAe_xWvpH zYHzQYqdc|sQk(dbKvo1(;tELHukU_*y$05x6LQXRzM^{E(_x0tAW@~(?J;WpxH@A0 zk#0#>JIQ9J@gFdl4Et(J!vh#2=Tb|9aZhLaR248p9NxNke_JY?f84ZzWl`C#?-zoG zAGUlV3g^DaSNz zj-XsjidO9?botfI#VW4$3l)4vn!a>KPY~$?33TgEYIb$GIL=tz?UfR3*$R1)#uwpU z*gMs3wDUe8oKDu)U$#O8`%P*otJSd28BK?Gqb7NAU@)u$fFL5<%r;W{~Zmb=%_Mec$_=kJ_#*z)lk?pJZ8HlUn4Hvs1*q?Ri%6b@%}Oe(PY) z$dU13*TI;RPw-i@`B%rrs-rtRFBmuBZG~)%E1tx~(JoXS#c*(Rh)Sl<7etLce(dJz zn(OU70WUeBQzbPDdl!9y1-04YaD(*RXXt{JA1Up0?K-k~9%9#2KYudK%EqQ> z7{j#!57y7N6oGd*z7fFSV^f(C{%)3%E*?gWkF>)$MsG{4_N$fV%Sa?7Gz!;uE|sVL z_6yHV(VzcP<=1sm7}6h^B`n)?5cRTxZOhpO6%#lxl+6w>BylnGsBLRrYnNkd$NLr* z^^mCnVf__!>1;_q#<#Slc;e20+1^}Fry@ei1bpgiaF5LRhoFY9#U;3&na@4nR(``q z4EnB6yrgo##Nn*b1~{B&-<3}#@2qvvuLI!?_^u|QF#Fu&);;GjJ2z_(&CN9x;xiv7 zsr0FEg+2pRle!Gw`=}cM(GduAbjr6Bg5iS38|OnWJUZrY)6VQ1$M>HsC|#0ga-S$w zI>qe&%rkeXk{K~ql9`nUhvM;5buv!#d*-+#A=19Bjh){+8`~Ni+gik3R-;*X;0_V) zuGLq|8;*Zz*_zB$76!F>#R+>!MB(TAp3@hK@*p(y`yHJvnoJ{rhH}neSFfMoR>uPb zSZ7IW`gj)DrB04}%;lq&?c9}~TE?SVSGuAHAubO*)Vq>NbFEu4g<^UR{c(yxQffKS z6ltrB)rRT3`Nkb#DX@R&n5xj9$8q*skP~C1)Bm%2}E^=7wL*Nq$+J^j-7tn z4eh!YGj_bLGqc^MJ22x*6EwG^tRHsJAle9<@{kpA&%*$!Y$5C z9L?*qKvilpj(?OrSB@2unHe8gnJt(nJRp>zS)BI3ZVHYIjKF3z3^Wn;;@ff?3Nhd> zo4Nh4j}#Jf(^CQs*v;iGn=q3YVCec=(Po-u@%xhf$LvA*TZ?`5@7U<}6y*$M_0!u9 zPZrcQ2rk6$BJq90InD$(fqrYWNFa#`H;rsQ?pdIRmv?%XXoNkut3-@TsKk0m*J~%N z=a7VyuM95bXgEX#w-mjqSnGX`o5+pz03GC$&j1OiI;dJeGGR~AasgM2e~e>!#5u1A z?32hH%K8$s+lwA8C+{+e+(X!YEm^!an4vhe$c1=@n8`QM+fiMe+a;weNLz|`)$XR z5?z|0rzB0HueQJP5)X*=)W1yMhLh^}B0&OveGmfyo7QXr&Wz~#*1T6}8!32j1FpGv z8yYE|E45sz9gt#Z?poa8eEsaTx^8RC9OE0a?~6w&oluJgh^%b|EHDs3^yE&EVpryZ zvMM$7OFG2(d_$P)a(~9HrPz7wU_FO!>%JM+>3FZMY9^&z^+L5hi?u49xKM*1G}|D& z3^HG0nSrD`kK=bEm!&JtX!tJt+w zgB1jUZ)s^RR{wN^pVu&yL#P6^hw2a^)=ZSeN}h4KOKlQ6V)kv5O;hrPD{>mvYN8KI zwu19eL2Yds3n?|?C0W-hC>QK)5KDRLY|~PCF431tqYDR*FK8U_p!wKU0!)llF)%S< z6NCQGOA5UV=0erIJ6pcnh0$#luTxJ!K)C;T6)ChkRjL4=r>|9sixR0q%PIZDFVxw6 zylGx~ch$`+ZNaJ?E%%n6%V#f*cc-VR(HX`Z(*ChA!|)ZLR7yV9i5mzYRq1EuaH_-d zOUZpdMFt#r#0N{3o(}8HNzFvfpRD9sf>pdymJ0qfd_)taWK*M-*e#gi_<`ivZ9vPt z=7BN@25AzB>nI2g)m3Vj17L#n{wR?<`J-TCc(W=jH7s5PFM4Mapts6Z(rbY_uH8hs zY~+u@{)q$_GaYQ-Pn=5Z==V~$Q=v);{-4w?FO~5XzyuS{eP_qSok+8CHkPbUai9(b zHRUVjxW^yOa$TPiwTS2;4}gP+pc}1iB#wPx`9031y35kW;#61slG2@-rGhk%qHM(@ zX4#0kH_mk4E`}+~?$gozV5dq^t&$~9M5LG|5M~uY3t3V$GDQPOcx6|Vt9z~gr+RJI zF(v*m59wiXVZ&v`D{^VkhVJ%)L~MwYd}5J|!_%se5e(j7h_2JiqVin@Q79&PCD40Hs)W);iQVLZWda-RO}+$}(U27QO2rrG&}Oyr9>XBiJ(qeSAeyseQINKQZGV z;sD1Of~#j(k1Ug~FCy;unn1u$yfz~d_SQkkV$A#ZkH*qo!>gg5QGH)gwxibg!v0mx zr9pp_ZxR9f-2HzRkII7buXB^6LnGpJmwQ9*v6+(%ZOkwwZ-j_cLSJ(U9d&e{V_zFO zeSBJR9JNxvS|it^efIJ4^}NU9ht=&>#2tcm(@D5``9-m=s0U$Vw&uS=FrZ6xYIv7JB4BJJTkNf-9>irAYeHBN*g+O_lo_T|w{Lh)IPh&OlV zv1l1cK*Bs{GzcM}()Q6sZH`eV30QLd^&b_#<_$DlTF-%Wvzm!!%-9n?!FtA(Lhszt zPxkY;*ulZ!PXEv^(IesmxD_LtuBwbH{s>EGMVgmH3c+sKHcTc6dA1w-c27TH8j)Tp zib;wm^`HG&h4~doHpDewBQ;2r71C#CpL1}CC<2?${i(;^(M6+sutjd@%+<57%RHies{<;a2cl;9IJ_mWII@-fZK{GJsU7s03B zTWobKEzz-K>^#2!emGdMfNQ>=FuSl~{+nLLu5gz{ zY6}&eC>;{*oC!S;1h<-xK%Ce5szq+f+(4gO04POr@@r|50(((kwp#2gc01)6Him{D zv{|bR;y~2%Z+Am!Ox8+uQfA3`lzAf4heY*W9X@hl*riDj!6pAyZgn0n>I^fR+Wllto<1BBENmE|usjhhuH@M}Chwj%;wJBWEWxQgQy_WE3BdX! z3Q_>`foo!<@+XD%wR4YL+rG+L%W}R<`K5LhQ^Y`s!8PZavA0iIQK23!AznYWme{V% z)GGoAQ9-dlvYy;`kd{FXgL{frzA_3ZemSI@5c?kG(Wkrf@e=GP;|BvwAi7QK%9b+( zpf?JPYhKj-GioGf3`*HohDO`0sH)g2hQRaBPbUdzc?rm4&Kx>)h%NzYmjgXDm8+Wu zBRD2Z-i+V(mw5i=-NDxq{5D0C9VQ*}EA1-Lt97tTFFg*%Ma32gBU~QnqWeGT9%0#b zjtz+cT+a%Z@G;;WS{$i=lb7w0W^1?-Na1g+oM_M$f8t z1#S~Hpe5T~;lS%i0ak-dliH&9c-!nz!cMa!cn?+sv8Z{9JK9yK*tHvEp=(biXa2?i z!}}yHdP+PF^J^!Nm+4mPm6?aQPuE+%G_0@=^GOSHYv*ck$*VG5Byg3WbB3d4cRsGr z_VX%^(nOuT&c3z!0Ps=-{B1E(lwZgZEd#x5M~+M7^TH+g9&q>l!sF@GMU2Wv^^`9I zVSSpY(v_;7)h0|hOG(h6m+iuRNiT_tFy(D73kYUGJ|i??K=btf)83VbL)nG>5^qJM zMUu5wrAR7;vPO|4it0s3%48j7H->jqq%5s=Lpvo)c4Lc5DNXie?2#?TI(EK$-g+L( zbbWt*e|#5z&2^bM&w1{1FTeXfzxy1-bHH|!E6&MnJQJH!M-n!d86N#)(%tSo(^OY5 ztT8+R)mf2L&5WMPZc44_n)K%{4KNIgyHHz{nyQ$lrc|8b&f%!p0j=^y+#c>T%eB0c ztP9>h9&-}cckYLck^()_7*=k6IG|5Tm!57ey%>-B?`TgY85%9np!YLWV!5{Tx9eeu zfzy2y+k_s)$$&U2p6ZkTQPOJYT#j4fck5RP)kgE_aBHq0cIRqf|YfVbinj zMn5LF$J?D6%l1{#Zq>@mA7yiaax*j5FQ}O0V(>^my~M8h@omMNjRcpj*RXhp}O(0yxKyw!pKJt8mC{6MA+s(kW}QP{X4DdeA&iAW_mPD;)mOC zMFeTNmyye*LRwGm=or~}JP4D@)k{5Y_V+++{w-G&+#&wF={#8WwU|+q$QZSOMc~}T zXsv^mRb92=y-&sAHG3xVB^9M+m`HLOE#{mI%XADp^D(8_yld|^PN{!w>XNOkR$i`d zthqA&Y2d4`c*fNQ3QicO*Axm5hM{xHd&{iMR9io#Ocj$&v&O~bdPL`X?0)xn%nNKf z-%hPi-P-y-rbgHd_P1|zX0Q5XczUhH+e93PX~Dt`c0*N5NgZo zWQM4KtS{ZC3Y`MEJ!N~cplN&VUMviJ3))RLV4tBfLZ31B!(Ax(2pev^gsRKj=llk` zaQy}(Ue00*GnM(1zX>NO@^t2JpI==5FPO|ekkA6gKzx}8=b0|yNgigJoh=sH=~?Eoc(9?(IEdP8F0pca+KaU zxncfWFBtDVV3ylyr6`m_Bsh0z2uDE%cX#=|`NK=iK6yJ_Ppp^qFz|ns!J44u%$&W#VNGtrNBM^Uj{zkefk@hEZMbP{qP}OaMg=J5Q6G^w*TrHEe4jeVLnQV zj_Gac%IhhlCut+x^%SF@RKZmqj#Pn7@}$(%9RTUg&CP?Qdhq=GB9PC!)F$6ol^$eg z2)9ZO5I!r0N6?ZPp=jmxgu#+HkFk;f_kp1gz1Pgbj|7(1)$O)4E2K{Mnm1!NU%q=e zlmX%t!G`Y6w1j>XUB29zU|yEm*G@R|wnikl| z+S-GACe5vaIdk|*_Fjy5S@LU~zpN6;Dsih=&LiBBg)!6rUe?*I^i_CdJ~crG@d z2EVRZ$F{uLrlvLth*9I?M&z0GE#zk!KMqVvt!`le@Y-hp$mzMTqxEU+m4t={e(Bis z7k#_Pws!4x8srGciP&RVIZgbAyix%-6)|5sMuA;+B1c?lU~Vhwe`>e=Hb{17>|f|P z3(>)OT{yvv!MKz$KO$5`JwHpty0z-bG?w(Y0~0T3XUN&=4+|)ObU@nx9;0G&GZSo_f^zS#*;iYQ(04$ z-IpTYH=8qLAu@Y`s(9B0E@JqNi#f05K`Q6={rL=0u|euw`<0_$_7_h=ec=6R9Yn5C?kuo@oJs1{5Brjv;!rvuVN?ND zw2n{MKl>rx>_Wx*S-11+T`nFyUw1ZV;Nhx=uXK?djo`Xr`*HaK2cpN=m{`(1SFh7- zVLF&%Ied2|vC4`4dH}DqwXCR9xi3#ym&>8z1!Z(V8_G*%6*O*|qxb6&_OkjOl`k+N zb8Zdn$G7pg+S-zD#n%slv0WS`8|9uad8|=1DEfpU%jnW*>KG4=t3m&9Q76i>KyqxI z4GiY{ocJ~;@k;)HgrS4QMEOO2o6Jj^RbIt4=hmHO$o*U3O4Sc30x!{>y^Z4ED5USN zq+^feknOudj+-2&xfI+YkHm5J?bkY8_9 zS9`FN95gktlV~feEPgRE(vU_iu+JZX%1Y$KWW-c1vFWCn0{R|kGP5vMBZ#h`Qz1So z6x>#6;5m_QZ5{E?jlYZfigw?fBxIX-ik(@7H5V^7i3r?nAQ!oPWk;G)2fvQ+)B z$%ON7Zvfxe;f^ZLj78;_vUcEShP*Ru(klFpVxJ>$h2q%5EH7R_x3|&7i;e|v^`@Z#C5vc>=K-y4D|@?PyF=P0xpkSZiYqQ3+efn$+e+~;G;e;k570HP~_wR4`@EJmSl@X<)D$Vk|t=53&^A9Wi zG`~}{LPEn;3EM}q$<3|tCS{njRg!i; z!Xw;|3!^rJeCVPd3LwH}9g4V7s}-?^6NgWA)CCX?2)E$M4YOj?W@*u}@q75;P}~t>lqd|AbPl(IvqXEhASrG9h(c+Dnv@Y zGOdtbm+x5GnoNP>_u4<*!}c0VZ3@vmRs|G`%sH0Jnyt^AEL8@l?I*HjFD znfyAau`j^{K@7jO2bwQ;HbqnPjSSJtv>-T^5t<~Q`d$*1wW>Kc6RVCZ>4a*Ct z!mhN(;&$vvpDK^J^wO<(D&WSTsoE+5sbKf<+5o@N0ZzdJRdRvqQ|}I=RxOh-^U}S1 zi3S(Biy5Zwu-Us`VNzsOCc7nuHTdJc#`TW0geoD zr|Sn={N8(5S>v&#&ITj9PxRwc7@%_bBk)%o-1QF-H&n8+H4T@V7ODNbCDp2vM}B}S zh%7L5X-i}H)uP=;1VrxTKR|sOVv#p>ppM#}8I4>CsBO%Yoh^HRWtxC=;&Dl8r;Ee8 zkDD+0Ig&8jhs&2$x#hSm!5p6YQ`80hV%tBvL!hg6o{}1Lk6WURnN6;5vH^gVviR13b1K zx%`pBR*WeZANXV!&h^GH6N(M0`pN3^WOkF495wIZ)Y1!7x^0kC7V!mE>9X zrR@vE)s{6jDztEcF#tz9V014rVA(R<{R^RCpZ(X7V^5=rCo-LlNpL9J7yDTz=!4@? zZ4kzCzGE=O2l4h9TJlprJ0yh1o>Nnr#g5V&nAFvaP#}YRct^jn%f+D_0W{Iv1b=gp zZ943v-JkchLo5x*`C&JuqVG0g!O=$-5ujGehp4(KxE4HlsVL zc{!oQa-=T1YXTM}pC_Z4ns2Nz9iICgPBUq}x7tRD!Jd0o^6B7o_6j5aAbeSw9I<l+znkR65pnyIshu@O z)negyNs#hq*pZ8+#d=gW&SbQENj8};Pb~ zJ-OTYLMCS1rn(0BKZU6U+ck%n?mEQ?TMSpqbfv}xQZ6FSr=Rsp2;Om5nrJyUN0@l` z{>YVwsz=6rUMcKt&rhNn4QGq1txX{U=wM+p3mV?tV7m%C{jQ!zQntlV@*#urT%hNF z$(zuFXUh|n(tp?ik;gAA^V(Z4_OQ=Rc(X@W0Eo63>(>G91>>=)AFs2MkKZt@Kk8;N z(JzUK5E%ISMwR%9ckqzak}Gd9&7#g|UV~UBA20!2R`4mJGtkvNPx~o(ziF`3um0gm zh5Ma1?qd}bM+-jL0rn-lmEX*&sUWrA4=Zm|dZkcvXJbt26Cah{Ft?f-mxc~w{49qK z!Kb#`kcl4Bb3-#gZ5F~<1b3a==g__u+i#XDgbEzL6mjpFEbV;gG1$k;M$TZDRd{vA zo14M<-)ytBuVg(VfkXm8C|h3CZ!5!fbz&?XiQeJWp%a1{?$Ao=nVR~^+p3#k-RMof z%#m0FXLK7eIx`R2dhTtiR2(XYG~#8IPBRm08t!XkdpL!45XX-^>$lq6u?EMQTBS9N zIG|1If)AMLU(M+Te++GZdZtOTKkcQgMz37W?(nhJG5ZqiSWVl6I0eL5-~fvS!Q|u$ zdq)nceQcU$BxGq-rsOS_cN)hwy}!48_9zBq4lFQ-4QJH-kU6-n4Q_OCucmG^JBJNZ zU`xaOb=_6p&JHbhA&dQh&VFDztb)$w!5N|4#ytShCxkR)ox`8pFo!GByq=~`4t?Kw zdf`d7O9(PB=PPlyUm|beAkImX{ed0N!d0y9dV2u?;nuQMBtz?wSZ^-rg|d%og;;p+ z{%k8w6f@J9VSpCa%@Nv(*ZOYk=Y&F@x1u@$Zj97?_fQmJi_JQKct9QvDJK#12cYja zk;x|yJpS(82`|p<&)=M8$-#G3)?=E5zwE@zijdIR0|F4l5(_hVGq4Hme>wk?scL8E zs`ebOX^4!_toSx@^?E0=?MLh2^U-^bJ9btKw(L6Nu)w7(Avl9P`s4hHPu-kA4TgQHike;8*WG_gu&boj6r z+$!H69}kDE?U|j&ubB~c0$5E-x3EB5O?eQVL(IvZx=;GL`N(ijkt2EGfsz+9=m@Q??e=|7$xysM)b;F@qrx6-)fQb~*-AoYgeSBf3b!5JVm8`o zZ>P6uVE;)J9eWN@n;y;pu5e4;q`83BhW*zQt#!)eD1CVZty5TMfBAcTFP?y!XU0|? zo-#S=^Q*D@X{4#GKY;Y%_jPW!y~_*Q#xhjIkaZ>@$_QRMH}p0EmQ zfwtcu^?7Z-ATlL(SaZwsYcvu4(GGyqz9#Fqd%JqwH;1?ZNMH#f6QR@|y_4vkIYE;Z z{3wW(wKFRW%1usxZGBbuh$usf03G(8zDF?WT^00mJ&{t7SjrfR|P%;?$ETXuWeXRBm#IgrKoY&(xZHl58 zAZxvgjLhi`y8*YUu4(S0BrHt^(51)5s#fdOEhjCPdLZ|+WY(^_j*QkW7b(}k(+%@Z z;h>9-0Bjvet!7jtQ5%@)_0jZUyTg+_QvZ65+Esq>FSS`huU)VkDsseNnhl9dPqyl@ z&D{Hf{_1xi{ul~_S&BVJ8=wh)Gb>MKK+P2n&D&TaWi=&d>WUkcgbghywx~|(e<@6O zVb@*ReqWrpd6ci@Nb6k2AYweg4Q7tydP4gfg`mgStm=|`-loiCJ+icea#9JEgQ8>* z5VrpDx7J6A=5Ed#OJd`(RZV#foVH%xeJ@^MCOQ%%?aj??I>J4s0pZD3J2FQFF!+^Q zkh_Yh_8dX~QL&~v8`n`1hk9dN8b5& z7}o9vz0@~08ANT~nVSGz_{P2U8P*iG?D)yYX-;aFhX+1S%Up;88NST2NU--<=17X15S?`?rzv=;}H%#2$L7Sm~GH-o$z`E?{vuD7Jh>Yk{TU03ML0D9%MTMfDLA2P{tvmC3XK2& diff --git a/wiki_assets/progress_tracker_anantomy.png b/wiki_assets/progress_tracker_anantomy.png deleted file mode 100644 index 8354a0a3fc8acaf8ac9b0061ef7144e16bc75ae7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51376 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 diff --git a/wiki_assets/progressbar_anatomy.png b/wiki_assets/progressbar_anatomy.png deleted file mode 100644 index 7a01a8fe0220b0f2d79ac0f01fc900dbdab47a74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25375 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 QrUp5GR#PSI^tHSH2Y|mS2><{9 diff --git a/wiki_assets/project-installation/current_branch.png b/wiki_assets/project-installation/current_branch.png deleted file mode 100644 index 98d1daca34c53486fcdb4efe5f38ac5bbe48e8cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14162 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 diff --git a/wiki_assets/project-installation/dependencies.png b/wiki_assets/project-installation/dependencies.png deleted file mode 100644 index 306121df0252abdf5e32e5466af8213fbfd75e90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86924 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�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 diff --git a/wiki_assets/radiobutton_anatomy.png b/wiki_assets/radiobutton_anatomy.png deleted file mode 100644 index 6e4b33cefa297394f1cc8484bf5b7d2d12858729..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45970 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 AA^-pY diff --git a/wiki_assets/slider_anatomy.png b/wiki_assets/slider_anatomy.png deleted file mode 100644 index ad721fa2a7b70fa6a3c2b5b825ed17c8b7ef8aaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14193 zcmeIZXH-+));AhL7ZI?4h!iQ(L3-~hB2uJxPzXp3y+bG}DhNnbx>V^%uL(t|(mM!2 zAhggsge33g?|Ghc#=W2KxbKIPG4{w_eXY6YnrqJAoa?=|rYhx?J6Av;5T)9ahdLk- z0Umg6l92#89PSF22`zgS6>T*Y6&7uGsI9#d1O(#x zW@V`6OE@v*OSNh5jC_dv*NoaGK*z#3w&OUX*$U^IH+kNI}ZhWru^_8avUm$Y3P~n~FO7Mgqy4u!0 zg*QXn@S|BCq6B;7)33;5F1#GvJ#f3Xx>bzhu3U9O2jq{Ct}=P1RGR(%n~t9QmM}l@ zmoNUvyeR)PuG_BFj-QYiCFER9jrF?I-ou5w(x2|yiTJ(knwlO^l)=KW z{(3%c^jq<^z!o1-imcHaxW{anbC}(oJ2ACbi<+%Yjzjql4eX6owwJ-Z8Hgal9z+DZ5dhB}-~oXMlt|(X*`p*hB^zpuz|jW2>3d?UN!;A`bq<@&JfQR zEWXZ8E*{dpa;*Q{JcM{yyW6{Z+CyDfF8h7)66)nC$I5y+(EtAa&J*Hm|L>7pJpQ#TV1a^{C4xc% z4+Q_WZ=k8{Wv;Zgy)VSc_@TWsKptQWd0`20*?;Q)ze@f+;(xR>`gcn)A))_l`5z_! z&zAZg5O)=*Gccs5{J+1U#9be6v&EBL?PO#Vvsxcws# zNaTy!LnS?5f~`3pPsS6kg*|mJ(Qp8VVujBKjm?{nbXXsG4I8kVMl<$Ar){cjN^%Bx z$A8?a!B6Rxdt8T#Gt9p4o7ta1?L? zESl=s_1nKLRt^|iWDa;wB-Y-#w7&&`$yh=_;0&7En_qZQ$~1}~0wQX~RvMO554Ot! zGL{7)f-poEPg>uje~LmD2IcMjbrvWAH4QBu3 z1GRn={xb@h@3WAFo$psx-T#gQ3_|taLBRhd*Z(Kn^)3Qn*lfeB;$qW*jMCV6PQl*1 zzVVTvA=XmoE%&8PN;S==32B?2+yXtx->oAd=hbd;NlAR+B$OZKDY9+ISg3D-!w*%K zAL}K&R%QuljR0$ClvYB}O4GGl6*qo)cnEXIvFRgp*tvz;&MXWky}n0;cXk>S7DMBr zqc#0pCy0!3Yj^48n-xi@R_-mRXyd{_;5u^Z3+U1UGucCfthzd_S{E61$H}dv*Z1~> zgMYhnbN%|{8I+^->@JT=#I;YSr@oXp)>^2RR=KCQw|YQ8fv8x`6Jx(TJ+s2ZM-ehBo_p`(wU&#l2W1^1*>_;au&d+U2mmXs+5PTM*=1BtS9A{_70u4GJ z=+%HVvmZ%GfO%ZEOj0k3G{BoL#M*|BMMmgkN4}UsW1130Yip(HyGW&dA7+Ump`< z-f}*t9vWh|8!+?U&b!6C(!IpHN{V8-GB@tN{Dgw~g3X*T4LJ%vPQq>Nz`6Ak4N-q0 z?4GE(;UbuK+d}Mj)<*USWl$UAwr8^l9GmkUO|s4xQjcSa9UK`@49Gx@bIecr^5@^6 z7-M65=~ttO>8axfLsL_%tActxUpwq<>IXo6M<VCVea_UJ-ftANXriF*~g%Oyy={*<)^o>_><>sV-jAhfmz@G zT@?u#`SEFh0f7ibQb(*U3|(9}eG&6$o;V3?m>3bjpO$8->FdjNlKp-jvH(-1g}+x^ zBffJd1%=hXKrT*o>|8A$q@3^QZ5$rn^ut-JqmCrR)igAg(!Ku#&|LpE8L&`}Ur`l? z^uD5;@yJso^kq@P=lratXHoHb7D9P5$aC6BZA6wDf1WOpP8%At-G!uiaCwFhfN8Ez zHK0Gkzf*n^%27WIpTTqc_4k*~R0kLo5HTg02SlT1&BMj%90R_`^siuNtJ^YL;KN-O z7hGxJ2|c_a3`@uu`Dyj)L{&t!m6NzYD+&2Ps>~x&@P}*cL0w;H??hR9L`~7KbXabV zaTqSLhHh`8D;-bV=w)o{MWkmlpJSb+D;)b%7~R*pjr;c}?*&O)SjFDo|6V62Ge9vY z*o6MR>AYXaN!N=XvO3tBD(TVPyT9hZ&1X$bIIb3TVoh|n6GLfw0rLsaE{H*5 z%Oij1BOiZU1XBvP2U8lKTlkU$SgP%G_PF zn)Jlq-{FbCFB6c{0`kz_8Zvi2CyI}FINd_Zc&gjgGWocN=AB7sW#yHg-e;3kxSNO- z%;Zts4tA?|6()eNZ->E`MZ1s&1N4(8jlOx0js^BD{C zIyg6LSB&2|?TTTzms1U!Ki}A?ohD58LjQ(S#?{^hFBFx?`?g^i_e0V1ov7bWCcYA3jLi-7zT2?B5}!N>7@Y@b)_#ZWvg2pnmS| zSVj8oSClBig!=JEo+K}HE%IC+7Znmx+E#&^o7>M53~pB&53FN7M2DN7ojpB^)+BBT zVqgznB3aMYq~G5ZiS&Ane?1GkV8B;CREodo9L85-U7?nhAb8v^itK! z`uZm&iphM_u(kF+-k~2fl@oNa)H(0xnuWSJv171E5lloJ-CrMsR%%rc9dFB_M2}4G z%UeRR6TduKOjmatXI7uHMM8e=PixXRWmy^fC2mgnO?`nIOE+t~dx&hC$flAQ#bz#D z6_E4Vi2F6R9^ynsebGQl{Vqh&G$110wM+gp>5P9sK-%!kL}q4OoUdi^5yUQEEq;L3 zDl#pkf9$iUTf&Oxu3%)29Lw@bZeKg`Xqqx|C?bqBKRVGNy#rbpBxq5zM}M)EtfL#B zRs+(^Xja^fF&kGM|3be@r$4UrqM6l7qfxvanfV+y0hM_;iZ!8rWxsXT)g)6;1$|QI zQT0f(b!0c1fo{r#!T>taXlA77W;pG^O z=KJm<>8)E@+$BE?iW-&XN0h>jrzU1+i|qr8kAlA?PFLOUA=0=mHk6b_MM-bWJ!9_b zmt}s|pn9op7QPY?X(SRB(e(sbMtJ%hJqhe>?MHa5Y9>`dk|7agV-g02YpDv*-UuoX ze@gGTFtPmJX&8aWiNvdDSn91-TB*?d6pG<4k@`+RT$eF+aSp2f;N0Y85(Xq zNpseQH)mrIvyY_Q-`GMYQ_%6GlfHkR^VZ&S_jV?lFipnr4^b3>W><^~msQ*ExZTsH zpZ4*HadZL9C+~naD5ZaIKhMM%J0b38w{@*&pl7V+9tqPAJ1Pcjrw8^R9F6KrtX6+7I*x|GF#15+~s$1KRj_DxrI1CFK_K;tCX<6-f)N#YHirv z?|hxO?pj|a4fT`B^GbxK9Wu3R_-mtji`QT^>)KR4*I3P7m)-Skq0m@H2 z5T0L4`ycCm0O{rO+v41jW?o40!B0qU!9Tp@wn|ozHl-m; zhPMTZan#lMY&7p(F|Iv~2}*yP`NoJvK~T4-xO_tv>uEQXfhcIwuRCtFA_UFOHXTBx z*R=5RLlZemM0}^K4lW>ZBEU`<3<^;sA_BNMeVH%mM_zSxt8w&r=1U`rkf7|O%@|hGm(-btG z+e-N#rd8c<-6Z9_<||~LJ_#Jijyl2@w&=DHMC&yn!3Vo+*1vAJhLXLuY@J^;C`&76 zUerkOEU-AQ-jR>(4@S=8E+{W*uP@~HW0rLVOAn`|sh`kKoy-&Q8D+|PwXI5DWtOY_ zcwAm)AM3TV?GB%xtuWL~lN4lAz1g!WkBN#uUC+TF*LNm1bWLa1CgDEkL2$YN;56v6 zNOylEK*Pn;`zeBMFqV*X@D(`8iCbWR!nns0(-Y80J>>aC((PgQKwBvGhEct1(FIn* z!lN_5#$hC{$@$pTZu%65m9fN?nmYv}u)j@0Nj8KlN!8UiZKyyZ-PZ^Z9YDjbV8}WB4;|3SJbEp-&Gx?y3NHJy%-8Ms!Zolx9uef3a`5# zXldH4Ib0=>fxM{X_!1xY_FO-6vaF_Oy6V=Yrl+QHuPa;-Du|6**vXq&G0D+nF*dZr zXu(c}3MW_aa{-!bvsSLFFPNoF?(aBHvB3@uVY&44!7aD7$CP}(DIO1<^zv%;Oi*2u z(G^wkTNm#|CQ-oy8%&r?g$8$iD1YU<(G`6SQ1v{=2=jI;S}R^O?7tC|6-1#7Z9Wdn z*5b~roKowU5||OSn(*_)WXR^!%k%T_&^x)-QFotnX~{PzlJ2$8jd=D^N_Rse1V~$+ zGzGpDnG$?bL&p$t@3iZ&ock>S+6oD!zL^sgBG7mhR@di)$MP3^nV6s1hE&1lc5t%} zet2xl{s|02oUZ?1c%2!ZY`rJ*m8L=(-!WJ|5*m1<>D{TFhDxywzECu!yMBFf`OPZx zpFe^(y9MM|SQynD4&F>rE3&kMk27D|O?ugVnE*83GAg2unnLfgo3yr=Th07yvrl$U zfWv>E;3AYHxuir?{*cYA9Igpd@;Jor(cud&whNdhAIjYb+`2O~DDA0DsiQET{z2E{t^DV7T0eDzaj22G$3%MPpczDRpjKl}VZc@i?1y` zlCWg5l-hv{4sS;u?QDNirz>6-#4U;l+g6vd_#z`vAR$0r#eBbla(}7X8rS6M>Dimn z!ij1P9o3XxxWd>BsRC~j+|#&0d~4ax9)@!Tl=V1GRBApI3B4vS0rT;81Jx!$tGis_ zo)%82AaJFd_k7ouUp&;sTWiW~-q$)_0LvfaC*v{9aH>8qX_ZtCy0j)%G$$oxFED( zWm@H>xp_`=m2Jf6FZ_Et=0vUFmv7`jSKS0jIH(#xZ3@dj(m}%n( z;V{?1_F>k0R#ZL^$gTaH$q95wKX#x##(uQ3_jE5WE;as7HQ6CrQ=DyiA0^%JjY4+X zbD(I0#P4|e=YEsF*3@kpaUzoIET>fLKL%;u2?N}np#S9E2kRa`ai|nm1XX6Dh@F58 z!TjT0QwptGZwkPmNmOf;Zw))sh0Hf|Yi8i7Z@ua0%mS?kSy$*ZuoF?cA`~A#E-LZg zezDqDmS0tD8hqMO6PH~q6TBj#RgVWZguT1+U{E3ZBJJ)!m`&&|z$zWk0s6mq%)4*E zO8;BO$f$?}sYP|M7#Blw?G@8bdUI4&)XYki%2 zz|idOpvs@pdjl)#S>@%HCZ@ATOfJdC$C~RCn3EBdiM``iESqW{J1?(BT>NzoettD! zkx`rO*w1*a89F)z6%c^}@yp}*?5ZkL9$qwRA9Y6x_SK}!tT4XVuv{%77}11jyn08p zoBV>FzA!F6{@%}@4ZBZ1+~#H95_7tGZIJW2?s=7(olydxzFWWTYkxD!At5p6r@OI(_|yDcOuVKy@q@IK z*LmFLZ@*`qQQS#iL^}JHxpQ(WSX2Sr<%Z}CbkT?>b1s>7SqlI(Fvsa}9;-a;3WDm7 z(Q+59gw`B_f}`Z8-j*inidD5_divvNLYKtNS=iGL5vZQ1^(C8`>hHoYZNC2K`uX!Z z)g*x7C|$pu3#FC_^DKGC+?jF|b61X&L3}&Dm-k2*)AnB2Zh&3w*dNi01h7m->Uv9pfLVp&w&1=b75As%_;1fU085cOE z**W$I&QlF8WPHIB8eR(v^nU6)mX4K{=ey4eF!s9(Z3B|G&&wyJ<%qkK2)q6x1|+gT$-lUHS0<(# z&@jVw_#8I`d3wF(D{7$%tWwkK!p|_e&RHxB;I``(n3ILCBKs>FR5q0)yA3lMkp9O0 zerNY*U8N=F1ynT*$CA)JjTwFj^F^KihJOulR!yl2Pt72H=u?$WF#IVA8f!0N$Tu{L zHO*Qwm^W+br(YGeefHjRjs)z8@$Hq^Gn zXU+nU0j5l!r6<;tsooTceVtju>unhZt)?>$kUDY=bO^O@4vl-6r=I&f3De|FFI;*$ zG7{%?7@S%HgVw?Kc0A?Rpe-m&v32Mo07WeWu+vj7jquT@4^&ifgK0ua@Dr?Qi~owH#b_4Hs~dG3GaPw13)r7Ho+St1@Y4b`&`B zsd~txTM#`QJlQvpj6ie+lbv3d^jL{YNHpB&K;Pan=yfGI-kNfISdhjv9(=B8iI83x zEa838Ka)OErqo`0t8caYXXN<6VH*ML3uE@v)_gCMix$L$97dpA>ymZ^C@wsDZoCw7fTt6 zp{WRXHA@-+F?ybnum;lY<1;q!{%OArg-`e^MBsLP>HsiVH74ywz(P%1XiVv6UWMQ- z^PupKY$7RhEJ9xD?J6Dj{m`Y(?q-<&i@A}}ct}EYer+8mIv%335KOemRQW@egTrqC zB*(A^ZJU8Jm;7R&U3PA@nk_eJSm{sSFLny8InnhDmiPr&uL|Cqu4e}d_^80`s=@A6 z%OC^z=^MISMUhW`zY8TU=_B-_UI($dKTBGKQmM1mXP}fbLRNckOXrWCmSG@vP`t5p zY43hPrleb2Ir0T3(e`~S`U@RTd65 zK}ILz$Be4sepjMzkq`ucxEL*124R7P*IX=e`5!mDkmN0R2LXtH2e2^6hx~;jAM_S_ z{3e_(@3C%yyrg5?!Ge{VHeXN?B2^=^cVxlRAowTJl6*FJqRzAo+m$p?pTEMR(exr7 zQFp_bA`Y^xK2GfCB;&UcCqj#wkM=%6R%Sta!d)ourir@c-W`Kg-3tk+BQ{H2UTq$* z54M?hLO2fQqn&4}kv(H|PV0OHJNV^7ib@qyLyeK&An{UYmh(5bRGz~DEKlJ2O^sAY z#4H?JVSyyDANm=2KT0sMAj9li;d}Lr)nwrd#|f87Fa9u*GM1Yt)Mv7XIf^DGCZe?HjK1ZW1xrID`*dT<7ll*} zSS6qRM3+NkP87X)NyDH>no{7z9V=e^$B7-vYGY-}zGj0wHF2k0?`o%Hud-7|1wXC$ z_5^hVe4SC+(}2WYdP0H-)6gAVFpi-LUS&htMuTtWe>S(2g*`Mv71vpjgQ|Tmduo&(?-7sfrsxc%t*ij zDC($w)k`8Nwx>upZuJve6=SE-F(}pYGyVM=rq^mrNsZDa{VW{3_(C*R4{D|pxE&vL z|9axlcqMR)VIY%ZgY^6nBcClaW}?|KJe+SW9wU=hLw(xdBnqF?HLz6ilupLlTq$n!e&4+lXXjt>vlQ?WFhG83$_0<>*zevMxcmT?S2oVM@J zlC(|_I@?@mSt7G|v(kqNUQ=!8!JAgM>+0^SU)B2zI|)8)Qi|BeB!%uSFVycVXnPwVVLO5tjHD;}4OcMXFXu6l8v(%nKyM%GLP;;o=bH#^r4 zM(_w7Q;50OBpMUW&E8RVe*1E#GH=I{h zka+wINM3!-x&gm4@eGPx?DOhfSbQ`Jb-Z=jzwzAwjIn~}hp#!6i86ps*dEv~lC4@e zIJmfV+8Z4|QD3~i$zcMx9Md8!FwnNfQohr{)q8uvMbXGdEcc1bFe?hr6=dT*s}I*l z=^!)JZayVf8P-IHvK7e5_k@GaC0EhWq-Wz2&bLZ8ib(7}u=Kq;k6X0EkzJisgwBa2 zCLRI)uOVZx5fc{yvm)=t?9CSk!4iFUPuD7?8m}JppY_;zy(mf;`;jFW6of!6pU^0> zT!71h4(f25NWVWc#!bO>2aj(&l};7X)}7^|6CCee{0TQDQ#QU9%_sw|<(ZA+5b@IQ zm8Luf;2K2Ue7cy2@{q0lltZYxmv(^$XZ3mzu!A|716+DStd(A>e&JODg*K7bi=WOC z^G`yaI^WEPs8gV9+19kuo4-M|dm07KPqi~XSO+|5JzwOXPRRRG`}@WO@zFPLC5_1> zzCHq1g;Z4O>1kJzwBz$Bx8p6DJP`~3Wg2E{lkL^O3jr`$y?X0-n}P_Nz6Gv@)8=4> zPon&x_`Pd^PV5UoSZIsR;H-ii0m zgvW5iocJH^tf&;hj1v28ckeq%0d$NJ2h;DtL%?(8cI?z5HKfLDuO~k-*|^< zba8S`BxNgDZWcH_tsdftID&VtzYUWkIJg18bh&C{pl9c`m!P1$p&Q$Dk@F zvezc9VHZiI5ov9HwPfQ{LLHFi`}pr=d=wjm&J!At_vM<7?M27O*ErpVM6?u^WgnjI zp$Qo?GTA~Duaf`J+pY8ePSrX$gNNp%@_+97xy~waCZ(hOnO`bp>37O@soHp0P(j)8 zqIgBkHNDdWo}(^eC_vTPRI#Bb$(X!Iq%7BFVb5tHSXtSv!7%7r24{O(^NCoDmW&6MGm2S25OI+Z z#iYNe8Tb| zIIvJtsYVJX0h``G)S9p_6xUDu*Ce$*DppU?g_2$Ql_I4ie>C#c50k!?a)K~r@_&_L zn-!@2_F4-91!MD7OS?lr#L5mpw$!o(k65$`)Rk)haKL^nDxvoCIH#pq8O8Z><%msI z>}CuIth~E=U$ONOc-_9I@rhQgz)32Llob851CQr(&-IA1)k5U)uZk#p0hj~``BJF3e=EOM0_aR)qZ#_Q zCs819CZ|xGi{fwP7t}1L5d>d!|J9QQnCQR5{{Kwm)+~m`N4lYxU+BUltEx&pnvpj- zoOH0aq4G*$p>teOk^lPER$^G#U2dLVpFXig+Vmzo7lv-z^!$1juM|##JqKM}@@{=N zs11LOE07}e3B{O%XkUt$3|p`Gc+%)MI(~Xt1qE6YWwYFfQK5OHH?I4 zm;Y{KQ$TK6+4FgeDwUa8nPoH{t4}X4n^_j}V1lhcxs?)dGfRwJheY{>_V!Aj6+m0= zsoaR@t>mbxR#52k8$~2j)X|0x47f};LlAc*T_3~u6Af+lb3e&~epB2>geyi}0sKDI zFB|wy$VA0!x+M^2cy=}seOiIf_oo4DN|G%6xXg0r6P8q~uhC0pITd*tmykqTPx>Yf zkh)KZFC4Y5+S5o%g6D5l$m5_ej79-7Y(9-U?nZ7aG@G1U#b$f6M;JOUdbV$f*Ec0B z3!qEkRRx940(-p9ZI_Cuowq`h^kjVFP>vxlGc)tnVgA|asibkk%Q7G+ruzhr*Yp*P z2CQDiF<_!OwGLcHB@|<0X5+zU;le>PM)Vff8R33Pi*p&WZhb<)JKf(x{L~$7{T}P#`W|2&!;d=0KC&qn$jCdW5 z#<)r_P)V}OTin;U%OMCq{g!ZZHAoVV??{*5r|UaEIn<3@*1~TxqqGo**#mejT({O4 zFQ8g!0K&fJ&Z>Wo^1|tRit7@R7VhrCqHbDggTlr zoL>ijs*mqy;x~0KHE=rvL{yY0y+WCu_ogHdui?OnpJwno5coU4VkXRKQqYht4-EJ` zJ)c^>%|$E9!s?`|ID4uF<=Kl39=s>K{{*3w4|aPc@K$TWQ%JK{-% z__$#Q6#v_kMiv@!bTmsAY#MzMTno4m(i1P@Je9H*<5EQk0vXGnH!%o{=mc%G4+3${ zgutGo(i2X;9RxORe5a+Z9^vVImI+0s96}FIWT0o*^VM~cUDtpUc@}&!`RDemxEUk(-VLr{Px!55be?)VHaG8FViDgzJ_}eE z;hKnGU60hU33D$dBEI24k3s&VuPH~+tgR^d2hY-sq=#HC0H@qbGvL%)y7de7sG#)+ zd7k^>lZNW*T6GJH{6XbTbhSLgZ6ov1g}~{7<0J+9b}~QhRh*!ywEA@{*~M{yYIH)6 zx*&fYq7s0XB7m?=rm{c*Id?D+`4ksDP@`ixxoop?kciqr;F7gS^RgyA6Y!`HcY=&Q z(y81B3t;t+TO_svOKAZefLO3P9~89*BOY_QR@N z8z85C=MHdEf+6+KNb}GxgI|e`{LYQPT+$B@58oatqpRP%SS@1%Odw{8bw5C`kkzXI z`y<*vJ3)xcD)PL(zE$;dPH(xn9e1c-jC31@@m6(i=v!dvAS$i5^X7c|* z585grnwnHoQcagj>uS~Gac4DnToDjntGIAN8XH8t8w+YfswDR3Jw_;MX=OGa9h2@K z%gp9guowaYy{Hs4J;VCo;bKiBi_5MReTikQam5ZAJy|ZF&OcPJ9yV*!HDK+&u)xju zL{b$na0}SS2RB291y#ra>U;*MW98t^M-;}ct?Qvb(b9AoLc~d~ay{+&Ih!i28W72o zYnLlY1!81O*D;*r^Q!r%2@i=(E`t1NC-sy@UIEOQg|?Diy4;P02V{V2yiH-HD1qVw zM<^g}q&(Ns?n?P<_V$)N(f0}$bdu1TopcV1y;d9f#EB|vtArv?>2ya=#{+62wDDL|B5$gp0O@eQ+ zoRac%;{Iwt^}rt5*}sx|`Zr0$qqs|@7lr++ zz|M$1-q^*zN$N{~xVF%->fcKdC_q>w8?yZTn*U;crqg z3s{cg&nad=p8>8$Bm=U`+#>Ym-(;vFP&f3$cR(?@R9^+?SWcUo!QAqHlmA`lSXiZm+m zzn_&*Y5#%&Ncd!qg8mnbCh!;e^9T5k>~sD1Rx0X$ModNf7c^#W>aBl0qlO^6@fGkk z0Dmy;BsIV&C@)?i|D%e6bvZ>c5nBgivX{)P%&g=fEHW}Oeg`8H zUL{fSzq$jT1jx-C9qo8oSX^9Om|ZxSZ5>Qm*m!t&SXkLv*x8u?2qv(bjpI94CL1ut z^&tNmN7NW>=wNQ=Xl`pmh8*{ufvuCH0694l(Z7GL>2x$V`41)=@L$scCdh(pVPRut zW%>8mKv#a`TV4eRb7KHAa(oaQ{}tr_@$N4?{47Z1|AUz8nO?mGrV7I1XZbhVKv+b& zGj~x?gixeJU#qyHu1#RZX%D(hdkr=oalI?p+*B|{g znkmz^c|#JK%62>jlQXJ!Df%>#EqHhy)PWjFWrh?#TkkNon7nxO6(`0hLtGd)B2}ws zGIn%$)0&HO5b;)3t;(V%XI0_xaf)))hTG<*BffTnJG?Bje_3qsP=P)=s7*Pr-6vw_ zGEap!URG~O7E7ErS(;jTFz5So3SSg746?ubWk#kC(_kVfKwq)@lkHt4cVSpDX6;-z zF;m(h_;g&nOr%^zWEM={O%y0f)kYLbZBX{gt=r&|%w7N;M+gP=Z-2osByk6p9jmn? z?o@R8waszL7-fYRexn9ajSyjo&ThNDdKN4`m&G|h`uDV55gn)5ypSO4Q#--T^Yy!; zavSf|G=a*jazwJyZz3%JTYl}cJ;w=KpuZ*Lx`Ck3cOI05J%9jrmiRkH-g~i4cia;g zXvg~G-Tg8Dy^QvTzva+XLzHGSV)w|CIZWd1&%s7)mAMwgfI+37(5q)(V%$4wK>xsD zWdD2WLJklvt_XA|t)jPbw#uC?IaXtIt0U6UrtI;31IeY2{)w09IIM}j-6s3)6I@)n zBRB(AW!+E14`NeQP*x@${9LFzb!_E~(SG=!1!#ivwNGI; zN3c8bC@KoEwbjxh`U@K@h5gUHFjQ)M+xsCVvYyaV>u4X7YOjA5a6eYu%X9PE7Q`<} z-v)lUQ?RW54G4ZsLMkdQPTV1Rw6al0*q;n<-OAW@{jMT}{^6hY3sRcms8T-6y4r}{ znot(i%(lmY3L0aTO+JiIu+={LzW!%c;t1(`qb+J2SMCJDx3_<9ZMm|?Cy-KY3jf1i zx{@IlmgKUjh)AfB5W|xX*gEYkrsUyko22(YWkgfyJN`3w&~d6I{eoh>-!g8~-5Yy= z9r9+$EnHBj{`o(z8`MY)mC8LBTA;#6FW88ZmG>!YJ37vt8`e6;W0}0D-&W2)BIp9i(!-EM6&zdV}6;Kf#`Rg@CX{v$Ur}(pwdUbf9@tEhTZ{N$f?LY z6d!_R)@whzK^VLDTISj(CtpiFkKr7^ee2n3usu1MX2h=AM?5`y6HoQMQS(l%%*`EZ zo>5^{s01oNQ>YSHnS=kCMBi*QXuqk}*evb>NG9S48~yu-4~7Ho7ALnP{@j2uN)YnxC|KH?8~JNNQ+`q{Slob&k=4)7 zugr{7cRZ2aWEO0p2cKo8Dosw?EmH$m)Y4wAfHV31X-@O|6H-iOG@Ykax>DGvS?LUIfo@li({@toII^d{|io5JqJZ64Yt_~w_z z_@gK;=eg~cvw(S>8<3Lu`o19}QF!nW4u-*xDM7}T@ZMgxFQy?WBscIC(BsnjR*X2< zJ*F@?4-LpzM~CjPdd=A(^w)20v=syqgieKNz( zaX_d2DmqJ8eUcRR&zl!$eawvWKte*|gFl4DwCBZkJ9!m2V!`O9nZ;E#|N8YY%A4Tg zfQJ&KF<)u-w&z9x1V5@SonBX;QKRMDCc+k1|9|ZD7~{?l=%TeBAsy#74=1PYG(OM$ zo0x$^8`Rv~Yz7z>+HD>X3DTE)gO7 z+pVqdp;L&$;Us}=IwF-00aK@W^qbg7>+fqM05Bh>>lV|J6XhW<`_ZHUi{{!;{gWA^ z!u^`AAf>(7bi^14M64g-@GL9ZVRVL4!wXts{PbC6dUFzvqEzj{_@5GHNIu`JxwZ8T$QjM$ZoiiEly>Hf zS5Z)!6uD8u;N(UDTn`n4Sj;LzGopjS>~Hd8QT&9_L(GALAzYL^-ihM3Fmw0+!(MVj zr!2}VbmpVjmKuWb_cznt#A~~7w7EHsA!u^=5ohs7kMIxM&NnR}q14A{X^ND)fOI&x zUWwmeJ5UAC(|cVPEvjs3Kp(ylWu>~0-+y!C=269x98z|G^jD<-YgGQw;l}-m5vNTC zW`I}WLCVy-=I!xFZj?SN(^snPcuC5P-j+S0XE*B&YQnFKh`pB(+qa(L8Brbyo=Qhy+fuU}P53veWM8S<24#S_8J#Tq zq{ta>TC~4da6;v?`HJ#X=;M>d*CTyyG-#;n=#+DGm+E+dna*i=tT)MyjFk!CQ{nb2 zVo&)Gb~moq6HH26%)BF`WUX_PFQ0Ex2Kt(S8>Kli4NmxQ>!z9hexnbajm)xfO5!oN zX>wne@WeY8QI7QX+D zd(@0ZXjZw7jza$?5)^Msdghat-y~WgK2%)Ds2maMjdQ055aXwX-ZYg_%&$iSHXRsG zZrY_P0OEWM>zn5BTi&-N$5~mt`6fxGVtqYQ5i`4S9?oc#po&Cxv>VrvDu9??_(pA0 zjz_kgHxc#d#x0r)AXa^JQ$&<3H5nyPVLdy<_eRm-1Biou-#CwTbV*MOZe#S2n?#on zAXfeO-zE6Jb@P9h;NR_Kq5m$y|NEx;uNM3}7v#TM@PDh9>*UaX4e{T40{=C{|99{I z|3_a?Uybiyo>sFK8lN-&>7U~BZ~CIV35hEWpji0o#+sG6O>yMZu#;1x6ZvI9sL*^x z!jgs_FWa$8Q{hJD&lKpZzL!2-D^l&72LW*}U4&C@F}HZUWDI7`HWl1cyfQb1SlNyz z>RMD06*XSY!w$BVu!6VVdU|7Jb~Y6j8!dtd9D383GW3)#N#)#Gg(>AfiCuIfpj_>n zY{SN?6aZBDV!OooX-b(WHG;FZ^ws)y=6Fg=-+}1)tmqhYDTwPH;%&jAd?Wba?4kRo zRy?VixtFmSLpB9vW6Zm5qZ3~`JvW$~F7m_GUvax%o<9VYgC)ptqO>fm&A0WAO!rq* z9qKLi^W})SnUl1N68Me}HWE%#_CqD8A;vLWl$HW{PZivCv#PSRmS8l1Rm&Uv zOo<}9h~HgBK2(H!TTE~NCc;HJ8bbH{rcQk>-1CQEU(K>kl-W}^YfMC5(17K$&|AF+ zc~w_6BvF*k>4+4MRe!Z7Dbt~Xa-t(xCaSAQ&q$RkQv9)B)F`z3>W+1}*uK!j=gY}L zTqfu5Gg7{fvn(gcF*$+S zeJ2O{6tA%Og?XRh`3a>q_g>M&J#OdahX8Aep)+6F7R}8FA2LR+0BCKdx?r+mi$T|! zVtPjXy4@bZd^-&6>wvy5-EESK5uUCjRZGTPKH^M%LLU`_e;osj*0@Fj@?{@Dcab3Rsw zv-ocmX+|mpKcfwP@1VESf^oML_)VnR?s*0?@ytKC9L00*Nx-6dE~kX8IZez{l!V6B zG-@sN_=Kk-47)CjWV>0KbD&IUiZa7UD+drE7CRFOQJfB{@fE`_i_4sI#|I+wIf;fPGu3i zh>P)#2dVx)qF1|I`JI=x!g)fe>-CGSk|* zd46DYR~99^@(Yg{Cy4U4LY83GN&j0s(@h2CZaRpvvYf=5_u*Z}tVGM9*o3-=JY2;_ z0#wNEoefMfe@}`rb*Tmu%I-^TKobAWG-)cFvURYv&r7$F!}+lwi7J znKq#&syAj#n~7le7svs&vpO?RU{A{ILT53o@^a>q}yiPn_4&ymYPw$PO?tS34 zneg5nEcCR@&r5nBP5_hhw~iQ)m4B64$U#)vxxH6V^FTWmeVhS}K!Sc4b5Bjt7jAxg zTZYro$I*(6mpbov|F>NUnx3Qw`-IS2OP^wG&W*-Y3q7^crD}e4j^ciRrWCE6EPqap zhRREv?F&1j+#m8JQ#{-*_m8$^iD;sClY>B=EL;D6H|1G(tDOLyKSVw4hll$2n~9E9I)q=9IDB=ufeo)gZMsN;00 zH=L6QfEXGj#8RFwV{SxFQPCvO(9)ee)7=@&4DEySOPaWuJE)F*fY<5ry(1>-z>9qB z_26RCNB$+}BF`tlW{@qzAqQd4p4{IP-Hkr3tV^1jDi!wTmiqHCTtet=#YDxbJ%CV{ z6K-Bq#N@Lpc#&V>e?9K9e`HVc-3FEYG(Ko&vL9PCzT^+VgRU zhlpa{QN2eu^pK^v+v)a6H+8AYRO55L%k|%BKWfk-gWz?I2hjzU)}2mh=O%c6&Y`D` z4c_e^7O@J4B5M)K8N)>g4eD5&^Q)Mc`@Pb(oXK5@y{k_qIA)fX?r<*Rh2eM(uUj89 z=)W4p{`T>?$c3+Jz_KT>QSp8hqP^7nK0*+o*N&NYQEtp5XdDPR{Pn)-pw`prp~j@7 znm_tc(p!wik3T9s2=vD4XBxVR7$0{nAHDwCU6|1(YEwiNwrPok-9`lLBHud)&26!3$d6GsIzV(cUb{X#4 zhBa5P7pJK2KkTF|^Wlt_AjGKXA|$IX?EV+26R_cZOXWm6G}y<1-d_8;`@>VNKkm~g zB(JUV+474xV|tw|n_8T4tqwU6>1DNjL@S7)th9a5Ua|r1NeO3!vqNQ;C_V`QihAH1 zECiG0>({Hb!Ti>@1@RMW=dV0vHo6goLc3Q-L?!e*$Lh9)5s&A5rg6COM6LXs%wC*w zvw?!lhH^}~oV?N01qa)_u91e3?dNz7?{s#oEgr6frAZ26H^4px7Hr5ad9o65P4=Xf zDHC+2?^k#pKDCw|rErr2hW}m}aZ#QPwZdNxI(sV|R>Q)vd><#~HQD;Mem2vV*29c<@hv~3{fg(%xTr0+;+CAqdDJ~GP?=C|C`FlE zN`32rVQ%0fLMMngLF6GY_ha;Kiu#4dn49hjM*lB^ofIc)dyPJ4a)b4a?j6S6?{ol3)R5QL z$*MTOqQc!$w+XI|$quHZ)JROX<-*|)33(!TWUT_-Nz`{mq-fs?NedeDDFjm_KXD-X z5LK(-A)pwddm#nML&ejc^8BUT&Sxawl zSntb4yVCnf=REe3ewW*)(BFlobV667AY~^Hyib!hb^MuN^ur(vk{y3Lb5FT)H)`ro zmv&o4I2uWZl?!6rOpynYO@t0KIzV<#{cf>5oeTe8G*TUE%t;A)Fm}R12E24@*lP>J zO^N#D$^5Ror7jI%IVZKd-nu>^`ePJoOfWA4EBa|{gV*-YxlQk3-uAl34$pho!h9Q; zff+@dMy)7N`QQ#@*yGNGuA#hX#Q0&M&Kgl?MM>4 zz=Z$!bKP5Uf!MnACN5@=)d!cnvt#tfy;uQlzjtxIuJcpq5L0%WYaa}G?Mb@+Fli6# znS<76y+?y4BTfm2*NNj6u2OPne%pHDPJ(PxRkMy9&#@mF^EyrOm^v+v z?poq!?$n~E-TLw+E>jR9u<`U^0i0donUjx^vieR2K6Y5zbyS=-9gs7*fdk#OoJ}wW zhEI)0#t0vhs5wBkU4?l|YTQ5cn?ttmJk2I7@8^y0-2YJ_krOMShKx$J@I9PEPcuZZ|craY1CCg3J8Zh|}`Sxs9!~ zQlBw$nV5xB&boH`d)%tZ3|Wwr4GQUv{s>7))1@F6W++Ea!nrIb&pI%qvIug>7->N+ z&KJb*YYQ!k#bA7ejdsMu#94dbDKMO_2CmLXU84XL{w`<6sKH2s7Zal9K~&v&^PmjO zPCOP5K&jOoc_bys(al^P9gPUdw&N%Kb+2b0L{4U$9C13nyY@)R*+iH1aZu}AdnmD* zv9m*=fAc)FN`|})JSj#eW6~U%_j`c6ZPv_$rcD!Li)YXvVe#=i0CC?ELUbm_&$U~} zO%%!^ZIiFXlT%<1B|^`RY1j`)i8L}b;g7(IPKi5pVdvu+64tjE(VCKh%Ro6e^rieY zXSbKKR<9b6D(UbTcF}8Q(K>Z2QIa|fkst7sY2!T^$8BJ&3E+SbvDUTr5dHnOtLJJA zQ+)C>%NMK_YwJB9bOGY;@RO9a;szIMbi!n_vD&VzSuNnm3^*-*zRJP~5u&!|S&f&j z1k&g!p@HnvKXrrf@pKX*Yv9>tg>5gIVjo}kAj(?URBo7e1~VyI2$WW zd8i&xJ)c?!HYa0gdZ5$RGa=c$z)LwTui{^}(YZ;zhK#NSaZ7zsT(-(KVQYtZ=xgrl z==Ub+scSinyS1czHlAbBqLMzj2c0egcmi~d-pfIPc^+-OyGGTKP{oz92WuNJjX%%P z28QdT#HB%!D?|ch=iKaEF|Q*3e(9KiULnDfkWX6bu1{xlUA6m@wZQ?4G}J*~b7Iq& ze4EeDaVv&oWU+se)sU;S3-8rS7z#w01Kx9HP*Rc`y}K+-f7p`ygH#D;1&EeV9NMu|HkJc?Z}b#GG;Vl%X&Z2l`T

#_*Fk%WU^5EuLC!KHl(`JVZ@u;2etC(Xg`&&bSrL{_q)FNwpbgC&=4bW~x zbb)J3yUNT}YEnoL^&n>0w&(NpCuKh^o5t*#8ePnbGCdNEI~3A1ayr2Afu?YSh%ZuV zz8BW$m~`3FXZ&FB)p1fj`AL;v#o7v0G4BG1M8JhCZn{qGO>Z_)?zwv3Wah`!d;IUD z;J=`>G`~4v%`K;bQC8}OqDy0k%Qh(NMBd{EOix-x>+k?SpoVEnM^ZD(4P>!aY+Usw;MF$I$ zF{XEXBHf(%lT(*C@I+yT!4xxVTj+=(2JWp47VCtW20Rb`Nqf?T-n*(E#8(&7 z>OzvV&4?$bPJ1$7NfJnaq`#TAS#O3d;rDetzEB?ekdspf%fiZIFM2X}E@M?K4=%#VzdH^sdOWAMeDM9Xquwr zgFyH=4GGZvBKvn*L7@EvQ7TQ1!GjLf6&SV{BnbC9iT{5jM^mes57yNfxN)$=qSzG| z&=Y{7o7=Y07{)p9KRjW5Qpyy7Yw%R=tzViqROfsnx8}RRF?cpy#^R^*E_ZXd1!oz@eso z>6ZOBD5eq9Nyx+6P6Cu$_&UHLw^xe6`&*$Hq{tk&7K0{H8u?+e0Ul2;`93}p0EADl>@srDh{<22m|0_y z!HVv?z1I=D558q_Yi5V>`m9R#4;vLXd)4>ipV(*3oqdXF#p^++%(Ztusx4dEUD$?w zL!WBksdrp@N#Mns(Z;y^^+6e)p);ysZ_esKL5$?9(~-RuGJVDqepUj&4|{Gv)4GP% znhqEF^iXEP^Z>Rz__|qr@A$jauxOkq(R1psCs#fI6mx5{)QW!Zy0S-68kLTE_jsU5 zxYMF4i<0HIB5SGNK!NT@>1YvJx*A{*baz!H%a!()VFgnSh0a#9zba?q-!N)d> zDy+}Ca&#xMb%Xo(8tZRMq*T(Q)8T;sy=+vW1+2jA$6hDr-c%-U>r{pIETnr)izK{a z-c@KdP=}jc5yK8tTbNkbU8mTE;?aw1PF$kG)z{ym_7*Q|=MQB7WH?9WqIV%wP&H7x z4>|LYL=>Es2h?xAwecIY!N*xoNMo4HYw1D~OI1=7#{I@bQloJG|)qmSdJM5d`nB1CF+6`8_c{*26q zwcvMB>1(eKJ;mhf2|r~VMvu~e2EanwOkEVap;7uq%o_BlS1%BOc=I5c+qwa(Pu1$4 zKh&y&Z!;?2@d&tzuhBLF%$ZGe5WCiqSdW(0%-+cJx$lMai72z6CJGj#Cq1#J6X4v( zod)L*d*b7+J8a@El*iH>ZKz6eTDCrqrG3Pg(H2FL;Evfc8bgQ5r_hj(WUPMCJC9Ss zw)gH6Xub4k1!_e~?hi&4|2BM02!m(7=^kGN<0N6Vaa@tak~?AWB{v?@-D@g zGg0)r7_yQt4wr1ZT>lIys@CQZz#Zy-#n5uKL=^9%b>7E^nD!i~{s?86_9!3B_4yc^ zWXCvtS`?rKD5Ncuk?MhU%NO)2dnpK@RQhPueM)Iig4EVh4Lb?pekBs^MHdEC(tDy; z9||=aG%=4_+VP|x$~B zDfAFN_opuBjP1?Z+=lID#PCy zuOUy*NL6&*vf?BmYB}E9U%g5r?S4J@N#5}DM@&~9>mV}bIhu!Py_yd_BP;8xe9`Is z>y?uE{Uw!tus|Mig91XnvIXY>Ejd#g%=4j1z>#0eK{1fYcws6BE6_ylbm=RueMff^ zJ!Jxy2L&6|>{C7;lB}<0Lu-6fIihQar_o4-u!jw}{1yLeiCzAlm$)wC_Upj-DE!D+ zA2NJ}l`L8Dva@6M_v(YsZ}9FKx1>Ak(pq)@xw_K3g^PZIk}EA>DVp3L z^{FLC7o0237cNPsQ{l|@GyQ6ZeG?K2{MwQO*aa|uI;eACYp`H~@h@9&8+BjLy!0DvOLKZIQ>?yQ1-R2_Ly8DCgd$>9nKb`G(HMLY>;Cn{!JKJvWYv zZu*tom^=-@wKG^|zv6xg`k>K)Ch6dTdw1H?^}tA)=;FbGw^zDgX+sraJjAr1Azs2m zsfkpxe%yd5+{w5u{}3`pudN#t%(b@3FOa1T(WLZi`K-y^N&*o}FZyE)#27;wjc$=9 zzsk@~6ac)n^@N@!Hmb#_A}ORb^4+aa1CB-ep;sIFAnbOu@7P44V>%!Qe#fQHxNO(^ zNbgk^sI#0KI#A@?FL_-h7zt>aW1j1}id1$q)1G`)iDV$IfQOYqAVo%0%Af`vY`Kyl za16F=#0orc%+=$uu8G~$^%sR@Q(V;I+K-QnB}0HfC6=hEtLI;ZFoTJ>XUfiEA+=>s>#Svx zS$3a&*~fk5cx0!)NCJU=Lu7{%@7LZ-?|$q$lL_YMpHm0~GStYRU}Rtm8*&HvzC>G@ zTIi*=L%Y(KLeorsKs+VV>nO&h(q__gSAb=sZ^I1;}6J@Fh!!5KGo;ZMdeg^qe2f;h~MHukqCP7mPO z3}+D(2enpTiT1TGoJ(rL_`e~dQ05-IJn~Fz0rGDLogr0B!H(9hb;IKlDIeersM2TB z3S8Wfr%yeoo`m0LpK4N1WWx0x70j2_ASCuQ`Mfq!wK?A&sl@CySZPf(kqsIL@(5Io zg9RhgyVemCEOh`O#g#`+s}*iBBW{XbG0PTn8jI^Z3m8a+CBy<~FZXW3Ym=Re=5yf~ zl0$-mBCv=2E&xKoula>}lZV={nP9!6T=xLFv3@oM372lJGyG+4HZ&G4sr`ACA2pow zIyo2bMmQ7l9)u8kN^fNcmA?PB#ZyG)9T;#Se)urc-C<>R_~m8KbB6t+4KFwCiRs-@ zYHR3gs)UZ^&X)&1V0TzosqL8h93n@LefOJVSO072t^PJu@6sdE%O0ndLAsB>@zuWA zG^yUh8MdT}dL4y?Zc~#Je?Q63<=1Io=>17PaN~#_0n*jKw^6R7%C>tkdTR zKQAR|rG7hLb@OCYEgG+DB3ivRYwcilk zYFmuT3xb{F$XoEKWz8XC*qn_Ig(Z* ziCtY}7Y-)<<=|S|sm8{}U1`@sFyC$HoYTy}>2T|al3R-iOAMam$LQ#H&}g+U@=d?~ZoQ*OQ(@x*rohmmj3VFcTlHA8}LV z%O$?7tgNI>9voR?%Y4{${_zf+{cs&n>P9Z`gF7k4`MWuBb;$D{!Fyol{;a{{$WXIl z1zLCaen*20s+r0C+?{-)ga~wY@n5`E{mO>21Rc20c&^lb(*%T2L`C46efXw2!rA@( zQIfneW84R-0Hd_Wz6te9U^3nWjVxX+LV)+@9OkkF(#X-4_Tm?LN~!LKK7#hF@1awz z5-N}?JeNexZ4U-nQ~Zcm%~yU016ueii6Z=!P!cq5@p@&uJ;)sDfAV5=TwX!ZGB|-T z3hj5-)8ES+-C(kIO~a1>0)f|^*zLj^!Y^TW98Y9-pGiW8knjnn??0m>cfURfU^5VY zty)(;R2O-v@op9fq7~RWF9w-9pJa*hrDCG2i=L+~P^jR%gAd^%Te$0Cqi+(90VUbuz)QJZ71Ayy3N*!#?XKF}kAaEedqh;NF8v zH%By5ZSz>;YBarAKal0lc_0fkG>H^B$-HEIpm!m^z?@%seDSlQ!rAE^Hgdf4BjOIv zin?b|zPvt-if-$%%Hwm3tYm;Qn>C1%sACIj>*xuo=$?FQZ+)-Kp$t%GT>a9nrQln^ zh|Z^YP(JfPd2;SA$P-#A3Q!cz>?<`-fHc`F_*?E1+>@nZH9j1KUM3S0TM3rl5;gu7 zHqt}=PEv}GSi!xBdFISk2Y6+v!@a@rd8B_nWeWDm6+7SH0{7-X>6QmV>OU2lwuWUJ z34Gup{k0Uw&k61|9nz8fF=^ewqypwxI6$v#P(zM|1Xr1l@-XSq51nwfOw{~kxtIafbGadVAUAmmoDwSJS%Bl{NwvZ0vy?j(3C1Yz}&~xEOD$dXL9R( zgUPmLI($Y>DK!mMM+fbFD*~bjo8FsZDE8aF4G zmcs69eU+|3cqGyuCT$x&+0!#+JSil;FqIi~;soU6~kL{fH+Gr zDYxuLxkE;Z;PDIi1Q6p|Na4tiy!tu^-xRd$)7ava5YemK8;Wdxg+GmM-7@5J25FVW zIbbuskkao6R@xXD*UoW#>&9W(Yf5hrT%XN+`>j2##s@Vt9Ks!dDKvqh;EQo4bfk7#gyQGCTxf6NoknfFx}WuK^6jo)XE_g5?D zImlOV!3exY#RDu-TwAOjb18tx{pj8RcTJ&wE6qgo8o?l{pGVI{g6vzz=zG5w=AdVX zFfOF5|7TQK$>!Pl!(0YIHEr>s>FDMt*35BOMTOXM;f@Iqui_TBnwqq(HI293EDmpu zI*xFFJO7Z+0s^VmXB^fVV2MO3;LKd-&X~!58Yyw-B^~}@ITSqjgPH|njVxB4k0Yv~#CmHiRh5E|xkp)?}z8?j)FCNle_s_CXN^>oB+49rfx4lJ+h=S2=0~)vE zGwuNpO-9lJ;JAV@YwLN=cOeH118)10+%&#l4GiRUYF$PDAE2WP-AgYIxjjbk6wF%# z@fK6*d*d!|it%IogsT(d3Gy1xzoX-iz8XMTyk{t+b2v$lzRj8TDtL@)%d|I!oNr=90Y1u6%Vx!2pguNS!=!6Ii#d{HZxbzY7Yyr<}bqXI7(u!mx@h? zvlMmhOq`x`)G6GR+n(^?b-ZZgPUn&sYf<(eL3;=N1i(!06tWfZsgzlvNv`;34 z+Z&z-E{QYD_@wrJritIF=gGUF($E~$cVUmc057BDiS60uX2W4idnA(Dq*rdUo15o% z=c+Y+yi9rPFQByN2B(4Xcb@(H;WW{Z?HaSZ;dJ&B4!kkCUx#J-RqHA=`dg>j8IkOh zc`AVO*Ii~MUZSgPi7IW%>0t&k3p+=h+F?#*-7%EnbKFhpFZ{+h>fvPd{;ka>?d41! zCjEWNeoRY;ah?edCmg{|`^t|@U&{ORda|w7iTKOazboHy(!sT3;+QaTmHvuvwR7!J z>dxjrq^2mjgFV$CG6AG(iMUaj-aOG@;lp{}aQdeyAY#R3^OIJU?$pkgLfbTD^i0RT zhzERzIgjrvnEA9+4OV03I%rBILMu#vG*4+NuMAt2F4mS_jz&Bv+W(p5*ayVl^aP8> z{Sm*uZ3T3jhe<56m?~EuXnS5Z;HI$WKF+^Wr3-*15tzB~-3lC-!Q6*!{h9s)qHHmu`lRw8NqfFBM$E<(D2p@Xo37DfjTtvqoZ+TrMHOfUEPr+|t%!>kRZfEbhzEf0BS2M+-_dvpRp>(%*> z7K(xo5xcYCaK*)FT)S6mi6!cFh!KiyS8|R!bQPXJ?J97Z-QU0Hx?q3SiB)yEPkb2m zCErI-lV0(1jkqdSFok{G!TwXJdjA)y@OCdw<(Mu8nGFumbQ!m3QL&1N578H6|FPy^ zx#hSM34G{F+wPdx1+ek$sF|2xK6FU`tb4+Yg?xdR<&RMyQH7__eYPPdei-&lPp@BN zlkw1{q|okkKjUFY>;0()=R+SoACKcXwjZ3`Om~7QR@^>u?SB*hiUgr6%SbCG$r9W_fLDc2=+a(TXy?&JI?lsiXfsTYpF4F7nX<96`V|ssnBm(Q-e}`x zgm`Vs$u_4UF3kjS9RV-on}OwOAkP6ZwJsXX$x=a`zeejH0(JSyBXex&y?>?J-Tri>e8KTNM+2f?kJ2LUzX*}AH_^g6(asLp$9Gn#R2z%C=WiOfF6X35{t zSflTyb34|i3=1tcRgJ>bMFSMytlrRnM`<~s(My5*qVp0;2|XkBoOiKhF{R9^Yh%~e zh$){?pF1~iN{EUWN2dFY85G?ru;0nv-`3xpPM$DD;*gor9DCCv+2P!o^+Xak2q-yQ zAKkBZuy6Oe0BQkpw}*cfbjV~`Zu}|RRk&Q`HYj6+^b4M|=>1P51CM}G7xtKWkk3)Q zLSJYiFBF4;hOoeG3U)E&lZxb)&Y7*CfKrcSkduV=U8Jr;@zVwlCE)hoSEb{?4QR#} z4Cc|uN~R1!&-wZ3X?0W6;3R#|iq8Vy^yeimPxihf!%Z3%t*dkoQ!HN^c?I?qq5%aF zYfJG!jgy`nc8zhnMxvnZ3X@>Jz2&@$^}I?b+nwd@LLzWw-f5(-$g2~>I|Ej{1UOB1TZH9jUE};bg-U_N0@chXKYZNtoiwcxf^g%o%poQ`^wv@k#L-iJ2V%<9x zH1rN_veP=mn91;Uj=W(n_r zR_T^_xS0lT52o)uTL*#R+&)oUyY$0Tjpr+2K6?{lM7jCQ3+q2Wi+BC zjVfWovlr@c_Z=IK(w9rX+|fF9fa@#2wjS_jmlnSw1t$lkHVm9MU)`C zo(FRUV$1g8dgjlL#tP%rYP?eq=Dv+SBrs0Rul=RMmbF@;7V5J8YM7#PQhJu7(J>x5 zlV8=rn{~rZXJ$hM?c$k!hhOE|Eh0;lgXoxo8Sl0Z{A^edC_$$HFhqIlE*{R;wTRU_ z>oaco$HLd1cVCbSyEvAgO+pugt39I|ouF}j(50<+PWOptUfJ(tGJ0su9m<@*M0r0w zAZ=U?yEO8t>?#O38i7w9K)erKWH|TG8rS%EfeSXeucCpV^H55(u`Lw`{W2UEv)-=n z_}E9Y9^o3(Yb-kQdGFq4_r)XTH~iXH^#&+@>=^1m{R-U=R_yEVZWD^XZIm@~mt;KI zQgLKw7YOTpj!ItOeMfR0u`^yar*SMW?2$FJ3sjnS@6(^duzpFyX6AUfU%UAiT zNO)JBumoQ#rGyV#EF5avhHEOQNyG?RbUA^}gMi|f+eWCIq&!uZjWC7z+Hjz7!KLs_ z5>^m5)ey0BCIu$}hEG*mJ)Rk2s?p8zd5a314Y)D{yPPz;Dx9sh5s{VI+B97mdP*t4 z845tY~S5XrO)g-27@}Db!R(PYSW_a-f z2HfvdHE^p*?!oB+;ERPkaE<(!5~A+3&v?B!=~5taHPr2dYT-B;cCLX}th^ba3D+7U zK-8eIED*W^Cy34Q(}y)ZBjxsUIxb#W6=>kWLN zMxM6RXDBLXAnpt2xgWOL^lcudSl`p7@PJPwQAlFRki?4DGCTV%X(*?dSGgPwUGmi$ z$Q4U2u_#UhQOA;|lU7%9(;?A5NJOCLEf#!cvPAbg518e}*4!oy zoM_hXzd`i<*2asB*o+#tSWDtouu*NTsKvWr2r!D`2b|^^Ai;O9s&ApoZ|M(rt&6?BbM$VsoF*O|=gJ zx8E4X-ixah#&~ffka%)Vm)>oDJCBj<@vI497$!YyxYQnB>Eo}0P3x@$wt`&dRTMa< z-p4B))?`h(^hcZkSN?eXBpY|z(*gm9Zoa*!-ak*G>Dg7T7dY^a%Pd({5WP}yI za9hY<>Q?)B%?mt<3BCueZ4^A<0^Aj7YfF>oyWHx*EdP6U)JWTcf@TaPbv5o71zhE{ zax;Z!AG?_$QrbxPSS@jsVn;@L`vwQg>S2ScV5xj)&GP#Gle>2$M7}L8b0CZ}ez)tH z!Vz#6`TI2ZS!1@1n#KI9OG%2=zD86OITq<9a9t@fr+DC2VFQSs&$pwD@nd3qQPeXF zUK=U-S4J7Xjhf?=N|F7=Xz{Z)qETx!XHdb5g@BArsroiP>a*|6ai@5dm-ue#)_K&p zwAUu|;7<~iOfcn1w`<5xl^7|A1m>>cy3eRhEi3w5cp?Uf1;=amBiGfWej@gqURArI zwo9M1ksip+Ym0lkW8`&)@G*x|nvJ^!KI;^&jvtY3Ptoju;j!lAaaM>-84)H3PVqR6 z=yyB$jfS-kW=qUW&)zN!x9g4!>*%P0=}ND#8(OcI;akpHclOyRKpWY5w+`sD55iy2 zkwohVKS$_RL00V#4(9*j$=v4US8eN|UY;~Nzd+F)AO`nvZ}F55OIfX;Vr=MSTx->H zr|wx*U9vTi<;;P&VZ^j26^=L66ECVKAc7F&C_1}CmDzl}F~5q_{qJ_g!>}ZOBF-Q3 zZ~gw_lejP2G*#t3xB`UQ3pFdGEjX#(jg0O%S3)<>+-e2_@t_JzbjNZREIy53i|Tbi zKIfHV^Bu4+J(&LYfMKd!hR3U1oCh^s@(Tp)7dp(;w6wpB5l2SgC6g@uf9+iPKh*8l z&x8;WN?9km>$a4bk|i@L6hcHQOG+ALnFftz7>2qjQY4ZYt>bW*es0teUoV%`xZ)X4x0&#pvC~|GFP|3`!0$gsZYBC?gnET~nF1 zH)&;?D5JDHlv;;s^YU^J*kam+HoVqbX+r{MTfMVpSt=M?yKhaITILnqdGu{dCMX20 zlH;VvmaZ5jJ8ZerMzdIK4gpRExW(@b8albKV;<>ks@1g8Kw6(dhe)}F{(nf;ylE{DvGz* zUaE7!K|r_hNSfvHQX8u}#Vn{HDygLSSmX5H;{I{h%$WE=qJ23oUK=`R_cH(P_@U#N zw9f^CUP>tx9bx~?hs)?f_ztaR=+E(cVVk>xDnV#o%}2I>{8ep&=f#ADCWY_ia&2tQ?a$0G4t$uNy?koV}cCN zV>dd?nI^ROKi7;J3hdf6ous?wS?IAdCb8@LN3^QT?1Z4}_{U78|9t>-UM)(Z;tyk} zH96^;>DHgGS8hX_%ZtrcUax+_7J`RRH1;V;H|#NvLhjO~wU^~YH79mBc+K->oETHQ z6j@T!;s+g~+H-d|{d&aYyjopMxYw<4CE82h7N$Iu>Wz&E4P8R zYpr*FKtUvZE^F{LbT)@jFIsBw;AC9kBme%LbMCJi3jR!Dm4}ZFvih%94rzto~`-j!8t#FY+Jvu~XU zukE^KEWhxt>A^-q9uQoj<$a=`C^Z-|9cw zs8Zvi9ru|Im_|`_U_m(!NG=JV5f_S6pEL4HSuvjXYW7I%8}y&D#xtcBzU*6}%DV1%;}4rsINjD^ za#SUb4mA_FaoXu60qZvMg462Umh0(g23294GzwbKAF2!a{y^XJM08WHP+{N5lRQ-x zzT2(^SK?$s-cYCXkdB|SYJYkLQ&D(xq+lin_Lg~gw->~ zCv0-B9D}C15rp?CZ!?U!&vmJ^T&PxV&z{fW?#VIBMl z@*f}NKBh{>%b+a8yG$3gtj%W+`dfcXR1_&kZLFoP4WMi{{M({L*mF0TC}!o_)zK1q z{f6^87Q4|?Up?>_J!pMR1Bn^ck9@d=3&sr{D$<*w-n=u#Cj!?C{(0v=d~tDbEJBFA zVCKqilKh7O|Io(pa8v5<3Hlh(*I4KU+kz5H>!anoP=1Hq|KG{U6@z^%O%;60Vt`x^{sjK{AqtNVp>dyj zED9WG3%_pu20d-?nF31Gh&PBAt#{QMh=ZP`7p$j2%uqTQ)_0SUs|L<8UxmBzlJ1RSd8xCRSy zPRceK?((oqL-|) zAs2wtp5mQQJo&Hti%piqqV1E+4HQSE@%mKXvHhUkL9pId7`cUDl^}h$=XTMAcp)*v z`2SFcJ#1ygy6?7VKrJOS)JR}L7_5-uvx9?!3|HMsUdq%$2i!Yi)G-}sL6fC=fi3Rv z03js4B2-2~Ef8o%#b%Dn={>&9=*w}o$Az{Y08Q2r%_Bdr()coIy+a1-*wi!sf<|6@ zb_5Gz&~tExeLu-O)I6=vW8jJ;$e)PhUE{b)1JXbUt#jKI@qzc#Sy2yw_>i{?#tNnR zHjlMsC)=utg#wq#@i@;8;Y#;KK_au*7ip^RbD=H^Xfd4vLM65y)h! zqPBZ$ubA_^@0s13)@QcH`H_tTO0MT9#x38j(m3?v?ECFV`nHNBQ;KPZJLL&PmPSNHPr~5p1_3rf#m9h5zp!>w38&q z*^8W6H*M|t$XuYYiG2Fw2)@NV@`l;WURBtmN~+--Rj@tp=C`s|JKjtZ3oztQL`rCt znSm6mXGo;e*L$a-W$u)|BF+jrTJkUgERnA}v2(hMDc3Tkj_Ql_A@1PqqY9Uv1##Iz zRDo^Vw*6|C7K#&Tu_Mer5m}HGmkQ+5v`bSfA*jW@_uUarq$#Ds0qvnB0kkK0-c6$B zaIs2+fvE&9Wwz>0r!D(A1IR?=Qd$+FN7rc&jfWB)-_54Bq_jo=A)E@MVHP9LBtb@1 zmzK5_vrJNEfM1okiun@C(Sr1CAJA^g(t>5KQaOfTp`MF!zT;@68xTdWnl#RRnG`es z3@Ft+pOy7&^;D8tjNCR#_Ql3j+I&{RVIY@t{Q8&nj(s(=C#@~h2N6jRxQj9xy29-D zfeva&9dpg!ZR54p(?iYY~jd$%@3bwhu8WaY9HP@|s6U!3~R&oxsoo zw@4m9*&$!qX>cOd$P~F^8e&+?>91tkY(U>&Ya0~SFtBZ zPJ^Ks3MAon^G|xZC;s?=KH=x7o}S@}rkavKyoj|t>)OM(nf!cJL8NVVdri%o_`vsq zeHVaQhs=oMi=8#$j=sheGX-XwJfubQ2P`hHFdf3dF(s-vl45-R85X`F{COoK` zoo$Ff9X&7aUuSEmu%hztW=4gtx-59{Y`G`CB3{-S^7dY`Qe1Jnp(_sZ$-M2bw{PY; zuA09v);*o|b`Wv9sOMQArF_QLB?>e-Q6BkAk-lngKeg2*3L;;52u`!TX9|fl5DlM* zuhJa?(oJJNa%iND_ff#~qVTJho{}I9OVgb|FC$12HNR_UShh!BFL>I$GB;vUu#0iq z$7d1K|4!;rZLWo#DUiO{3~ouQH7^a+_st$eNa`;vtuUdFoNVF}7_U48-rb(q`#N_| zfZ=>tn8oBuY~b`LGv_iWN~iN=WR#Y2QdA;3w%)36_>nGs`f+oH^|#eT{i}C7AA(5X zpJS^eEoplu7}K1rL$&nTo)Wp7T+EA?7GQ(P78%FsjE$YiZfAIW5`1` zSih~z7K#BHn>Q4~`|IoB6)oz!+9d*NT@I_FD)5Wgqmrr!buc#PrN9inXzR+bSHfJS zHm-!yo`0*Z9&b+cSzHu$P5c`ir?M_M>Z;C@UVJHoyjL!FHPQ)#3g8?)CVmwNf}J?x zt5?;U{Wu|rIHW-&pj{!{2}6HIDJGa@hXK=}Vk>x5iLQ>$1WxC2E#(j=va9xO$!r5H zGc!>I(Gd&OQYyDLd5y)oc6qnyF1sSLmcatq%xp?#PQnUETDK@YH@5T+{Oob&leuja zwIEv*Yh8346dDGdVc5K>rKK;+1LImc-H-T?jBEB+2j1a*eq>V$B8CM3m`F4gOV1dZ zblY>V*W*)_#-E%l17twy^33D(OlAC zGVe`;e0o&iYc%>@ig8NGt5`g_K#y zVI|bC6hK~_dq|}G1hi@Te#c9JcEzzRl;$6MchwJ%XZBHY*CH~JAeL7gp9&X~?O#pt zA?4}<{g!^LttiiqDTE7yCMPK|?!8dn7fPrsATsCdj&>c|j%3{G?d>J3aunBP9@R!1 z7}W-F<$@{me@*Mgm6Q39HvV_SJo+033;vRNT-VV-QB}prbMJAqOe$ibq$7yQY- z@ngH)XGt&nhXz^L1GBuJKOu8->C9%j0XT5^8p0QRihI7b%iJn=F+`o7S@QOX4s@^& zjEWEL@9#%dOe28e?>isoT+$Q8a$8)?oa!6UDvw3k&MawGck=q<@e(>GQ9M*-hq=l?Wl4Vdr$da;W$-Yn4$uOjivQ>)g%9cIZx1nTTCi^nX z$iB-mGxm8e-Hr4-zvq7cec$8gzK;%Le81OnUg!Eb&+~ewp{7W4kmVp585xbz^=q1B zWCuQwk&)9-k%MoHcw>%&9~4$sRIiYc<%Cdg+@%D+bDCY(R3#&WpCu!E@R*Ej4Se-r zf{e`VJQ>-P2^pEhTQV}nd(kB~rNAE^Tj(lTs;ZLlg3nZBlzWbn?FFCqfd9$%u#jz! z20oD~?_u5fthtBh*E3)Vel}zjzn;+tKezwu0spq&`RC`}_&vWzj3*~Oec(g$#AToxHsLZ;yPKun8DV&*rwStFGz|2{WjJ;9Ya5sfFNuhkM&^ zkxAW`03RJJT<>z;cd&PKk+=`x`uT(e_`LnK5EtjqM_lb7T)L_noN`cS3(iY|=LOGm zNgw3oJRLfzTM0?fQUzO=B^&nN%*?$f>XYBZHqB6eSFwA86z6>3>SZP zL%6@>Y~^dJ11@p2L#MmWl<}SR*A+QESel@-nlP~xL{PqG^h}vcjV|r_)5;0fqg$8v zo^D}(D9eArjEj9*n&oOF)6tUylZS_?=J!kXvaQX}wIkH#5iNeo)d;+v=PY(D1Ch}t zWH(Ddu8u{`FVOBGr=U8*Df@uz|NQWr|K9DF0XmZZ=O1_8!yG;01wXv=>;LtEr|iLC z?vGp6WdAlB6_qq(@4r59+(`C^-paX)WdFKqin!Q+U+_U8&K3TR<1GL8rb+96sE+#g z2c9q<@nY~kNb^64_iLQbDOUgbfbSP-Drst&C;R@_A^aTY!Cp3hd_Q@)Y?BEtWDUVv@( z{p$mN+2((W!(X=f-{SC>ZT_;&e`~qF+U9?9#h$-L>VMkkzijiDZT_IZU*G0``)q$* z;{O>k{AHWJZ1dkP>;LZ}slm5jLMnfUb-bM4Iy>9D3Z{z-7>&JGVeIVd?y#BpnVDc2 zC+TtnGc&nc?JG9Tw<051BBN5G?|s>?<^V~}cY{Z(qc3x69QWl%dEKnJ&@ZtW=Es)| z`6F&0xiwf0MXp*(^WCB+{XzB(CD)3FS9`8O;nrA0rURXO7=qB{FJpBh4zgZl)#T{l z7<DkCW*Y;|M3BJx0 zU0I$p!CI_b`+3VRFSdxFq}=9e)%hf6_-6$m!PW6}4;?WZ_SqD-&0ifA|L{<@0_8x^brIPaZ_^afIxYdILA6iOr23bq{D}Nl=g;TuX04w(%yLyJS za^(ZpdOW5Z)-O4DUn;-S$%R@DuK%3ejGikcjwdaQS5vn99%qH~^0shm9FHNi+Ly?mR+ zM_*9;`SAS_^77c#m#`0hf#lwe%T5m4wzwM4F6qcH z=hi1y#_;-DzVaLgcCbl8kQCWf4GZO+>cT7r%&f-rl#Mz);87 z#h|3z!J7e&XIFv|-r)#P?;oF2US)F$twOQLG`E^%q(IEc^Zv5gR^BhdkZHjWu+DDL z15f@SwK71ul30RCl+NcoB6NA9uX|ou;b>`J4W@w3Ys|}Jp$k>owZSj2n9M3l4tq&7DC9{;F44Ls&*m0ah<%Kc>E*9i(&z##hOwa&Gcz{mVmrbd zLJ~Fyx14TSM!#fR5eT`4%HJ(@oacccjNhxyf`XVe5ffvEA7#t~r`03f@mB10Bq|*Lv(o0^&kCe&hIpLK4c0ZkhuZ zd1~=a*+f}&K#PDGV*Ew*SN9@X`}@vSPX$H=$Z!THG!Gui34+dREp2A7tdSJP% z0XnNukf^Pxm>8SWC4!j~$6LJ7x&pYx0OiAXCri>`Ko)@h}&evdXYS6)oW~X&a z#vN|msv3Z8c8`tKBcjA_6_SHp4R9syl`+EyvO4Ns$n;Q#C%5hY&M|)!ljni{=r~d+ z#V;5{;>4dv5q}6M8D?w2lO(RV9-p2*E+eq2Eszv@z|jWZ+b?yd>$#}trI9+rb7tHc zYfT}B^unbiP^_W0b9(DNQydO}6YITU6{9oS`Xtriz8DUV@QCgBo4V zF16~m!bJZ7K`F_tK&LD8MVki0B@-Em!+_YUe%Hx{ByV*uWlO$~ccj)D0+s7> zyZ1xbrysA1g)1j67@a9vEWjo76}B8a`$Bq>rlbqhW#TmKz^Kux)t4w_Z0sLGb&P zn&6Evk*hWee{8D?2=HN@8=fSF6r?b%Lal_pcE*hhs$rf)1T-j=`Y#OER+EJqabTBc zfKE_cKfK2GRCZFtIPf{2hD8RhDON~(aO;s9^D}$ATby^}qnm5Ne{wyTB#dud;@2dV zZx1F$^|!=$_GLr}nCYujLr?6zAy&58Ct%0xEhAQ!B)ir${;9U{IolK_7jsL;lUUcIXJrnQ-sj`|bmhjp(5i6}2*Lz=dzs_O7I z%Pqk29d)-5ku0nj!TggdjNXlUuk%P!`qNABodE~GOV*rEz7RwNjK+eg+y!mZlfV0V!*d?cPT;{vmkF24#1QQBkX*4`!`N67H>%4oQUouUR?z(HgDBx5D# z^MGb(lsnUFw=J?4kTdO2@zS)gKMhi1rLbJVw;kGX%8-XZ`@)a|E|~uIn#7O$Io7Hl z(MrF;tTl-k2pKOvTQ-xYHEkW2qm^;{G5Ailr^Ic1rdNa)E*Ud^g%dHhq|x-Y3#Hm(!x-@D#Fguv zb5RQl>h>N}YeL>ai2e=+#(0)XBWZW!bv~wBtc~0)s+-1A{NdeN+Ui2o~pN0xm;#P-flcMSP4fGRL z9aR*W!1)@vdQV392J>wi1b4V}zm(bAdH(FUi-B(1%Mv#?w`6YiU0$IqCkh<96e_rn zq)z9L`X}8Cfx<6|=L12XU(5Q!VfxZyB<3Yt0k8MnfTk6}zji}kDmOnf*UrD%TyA-E zx}W;B^ir{q`!aRl%)QSK#S^g5e0U%`JSvjqX;~C@@x?Wk-BUi0RRC5(fv1@?rJ2x= zrmh$zx?IaxDYvP_H2UM7Xrt00&jncL+~v9;a7aL4l^)U1N(o$8Px4rWm|#ABp-JLckG{U-A;eJTNPSwJ>CK- zm#9)B6a>-7brze2m3}pl54DWCM05$m_Z`;(91tM+_~mV*iP6~lN)f@eWrGv-E<^ge zPw>cfF!Z@6B(ou#MQMsc^&GiyPS|i!xA^%|ZlMEKxFyO{-?{B@1j5O7@&-^|D#Hzz z!vYwa&MiBm%-`lrxP0Dt2b;zjw5;5={!>Rj0=d!sgav_L#)nggd@?q!vv(Zt2iq`I z!xzj-?nx>gaFkeVYezOmpDO`mVMK6_c517e>nq|WWxQ#bA=3O0t z7GFJp@I%~4|`M$OH)EK$;BNiHXp^-Je+)D4}7JG;FIpBgJVy9(@d z&%_9ODgf`|Mfi#Uty1D1k~RIgf!Ga3lEl#w+kQmdo|eeYS9u<55mjj7VUK6~o*cUlES{G1;=@z?Y#!VR zomFX_tWd*ZWNxmcMBt`h#a57L)vS1(Hk&Yw^Y*bb2os2Et^z0U>)DYcG;a!8K6dljRGrABbm##`t@8Q5T@uVJY>H-D}r z!n`F~1Qp?e|N2a|>WOk6w0E&1yMPE^#~X7a6l8KOW7g`aw=Tm46RIX=zbl>y(xhdZ z2y6_+ye|`XY2i%}3*Rl+oZ~>3M6178BndY3Kw={##rvM7sJwH2LvDAay7cPf(<_JA zK3fSz2XYLbiHy1g^#p)MPLH6V4Zub~3}m4)E+(el(jzw-2^`Q-$Cv0>7qF{}R4b{I zIit9HlfZ@zWhTYM$d$3>@l=dnKQdqDqIxY>-F{Wx`1$441j+g5OWgv*FN>qy%MR!3 zBCbnh6Nhxx+}D4cB7R&fK%dIoXaS+~@?`tI$#=`?Xvb0nF2;`QwE%><7K+2iqEpmm zoT?Y?q|1NYDo5g8?&gyxz>j8tuN_za$INsD$ST%unST%v;7AJ>rWI|`eO|IUd#N;J z@Q~Y+c`V(;)YLQG>Dc@bq|4N-7vxpS{!3B|5%S|ghL>c%q_siQy`rU;w2;sfj$&pt z%fdot@f}O+g~sTPi^#x5OJZx_NkUyS5i0&nYF+i%dSpKwO*jD`;n=dAHMY95R=kCb z>)*()n2EZT3{m7Db+@p@a%Fu37yjlH$OB0ozy*kIK!aJ`= zm=LMxU)BC9>fUPwR8E!a?eyqNU3`v;5Zs|yoA%^G&t4O;&NH&CPL`9hrUN7578^o>kkJp%1UNZ4Jd3@vaSJ4baML5MMc;rqhw9~wSy znt4SNm6%}t5#Cut&nL*PP?6Ag5#S#A-KMS#(64M&SAjn925~N&{)l9MSb3Mozta1y*CtW&HLhnR)X5$Av zlkd`^4<8*hUSBh=i7j;Uet~IAXWK1*@jT>;^hp|Sq)=f>&xC-^HaXP^; zNYGf|d0b$hT1>0DY)|^w)r~N-RExxC#9ZnH=iQ%Bs4oh+K$! zbl|Hq>NC=t00aIDet7 z<{YUQr8>J``IyT7qUGm}?IFN(JCkO$)ZsnfOe#-YG+3pe+U6Gvbbm zft{h@QZURJqJGpb$nXwx-<%fX?a4Z#Z%nt#_24WdFDPlA?cgEB;y;3AOODund0Zt_ z;5A+fhbW5-_8fBqQ9G{T{EU-~NVI%3(1)dt)nD#n%`OT8bJjYMVS>2P_>s@PLawhQ zKht4cB~wH#8d92%-S;S(?KF;UByGl6)Qai0z$}z4$29S6{ZQj9I*#=yuXBSgx5WM2 ziKSc}{%4{wO1XX~w)~7r2c$RZuqlw&g5s*`cnSX_V!g#;Eri)j9WxG+Prj`rG^l`! zb~0J%<&MU9bl{L3{jdr9>f8W%j?i4G`pRo#-xc~q;=W2NhPXmgkFKv0Lp5&-GdZj? zt-SX~uQy;YX80tYrAptuatxl;^x-tUXIX%g)jqiNG7iFGr>>G+0^1};c?J5r)-j3N zh}(6g=JbmM$9XMIwq>#Xa@T&&A0H1WDKPQ%T-0u5JWplM)0>0`>b3e{3k=D+!lQ{o z0gw&b;j-|4319)f*-Ope$R`W(_8)dV+$(_?7#kEYqqiwxcjV_5m=zB$gkoRbiOngr zma^+oe5GR4pKtfWU_EU298Zpb==(@};`JR9{h1<6AV#LA8_7qfTZ+$_DXlALd28gH zT6Kro6}fIvs1zoC|9(Vbtb?Q{x6#5QeGma>DMm#NaXAK(E$X0s*y+!(qLY)#i>5P2 zW0{Fcd+Sc3YmLoIEx zX?BaUGml9+noaHbE7Ic2DOsXwxKrBlvS9&h>s~(wr|aWcS!stm;;x9FiWQq*o(UHy zb#4l4OGbI&ZKumdw=spXi47yFoJbM(PeFmX-K)XDn@-3`JIH$1xAepnU4N|ewOGcM z3fD<39|jd(MLyh&%J)ONS3&if+oamc!!e8N7fN+oD?Zi6>yuGh>H57solf-lqNS&? zhiGbgV7^l^o5q{x5&!~|4`?w6zuRh6HkwoH;rymy$|Y(!Vtz?=Hm;|5DQPiws!rOh zbp*Ja%0aJYa$N_kUcqZh+S)9g?zFNymX^T%vdiaAa5S>IVZC`_XEjkWh8uD1TimMA z_a~Ex3ke)C;!Zf$w;j@HynZo_7~6|x>e(n?iHH&iC2Vy!>j{Llcgnj z^Gr)b1LRzQu~H*)C|4!RzzO(WlMyf3%(0dQuAb;N8fW!LX#oL%`KdgQu)VxX7sh8% z*t;3%v^#wKN1@2xq*S~7{l}NmxkrA+_Ie#J1C0hi9*BD~qu8Ti0;kUiseWHM?=#=0 zOA!BNZI(GsfL0XbI^X9hFi>1%@(9@{7^L#$_?ioMYwZL{`F47;54`4*}z+!4=ipDj}AK0D`T86Sc! zoWEWXDLQ>>(#b}+Vy)9<(qsRQ>H{A>@Bw{js9N~>mu;1&zYb$VY(1&6F}|sI0q$0D z?=e@{zF?zlT-z=4xbAG-mG+8Fki+7zbAX&(D|rXypo??c26f&9`JE>?|kG+M?7rGtHKdBfxlwRGf;AAGi}*q zb1g2JLk6EC(mp&g_>j6q1}!RTTISR zTEzy)loiwj!@W(6Bu2a7C%Ei^9LC2WSiy4m^MflCLg)Up=azlM#{iyLxc(w%qv4@lY+f1i&kCxpYeIo!MWD9~uKed>B|52zu`qeyOeN}S>47K3Ouz9A{*XMENru7brVa^JNJlcbd*OtGizri2d z^BrfrF;x|^fw=Ze^;p!W+Bj;VO>7sI&~5--H!4$d1+krSd&9(1i)42O(nYnTdrqlc z{f?6Zan;&9G(>7q_tSTW{!J{=PyOqKZ$|8ADHZQ|0;8O)a(%toiBWK)BV_JQXr4Ml zVsfw5ZPAgMvZM=tN*g=a{I0|uVBp?d1F(7XT0UV9{!ISsvfp(*puloKt_tMepiGmGpuxkvXQ+@wq~sbXsY% z&Q$^I>Ts1XX0ZCTH!C$qyQ*Wh!Pb*F$f7PkA#fkmD%$wjWa*lDx-RB9O`5l3g;Ky(UPwf&N+u zGEteQC(rjO-ige6CT+H9&604Id$`L5)9$%v`i&01(jycTG~-N=Osf`~pAuI7y$#JD z9TDfXfzOij3T8}-G=ETl)cz)9-ea2hDa6odm?rYr*+5ndPP79)kI`w*bX4SHPCFUf z$E*yHzLpnHMP9Y`&0Z=V2aYBTVDl0Zc+^`bQ}ej0bLNQ-xic=T1fB+{cpI`!7epRC zdkeyE^4H#hSC7(p;U#41YIPyZ5F!1IUMkK6z3h)7<>$XNT3@<8#Vin-Wx8Npa#>VO zA!cvV$MI+v{Gwg9cuIwX=dgmOiYn2?&-;S&2L!e48%q!e!LB0CgbsW^%xct=U%sPxR+DgtEN%9 zJ@(YwrnfLW+GQEv=iAcuCep%HS6}ncQbMj8yOPpo*W~Hl>R&++;DN>OPLP(Ub<}^< z*k|LJ&{;79L@f?ijCPoDSs!n#2=8OJ?ax; zL3gSM#!uMT5G}Ou_?&`Wqeq7{tAlbCbsqWjyz|I) zpV(z7=$wrv>fp)o6f{WP$Y^P0zN$lXXL;t?7P8`4tIrndX{uL@-VE>l!-h?1gEU9s zE$HAsL(86|3=@or>)0HWQ)idD|KWuhlU{56*}~S^?btelTZd6@8*jcjXRTB0K44ZV zi4Sql?wk+`^R(<8xum>ckX7SVw0}31rsTi`9831Ee@&1{6)o&plQ(uwI1*IUxD`#f zw2wJ>5|9j4w2wWO)Uw~`i3WcL$tUlR3i<1X{A*>SVWrP6Bi|GbL?zi*jPfi05oG)q zLhnJRn>W#)_*&x5vIllEDm15imAA4?Dxfp%6o@9=s=NlqBQhnsvjlpc7UpQ+7F5xn zF0cJ}oyY}%vhGR+)&*38Dia%9dw<(0hgPeMd#d4{eXJrvA!b@rKq02}3xtU2;Sz!< z`+neORUO4FodQQ=LFOv{KQdQ=K)nQNM3Q3fMXqlh9Uoow2FfR0eyS6M09|8au5P9T z5JcMn5o5wL8mNp-m*)WwIc`C`BK(Ls1@op@a_d5GWF~Cbuh@v<)Zc9>Jx>R14 zt*et!((|B5#_Zc+`(ffCyP=B?mJNCO7+G8Lgk`S_D#mvjb>bd{@J3CZY8^ z9_Y1(Wr##Mro?%3I4ZWeCmid%E2Z+2EjUX7!dLo+lqd6LJFv|pWyLAnl$(QoJ}{8> zVxt#JOZxiF^np@1qp?@87K#EAG+YEzFO@}%=t9KV127}ZRi~U?gtL6 z{&#)B(=$M58NzEwlEil4PT^1+NXhW<;2&gur-7=76&t#beh-ftvu1g%DenuQrXh$6 zEWM7=rD#s13vQ`yZWam<@_vZ!K~h-uGkc>-#kQ@qy1&XEA~}Q_FmjO9Ot}&9c4p6d z6t?W*U9YM@X6~-HSM5ilEKB&b`2$6LL@&u!F^6rtOc~U684DE~jZ2`QB~oNOq4vZ& zQkX;E$_lkek6rJ@%7m65{qHJ+%8wKoe{9L?z*l5;07Fc8N8Z*}^X#nWq87F;trt{Z zZI{|ingPhc-dXZO3rlec*xbvLZ$txY#T-g!7NN6qJ;q&~3GO!mR(vMhhtEhlvRv{1 znDHLSCRNe5lM)jzgnZM(1pN`G5|V*PWA!|skDsijW%t{fa&Fhtj7%{NYBe&vMu$$@ zssim{=U`IBrmk3vJQGed2Ki=*rfG#tkFwUK*&lAk_!FC9xdt{$f2cnTsxm^e7?ywd z!fzCRM3Hkbq%q7Z9l(UjkrHlgG{(jDQys&Lw~UzIoKCeWE4`Q%lsE$F380|XP(em8 zRm&FyGupLp5qHL0Bpp^4)z#C7pr;w&-kV{iPxHTtJRL5KMyTcgS!A;a$_7V^i=Y2; z@xfL>fmF^iZ)z$i8LX4$zQGipeN;rp1#qbDbc7??lVE7<=nxaTYD+UMzg@`z&~}Ly zkSY+;y{DEmjJlsN-scvQWk|ShWkrY|U0wo}4Kh4xB#rRXmQ;Q?_>+}FZ2cs5%FUSe z`kuRW_L7)-Z=Q!i|9XD+Vd>IYpIGlcqUP5bz?WNCtWi{QtQh9wNVJP((4G$lV@6

Pj1 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 diff --git a/wiki_assets/tab_anatomy.png b/wiki_assets/tab_anatomy.png deleted file mode 100644 index db9ce7c03304da54bd6e23678f42de798c99bf98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65693 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 diff --git a/wiki_assets/textlink_anatomy.png b/wiki_assets/textlink_anatomy.png deleted file mode 100644 index 14774443ad0e5e23e768033e3ce5f5f3c17ce027..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3305 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 diff --git a/xcodegen/spark-core-snapshot-tests.yml b/xcodegen/spark-core-snapshot-tests.yml deleted file mode 100644 index 1d1e0a1b4..000000000 --- a/xcodegen/spark-core-snapshot-tests.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Spark -include: - - spark-core.yml - -packages: - SnapshotTesting: - url: https://github.com/pointfreeco/swift-snapshot-testing - from: 1.11.0 - -targetTemplates: - SparkCoreSnapshotTestsTemplate: - type: bundle.unit-test - platform: iOS - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark.core.snapshot-tests" - SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" - SUPPORTS_MACCATALYST: NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO - PRODUCT_NAME: SparkCoreSnapshotTests - sources: - - path: ../core/Unit-tests - - path: ../core/Sources - includes: - - "**/*SnapshotTest*.swift" - - path: ../spark/Sources/Resources - excludes: - - "**/*Tests*.swift" - - path: ../spark/Sources/Theming - excludes: - - "**/*Tests*.swift" - - dependencies: - - target: SparkCore - - package: SnapshotTesting - -targets: - SparkCoreSnapshotTests: - templates: - - SparkCoreSnapshotTestsTemplate diff --git a/xcodegen/spark-core-unit-tests.yml b/xcodegen/spark-core-unit-tests.yml deleted file mode 100644 index 521a2fdd4..000000000 --- a/xcodegen/spark-core-unit-tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Spark -include: - - spark-core.yml - -targetTemplates: - SparkCoreUnitTestsTemplate: - type: bundle.unit-test - platform: iOS - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark.core.unit-tests" - SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" - SUPPORTS_MACCATALYST: NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO - sources: - - path: ../core/Unit-tests - excludes: - - "**/*SnapshotTest*.swift" - - path: ../core/Sources - includes: - - "**/*Tests.swift" - excludes: - - "**/*SnapshotTest*.swift" - - path: ../spark/Sources/Resources - excludes: - - "**/*Tests*.swift" - - path: ../spark/Sources/Theming - excludes: - - "**/*Tests*.swift" - - dependencies: - - target: SparkCore - -targets: - SparkCoreUnitTests: - templates: - - SparkCoreUnitTestsTemplate - diff --git a/xcodegen/spark-core.yml b/xcodegen/spark-core.yml deleted file mode 100644 index b4d02137c..000000000 --- a/xcodegen/spark-core.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Spark -include: - - spark-shared.yml - -targetTemplates: - SparkCoreTemplate: - platform: iOS - type: framework - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark.core" - BUILD_LIBRARY_FOR_DISTRIBUTION: YES - SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" - SUPPORTS_MACCATALYST: NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO - PRODUCT_NAME: SparkCore - info: - path: ../core/Sources/Info.plist - sources: - - path: ../core/Sources - excludes: - - "**/*Tests.swift" - postCompileScripts: - - script: ./scripts/swiftlint.sh - name: SwiftLint diff --git a/xcodegen/spark-shared.yml b/xcodegen/spark-shared.yml deleted file mode 100644 index 0762acb6e..000000000 --- a/xcodegen/spark-shared.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Spark -configs: - Debug: debug - Release: release -options: - createIntermediateGroups: true - defaultConfig: Release - groupSortPosition: top - deploymentTarget: - iOS: 15.0 - useBaseInternationalization: false - groupOrdering: - - order: [core, spark] - postGenCommand: sh postGenCommand.sh diff --git a/xcodegen/spark.yml b/xcodegen/spark.yml deleted file mode 100644 index b03c92652..000000000 --- a/xcodegen/spark.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Spark -include: - - spark-shared.yml - -targetTemplates: - SparkSchemeTemplate: - platform: iOS - type: framework - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark" - BUILD_LIBRARY_FOR_DISTRIBUTION: YES - SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" - SUPPORTS_MACCATALYST: NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO - PRODUCT_NAME: Spark - info: - path: ../spark/Sources/Info.plist - properties: - UIAppFonts: - - "NunitoSans-Bold.ttf" - - "NunitoSans-Regular.ttf" - sources: - - path: ../spark/Sources - excludes: - - "**/*Tests.swift" - scheme: - testTargets: - - name: SparkTests - gatherCoverageData: true - dependencies: - - target: SparkCore - - SparkTestsTemplate: - type: bundle.unit-test - platform: iOS - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark.tests" - SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" - SUPPORTS_MACCATALYST: NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO - info: - path: ../spark/Unit-tests/Info.plist - sources: - - path: ../spark/Unit-tests - - path: ../spark/Sources - includes: # includes some files from theme folder - - "**/*Tests.swift" - - dependencies: - - target: Spark - - SparkDemoTemplate: - type: application - platform: iOS - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "com.adevinta.spark.demo" - SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" - SUPPORTS_MACCATALYST: NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO - info: - path: ../spark/Demo/Info.plist - properties: - UILaunchScreen: [] - - UIApplicationSceneManifest: - UIApplicationSupportsMultipleScenes: false - UISceneConfigurations: - UIWindowSceneSessionRoleApplication: - - UISceneConfigurationName: Default Configuration - UISceneDelegateClassName: $(PRODUCT_MODULE_NAME).SceneDelegate - sources: - - path: ../spark/Demo - - path: ../spark/Sources/Resources - excludes: - - "Generated/" - - path: ../spark/Sources/Theming - excludes: - - "**/*Tests.swift" - - path: ../core/Sources/Common/SwiftUI/GlobalExtension - - path: ../core/Sources/Common/UIKit/GlobalExtension - - path: ../core/Sources/Common/Combine/Global - dependencies: - - target: Spark - -targets: - Spark: - templates: - - SparkSchemeTemplate - - SparkTests: - templates: - - SparkTestsTemplate - - SparkDemo: - templates: - - SparkDemoTemplate From 401cfac9ad97d8368ea94810ff41238762838043 Mon Sep 17 00:00:00 2001 From: "louis.borlee" Date: Mon, 15 Jul 2024 11:33:02 +0200 Subject: [PATCH 2/2] [Popover] Added demos --- .Demo/Classes/Enum/UIComponent.swift | 2 + .../View/Components/ComponentsView.swift | 12 +++- .../Components/ComponentsViewController.swift | 2 + .../Popover/SwiftUI/PopoverDemoView.swift | 46 ++++++++++++++ .../PopoverContentDemoViewController.swift | 50 +++++++++++++++ .../PopoverPresentingUIViewController.swift | 63 +++++++++++++++++++ Package.swift | 9 +++ 7 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 .Demo/Classes/View/Components/Popover/SwiftUI/PopoverDemoView.swift create mode 100644 .Demo/Classes/View/Components/Popover/UIKit/PopoverContentDemoViewController.swift create mode 100644 .Demo/Classes/View/Components/Popover/UIKit/PopoverPresentingUIViewController.swift diff --git a/.Demo/Classes/Enum/UIComponent.swift b/.Demo/Classes/Enum/UIComponent.swift index da951380f..7f0784874 100644 --- a/.Demo/Classes/Enum/UIComponent.swift +++ b/.Demo/Classes/Enum/UIComponent.swift @@ -18,6 +18,7 @@ struct UIComponent: RawRepresentable, CaseIterable, Equatable { .chip, .formField, .icon, + .popover, .progressBarIndeterminate, .progressBarSingle, .progressTracker, @@ -45,6 +46,7 @@ struct UIComponent: RawRepresentable, CaseIterable, Equatable { static let formField = UIComponent(rawValue: "FormField") static let icon = UIComponent(rawValue: "Icon") static let iconButton = UIComponent(rawValue: "Icon Button") + static let popover = UIComponent(rawValue: "Popover") static let progressBarIndeterminate = UIComponent(rawValue: "Progress Bar Indeterminate") static let progressBarSingle = UIComponent(rawValue: "Progress Bar Single") static let progressTracker = UIComponent(rawValue: "Progress Tracker") diff --git a/.Demo/Classes/View/Components/ComponentsView.swift b/.Demo/Classes/View/Components/ComponentsView.swift index 9dbc67761..d9bc13b07 100644 --- a/.Demo/Classes/View/Components/ComponentsView.swift +++ b/.Demo/Classes/View/Components/ComponentsView.swift @@ -32,9 +32,6 @@ struct ComponentsView: View { Button("Button") { self.navigateToView(ButtonComponentView()) } - Button("Icon Button") { - self.navigateToView(IconButtonComponentView()) - } } Group { @@ -59,6 +56,15 @@ struct ComponentsView: View { self.navigateToView(IconComponentView()) } + if #available(iOS 16.4, *) { + Button("Popover") { + self.navigateToView(PopoverDemoView()) + } + } else { + Text("Popover: unavailable below iOS version 16.4") + .foregroundStyle(.red) + } + Group { Button("Progress Bar - Indeterminate") { self.navigateToView(ProgressBarIndeterminateComponentView()) diff --git a/.Demo/Classes/View/Components/ComponentsViewController.swift b/.Demo/Classes/View/Components/ComponentsViewController.swift index 0acc6f9ae..fa6876b9e 100644 --- a/.Demo/Classes/View/Components/ComponentsViewController.swift +++ b/.Demo/Classes/View/Components/ComponentsViewController.swift @@ -87,6 +87,8 @@ extension ComponentsViewController { viewController = FormFieldComponentUIViewController.build() case .icon: viewController = IconComponentUIViewController.build() + case .popover: + viewController = PopoverPresentingUIViewController.build() case .progressBarIndeterminate: viewController = ProgressBarIndeterminateComponentUIViewController.build() case .progressBarSingle: diff --git a/.Demo/Classes/View/Components/Popover/SwiftUI/PopoverDemoView.swift b/.Demo/Classes/View/Components/Popover/SwiftUI/PopoverDemoView.swift new file mode 100644 index 000000000..c1e548321 --- /dev/null +++ b/.Demo/Classes/View/Components/Popover/SwiftUI/PopoverDemoView.swift @@ -0,0 +1,46 @@ +// +// PopoverDemoView.swift +// SparkDemo +// +// Created by louis.borlee on 15/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import SwiftUI +import SparkCore +import SparkPopover + +@available(iOS 16.4, *) +struct PopoverDemoView: View { + + private let theme = SparkTheme() + + var body: some View { + ForEach(PopoverIntent.allCases, id: \.self) { intent in + let colors = intent.getColors(theme: theme) + PopoverDemoItem(intent: intent, colors: colors) + } + .buttonStyle(.borderedProminent) + } +} + +@available(iOS 16.4, *) +struct PopoverDemoItem: View { + let intent: PopoverIntent + let colors: PopoverColors + @State var isPresented: Bool = false + + var body: some View { + Button(intent.name) { + isPresented = true + } + .tint(colors.background.color) + .foregroundStyle(colors.foreground.color) + .popover(theme: SparkTheme(), intent: intent, isPresented: $isPresented) { colors in + Text("This is a label that should be multiline, depending on the content size. This is a label that should be multiline, depending on the content size.") + .foregroundStyle(colors.foreground.color) + .frame(width: 300) + } + + } +} diff --git a/.Demo/Classes/View/Components/Popover/UIKit/PopoverContentDemoViewController.swift b/.Demo/Classes/View/Components/Popover/UIKit/PopoverContentDemoViewController.swift new file mode 100644 index 000000000..283d37383 --- /dev/null +++ b/.Demo/Classes/View/Components/Popover/UIKit/PopoverContentDemoViewController.swift @@ -0,0 +1,50 @@ +// +// PopoverContentDemoViewController.swift +// SparkDemo +// +// Created by louis.borlee on 15/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import UIKit +import SparkCore +import SparkPopover + +final class PopoverContentDemoViewController: UIViewController { + + let label: UILabel = { + let label = UILabel() + label.text = "This is a label that should be multiline, depending on the content size. This is a label that should be multiline, depending on the content size." + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + init(theme: Theme, intent: PopoverIntent) { + super.init(nibName: nil, bundle: nil) + self.label.textColor = intent.getColors(theme: theme).foreground.uiColor + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .clear + + self.view.addSubview(self.label) + NSLayoutConstraint.activate([ + self.label.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0), + self.label.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0), + self.label.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0), + self.label.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0), + self.label.widthAnchor.constraint(lessThanOrEqualToConstant: 300) + ]) + + } + + override func viewDidLayoutSubviews() { + self.preferredContentSize = self.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + } +} diff --git a/.Demo/Classes/View/Components/Popover/UIKit/PopoverPresentingUIViewController.swift b/.Demo/Classes/View/Components/Popover/UIKit/PopoverPresentingUIViewController.swift new file mode 100644 index 000000000..cbd851758 --- /dev/null +++ b/.Demo/Classes/View/Components/Popover/UIKit/PopoverPresentingUIViewController.swift @@ -0,0 +1,63 @@ +// +// PopoverPresentingUIViewController.swift +// SparkDemo +// +// Created by louis.borlee on 15/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import UIKit +import SparkCore +import SparkPopover + +final class PopoverPresentingUIViewController: UIViewController { + + private let theme = SparkTheme() + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .systemBackground + + let buttons: [UIView] = PopoverIntent.allCases.enumerated().map { index, intent in + let popoverColors = intent.getColors(theme: self.theme) + let button = UIButton(configuration: .filled()) + button.setTitle(intent.name, for: .normal) + button.setTitleColor(popoverColors.foreground.uiColor, for: .normal) + button.tintColor = popoverColors.background.uiColor + button.addAction(.init(handler: { [weak self] _ in + self?.showPopover(sourceView: button, intent: intent, withArrow: index % 2 == 0) + }), for: .touchUpInside) + return button + } + + let stackView = UIStackView(arrangedSubviews: buttons) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 12 + + self.view.addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor) + ]) + } + + private func showPopover(sourceView: UIView, intent: PopoverIntent, withArrow showArrow: Bool) { + if let presentedViewController { + presentedViewController.dismiss(animated: true) + } else { + let theme = SparkTheme() + let popoverViewController = PopoverViewController(contentViewController: PopoverContentDemoViewController(theme: theme, intent: intent), theme: theme, intent: intent, showArrow: showArrow) + self.presentPopover(popoverViewController, sourceView: sourceView) + } + } +} + +// MARK: - Builder +extension PopoverPresentingUIViewController { + + static func build() -> PopoverPresentingUIViewController { + return PopoverPresentingUIViewController() + } +} diff --git a/Package.swift b/Package.swift index 8a50c9b07..ec191d22f 100644 --- a/Package.swift +++ b/Package.swift @@ -66,6 +66,11 @@ let package = Package( // path: "../spark-ios-component-icon" /*version*/ "0.0.1"..."999.999.999" ), + .package( + url: "https://github.com/adevinta/spark-ios-component-popover.git", +// path: "../spark-ios-component-popover" + /*version*/ "0.0.1"..."999.999.999" + ), .package( url: "https://github.com/adevinta/spark-ios-component-progress-bar.git", // path: "../spark-ios-component-progress-bar" @@ -164,6 +169,10 @@ let package = Package( name: "SparkIcon", package: "spark-ios-component-icon" ), + .product( + name: "SparkPopover", + package: "spark-ios-component-popover" + ), .product( name: "SparkProgressBar", package: "spark-ios-component-progress-bar"