Skip to content

Commit

Permalink
[Refacto-557] Button: Update View and Demo
Browse files Browse the repository at this point in the history
  • Loading branch information
robergro committed Nov 6, 2023
1 parent 4b68426 commit f64a107
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class UIControlStateLabel: UILabel {

override var attributedText: NSAttributedString? {
didSet {
self.isText = attributedText != nil
self.isText = self.attributedText != nil
}
}

Expand Down
54 changes: 28 additions & 26 deletions core/Sources/Components/Button/View/UIKit/ButtonUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class ButtonUIView: UIControl {
)
stackView.axis = .horizontal
stackView.accessibilityIdentifier = AccessibilityIdentifier.contentStackView
stackView.isUserInteractionEnabled = false
return stackView
}()

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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()
}
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ final class ButtonComponentUIView: ComponentUIView {
private let viewModel: ButtonComponentUIViewModel
private var cancellables: Set<AnyCancellable> = []

private lazy var buttonAction: UIAction = .init { _ in
self.showAlert()
}
private var buttonControlCancellable: AnyCancellable?

// MARK: - Initializer

init(viewModel: ButtonComponentUIViewModel) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel {
.eraseToAnyPublisher()
}

var showControlType: AnyPublisher<[ButtonControlType], Never> {
showControlTypeSheetSubject
.eraseToAnyPublisher()
}

let themes = ThemeCellModel.themes

@Published var theme: Theme
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -209,7 +223,8 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel {
self.contentSelectedConfigurationItemViewModel,
self.isEnabledConfigurationItemViewModel,
self.isSelectedConfigurationItemViewModel,
self.isAnimatedConfigurationItemViewModel
self.isAnimatedConfigurationItemViewModel,
self.controlTypeConfigurationItemViewModel
]
}

Expand All @@ -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

Expand All @@ -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()
Expand All @@ -267,6 +284,7 @@ final class ButtonComponentUIViewModel: ComponentUIViewModel {
self.isEnabled = isEnabled
self.isSelected = isSelected
self.isAnimated = isAnimated
self.controlType = controlType

super.init(identifier: "Button")
}
Expand Down Expand Up @@ -327,4 +345,8 @@ extension ButtonComponentUIViewModel {
@objc func isAnimatedChanged() {
self.isAnimated.toggle()
}

@objc func presentControlTypeSheet() {
self.showControlTypeSheetSubject.send(ButtonControlType.allCases)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -204,4 +208,13 @@ extension ButtonComponentViewController {
}
self.present(actionSheet, animated: true)
}

private func presentControlTypeActionSheet(_ contents: [ButtonControlType]) {
let actionSheet = SparkActionSheet<ButtonControlType>.init(
values: contents,
texts: contents.map { $0.name }) { content in
self.viewModel.controlType = content
}
self.present(actionSheet, animated: true)
}
}
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit f64a107

Please sign in to comment.