From 096dc802043dfac6ff5acd24a75891c150aa56b8 Mon Sep 17 00:00:00 2001 From: Robin Lemaire Date: Tue, 7 Nov 2023 09:36:35 +0100 Subject: [PATCH] [Refacto-557] Button: Remove clear button - Improve State --- .../ControlPropertyStates.swift | 2 +- .../UIView/UIControlStateImageView.swift | 18 +- .../Control/UIView/UIControlStateLabel.swift | 73 +++++--- .../Button/View/UIKit/ButtonUIView.swift | 166 ++++++++---------- .../Components/Button/ButtonContent.swift | 1 + .../UIKit/ButtonComponentItemsUIView.swift | 16 ++ .../Button/UIKit/ButtonComponentUIView.swift | 28 +-- 7 files changed, 176 insertions(+), 128 deletions(-) diff --git a/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift b/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift index 640543d2b..1d23d4a2f 100644 --- a/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift +++ b/core/Sources/Common/Control/PropertyStates/ControlPropertyStates.swift @@ -61,7 +61,7 @@ final class ControlPropertyStates { /// Get the value for the status of the control. /// - Parameters: - /// - state: the status of the control + /// - status: the status of the control func value(forStatus status: ControlStatus) -> PropertyType? { // isHighlighted has the highest priority, // then isDisabled, diff --git a/core/Sources/Common/Control/UIView/UIControlStateImageView.swift b/core/Sources/Common/Control/UIView/UIControlStateImageView.swift index 95984ecbb..02ddb06ee 100644 --- a/core/Sources/Common/Control/UIView/UIControlStateImageView.swift +++ b/core/Sources/Common/Control/UIView/UIControlStateImageView.swift @@ -16,6 +16,13 @@ final class UIControlStateImageView: UIImageView { private let imageStates = ControlPropertyStates() + private var storedImage: UIImage? { + didSet { + self.isImage = self.storedImage != nil + self.image = self.storedImage + } + } + // MARK: - Published @Published var isImage: Bool = false @@ -23,8 +30,13 @@ final class UIControlStateImageView: UIImageView { // MARK: - Override Properties override var image: UIImage? { - didSet { - self.isImage = self.image != nil + get { + return super.image + } + set { + if newValue == self.storedImage { + super.image = newValue + } } } @@ -62,6 +74,6 @@ final class UIControlStateImageView: UIImageView { ) // Set the image from states - self.image = self.imageStates.value(forStatus: status) + self.storedImage = self.imageStates.value(forStatus: status) } } diff --git a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift b/core/Sources/Common/Control/UIView/UIControlStateLabel.swift index b64a2727b..78818d3f9 100644 --- a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift +++ b/core/Sources/Common/Control/UIView/UIControlStateLabel.swift @@ -18,6 +18,27 @@ final class UIControlStateLabel: UILabel { private let attributedTextStates = ControlPropertyStates() private let textTypesStates = ControlPropertyStates() + 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 + } + } + } + private var storedAttributedText: NSAttributedString? { + didSet { + self.isText = self.storedAttributedText != nil + self.attributedText = self.storedAttributedText + } + } + private var storedTextFont: UIFont? private var storedTextColor: UIColor? @@ -28,14 +49,26 @@ final class UIControlStateLabel: UILabel { // MARK: - Override Properties override var text: String? { - didSet { - self.isText = self.text != nil + get { + return super.text + } + set { + // Set the attributedText only if the current come from setText + if newValue == self.storedText { + super.text = newValue + } } } override var attributedText: NSAttributedString? { - didSet { - self.isText = self.attributedText != nil + get { + return super.attributedText + } + set { + // Set the attributedText only if the current come from setAttributedText + if newValue == self.storedAttributedText { + super.attributedText = newValue + } } } @@ -129,26 +162,24 @@ final class UIControlStateLabel: UILabel { ) // Get the current textType from status - let textType = textTypesStates.value(forStatus: status) + let textType = self.textTypesStates.value(forStatus: 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(forStatus: status), + textType == .text || !textTypeContainsText { + self.storedText = text + + } else if let attributedText = self.attributedTextStates.value(forStatus: status), + textType == .attributedText || !textTypeContainsText { + self.storedAttributedText = attributedText + + } else { // No text to displayed + self.text = nil } } @@ -160,7 +191,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 57805b9f3..f0d50bc28 100644 --- a/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift +++ b/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift @@ -23,7 +23,7 @@ public final class ButtonUIView: UIControl { let stackView = UIStackView( arrangedSubviews: [ - self.iconView, + self.imageContentView, self.titleLabel ] ) @@ -33,9 +33,9 @@ public final class ButtonUIView: UIControl { 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) @@ -44,14 +44,22 @@ public final class ButtonUIView: UIControl { return view }() - private var iconImageView: UIControlStateImageView = { + 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: UIControlStateLabel = { + public var titleLabel: UILabel { + return self.titleStateLabel + } + + private var titleStateLabel: UIControlStateLabel = { let label = UIControlStateLabel() label.numberOfLines = 1 label.lineBreakMode = .byWordWrapping @@ -62,30 +70,11 @@ public final class ButtonUIView: UIControl { return label }() - /// Clear button to manage the action when the delegate is set. - @available(*, deprecated, message: "Remove this subview when the delegate will be removed") - 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 - button.isHidden = true // Show only if the delegate is set - 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? { - didSet { - self.clearButton.isHidden = self.delegate == nil - self.contentStackView.isUserInteractionEnabled = self.delegate != nil // Needed for the clearButton - } - } + public weak var delegate: ButtonUIViewDelegate? /// The tap publisher. Alternatively, you can use the native **action** (addAction) or **target** (addTarget). public var tapPublisher: UIControl.EventPublisher { @@ -220,24 +209,24 @@ public final class ButtonUIView: UIControl { set { super.isEnabled = newValue self.viewModel.set(isEnabled: newValue) - self.titleLabel.updateContent(from: self) - self.iconImageView.updateContent(from: self) + 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.titleLabel.updateContent(from: self) - self.iconImageView.updateContent(from: self) + 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.titleLabel.updateContent(from: self) - self.iconImageView.updateContent(from: self) + self.titleStateLabel.updateContent(from: self) + self.imageStateView.updateContent(from: self) if self.isHighlighted { self.viewModel.pressedAction() @@ -262,7 +251,7 @@ public final class ButtonUIView: UIControl { 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 @@ -531,7 +520,6 @@ public final class ButtonUIView: UIControl { 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 @@ -577,7 +565,6 @@ public final class ButtonUIView: UIControl { self.setupContentStackViewConstraints() self.setupIconViewConstraints() self.setupIconImageViewConstraints() - self.setupClearButtonConstraints() } private func setupViewConstraints() { @@ -606,24 +593,49 @@ public final class ButtonUIView: UIControl { } 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 - self.iconImageViewHeightConstraint = self.iconImageView.heightAnchor.constraint(equalToConstant: self.iconHeight) - self.iconImageViewHeightConstraint?.isActive = true + self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor).isActive = true + self.imageView.centerXAnchor.constraint(equalTo: self.imageContentView.centerXAnchor).isActive = true + self.imageView.centerYAnchor.constraint(equalTo: self.imageContentView.centerYAnchor).isActive = true + } + + // MARK: - Tracking + + public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.delegate?.button(self, didReceive: .touchDown) + + return super.beginTracking(touch, with: event) + } + + public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + if let touch { + // Tap is inside the view ? + let point = touch.location(in: self) + let touchIsInside = self.hitTest(point, with: event) == self + + // Send delegate actions + self.delegate?.button(self, didReceive: touchIsInside ? .touchUpInside : .touchUpOutside) + if touchIsInside { + self.delegate?.buttonWasTapped(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 + return super.endTracking(touch, with: event) } - private func setupClearButtonConstraints() { - self.clearButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.stickEdges(from: self.clearButton, to: self) + public override func cancelTracking(with event: UIEvent?) { + self.delegate?.button(self, didReceive: .touchCancel) + + return super.cancelTracking(with: event) } // MARK: - Setter & Getter @@ -631,7 +643,7 @@ public final class ButtonUIView: UIControl { /// The image of the button for a state. /// - parameter state: state of the image public func image(for state: ControlState) -> UIImage? { - return self.iconImageView.image(for: state) + return self.imageStateView.image(for: state) } /// Set the image of the button for a state. @@ -642,13 +654,13 @@ public final class ButtonUIView: UIControl { self.viewModel.set(iconImage: image.map { .left($0) }) } - self.iconImageView.setImage(image, for: state, on: self) + self.imageStateView.setImage(image, for: state, on: self) } /// The title of the button for a state. /// - parameter state: state of the title public func title(for state: ControlState) -> String? { - return self.titleLabel.text(for: state) + return self.titleStateLabel.text(for: state) } /// Set the title of the button for a state. @@ -659,13 +671,13 @@ public final class ButtonUIView: UIControl { self.viewModel.set(title: title) } - self.titleLabel.setText(title, for: state, on: self) + 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.titleLabel.attributedText(for: state) + return self.titleStateLabel.attributedText(for: state) } /// Set the attributedTitle of the button for a state. @@ -676,7 +688,7 @@ public final class ButtonUIView: UIControl { self.viewModel.set(attributedTitle: attributedTitle.map { .left($0) }) } - self.titleLabel.setAttributedText(attributedTitle, for: state, on: self) + self.titleStateLabel.setAttributedText(attributedTitle, for: state, on: self) } // MARK: - Update UI @@ -723,9 +735,9 @@ public final class ButtonUIView: UIControl { 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() } } @@ -774,7 +786,7 @@ public final class ButtonUIView: UIControl { 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 } @@ -835,7 +847,7 @@ public final class ButtonUIView: UIControl { 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 @@ -846,8 +858,8 @@ public final class ButtonUIView: UIControl { 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 { @@ -872,52 +884,20 @@ public final class ButtonUIView: UIControl { // ** // Is Image ? - self.iconImageView.$isImage.subscribe(in: &self.subscriptions) { [weak self] isImage in + self.imageStateView.$isImage.subscribe(in: &self.subscriptions) { [weak self] isImage in guard let self else { return } - self.iconView.isHidden = !isImage + self.imageContentView.isHidden = !isImage } // ** // Is Text ? - self.titleLabel.$isText.subscribe(in: &self.subscriptions) { [weak self] isText in + self.titleStateLabel.$isText.subscribe(in: &self.subscriptions) { [weak self] isText in guard let self else { return } - self.titleLabel.isHidden = !isText } } - // MARK: - Actions - - @available(*, deprecated, message: "Remove this action when the delegate will be removed") - @objc private func touchUpInsideAction() { - self.isHighlighted = false - - self.delegate?.button(self, didReceive: .touchUpInside) - self.delegate?.buttonWasTapped(self) - } - - @available(*, deprecated, message: "Remove this action when the delegate will be removed") - @objc private func touchDownAction() { - self.isHighlighted = true - - self.delegate?.button(self, didReceive: .touchDown) - } - - @available(*, deprecated, message: "Remove this action when the delegate will be removed") - @objc private func touchUpOutsideAction() { - self.isHighlighted = false - - self.delegate?.button(self, didReceive: .touchUpOutside) - } - - @available(*, deprecated, message: "Remove this action when the delegate will be removed") - @objc private func touchCancelAction() { - self.isHighlighted = false - - self.delegate?.button(self, didReceive: .touchCancel) - } - // MARK: - Trait Collection public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 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/UIKit/ButtonComponentItemsUIView.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift index 2103181ea..02d6cfe4e 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentItemsUIView.swift @@ -137,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: [ @@ -198,6 +209,11 @@ 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 { diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift index e6f050e48..a025fcd52 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift @@ -35,7 +35,7 @@ final class ButtonComponentUIView: ComponentUIView { private var cancellables: Set = [] private lazy var buttonAction: UIAction = .init { _ in - self.showAlert() + self.showAlert(for: .action) } private var buttonControlCancellable: AnyCancellable? @@ -164,19 +164,28 @@ final class ButtonComponentUIView: ComponentUIView { 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) } } @@ -185,10 +194,9 @@ final class ButtonComponentUIView: ComponentUIView { self.buttonView.delegate = controlType == .delegate ? self : nil // Publisher ? - var subscription: AnyCancellable? if controlType == .publisher { self.buttonControlCancellable = self.buttonView.tapPublisher.sink { _ in - self.showAlert() + self.showAlert(for: .publisher) } } else { self.buttonControlCancellable?.cancel() @@ -204,9 +212,9 @@ final class ButtonComponentUIView: ComponentUIView { // Target ? if controlType == .target { - self.buttonView.addTarget(self, action: #selector(self.touchUpInside), for: .touchUpInside) + self.buttonView.addTarget(self, action: #selector(self.touchUpInsideTarget), for: .touchUpInside) } else { - self.buttonView.removeTarget(self, action: #selector(self.touchUpInside), for: .touchUpInside) + self.buttonView.removeTarget(self, action: #selector(self.touchUpInsideTarget), for: .touchUpInside) } } @@ -256,15 +264,15 @@ final class ButtonComponentUIView: ComponentUIView { // MARK: - Action - @objc func touchUpInside() { - self.showAlert() + @objc func touchUpInsideTarget() { + self.showAlert(for: .target) } // MARK: - Alert - func showAlert() { + func showAlert(for controlType: ButtonControlType) { let alertController = UIAlertController( - title: "Button tap from " + self.viewModel.controlType.name, + title: "Button tap from " + controlType.name, message: nil, preferredStyle: .alert ) @@ -278,6 +286,6 @@ final class ButtonComponentUIView: ComponentUIView { extension ButtonComponentUIView: ButtonUIViewDelegate { func buttonWasTapped(_ button: ButtonUIView) { - self.showAlert() + self.showAlert(for: .delegate) } }