diff --git a/core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift b/core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift new file mode 100644 index 000000000..5aee37fa0 --- /dev/null +++ b/core/Sources/Common/Foundation/Extension/CGFloat+ScaledMetricExtension.swift @@ -0,0 +1,56 @@ +// +// 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/Components/Switch/View/Common/SwitchSutTests.swift b/core/Sources/Components/Switch/View/Common/SwitchSutTests.swift index 03f8cd38c..3ae0b9a00 100644 --- a/core/Sources/Components/Switch/View/Common/SwitchSutTests.swift +++ b/core/Sources/Components/Switch/View/Common/SwitchSutTests.swift @@ -79,7 +79,8 @@ struct SwitchSutTests { (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: attributedText), // Only attributed text + (images: nil, text: nil, attributedText: nil) // Nothing ] return items.map { item -> SwitchSutTests in diff --git a/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift b/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift new file mode 100644 index 000000000..0b9167bcf --- /dev/null +++ b/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewType.swift @@ -0,0 +1,25 @@ +// +// 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: - Properties + + static var leftAlignmentCases: [Self] { + return [.toggle, .space, .text] + } + + static var rightAlignmentCases: [Self] { + return [.text, .space, .toggle] + } +} diff --git a/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift b/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift new file mode 100644 index 000000000..576559315 --- /dev/null +++ b/core/Sources/Components/Switch/View/SwiftUI/SubviewType/SwitchSubviewTypeTests.swift @@ -0,0 +1,37 @@ +// +// 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_leftAlignmentCases() { + // GIVEN / WHEN + let allCases = SwitchSubviewType.leftAlignmentCases + + // THEN + XCTAssertEqual( + allCases, + [.toggle, .space, .text] + ) + } + + func test_rightAlignmentCases() { + // GIVEN / WHEN + let allCases = SwitchSubviewType.rightAlignmentCases + + // 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 new file mode 100644 index 000000000..7381d065a --- /dev/null +++ b/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift @@ -0,0 +1,275 @@ +// +// 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. + /// - isOn: The Binding value of the switch. + public init( + theme: any Theme, + isOn: Binding + ) { + self.viewModel = .init( + for: .swiftUI, + theme: theme, + isOn: isOn.wrappedValue, + alignment: .left, + intent: .main, + isEnabled: false, + 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 + } + } + + // MARK: - Subview Maker + + private func subviewsTypes() -> [SwitchSubviewType] { + return (self.viewModel.isToggleOnLeft == true) ? SwitchSubviewType.leftAlignmentCases : SwitchSubviewType.rightAlignmentCases + } + + @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) + .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 + ) + } + .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 + ) + .accessibilityIdentifier(AccessibilityIdentifier.toggleView) + .onTapGesture { + withAnimation(.custom) { + self.viewModel.toggle() + } + } + } + + // MARK: - Modifier + + /// Set the alignment on switch. + /// - Parameters: + /// - alignment: The alignment of the switch. + /// - Returns: Current Switch View. + public func alignment(_ alignment: SwitchAlignment) -> Self { + self.viewModel.set(alignment: alignment) + return self + } + + /// Set the intent on switch. + /// - Parameters: + /// - intent: The intent of the switch. + /// - Returns: Current Switch View. + public func intent(_ intent: SwitchIntent) -> Self { + self.viewModel.set(intent: intent) + return self + } + + /// Set the isEnabled on switch. + /// - Parameters: + /// - isEnabled: The state of the switch: enabled or not. + /// - Returns: Current Switch View. + public func isEnabled(_ isEnabled: Bool) -> Self { + self.viewModel.set(isEnabled: isEnabled) + 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 { + + func applyStyle( + isForOnImage: Bool, + viewModel: SwitchViewModel + ) -> some View { + self.resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(viewModel.toggleDotForegroundColorToken?.color) + .if(isForOnImage) { + $0.opacity(viewModel.toggleDotImagesState?.onImageOpacity ?? 0) + } else: { + $0.opacity(viewModel.toggleDotImagesState?.offImageOpacity ?? 0) + } + .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/SwitchViewTests.swift b/core/Sources/Components/Switch/View/SwiftUI/SwitchViewTests.swift new file mode 100644 index 000000000..09e31d499 --- /dev/null +++ b/core/Sources/Components/Switch/View/SwiftUI/SwitchViewTests.swift @@ -0,0 +1,78 @@ +// +// SwitchViewTests.swift +// SparkCoreTests +// +// Created by robin.lemaire on 14/09/2023. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import XCTest +import SnapshotTesting +@testable import SparkCore +@testable import Spark +import SwiftUI + +final class SwitchViewTests: SwiftUIComponentTestCase { + + // MARK: - Properties + + private let theme: Theme = SparkTheme.shared + + // MARK: - Tests + + func test_swiftUI_switch_colors() throws { + let suts = try SwitchSutTests.allColorsCases(isSwiftUIComponent: true) + self.test(suts: suts) + } + + func test_swiftUI_switch_contens() throws { + let suts = try SwitchSutTests.allContentsCases(isSwiftUIComponent: true) + self.test(suts: suts) + } + + func test_swiftUI_switch_positions() throws { + let suts = try SwitchSutTests.allPositionsCases(isSwiftUIComponent: true) + self.test(suts: suts) + } +} + +// MARK: - Testing + +private extension SwitchViewTests { + + func test(suts: [SwitchSutTests], function: String = #function) { + for sut in suts { + var view = SwitchView( + theme: self.theme, + isOn: .constant(sut.isOn) + ) + .alignment(sut.alignment) + .intent(sut.intent) + .isEnabled(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/SwitchUIViewTests.swift b/core/Sources/Components/Switch/View/UIKit/SwitchUIViewTests.swift index 7e9d24d34..225177d32 100644 --- a/core/Sources/Components/Switch/View/UIKit/SwitchUIViewTests.swift +++ b/core/Sources/Components/Switch/View/UIKit/SwitchUIViewTests.swift @@ -83,8 +83,14 @@ private extension SwitchUIViewTests { isEnabled: sut.isEnabled, attributedText: attributedText.leftValue ) - } else { - XCTFail("Missing case, view should be init") + } 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 diff --git a/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift b/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift index d4b1ef108..107c7768a 100644 --- a/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift +++ b/core/Sources/Components/Switch/ViewModel/SwitchViewModelTests.swift @@ -145,6 +145,7 @@ final class SwitchViewModelTests: XCTestCase { } + // MARK: - Load Tests func test_published_properties_on_load_when_frameworkType_is_UIKit() throws { diff --git a/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift b/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift index 6405b11c8..a7fe66bf4 100644 --- a/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift +++ b/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift @@ -16,37 +16,16 @@ struct ButtonComponentView: View { let viewModel = ButtonComponentViewModel() - @ObservedObject private var themePublisher = SparkThemePublisher.shared - - var theme: Theme { - self.themePublisher.theme - } - @State var isThemePresented = false - - let themes = ThemeCellModel.themes - @State private var uiKitViewHeight: CGFloat = .zero - @State private var intentSheetIsPresented = false + @State var theme: Theme = SparkThemePublisher.shared.theme @State var intent: ButtonIntent = .main - - @State private var variantSheetIsPresented = false @State var variant: ButtonVariant = .filled - - @State private var sizeSheetIsPresented = false @State var size: ButtonSize = .medium - - @State private var shapeSheetIsPresented = false @State var shape: ButtonShape = .rounded - - @State private var alignmentSheetIsPresented = false @State var alignment: ButtonAlignment = .leadingIcon - - @State private var contentSheetIsPresented = false @State var content: ButtonContentDefault = .text - - @State private var isEnabledSheetIsPresented = false - @State var isEnabled: Bool = true + @State var isEnabled: CheckboxSelectionState = .selected @State var shouldShowReverseBackgroundColor: Bool = false @@ -60,144 +39,60 @@ struct ButtonComponentView: View { .bold() VStack(alignment: .leading, spacing: 16) { - // ** - // Version - HStack() { - Text("Theme: ").bold() - let selectedTheme = self.theme is SparkTheme ? themes.first : themes.last - Button(selectedTheme?.title ?? "") { - self.isThemePresented = true - } - .confirmationDialog("Select a theme", - isPresented: self.$isThemePresented) { - ForEach(themes, id: \.self) { theme in - Button(theme.title) { - themePublisher.theme = theme.theme - } - } - } - .onChange(of: self.intent) { newValue in - self.shouldShowReverseBackgroundColor = (newValue == .surface) - } - Spacer() - } - // ** - - // ** - // Intent - HStack() { - Text("Intent: ") - .bold() - Button("\(self.intent.name)") { - self.intentSheetIsPresented = true - } - .confirmationDialog("Select an intent", isPresented: self.$intentSheetIsPresented) { - ForEach(ButtonIntent.allCases, id: \.self) { intent in - Button("\(intent.name)") { - self.intent = intent - } - } - } - } - // ** - - // ** - // Variant - HStack() { - Text("Variant: ") - .bold() - Button("\(self.variant.name)") { - self.variantSheetIsPresented = true - } - .confirmationDialog("Select a variant", isPresented: self.$variantSheetIsPresented) { - ForEach(ButtonVariant.allCases, id: \.self) { variant in - Button("\(variant.name)") { - self.variant = variant - } - } - } - } - // ** - - // ** - // Size - HStack() { - Text("Size: ") - .bold() - Button("\(self.size.name)") { - self.sizeSheetIsPresented = true - } - .confirmationDialog("Select a size", isPresented: self.$sizeSheetIsPresented) { - ForEach(ButtonSize.allCases, id: \.self) { size in - Button("\(size.name)") { - self.size = size - } - } - } - } - // ** - - // ** - // Shape - HStack() { - Text("Shape: ") - .bold() - Button("\(self.shape.name)") { - self.shapeSheetIsPresented = true - } - .confirmationDialog("Select a shape", isPresented: self.$shapeSheetIsPresented) { - ForEach(ButtonShape.allCases, id: \.self) { shape in - Button("\(shape.name)") { - self.shape = shape - } - } - } - } - // ** - - // ** - // Alignment - HStack() { - Text("Alignment: ") - .bold() - Button("\(self.alignment.name)") { - self.alignmentSheetIsPresented = true - } - .confirmationDialog("Select a alignment", isPresented: self.$alignmentSheetIsPresented) { - ForEach(ButtonAlignment.allCases, id: \.self) { alignment in - Button("\(alignment.name)") { - self.alignment = alignment - } - } - } - } - // ** - - // ** - // Content - HStack() { - Text("Content: ") - .bold() - Button("\(self.content.name)") { - self.contentSheetIsPresented = true - } - .confirmationDialog("Select a content", isPresented: self.$contentSheetIsPresented) { - ForEach(ButtonContentDefault.allCases, id: \.self) { content in - Button("\(content.name)") { - self.content = content - } - } - } - } - // ** - - // Is Enabled - HStack() { - Text("Is enabled: ") - .bold() - Toggle("", isOn: self.$isEnabled) - .labelsHidden() + ThemeSelector(theme: self.$theme) + + EnumSelector( + title: "Intent", + dialogTitle: "Select an intent", + values: ButtonIntent.allCases, + value: self.$intent + ) + .onChange(of: self.intent) { newValue in + self.shouldShowReverseBackgroundColor = (newValue == .surface) } + + EnumSelector( + title: "Variant", + dialogTitle: "Select a variant", + values: ButtonVariant.allCases, + value: self.$variant + ) + + EnumSelector( + title: "Size", + dialogTitle: "Select a size", + values: ButtonSize.allCases, + value: self.$size + ) + + EnumSelector( + title: "Shape", + dialogTitle: "Select a shape", + values: ButtonShape.allCases, + value: self.$shape + ) + + EnumSelector( + title: "Alignment", + dialogTitle: "Select an alignment", + values: ButtonAlignment.allCases, + value: self.$alignment + ) + + EnumSelector( + title: "Content", + dialogTitle: "Select an content", + values: ButtonContentDefault.allCases, + value: self.$content + ) + + CheckboxView( + text: "Is enabled", + checkedImage: DemoIconography.shared.checkmark, + theme: self.theme, + state: .enabled, + selectionState: self.$isEnabled + ) } Divider() @@ -217,7 +112,7 @@ struct ButtonComponentView: View { shape: self.$shape.wrappedValue, alignment: self.$alignment.wrappedValue, content: self.$content.wrappedValue, - isEnabled: self.$isEnabled.wrappedValue + isEnabled: self.$isEnabled.wrappedValue == .selected ) .frame(width: geometry.size.width, height: self.uiKitViewHeight, alignment: .center) .padding(.horizontal, self.shouldShowReverseBackgroundColor ? 4 : 0) diff --git a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift b/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift index 65a166869..d9ddc9960 100644 --- a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift +++ b/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentView.swift @@ -16,35 +16,12 @@ struct SwitchComponentView: View { let viewModel = SwitchComponentViewModel() - @ObservedObject private var themePublisher = SparkThemePublisher.shared - var theme: Theme { - self.themePublisher.theme - } - @State private var isThemePresented = false - - let themes = ThemeCellModel.themes - - @State private var uiKitViewHeight: CGFloat = .zero - - @State private var isOnSheetIsPresented = false + @State var theme: Theme = SparkThemePublisher.shared.theme @State var isOn: Bool = true - - @State private var alignmentSheetIsPresented = false @State var alignment: SwitchAlignment = .left - - @State private var intentSheetIsPresented = false @State var intent: SwitchIntent = .main - - @State private var isEnabledSheetIsPresented = false - @State var isEnabled: Bool = true - - @State private var hasImagesSheetIsPresented = false - @State var hasImages: Bool = false - - @State private var isMultilineTextSheetIsPresented = false - @State var isMultilineText: Bool = true - - @State private var textContentSheetIsPresented = false + @State var isEnabled: CheckboxSelectionState = .selected + @State var hasImages: CheckboxSelectionState = .unselected @State var textContent: SwitchTextContentDefault = .text // MARK: - View @@ -57,101 +34,55 @@ struct SwitchComponentView: View { .bold() VStack(alignment: .leading, spacing: 16) { - // ** - // Theme - HStack() { - Text("Theme: ").bold() - let selectedTheme = self.theme is SparkTheme ? themes.first : themes.last - Button(selectedTheme?.title ?? "") { - self.isThemePresented = true - } - .confirmationDialog("Select a theme", - isPresented: self.$isThemePresented) { - ForEach(themes, id: \.self) { theme in - Button(theme.title) { - themePublisher.theme = theme.theme - } - } - } - Spacer() - } - // ** - - // Is On - HStack() { - Text("Is on: ") - .bold() - Toggle("", isOn: self.$isOn) - .labelsHidden() - } - - // ** - // Alignment - HStack() { - Text("Alignment: ") - .bold() - Button("\(self.alignment.name)") { - self.alignmentSheetIsPresented = true - } - .confirmationDialog("Select an alignment", isPresented: self.$alignmentSheetIsPresented) { - ForEach(SwitchAlignment.allCases, id: \.self) { alignment in - Button("\(alignment.name)") { - self.alignment = alignment - } - } - } - } - // ** - - // ** - // Intent - HStack() { - Text("Intent: ") - .bold() - Button("\(self.intent.name)") { - self.intentSheetIsPresented = true - } - .confirmationDialog("Select an intent", isPresented: self.$intentSheetIsPresented) { - ForEach(SwitchIntent.allCases, id: \.self) { intent in - Button("\(intent.name)") { - self.intent = intent - } - } - } - } - // ** - - // Is Enabled - HStack() { - Text("Is enabled: ") - .bold() - Toggle("", isOn: self.$isEnabled) - .labelsHidden() - } - - // Has Images - HStack() { - Text("Has images: ") - .bold() - Toggle("", isOn: self.$hasImages) - .labelsHidden() - } - - // Text Content - HStack() { - Text("Text content: ") - .bold() - Button("\(self.textContent.name)") { - self.textContentSheetIsPresented = true - } - .confirmationDialog("Select an text content", isPresented: self.$textContentSheetIsPresented) { - ForEach(SwitchTextContentDefault.allCases, id: \.self) { textContent in - Button("\(textContent.name)") { - self.textContent = textContent - } - } - } - } + ThemeSelector(theme: self.$theme) + + EnumSelector( + title: "Alignment", + dialogTitle: "Select an alignment", + values: SwitchAlignment.allCases, + value: self.$alignment + ) + + EnumSelector( + title: "Intent", + dialogTitle: "Select an intent", + values: SwitchIntent.allCases, + value: self.$intent + ) + + EnumSelector( + title: "Text content", + dialogTitle: "Select an text content", + values: SwitchTextContentDefault.allCases, + value: self.$textContent + ) + + CheckboxView( + text: "Is on", + checkedImage: DemoIconography.shared.checkmark, + theme: self.theme, + state: .enabled, + selectionState: Binding( + get: { self.isOn ? .selected : .unselected }, + set: { self.isOn = ($0 == .selected) } + ) + ) + + CheckboxView( + text: "Is enabled", + checkedImage: DemoIconography.shared.checkmark, + theme: self.theme, + state: .enabled, + selectionState: self.$isEnabled + ) + + CheckboxView( + text: "Has images", + checkedImage: DemoIconography.shared.checkmark, + theme: self.theme, + state: .enabled, + selectionState: self.$hasImages + ) } Divider() @@ -160,20 +91,19 @@ struct SwitchComponentView: View { .font(.title2) .bold() - GeometryReader { geometry in - SwitchComponentViewRepresentable( - viewModel: self.viewModel, - width: geometry.size.width, - height: self.$uiKitViewHeight, - isOn: self.$isOn, - alignment: self.$alignment.wrappedValue, - intent: self.$intent.wrappedValue, - isEnabled: self.$isEnabled.wrappedValue, - hasImages: self.$hasImages.wrappedValue, - textContent: self.$textContent.wrappedValue - ) - .frame(width: geometry.size.width, height: self.uiKitViewHeight, alignment: .leading) - } + SwitchView( + theme: self.theme, + isOn: self.$isOn + ) + .alignment(self.alignment) + .intent(self.intent) + .isEnabled(self.isEnabled == .selected) + .images(self.hasImages == .selected ? self.images() : nil) + .textContent( + viewModel: self.viewModel, + textContent: self.textContent, + attributedText: self.attributedText() + ) Spacer() } @@ -181,68 +111,54 @@ struct SwitchComponentView: View { } .navigationBarTitle(Text("Switch")) } -} -struct SwitchComponentView_Previews: PreviewProvider { - static var previews: some View { - SwitchComponentView() - } -} + // MARK: - UIKit -// MARK: - Extension + private func images() -> SwitchImages { + let onImage = Image(self.viewModel.onImageNamed) + let offImage = Image(self.viewModel.offImageNamed) -private extension SwitchAlignment { - - var name: String { - switch self { - case .left: - return "Toggle on left" - case .right: - return "Toggle on right" - @unknown default: - return "Please, add this unknow alignment value" - } + return SwitchImages( + on: onImage, + off: offImage + ) } -} -private extension SwitchIntent { - - var name: String { - switch self { - case .alert: - return "Alert" - case .error: - return "Error" - case .info: - return "Info" - case .neutral: - return "Neutral" - case .main: - return "Main" - case .support: - return "Support" - case .success: - return "Success" - case .accent: - return "Accent" - case .basic: - return "Basic" - @unknown default: - return "Please, add this unknow intent value" - } + private func attributedText() -> AttributedString { + var attributedText = AttributedString(self.viewModel.text) + attributedText.font = SparkTheme.shared.typography.body2.font + attributedText.foregroundColor = SparkTheme.shared.colors.main.main.color + attributedText.backgroundColor = SparkTheme.shared.colors.main.onMain.color + return attributedText } } -private extension SwitchTextContentDefault { +// MARK: - Modifier + +extension SwitchView { - var name: String { - switch self { + func textContent( + viewModel: SwitchComponentViewModel, + textContent: SwitchTextContentDefault, + attributedText: AttributedString + ) -> SwitchView { + switch textContent { case .text: - return "Text" + return self.text(viewModel.text(isMultilineText: false)) case .attributedText: - return "Attributed Text" + return self.attributedText(attributedText) case .multilineText: - return "Multiline Text" + return self.text(viewModel.text(isMultilineText: true)) + case .none: + return self.text(nil).attributedText(nil) } } } + +// MARK: - Preview + +struct SwitchComponentView_Previews: PreviewProvider { + static var previews: some View { + SwitchComponentView() + } +} diff --git a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewRepresentable.swift b/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewRepresentable.swift deleted file mode 100644 index 35ffb656d..000000000 --- a/spark/Demo/Classes/View/Components/Switch/SwiftUI/SwitchComponentViewRepresentable.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// SwitchComponentItemsUIView.swift -// SparkDemo -// -// Created by robin.lemaire on 26/05/2023. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import Spark -import SparkCore -import Combine - -struct SwitchComponentViewRepresentable: UIViewRepresentable { - - // MARK: - Properties - - private let viewModel: SwitchComponentViewModel - private let attributedText: NSAttributedString - - var width: CGFloat - @Binding var height: CGFloat - - @Binding var isOn: Bool - private let alignment: SwitchAlignment - private let intent: SwitchIntent - private let isEnabled: Bool - private let hasImages: Bool - private let textContent: SwitchTextContentDefault - - // MARK: - Initialization - - init( - viewModel: SwitchComponentViewModel, - width: CGFloat, - height: Binding, - isOn: Binding, - alignment: SwitchAlignment, - intent: SwitchIntent, - isEnabled: Bool, - hasImages: Bool, - textContent: SwitchTextContentDefault - ) { - self.viewModel = viewModel - self.attributedText = .init( - string: viewModel.text, - attributes: [ - .foregroundColor: SparkTheme.shared.colors.main.main.uiColor, - .font: SparkTheme.shared.typography.body2Highlight.uiFont - ] - ) - self.width = width - self._height = height - self._isOn = isOn - self.alignment = alignment - self.intent = intent - self.isEnabled = isEnabled - self.hasImages = hasImages - self.textContent = textContent - } - - // MARK: - Maker - - func makeUIView(context: Context) -> UIView { - var switchView: SwitchUIView - - switch self.textContent { - case .text: - switchView = self.makeView(isMultilineText: false) - case .attributedText: - if self.hasImages { - switchView = SwitchUIView( - theme: SparkTheme.shared, - isOn: self.isOn, - alignment: self.alignment, - intent: self.intent, - isEnabled: self.isEnabled, - images: self.images(), - attributedText: self.attributedText - ) - } else { - switchView = SwitchUIView( - theme: SparkTheme.shared, - isOn: self.isOn, - alignment: self.alignment, - intent: self.intent, - isEnabled: self.isEnabled, - attributedText: self.attributedText - ) - } - case .multilineText: - switchView = self.makeView(isMultilineText: false) - } - switchView.delegate = context.coordinator - - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.widthAnchor.constraint(equalToConstant: width).isActive = true - view.addSubview(switchView) - - switchView.translatesAutoresizingMaskIntoConstraints = false - switchView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - switchView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - switchView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - switchView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - view.heightAnchor.constraint(equalTo: switchView.heightAnchor).isActive = true - - return view - } - - func updateUIView(_ stackView: UIView, context: Context) { - guard let switchView = stackView.subviews.compactMap({ $0 as? SwitchUIView }).first else { - return - } - - if switchView.isOn != self.isOn { - switchView.isOn = self.isOn - } - - if switchView.alignment != self.alignment { - switchView.alignment = self.alignment - } - - if switchView.intent != self.intent { - switchView.intent = self.intent - } - - if switchView.isEnabled != self.isEnabled { - switchView.isEnabled = self.isEnabled - } - - if (switchView.images == nil && self.hasImages) || - (switchView.images != nil && !self.hasImages) { - switchView.images = self.hasImages ? self.images() : nil - } - - switch self.textContent { - case .text: - switchView.text = self.viewModel.text(isMultilineText: false) - case .attributedText: - switchView.attributedText = self.attributedText - case .multilineText: - switchView.text = self.viewModel.text(isMultilineText: true) - } - - DispatchQueue.main.async { - self.height = switchView.frame.height - } - } - - // MARK: - Coordinator - - func makeCoordinator() -> Coordinator { - return Coordinator(isOn: self.$isOn) - } - - // MARK: - Getter & Maker - - private func images() -> SwitchUIImages { - let onImage = UIImage(named: self.viewModel.onImageNamed) ?? UIImage() - let offImage = UIImage(named: self.viewModel.offImageNamed) ?? UIImage() - - return SwitchUIImages( - on: onImage, - off: offImage - ) - } - - private func makeView(isMultilineText: Bool) -> SwitchUIView { - if self.hasImages { - return SwitchUIView( - theme: SparkTheme.shared, - isOn: self.isOn, - alignment: self.alignment, - intent: self.intent, - isEnabled: self.isEnabled, - images: self.images(), - text: self.viewModel.text(isMultilineText: isMultilineText) - ) - } else { - return SwitchUIView( - theme: SparkTheme.shared, - isOn: self.isOn, - alignment: self.alignment, - intent: self.intent, - isEnabled: self.isEnabled, - text: self.viewModel.text(isMultilineText: isMultilineText) - ) - } - } -} - -extension SwitchComponentViewRepresentable { - - class Coordinator: NSObject, SwitchUIViewDelegate { - - // MARK: - Properties - - @Binding var isOn: Bool - - var subscriptions = Set() - - // MARK: - Initialization - - init(isOn: Binding) { - _isOn = isOn - } - - // MARK: - Delegate - - func switchDidChange(_ switchView: SwitchUIView, isOn: Bool) { - self.isOn = isOn - } - } -} diff --git a/spark/Demo/Classes/View/Components/Switch/SwitchTextContent.swift b/spark/Demo/Classes/View/Components/Switch/SwitchTextContent.swift index 843653731..1c0c83acf 100644 --- a/spark/Demo/Classes/View/Components/Switch/SwitchTextContent.swift +++ b/spark/Demo/Classes/View/Components/Switch/SwitchTextContent.swift @@ -10,4 +10,5 @@ enum SwitchTextContentDefault: CaseIterable { case text case attributedText case multilineText + case none } diff --git a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift b/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift index c40b4e462..84cfb021f 100644 --- a/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift +++ b/spark/Demo/Classes/View/Components/Switch/UIKit/SwitchComponentUIView.swift @@ -77,6 +77,10 @@ final class SwitchComponentUIView: ComponentUIView { case .multilineText: self.componentView.text = viewModel.multilineText + + case .none: + self.componentView.text = nil + self.componentView.attributedText = nil } }