diff --git a/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift b/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift index 640543d2b..89263a818 100644 --- a/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift +++ b/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift @@ -50,7 +50,7 @@ final class ControlPropertyStates { /// Get the value for a state. /// - Parameters: /// - state: the state of the value - func value(forState state: ControlState) -> PropertyType? { + func value(for state: ControlState) -> PropertyType? { switch state { case .normal: return self.normalState.value case .highlighted: return self.highlightedState.value @@ -61,8 +61,8 @@ final class ControlPropertyStates { /// Get the value for the status of the control. /// - Parameters: - /// - state: the status of the control - func value(forStatus status: ControlStatus) -> PropertyType? { + /// - status: the status of the control + func value(for status: ControlStatus) -> PropertyType? { // isHighlighted has the highest priority, // then isDisabled, // then isSelected, diff --git a/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift b/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift index 56bb4cb57..0f2afb91d 100644 --- a/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift +++ b/core/Sources/Common/Control/PropertyStates/ControlPropertyStatesTests.swift @@ -25,7 +25,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(expectedValue, for: state) // WHEN - let value = states.value(forState: state) + let value = states.value(for: state) // THEN XCTAssertEqual( @@ -45,7 +45,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(nil, for: state) // WHEN - let value = states.value(forState: state) + let value = states.value(for: state) // THEN XCTAssertNil( @@ -73,7 +73,7 @@ final class ControlPropertyStatesTests: XCTestCase { var status = ControlStatus(isHighlighted: true) states.setValue(highlightedValue, for: .highlighted) - var value = states.value(forStatus: status) + var value = states.value(for: status) // THEN XCTAssertEqual( @@ -89,7 +89,7 @@ final class ControlPropertyStatesTests: XCTestCase { status = ControlStatus(isHighlighted: true) states.setValue(nil, for: .highlighted) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -106,7 +106,7 @@ final class ControlPropertyStatesTests: XCTestCase { status = .init(isHighlighted: false) states.setValue(highlightedValue, for: .highlighted) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -134,7 +134,7 @@ final class ControlPropertyStatesTests: XCTestCase { var status = ControlStatus(isDisabled: true) states.setValue(disabledValue, for: .disabled) - var value = states.value(forStatus: status) + var value = states.value(for: status) // THEN XCTAssertEqual( @@ -150,7 +150,7 @@ final class ControlPropertyStatesTests: XCTestCase { status = ControlStatus(isDisabled: true) states.setValue(nil, for: .disabled) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -167,7 +167,7 @@ final class ControlPropertyStatesTests: XCTestCase { status = .init(isDisabled: false) states.setValue(disabledValue, for: .disabled) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -186,7 +186,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(disabledValue, for: .disabled) states.setValue(highlightedValue, for: .highlighted) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -205,7 +205,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(disabledValue, for: .disabled) states.setValue(nil, for: .highlighted) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -235,7 +235,7 @@ final class ControlPropertyStatesTests: XCTestCase { // Test with .selected value and true isSelected status. states.setValue(selectedValue, for: .selected) - var value = states.value(forStatus: status) + var value = states.value(for: status) // THEN XCTAssertEqual( @@ -250,7 +250,7 @@ final class ControlPropertyStatesTests: XCTestCase { // Test without .selected value and true isSelected status. states.setValue(nil, for: .selected) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -267,7 +267,7 @@ final class ControlPropertyStatesTests: XCTestCase { status = .init(isSelected: false) states.setValue(selectedValue, for: .selected) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -286,7 +286,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(selectedValue, for: .selected) states.setValue(highlightedValue, for: .highlighted) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -305,7 +305,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(selectedValue, for: .selected) states.setValue(nil, for: .highlighted) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -324,7 +324,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(selectedValue, for: .selected) states.setValue(disabledValue, for: .disabled) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -343,7 +343,7 @@ final class ControlPropertyStatesTests: XCTestCase { states.setValue(selectedValue, for: .selected) states.setValue(nil, for: .disabled) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertEqual( @@ -366,7 +366,7 @@ final class ControlPropertyStatesTests: XCTestCase { var status = ControlStatus() states.setValue(normalStateValue, for: .normal) - var value = states.value(forStatus: status) + var value = states.value(for: status) // THEN XCTAssertEqual( @@ -382,7 +382,7 @@ final class ControlPropertyStatesTests: XCTestCase { status = ControlStatus() states.setValue(nil, for: .normal) - value = states.value(forStatus: status) + value = states.value(for: status) // THEN XCTAssertNil( diff --git a/core/Sources/Common/Control/UIView/UIControlStateImageView.swift b/core/Sources/Common/Control/UIView/UIControlStateImageView.swift index 8da3d72e0..3f7d744ae 100644 --- a/core/Sources/Common/Control/UIView/UIControlStateImageView.swift +++ b/core/Sources/Common/Control/UIView/UIControlStateImageView.swift @@ -16,12 +16,40 @@ final class UIControlStateImageView: UIImageView { 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(forState: state) + return self.imageStates.value(for: state) } /// Set the image for a state. @@ -50,6 +78,6 @@ final class UIControlStateImageView: UIImageView { ) // Set the image from states - self.image = self.imageStates.value(forStatus: status) + 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 index 3310e2934..6cc425ce8 100644 --- a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift +++ b/core/Sources/Common/Control/UIView/UIControlStateLabel.swift @@ -18,11 +18,76 @@ final class UIControlStateLabel: UILabel { 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 @@ -58,7 +123,7 @@ final class UIControlStateLabel: UILabel { /// The text for a state. /// - parameter state: state of the text func text(for state: ControlState) -> String? { - return self.textStates.value(forState: state) + return self.textStates.value(for: state) } /// Set the text for a state. @@ -81,7 +146,7 @@ final class UIControlStateLabel: UILabel { /// The attributedText for a state. /// - parameter state: state of the attributedText func attributedText(for state: ControlState) -> NSAttributedString? { - return self.attributedTextStates.value(forState: state) + return self.attributedTextStates.value(for: state) } /// Set the attributedText of the button for a state. @@ -113,26 +178,24 @@ final class UIControlStateLabel: UILabel { ) // Get the current textType from status - let textType = textTypesStates.value(forStatus: status) + let textType = self.textTypesStates.value(for: status) + let textTypeContainsText = textType?.containsText ?? false // Reset attributedText & text - self.attributedText = nil - self.text = nil + self.storedAttributedText = nil + self.storedText = nil // Set the text or the attributedText from textType and states - switch textType { - case .text: - self.text = self.textStates.value(forStatus: status) - if let storedTextFont = self.storedTextFont { - self.font = storedTextFont - } - if let storedTextColor = self.storedTextColor { - self.textColor = storedTextColor - } - case .attributedText: - self.attributedText = self.attributedTextStates.value(forStatus: status) - default: - break + if let text = self.textStates.value(for: status), + textType == .text || !textTypeContainsText { + self.storedText = text + + } else if let attributedText = self.attributedTextStates.value(for: status), + textType == .attributedText || !textTypeContainsText { + self.storedAttributedText = attributedText + + } else { // No text to displayed + self.text = nil } } @@ -144,7 +207,7 @@ final class UIControlStateLabel: UILabel { guard let attributedText = self.attributedText else { return false } - + // The attributedText contains attributes ? return !attributedText.attributes(at: 0, effectiveRange: nil).isEmpty } diff --git a/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift b/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift index cb09385a3..91c2771b2 100644 --- a/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift +++ b/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift @@ -10,7 +10,7 @@ import Combine import UIKit /// The UIKit version for the button. -public final class ButtonUIView: UIView { +public final class ButtonUIView: UIControl { // MARK: - Type alias @@ -23,18 +23,19 @@ public final class ButtonUIView: UIView { let stackView = UIStackView( arrangedSubviews: [ - self.iconView, + self.imageContentView, self.titleLabel ] ) stackView.axis = .horizontal stackView.accessibilityIdentifier = AccessibilityIdentifier.contentStackView + stackView.isUserInteractionEnabled = false return stackView }() - private lazy var iconView: UIView = { + private lazy var imageContentView: UIView = { let view = UIView() - view.addSubview(self.iconImageView) + view.addSubview(self.imageView) view.accessibilityIdentifier = AccessibilityIdentifier.icon view.setContentCompressionResistancePriority(.required, for: .vertical) @@ -43,15 +44,23 @@ public final class ButtonUIView: UIView { return view }() - private var iconImageView: UIImageView = { - let imageView = UIImageView() + public var imageView: UIImageView { + return self.imageStateView + } + + private var imageStateView: UIControlStateImageView = { + let imageView = UIControlStateImageView() imageView.contentMode = .scaleAspectFit imageView.accessibilityIdentifier = AccessibilityIdentifier.iconImage return imageView }() - private var titleLabel: UILabel = { - let label = UILabel() + public var titleLabel: UILabel { + return self.titleStateLabel + } + + private var titleStateLabel: UIControlStateLabel = { + let label = UIControlStateLabel() label.numberOfLines = 1 label.lineBreakMode = .byWordWrapping label.textAlignment = .left @@ -61,41 +70,31 @@ public final class ButtonUIView: UIView { return label }() - private lazy var clearButton: UIButton = { - let button = UIButton() - button.isAccessibilityElement = false - button.addTarget(self, action: #selector(self.touchUpInsideAction), for: .touchUpInside) - button.addTarget(self, action: #selector(self.touchDownAction), for: .touchDown) - button.addTarget(self, action: #selector(self.touchUpOutsideAction), for: .touchUpOutside) - button.addTarget(self, action: #selector(self.touchCancelAction), for: .touchCancel) - button.accessibilityIdentifier = AccessibilityIdentifier.clearButton - return button - }() - // MARK: - Public Properties /// The delegate used to notify about some changed on button. + @available(*, deprecated, message: "Use native **action** or **target** on UIControl or publisher instead") public weak var delegate: ButtonUIViewDelegate? - /// The tap publisher. Alternatively, you can set a delegate. + /// The tap publisher. Alternatively, you can use the native **action** (addAction) or **target** (addTarget). public var tapPublisher: UIControl.EventPublisher { - return self.clearButton.publisher(for: .touchUpInside) + return self.publisher(for: .touchUpInside) } /// Publishes when a touch was cancelled (e.g. by the system). public var touchCancelPublisher: UIControl.EventPublisher { - return self.clearButton.publisher(for: .touchCancel) + 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.clearButton.publisher(for: .touchUpOutside) + 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.clearButton.publisher(for: .touchDown) + return self.publisher(for: .touchDown) } /// The spark theme of the button. @@ -159,12 +158,13 @@ public final class ButtonUIView: UIView { } /// The icon image of the button. + @available(*, deprecated, message: "Use setImage(_:, for:) and image(for:) instead") public var iconImage: UIImage? { get { - return self.viewModel.iconImage?.leftValue + return self.image(for: .normal) } set { - self.viewModel.set(iconImage: newValue.map { .left($0) }) + self.setImage(newValue, for: .normal) } } @@ -172,11 +172,10 @@ public final class ButtonUIView: UIView { @available(*, deprecated, message: "Use setTitle(_:, for:) and title(for:) instead") public var text: String? { get { - return self.titleLabel.text + return self.title(for: .normal) } set { - self.titleLabel.text = newValue - self.viewModel.set(title: newValue) + self.setTitle(newValue, for: .normal) } } @@ -184,11 +183,10 @@ public final class ButtonUIView: UIView { @available(*, deprecated, message: "Use setAttributedTitle(_:, for:) and attributedTitle(for:) instead") public var attributedText: NSAttributedString? { get { - return self.titleLabel.attributedText + return self.attributedTitle(for: .normal) } set { - self.titleLabel.attributedText = newValue - self.viewModel.set(attributedTitle: newValue.map { .left($0) }) + self.setAttributedTitle(newValue, for: .normal) } } @@ -203,13 +201,38 @@ public final class ButtonUIView: UIView { } } - /// The state of the button: enabled or not. - public var isEnabled: Bool { + /// 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.set(isEnabled: newValue) + self.titleStateLabel.updateContent(from: self) + self.imageStateView.updateContent(from: self) + } + } + + /// A Boolean value indicating whether the button is in the selected state. + public override var isSelected: Bool { + didSet { + self.titleStateLabel.updateContent(from: self) + self.imageStateView.updateContent(from: self) + } + } + + /// A Boolean value indicating whether the button draws a highlight. + public override var isHighlighted: Bool { + didSet { + self.titleStateLabel.updateContent(from: self) + self.imageStateView.updateContent(from: self) + + if self.isHighlighted { + self.viewModel.pressedAction() + } else { + self.viewModel.unpressedAction() + } } } @@ -228,7 +251,7 @@ public final class ButtonUIView: UIView { private var contentStackViewTopConstraint: NSLayoutConstraint? private var contentStackViewBottomConstraint: NSLayoutConstraint? - private var iconImageViewHeightConstraint: NSLayoutConstraint? + private var imageViewHeightConstraint: NSLayoutConstraint? @ScaledUIMetric private var height: CGFloat = 0 @ScaledUIMetric private var verticalSpacing: CGFloat = 0 @@ -244,6 +267,38 @@ public final class ButtonUIView: UIView { // 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. + /// - isEnabled: The state of the button: enabled or not. + public convenience init( + theme: Theme, + intent: ButtonIntent, + variant: ButtonVariant, + size: ButtonSize, + shape: ButtonShape, + alignment: ButtonAlignment, + isEnabled: Bool + ) { + self.init( + theme, + intent: intent, + variant: variant, + size: size, + shape: shape, + alignment: alignment, + iconImage: nil, + text: nil, + attributedText: nil, + isEnabled: isEnabled + ) + } + /// Initialize a new button view with a text. /// - Parameters: /// - theme: The spark theme of the button. @@ -254,6 +309,7 @@ public final class ButtonUIView: UIView { /// - alignment: The alignment of the button. /// - text: The text of the button. /// - isEnabled: The state of the button: enabled or not. + @available(*, deprecated, message: "Use init(theme: , intent: , variant: , size: , shape: , alignment: , isEnabled) instead") public convenience init( theme: Theme, intent: ButtonIntent, @@ -288,6 +344,7 @@ public final class ButtonUIView: UIView { /// - alignment: The alignment of the button. /// - attributedText: The attributed text of the button. /// - isEnabled: The state of the button: enabled or not. + @available(*, deprecated, message: "Use init(theme: , intent: , variant: , size: , shape: , alignment: , isEnabled) instead") public convenience init( theme: Theme, intent: ButtonIntent, @@ -322,6 +379,7 @@ public final class ButtonUIView: UIView { /// - alignment: The alignment of the button. /// - iconImage: The icon image of the button. /// - isEnabled: The state of the button: enabled or not. + @available(*, deprecated, message: "Use init(theme: , intent: , variant: , size: , shape: , alignment: , isEnabled) instead") public convenience init( theme: Theme, intent: ButtonIntent, @@ -357,6 +415,7 @@ public final class ButtonUIView: UIView { /// - iconImage: The icon image of the button. /// - text: The text of the button. /// - isEnabled: The state of the button: enabled or not. + @available(*, deprecated, message: "Use init(theme: , intent: , variant: , size: , shape: , alignment: , isEnabled) instead") public convenience init( theme: Theme, intent: ButtonIntent, @@ -393,6 +452,7 @@ public final class ButtonUIView: UIView { /// - iconImage: The icon image of the button. /// - attributedText: The attributed text of the button. /// - isEnabled: The state of the button: enabled or not. + @available(*, deprecated, message: "Use init(theme: , intent: , variant: , size: , shape: , alignment: , isEnabled) instead") public convenience init( theme: Theme, intent: ButtonIntent, @@ -460,7 +520,6 @@ public final class ButtonUIView: UIView { self.accessibilityTraits = [.button] // Add subviews self.addSubview(self.contentStackView) - self.addSubview(self.clearButton) // Needed values from viewModel (important for superview) self.height = self.viewModel.sizes?.height ?? 0 @@ -471,6 +530,9 @@ public final class ButtonUIView: UIView { // Setup publisher subcriptions self.setupSubscriptions() + // Setup actions + self.setupActions() + // Load view model self.viewModel.load() } @@ -506,7 +568,6 @@ public final class ButtonUIView: UIView { self.setupContentStackViewConstraints() self.setupIconViewConstraints() self.setupIconImageViewConstraints() - self.setupClearButtonConstraints() } private func setupViewConstraints() { @@ -535,24 +596,74 @@ public final class ButtonUIView: UIView { } private func setupIconViewConstraints() { - self.iconView.translatesAutoresizingMaskIntoConstraints = false - self.iconView.widthAnchor.constraint(greaterThanOrEqualTo: self.iconImageView.widthAnchor).isActive = true + self.imageContentView.translatesAutoresizingMaskIntoConstraints = false + self.imageContentView.widthAnchor.constraint(greaterThanOrEqualTo: self.imageView.widthAnchor).isActive = true } private func setupIconImageViewConstraints() { - self.iconImageView.translatesAutoresizingMaskIntoConstraints = false + self.imageView.translatesAutoresizingMaskIntoConstraints = false + + self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.iconHeight) + self.imageViewHeightConstraint?.isActive = true + + NSLayoutConstraint.activate([ + self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor), + self.imageView.centerXAnchor.constraint(equalTo: self.imageContentView.centerXAnchor), + self.imageView.centerYAnchor.constraint(equalTo: self.imageContentView.centerYAnchor) + ]) + } + + // 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) + } - self.iconImageViewHeightConstraint = self.iconImageView.heightAnchor.constraint(equalToConstant: self.iconHeight) - self.iconImageViewHeightConstraint?.isActive = true + /// 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) { + if state == .normal { + self.viewModel.set(iconImage: image.map { .left($0) }) + } + + self.imageStateView.setImage(image, for: state, on: self) + } - self.iconImageView.widthAnchor.constraint(equalTo: self.iconImageView.heightAnchor).isActive = true - self.iconImageView.centerXAnchor.constraint(equalTo: self.iconView.centerXAnchor).isActive = true - self.iconImageView.centerYAnchor.constraint(equalTo: self.iconView.centerYAnchor).isActive = true + /// 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) } - private func setupClearButtonConstraints() { - self.clearButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.stickEdges(from: self.clearButton, to: self) + /// 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) { + if state == .normal { + self.viewModel.set(title: title) + } + + self.titleStateLabel.setText(title, for: state, on: self) + } + + /// 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) { + if state == .normal { + self.viewModel.set(attributedTitle: attributedTitle.map { .left($0) }) + } + + self.titleStateLabel.setAttributedText(attributedTitle, for: state, on: self) } // MARK: - Update UI @@ -599,9 +710,9 @@ public final class ButtonUIView: UIView { private func updateIconHeight() { // Reload height only if value changed - if self.iconImageViewHeightConstraint?.constant != self.iconHeight { - self.iconImageViewHeightConstraint?.constant = self.iconHeight - self.iconImageView.updateConstraintsIfNeeded() + if self.imageViewHeightConstraint?.constant != self.iconHeight { + self.imageViewHeightConstraint?.constant = self.iconHeight + self.imageView.updateConstraintsIfNeeded() } } @@ -614,7 +725,7 @@ public final class ButtonUIView: UIView { guard let self, let state else { return } // Update the user interaction enabled - self.clearButton.isUserInteractionEnabled = state.isUserInteractionEnabled + self.isUserInteractionEnabled = state.isUserInteractionEnabled if !state.isUserInteractionEnabled { self.accessibilityTraits.insert(.notEnabled) } else { @@ -641,7 +752,7 @@ public final class ButtonUIView: UIView { // 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 } @@ -650,7 +761,7 @@ public final class ButtonUIView: UIView { self.setBorderColor(from: colors.borderColor) // Foreground Color - self.iconImageView.tintColor = colors.iconTintColor.uiColor + self.imageView.tintColor = colors.iconTintColor.uiColor if let titleColor = colors.titleColor { self.titleLabel.textColor = titleColor.uiColor } @@ -711,7 +822,7 @@ public final class ButtonUIView: UIView { guard let self, let content else { return } // Icon ImageView - self.iconImageView.image = content.iconImage?.leftValue + self.imageView.image = content.iconImage?.leftValue // Subviews positions and visibilities let isAnimated = self.isAnimated && !self.firstContentStackViewSubviewAnimation @@ -722,8 +833,8 @@ public final class ButtonUIView: UIView { self.firstContentStackViewSubviewAnimation = false - if self.iconView.isHidden == content.shouldShowIconImage { - self.iconView.isHidden = !content.shouldShowIconImage + if self.imageContentView.isHidden == content.shouldShowIconImage { + self.imageContentView.isHidden = !content.shouldShowIconImage } if self.titleLabel.isHidden == content.shouldShowTitle { @@ -745,36 +856,50 @@ public final class ButtonUIView: UIView { self.titleLabel.font = titleFontToken.uiFont } // ** - } - - // MARK: - Actions - @objc private func touchUpInsideAction() { - self.unpressedAction() - - self.delegate?.button(self, didReceive: .touchUpInside) - self.delegate?.buttonWasTapped(self) - } + // ** + // Is Image ? + self.imageStateView.$isImage.subscribe(in: &self.subscriptions) { [weak self] isImage in + guard let self else { return } - @objc private func touchDownAction() { - self.viewModel.pressedAction() - self.delegate?.button(self, didReceive: .touchDown) - } + self.imageContentView.isHidden = !isImage + } - @objc private func touchUpOutsideAction() { - self.unpressedAction() - self.delegate?.button(self, didReceive: .touchUpOutside) + // ** + // Is Text ? + self.titleStateLabel.$isText.subscribe(in: &self.subscriptions) { [weak self] isText in + guard let self else { return } + self.titleLabel.isHidden = !isText + } } - @objc private func touchCancelAction() { - self.unpressedAction() - self.delegate?.button(self, didReceive: .touchCancel) - } + // MARK: - Actions - private func unpressedAction() { - DispatchQueue.main.asyncAfter(deadline: .now() + Animation.fastDuration, execute: { [weak self] in - self?.viewModel.unpressedAction() - }) + private func setupActions() { + // Touch down + self.addAction(.init(handler: { [weak self] _ in + guard let self else { return } + self.delegate?.button(self, didReceive: .touchDown) + }), for: .touchDown) + + // Touch Up Inside + self.addAction(.init(handler: { [weak self] _ in + guard let self else { return } + self.delegate?.button(self, didReceive: .touchUpInside) + self.delegate?.buttonWasTapped(self) + }), for: .touchUpInside) + + // Touch Up Outside + self.addAction(.init(handler: { [weak self] _ in + guard let self else { return } + self.delegate?.button(self, didReceive: .touchUpOutside) + }), for: .touchUpOutside) + + // Touch Cancel + self.addAction(.init(handler: { [weak self] _ in + guard let self else { return } + self.delegate?.button(self, didReceive: .touchCancel) + }), for: .touchCancel) } // MARK: - Trait Collection diff --git a/core/Sources/Components/Button/View/UIKit/ButtonUIViewDelegate.swift b/core/Sources/Components/Button/View/UIKit/ButtonUIViewDelegate.swift index 5d124e1c4..394afdd87 100644 --- a/core/Sources/Components/Button/View/UIKit/ButtonUIViewDelegate.swift +++ b/core/Sources/Components/Button/View/UIKit/ButtonUIViewDelegate.swift @@ -9,6 +9,7 @@ import Foundation /// Implement the delegate to receive tap and touch events. +@available(*, deprecated, message: "Use native **action** or **target** on UIControl or publisher instead") public protocol ButtonUIViewDelegate: AnyObject { /// Optionally implement this method to receive tap events and perform actions. /// - Parameter button: the button that was tapped diff --git a/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift b/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift index 8b3541acd..bfe46f358 100644 --- a/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift +++ b/core/Sources/Components/Switch/View/SwiftUI/SwitchView.swift @@ -31,10 +31,10 @@ public struct SwitchView: View { /// Initialize a new switch view /// - Parameters: - /// - isOn: The Binding value of the switch. /// - 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, diff --git a/spark/Demo/Classes/View/Components/Button/ButtonContent.swift b/spark/Demo/Classes/View/Components/Button/ButtonContent.swift index bd079bbff..fa120a16d 100644 --- a/spark/Demo/Classes/View/Components/Button/ButtonContent.swift +++ b/spark/Demo/Classes/View/Components/Button/ButtonContent.swift @@ -12,4 +12,5 @@ enum ButtonContentDefault: CaseIterable { case attributedText case iconAndText case iconAndAttributedText + case none } diff --git a/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift b/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift index de7c8f250..c9594a1a8 100644 --- a/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift +++ b/spark/Demo/Classes/View/Components/Button/SwiftUI/ButtonComponentView.swift @@ -26,6 +26,7 @@ struct ButtonComponentView: View { @State private var alignment: ButtonAlignment = .leadingIcon @State private var content: ButtonContentDefault = .text @State private var isEnabled: CheckboxSelectionState = .selected + @State private var isSelected: CheckboxSelectionState = .selected @State private var isAnimated: CheckboxSelectionState = .selected @State private var shouldShowReverseBackgroundColor: Bool = false @@ -91,6 +92,14 @@ struct ButtonComponentView: View { selectionState: self.$isEnabled ) + CheckboxView( + text: "Is selected", + checkedImage: DemoIconography.shared.checkmark, + theme: self.theme, + state: .enabled, + selectionState: self.$isSelected + ) + CheckboxView( text: "Is animated", checkedImage: DemoIconography.shared.checkmark, @@ -112,6 +121,7 @@ struct ButtonComponentView: View { alignment: self.$alignment.wrappedValue, content: self.$content.wrappedValue, isEnabled: self.$isEnabled.wrappedValue == .selected, + isSelected: self.$isSelected.wrappedValue == .selected, isAnimated: self.$isAnimated.wrappedValue == .selected ) .frame(width: geometry.size.width, height: self.uiKitViewHeight, alignment: .center) diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift index ec0874cea..02d6cfe4e 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift @@ -29,6 +29,7 @@ struct ButtonComponentItemsUIView: UIViewRepresentable { private let alignment: ButtonAlignment private let content: ButtonContentDefault private let isEnabled: Bool + private let isSelected: Bool private let isAnimated: Bool // MARK: - Initialization @@ -44,6 +45,7 @@ struct ButtonComponentItemsUIView: UIViewRepresentable { alignment: ButtonAlignment, content: ButtonContentDefault, isEnabled: Bool, + isSelected: Bool, isAnimated: Bool ) { self.viewModel = viewModel @@ -64,6 +66,7 @@ struct ButtonComponentItemsUIView: UIViewRepresentable { self.alignment = alignment self.content = content self.isEnabled = isEnabled + self.isSelected = isSelected self.isAnimated = isAnimated } @@ -134,6 +137,17 @@ struct ButtonComponentItemsUIView: UIViewRepresentable { attributedText: self.attributedText, isEnabled: self.isEnabled ) + + default: + buttonView = ButtonUIView( + theme: SparkTheme.shared, + intent: self.intent, + variant: self.variant, + size: self.size, + shape: self.shape, + alignment: self.alignment, + isEnabled: self.isEnabled + ) } let stackView = UIStackView(arrangedSubviews: [ @@ -195,12 +209,21 @@ struct ButtonComponentItemsUIView: UIViewRepresentable { case .iconAndAttributedText: buttonView.iconImage = self.iconImage buttonView.attributedText = self.attributedText + + default: + buttonView.text = nil + buttonView.attributedText = nil + buttonView.iconImage = nil } if buttonView.isEnabled != self.isEnabled { buttonView.isEnabled = self.isEnabled } + if buttonView.isSelected != self.isSelected { + buttonView.isSelected = self.isSelected + } + if buttonView.isAnimated != self.isAnimated { buttonView.isAnimated = self.isAnimated } diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift index e9ea132a1..a025fcd52 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift @@ -18,69 +18,15 @@ final class ButtonComponentUIView: ComponentUIView { private let buttonView: ButtonUIView private static func makeButtonView(_ viewModel: ButtonComponentUIViewModel) -> ButtonUIView { - switch viewModel.content { - case .icon: - return .init( - theme: viewModel.theme, - intent: viewModel.intent, - variant: viewModel.variant, - size: viewModel.size, - shape: viewModel.shape, - alignment: viewModel.alignment, - iconImage: viewModel.iconImage, - isEnabled: viewModel.isEnabled - ) - - case .text: - return .init( - theme: viewModel.theme, - intent: viewModel.intent, - variant: viewModel.variant, - size: viewModel.size, - shape: viewModel.shape, - alignment: viewModel.alignment, - text: viewModel.text, - isEnabled: viewModel.isEnabled - ) - - case .attributedText: - return .init( - theme: viewModel.theme, - intent: viewModel.intent, - variant: viewModel.variant, - size: viewModel.size, - shape: viewModel.shape, - alignment: viewModel.alignment, - attributedText: viewModel.attributedText, - isEnabled: viewModel.isEnabled - ) - - case .iconAndText: - return .init( - theme: viewModel.theme, - intent: viewModel.intent, - variant: viewModel.variant, - size: viewModel.size, - shape: viewModel.shape, - alignment: viewModel.alignment, - iconImage: viewModel.iconImage, - text: viewModel.text, - isEnabled: viewModel.isEnabled - ) - - case .iconAndAttributedText: - return .init( - theme: viewModel.theme, - intent: viewModel.intent, - variant: viewModel.variant, - size: viewModel.size, - shape: viewModel.shape, - alignment: viewModel.alignment, - iconImage: viewModel.iconImage, - attributedText: viewModel.attributedText, - isEnabled: viewModel.isEnabled - ) - } + return .init( + theme: viewModel.theme, + intent: viewModel.intent, + variant: viewModel.variant, + size: viewModel.size, + shape: viewModel.shape, + alignment: viewModel.alignment, + isEnabled: viewModel.isEnabled + ) } // MARK: - Properties @@ -88,6 +34,11 @@ final class ButtonComponentUIView: ComponentUIView { private let viewModel: ButtonComponentUIViewModel private var cancellables: Set = [] + private lazy var buttonAction: UIAction = .init { _ in + self.showAlert(for: .action) + } + private var buttonControlCancellable: AnyCancellable? + // MARK: - Initializer init(viewModel: ButtonComponentUIViewModel) { @@ -151,34 +102,33 @@ final class ButtonComponentUIView: ComponentUIView { self.buttonView.alignment = alignment } - self.viewModel.$content.subscribe(in: &self.cancellables) { [weak self] content in + self.viewModel.$contentNormal.subscribe(in: &self.cancellables) { [weak self] content in guard let self = self else { return } - self.viewModel.contentConfigurationItemViewModel.buttonTitle = content.name - + self.viewModel.contentNormalConfigurationItemViewModel.buttonTitle = content.name self.showRightSpacing = content != .icon - switch content { - case .icon: - self.buttonView.text = nil - self.buttonView.attributedText = nil - self.buttonView.iconImage = self.viewModel.iconImage - - case .text: - self.buttonView.iconImage = nil - self.buttonView.text = self.viewModel.text - - case .attributedText: - self.buttonView.iconImage = nil - self.buttonView.attributedText = self.viewModel.attributedText - - case .iconAndText: - self.buttonView.iconImage = self.viewModel.iconImage - self.buttonView.text = self.viewModel.text - - case .iconAndAttributedText: - self.buttonView.iconImage = self.viewModel.iconImage - self.buttonView.attributedText = self.viewModel.attributedText - } + self.setContent(content, for: .normal) + } + + self.viewModel.$contentHighlighted.subscribe(in: &self.cancellables) { [weak self] content in + guard let self = self else { return } + + self.viewModel.contentHighlightedConfigurationItemViewModel.buttonTitle = content.name + self.setContent(content, for: .highlighted) + } + + self.viewModel.$contentDisabled.subscribe(in: &self.cancellables) { [weak self] content in + guard let self = self else { return } + + self.viewModel.contentDisabledConfigurationItemViewModel.buttonTitle = content.name + self.setContent(content, for: .disabled) + } + + self.viewModel.$contentSelected.subscribe(in: &self.cancellables) { [weak self] content in + guard let self = self else { return } + + self.viewModel.contentSelectedConfigurationItemViewModel.buttonTitle = content.name + self.setContent(content, for: .selected) } self.viewModel.$isEnabled.subscribe(in: &self.cancellables) { [weak self] isEnabled in @@ -186,9 +136,156 @@ final class ButtonComponentUIView: ComponentUIView { self.buttonView.isEnabled = isEnabled } + self.viewModel.$isSelected.subscribe(in: &self.cancellables) { [weak self] isSelected in + guard let self = self else { return } + self.buttonView.isSelected = isSelected + } + self.viewModel.$isAnimated.subscribe(in: &self.cancellables) { [weak self] isAnimated in guard let self = self else { return } self.buttonView.isAnimated = isAnimated } + + self.viewModel.$controlType.subscribe(in: &self.cancellables) { [weak self] controlType in + guard let self = self else { return } + self.viewModel.controlTypeConfigurationItemViewModel.buttonTitle = controlType.name + self.setControl(from: controlType) + } + } + + // MARK: - Setter + + private func setContent(_ content: ButtonContentDefault, for state: ControlState) { + switch content { + case .icon: + self.buttonView.setTitle(nil, for: state) + self.buttonView.setAttributedTitle(nil, for: state) + self.buttonView.setImage(self.image(for: state), for: state) + + case .text: + self.buttonView.setImage(nil, for: state) + self.buttonView.setAttributedTitle(nil, for: state) + self.buttonView.setTitle(self.title(for: state), for: state) + + case .attributedText: + self.buttonView.setImage(nil, for: state) + self.buttonView.setTitle(nil, for: state) + self.buttonView.setAttributedTitle(self.attributedTitle(for: state), for: state) + + case .iconAndText: + self.buttonView.setImage(self.image(for: state), for: state) + self.buttonView.setAttributedTitle(nil, for: state) + self.buttonView.setTitle(self.title(for: state), for: state) + + case .iconAndAttributedText: + self.buttonView.setImage(self.image(for: state), for: state) + self.buttonView.setTitle(nil, for: state) + self.buttonView.setAttributedTitle(self.attributedTitle(for: state), for: state) + + case .none: + self.buttonView.setTitle(nil, for: state) + self.buttonView.setAttributedTitle(nil, for: state) + self.buttonView.setImage(nil, for: state) + } + } + + private func setControl(from controlType: ButtonControlType) { + // Delegate ? + self.buttonView.delegate = controlType == .delegate ? self : nil + + // Publisher ? + if controlType == .publisher { + self.buttonControlCancellable = self.buttonView.tapPublisher.sink { _ in + self.showAlert(for: .publisher) + } + } else { + self.buttonControlCancellable?.cancel() + self.buttonControlCancellable = nil + } + + // Action ? + if controlType == .action { + self.buttonView.addAction(self.buttonAction, for: .touchUpInside) + } else { + self.buttonView.removeAction(self.buttonAction, for: .touchUpInside) + } + + // Target ? + if controlType == .target { + self.buttonView.addTarget(self, action: #selector(self.touchUpInsideTarget), for: .touchUpInside) + } else { + self.buttonView.removeTarget(self, action: #selector(self.touchUpInsideTarget), for: .touchUpInside) + } + } + + // MARK: - Getter + + private func image(for state: ControlState) -> UIImage? { + switch state { + case .normal: return UIImage(named: "arrow") + case .highlighted: return UIImage(named: "close") + case .disabled: return UIImage(named: "check") + case .selected: return UIImage(named: "alert") + @unknown default: return nil + } + } + + private func title(for state: ControlState) -> String? { + switch state { + case .normal: return "My Title" + case .highlighted: return "My Highlighted" + case .disabled: return "My Disabled" + case .selected: return "My Selected" + @unknown default: return nil + } + } + + private func attributedTitle(for state: ControlState) -> NSAttributedString? { + + func attributedText(_ text: String) -> NSAttributedString { + return .init( + string: text, + attributes: [ + .foregroundColor: UIColor.purple, + .font: UIFont.italicSystemFont(ofSize: 20), + .underlineStyle: NSUnderlineStyle.single.rawValue + ] + ) + } + + switch state { + case .normal: return attributedText("My A_Title") + case .highlighted: return attributedText("My A_Highlighted") + case .disabled: return attributedText("My A_Disabled") + case .selected: return attributedText("My A_Selected") + @unknown default: return nil + } + } + + // MARK: - Action + + @objc func touchUpInsideTarget() { + self.showAlert(for: .target) + } + + // MARK: - Alert + + func showAlert(for controlType: ButtonControlType) { + let alertController = UIAlertController( + title: "Button tap from " + controlType.name, + message: nil, + preferredStyle: .alert + ) + alertController.addAction(.init(title: "Ok", style: .default)) + self.viewController?.present(alertController, animated: true) + } +} + +// MARK: - ButtonUIViewDelegate + +extension ButtonComponentUIView: ButtonUIViewDelegate { + + func buttonWasTapped(_ button: ButtonUIView) { + self.showAlert(for: .delegate) } } diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift index 7ba87b7f2..ebd06f55a 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift @@ -45,8 +45,28 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { .eraseToAnyPublisher() } - var showContentSheet: AnyPublisher<[ButtonContentDefault], Never> { - showContentSheetSubject + var showContentNormalSheet: AnyPublisher<[ButtonContentDefault], Never> { + showContentNormalSheetSubject + .eraseToAnyPublisher() + } + + var showContentHighlightedSheet: AnyPublisher<[ButtonContentDefault], Never> { + showContentHighlightedSheetSubject + .eraseToAnyPublisher() + } + + var showContentDisabledSheet: AnyPublisher<[ButtonContentDefault], Never> { + showContentDisabledSheetSubject + .eraseToAnyPublisher() + } + + var showContentSelectedSheet: AnyPublisher<[ButtonContentDefault], Never> { + showContentSelectedSheetSubject + .eraseToAnyPublisher() + } + + var showControlType: AnyPublisher<[ButtonControlType], Never> { + showControlTypeSheetSubject .eraseToAnyPublisher() } @@ -58,9 +78,14 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { @Published var size: ButtonSize @Published var shape: ButtonShape @Published var alignment: ButtonAlignment - @Published var content: ButtonContentDefault + @Published var contentNormal: ButtonContentDefault + @Published var contentHighlighted: ButtonContentDefault + @Published var contentDisabled: ButtonContentDefault + @Published var contentSelected: ButtonContentDefault @Published var isEnabled: Bool + @Published var isSelected: Bool @Published var isAnimated: Bool + @Published var controlType: ButtonControlType // MARK: - Items Properties @@ -112,11 +137,35 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { ) }() - lazy var contentConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + lazy var contentNormalConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Content (normal state)", + type: .button, + target: (source: self, action: #selector(self.presentContentNormalSheet)) + ) + }() + + lazy var contentHighlightedConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Content (highlighted state)", + type: .button, + target: (source: self, action: #selector(self.presentContentHighlightedCSheet)) + ) + }() + + lazy var contentDisabledConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Content (disabled state)", + type: .button, + target: (source: self, action: #selector(self.presentContentDisabledSheet)) + ) + }() + + lazy var contentSelectedConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { return .init( - name: "Content", + name: "Content (selected state)", type: .button, - target: (source: self, action: #selector(self.presentContentSheet)) + target: (source: self, action: #selector(self.presentContentSelectedSheet)) ) }() @@ -128,6 +177,14 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { ) }() + lazy var isSelectedConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Is Selected", + type: .toggle(isOn: self.isSelected), + target: (source: self, action: #selector(self.isSelectedChanged)) + ) + }() + lazy var isAnimatedConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { return .init( name: "Is Animated", @@ -136,6 +193,14 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { ) }() + lazy var controlTypeConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Control Type", + type: .button, + target: (source: self, action: #selector(self.presentControlTypeSheet)) + ) + }() + // MARK: - Properties let text: String @@ -152,9 +217,14 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { self.sizeConfigurationItemViewModel, self.shapeConfigurationItemViewModel, self.alignmentConfigurationItemViewModel, - self.contentConfigurationItemViewModel, + self.contentNormalConfigurationItemViewModel, + self.contentHighlightedConfigurationItemViewModel, + self.contentDisabledConfigurationItemViewModel, + self.contentSelectedConfigurationItemViewModel, self.isEnabledConfigurationItemViewModel, - self.isAnimatedConfigurationItemViewModel + self.isSelectedConfigurationItemViewModel, + self.isAnimatedConfigurationItemViewModel, + self.controlTypeConfigurationItemViewModel ] } @@ -166,7 +236,11 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { private var showSizeSheetSubject: PassthroughSubject<[ButtonSize], Never> = .init() private var showShapeSheetSubject: PassthroughSubject<[ButtonShape], Never> = .init() private var showAlignmentSheetSubject: PassthroughSubject<[ButtonAlignment], Never> = .init() - private var showContentSheetSubject: PassthroughSubject<[ButtonContentDefault], Never> = .init() + private var showContentNormalSheetSubject: PassthroughSubject<[ButtonContentDefault], Never> = .init() + private var showContentHighlightedSheetSubject: PassthroughSubject<[ButtonContentDefault], Never> = .init() + private var showContentDisabledSheetSubject: PassthroughSubject<[ButtonContentDefault], Never> = .init() + private var showContentSelectedSheetSubject: PassthroughSubject<[ButtonContentDefault], Never> = .init() + private var showControlTypeSheetSubject: PassthroughSubject<[ButtonControlType], Never> = .init() // MARK: - Initialization @@ -179,9 +253,14 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { size: ButtonSize = .medium, shape: ButtonShape = .rounded, alignment: ButtonAlignment = .leadingIcon, - content: ButtonContentDefault = .text, + contentNormal: ButtonContentDefault = .text, + contentHighlighted: ButtonContentDefault = .text, + contentDisabled: ButtonContentDefault = .text, + contentSelected: ButtonContentDefault = .text, isEnabled: Bool = true, - isAnimated: Bool = true + isSelected: Bool = false, + isAnimated: Bool = true, + controlType: ButtonControlType = .action ) { self.text = text self.iconImage = .init(named: iconImageNamed) ?? UIImage() @@ -198,9 +277,14 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { self.size = size self.shape = shape self.alignment = alignment - self.content = content + self.contentNormal = contentNormal + self.contentHighlighted = contentHighlighted + self.contentDisabled = contentDisabled + self.contentSelected = contentSelected self.isEnabled = isEnabled + self.isSelected = isSelected self.isAnimated = isAnimated + self.controlType = controlType super.init(identifier: "Button") } @@ -234,15 +318,35 @@ extension ButtonComponentUIViewModel { self.showAlignmentSheetSubject.send(ButtonAlignment.allCases) } - @objc func presentContentSheet() { - self.showContentSheetSubject.send(ButtonContentDefault.allCases) + @objc func presentContentNormalSheet() { + self.showContentNormalSheetSubject.send(ButtonContentDefault.allCases) + } + + @objc func presentContentHighlightedCSheet() { + self.showContentHighlightedSheetSubject.send(ButtonContentDefault.allCases) + } + + @objc func presentContentDisabledSheet() { + self.showContentDisabledSheetSubject.send(ButtonContentDefault.allCases) + } + + @objc func presentContentSelectedSheet() { + self.showContentSelectedSheetSubject.send(ButtonContentDefault.allCases) } @objc func isEnabledChanged() { self.isEnabled.toggle() } + @objc func isSelectedChanged() { + self.isSelected.toggle() + } + @objc func isAnimatedChanged() { self.isAnimated.toggle() } + + @objc func presentControlTypeSheet() { + self.showControlTypeSheetSubject.send(ButtonControlType.allCases) + } } diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift index aba85c867..ff6989af4 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift @@ -84,8 +84,24 @@ final class ButtonComponentViewController: UIViewController { self.presentAlignmentActionSheet(alignments) } - self.viewModel.showContentSheet.subscribe(in: &self.cancellables) { contents in - self.presentContentActionSheet(contents) + self.viewModel.showContentNormalSheet.subscribe(in: &self.cancellables) { contents in + self.presentContentNormalActionSheet(contents) + } + + self.viewModel.showContentHighlightedSheet.subscribe(in: &self.cancellables) { contents in + self.presentContentHighlightedActionSheet(contents) + } + + self.viewModel.showContentDisabledSheet.subscribe(in: &self.cancellables) { contents in + self.presentContentDisabledActionSheet(contents) + } + + self.viewModel.showContentSelectedSheet.subscribe(in: &self.cancellables) { contents in + self.presentContentSelectedActionSheet(contents) + } + + self.viewModel.showControlType.subscribe(in: &self.cancellables) { types in + self.presentControlTypeActionSheet(types) } } } @@ -157,13 +173,48 @@ extension ButtonComponentViewController { self.present(actionSheet, animated: true) } - private func presentContentActionSheet(_ contents: [ButtonContentDefault]) { + private func presentContentNormalActionSheet(_ contents: [ButtonContentDefault]) { + let actionSheet = SparkActionSheet.init( + values: contents, + texts: contents.map { $0.name }) { content in + self.viewModel.contentNormal = content + } + self.present(actionSheet, animated: true) + } + + private func presentContentHighlightedActionSheet(_ contents: [ButtonContentDefault]) { + let actionSheet = SparkActionSheet.init( + values: contents, + texts: contents.map { $0.name }) { content in + self.viewModel.contentHighlighted = content + } + self.present(actionSheet, animated: true) + } + + private func presentContentDisabledActionSheet(_ contents: [ButtonContentDefault]) { let actionSheet = SparkActionSheet.init( values: contents, texts: contents.map { $0.name }) { content in - self.viewModel.content = content + self.viewModel.contentDisabled = content } self.present(actionSheet, animated: true) } + private func presentContentSelectedActionSheet(_ contents: [ButtonContentDefault]) { + let actionSheet = SparkActionSheet.init( + values: contents, + texts: contents.map { $0.name }) { content in + self.viewModel.contentSelected = content + } + self.present(actionSheet, animated: true) + } + + private func presentControlTypeActionSheet(_ contents: [ButtonControlType]) { + let actionSheet = SparkActionSheet.init( + values: contents, + texts: contents.map { $0.name }) { content in + self.viewModel.controlType = content + } + self.present(actionSheet, animated: true) + } } diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift new file mode 100644 index 000000000..4e91103c2 --- /dev/null +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonControlType.swift @@ -0,0 +1,16 @@ +// +// ButtonActionType.swift +// SparkCore +// +// Created by robin.lemaire on 06/11/2023. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +enum ButtonControlType: CaseIterable { + case delegate + case publisher + case action + case target +}