diff --git a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift b/core/Sources/Common/Control/UIView/UIControlStateLabel.swift index a6d7ec053..b64a2727b 100644 --- a/core/Sources/Common/Control/UIView/UIControlStateLabel.swift +++ b/core/Sources/Common/Control/UIView/UIControlStateLabel.swift @@ -35,7 +35,7 @@ final class UIControlStateLabel: UILabel { override var attributedText: NSAttributedString? { didSet { - self.isText = attributedText != nil + self.isText = self.attributedText != nil } } diff --git a/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift b/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift index 3c86f3318..57805b9f3 100644 --- a/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift +++ b/core/Sources/Components/Button/View/UIKit/ButtonUIView.swift @@ -29,6 +29,7 @@ public final class ButtonUIView: UIControl { ) stackView.axis = .horizontal stackView.accessibilityIdentifier = AccessibilityIdentifier.contentStackView + stackView.isUserInteractionEnabled = false return stackView }() @@ -61,6 +62,8 @@ 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 @@ -69,33 +72,40 @@ public final class ButtonUIView: UIControl { 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. - public weak var delegate: ButtonUIViewDelegate? + @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 + } + } - /// 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. @@ -228,6 +238,12 @@ public final class ButtonUIView: UIControl { didSet { self.titleLabel.updateContent(from: self) self.iconImageView.updateContent(from: self) + + if self.isHighlighted { + self.viewModel.pressedAction() + } else { + self.viewModel.unpressedAction() + } } } @@ -722,7 +738,7 @@ public final class ButtonUIView: UIControl { 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 { @@ -749,7 +765,7 @@ public final class ButtonUIView: UIControl { // 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 } @@ -873,47 +889,33 @@ public final class ButtonUIView: UIControl { // MARK: - Actions + @available(*, deprecated, message: "Remove this action when the delegate will be removed") @objc private func touchUpInsideAction() { self.isHighlighted = false - self.unpressedAction() - self.delegate?.button(self, didReceive: .touchUpInside) self.delegate?.buttonWasTapped(self) - self.sendActions(for: .touchUpInside) } + @available(*, deprecated, message: "Remove this action when the delegate will be removed") @objc private func touchDownAction() { self.isHighlighted = true - self.viewModel.pressedAction() - self.delegate?.button(self, didReceive: .touchDown) - self.sendActions(for: .touchDown) } + @available(*, deprecated, message: "Remove this action when the delegate will be removed") @objc private func touchUpOutsideAction() { self.isHighlighted = false - self.unpressedAction() - self.delegate?.button(self, didReceive: .touchUpOutside) - self.sendActions(for: .touchUpOutside) } + @available(*, deprecated, message: "Remove this action when the delegate will be removed") @objc private func touchCancelAction() { self.isHighlighted = false - self.unpressedAction() - self.delegate?.button(self, didReceive: .touchCancel) - self.sendActions(for: .touchCancel) - } - - private func unpressedAction() { - DispatchQueue.main.asyncAfter(deadline: .now() + Animation.fastDuration, execute: { [weak self] in - self?.viewModel.unpressedAction() - }) } // 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/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift index 66b5bd389..e6f050e48 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIView.swift @@ -34,6 +34,11 @@ final class ButtonComponentUIView: ComponentUIView { private let viewModel: ButtonComponentUIViewModel private var cancellables: Set = [] + private lazy var buttonAction: UIAction = .init { _ in + self.showAlert() + } + private var buttonControlCancellable: AnyCancellable? + // MARK: - Initializer init(viewModel: ButtonComponentUIViewModel) { @@ -140,6 +145,12 @@ final class ButtonComponentUIView: ComponentUIView { 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 @@ -169,6 +180,36 @@ final class ButtonComponentUIView: ComponentUIView { } } + private func setControl(from controlType: ButtonControlType) { + // Delegate ? + self.buttonView.delegate = controlType == .delegate ? self : nil + + // Publisher ? + var subscription: AnyCancellable? + if controlType == .publisher { + self.buttonControlCancellable = self.buttonView.tapPublisher.sink { _ in + self.showAlert() + } + } 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.touchUpInside), for: .touchUpInside) + } else { + self.buttonView.removeTarget(self, action: #selector(self.touchUpInside), for: .touchUpInside) + } + } + // MARK: - Getter private func image(for state: ControlState) -> UIImage? { @@ -212,4 +253,31 @@ final class ButtonComponentUIView: ComponentUIView { @unknown default: return nil } } + + // MARK: - Action + + @objc func touchUpInside() { + self.showAlert() + } + + // MARK: - Alert + + func showAlert() { + let alertController = UIAlertController( + title: "Button tap from " + self.viewModel.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() + } } diff --git a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift index 03cde5480..ebd06f55a 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentUIViewModel.swift @@ -65,6 +65,11 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { .eraseToAnyPublisher() } + var showControlType: AnyPublisher<[ButtonControlType], Never> { + showControlTypeSheetSubject + .eraseToAnyPublisher() + } + let themes = ThemeCellModel.themes @Published var theme: Theme @@ -80,6 +85,7 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { @Published var isEnabled: Bool @Published var isSelected: Bool @Published var isAnimated: Bool + @Published var controlType: ButtonControlType // MARK: - Items Properties @@ -187,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 @@ -209,7 +223,8 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { self.contentSelectedConfigurationItemViewModel, self.isEnabledConfigurationItemViewModel, self.isSelectedConfigurationItemViewModel, - self.isAnimatedConfigurationItemViewModel + self.isAnimatedConfigurationItemViewModel, + self.controlTypeConfigurationItemViewModel ] } @@ -225,6 +240,7 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { 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 @@ -243,7 +259,8 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { contentSelected: ButtonContentDefault = .text, isEnabled: Bool = true, isSelected: Bool = false, - isAnimated: Bool = true + isAnimated: Bool = true, + controlType: ButtonControlType = .action ) { self.text = text self.iconImage = .init(named: iconImageNamed) ?? UIImage() @@ -267,6 +284,7 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel { self.isEnabled = isEnabled self.isSelected = isSelected self.isAnimated = isAnimated + self.controlType = controlType super.init(identifier: "Button") } @@ -327,4 +345,8 @@ extension ButtonComponentUIViewModel { @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 8bcb91beb..ff6989af4 100644 --- a/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift +++ b/spark/Demo/Classes/View/Components/Button/UIKit/ButtonComponentViewController.swift @@ -99,6 +99,10 @@ final class ButtonComponentViewController: UIViewController { self.viewModel.showContentSelectedSheet.subscribe(in: &self.cancellables) { contents in self.presentContentSelectedActionSheet(contents) } + + self.viewModel.showControlType.subscribe(in: &self.cancellables) { types in + self.presentControlTypeActionSheet(types) + } } } @@ -204,4 +208,13 @@ extension ButtonComponentViewController { } 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 +}