Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Button): Make Button a UIControl BREAKING CHANGE: Button.State enum has changed to UIControl.State. #79

Merged
merged 19 commits into from
Feb 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Mistica.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@
18D8893F25ADA08A0098EED3 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
18F193E3259C8E9A00A09E20 /* SegmentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentView.swift; sourceTree = "<group>"; };
18F193E4259C8E9A00A09E20 /* StepView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepView.swift; sourceTree = "<group>"; };
18F342CC25D1A67700B09500 /* testStartAndStopLoadingBacksToNormal.finalState.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testStartAndStopLoadingBacksToNormal.finalState.png; sourceTree = "<group>"; };
18F342CD25D1A67700B09500 /* testSelectAndDeselectBacksToNormal.finalState.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testSelectAndDeselectBacksToNormal.finalState.png; sourceTree = "<group>"; };
18F342CE25D1A67700B09500 /* testDisableAndEnableBacksToNormal.finalState.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testDisableAndEnableBacksToNormal.finalState.png; sourceTree = "<group>"; };
18F342CF25D1A67700B09500 /* testDisableAndEnableBacksToNormal.assertInitialState.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testDisableAndEnableBacksToNormal.assertInitialState.png; sourceTree = "<group>"; };
18F342D025D1A67700B09500 /* testSelectAndDeselectBacksToNormal.assertInitialState.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testSelectAndDeselectBacksToNormal.assertInitialState.png; sourceTree = "<group>"; };
18F342D125D1A67700B09500 /* testStartAndStopLoadingBacksToNormal.assertInitialState.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testStartAndStopLoadingBacksToNormal.assertInitialState.png; sourceTree = "<group>"; };
18F65F48542EBF9B8472E8C1 /* PopoverContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverContentView.swift; sourceTree = "<group>"; };
1929FD98AEC3F04A744E1458 /* BoundsChangeInvalidatingFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundsChangeInvalidatingFlowLayout.swift; sourceTree = "<group>"; };
1A6C443D987AD4CDAFD35525 /* MovistarColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovistarColorPalette.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -940,6 +946,12 @@
B8BCF995256D1AB600D2BCCC /* testSmallSizeWithSecondaryStyle.with-o2-style.png */,
B8BCF992256D1AB600D2BCCC /* testSmallSizeWithSecondaryStyle.with-o2Classic-style.png */,
B8BCF979256D1AB500D2BCCC /* testSmallSizeWithSecondaryStyle.with-vivo-style.png */,
18F342CF25D1A67700B09500 /* testDisableAndEnableBacksToNormal.assertInitialState.png */,
18F342CE25D1A67700B09500 /* testDisableAndEnableBacksToNormal.finalState.png */,
18F342D025D1A67700B09500 /* testSelectAndDeselectBacksToNormal.assertInitialState.png */,
18F342CD25D1A67700B09500 /* testSelectAndDeselectBacksToNormal.finalState.png */,
18F342D125D1A67700B09500 /* testStartAndStopLoadingBacksToNormal.assertInitialState.png */,
18F342CC25D1A67700B09500 /* testStartAndStopLoadingBacksToNormal.finalState.png */,
B8BCF9AC256D1AB700D2BCCC /* testTextIsAlwaysSingleLine.1.png */,
);
path = ButtonTests;
Expand Down
190 changes: 88 additions & 102 deletions Mistica/Source/Components/Button/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import UIKit

open class Button: UIView {
open class Button: UIControl {
private enum Constants {
static let animationDuration: TimeInterval = 0.3
static let animationCurveControlPoint1 = CGPoint(x: 0.77, y: 0)
Expand All @@ -17,14 +17,6 @@ open class Button: UIView {
static let borderWidth: CGFloat = 1.5
}

@frozen
public enum State {
case normal
case selected
case disabled
case loading
}

public struct Style {
public let allowsBleedingAlignment: Bool
public let stateStyleByState: [State: StateStyle]
Expand All @@ -43,7 +35,7 @@ open class Button: UIView {
}

public init(allowsBleedingAlignment: Bool,
stateStyleByState: [Button.State: Button.StateStyle],
stateStyleByState: [State: Button.StateStyle],
overriddenSizes: Button.Style.OverriddenSizes? = nil) {
self.allowsBleedingAlignment = allowsBleedingAlignment
self.stateStyleByState = stateStyleByState
Expand Down Expand Up @@ -85,21 +77,15 @@ open class Button: UIView {
set { container.loadingTitle = newValue }
}

public var state: State = .normal {
didSet {
didUpdateState(previousState: oldValue)
}
}

private var overridenAccessibilityLabel: String?
private var isShowingLoadingAnimation = false

private lazy var animator = UIViewPropertyAnimator(
duration: Constants.animationDuration,
controlPoint1: Constants.animationCurveControlPoint1,
controlPoint2: Constants.animationCurveControlPoint2
)

private lazy var backingButton = BackingButton()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for backingButton anymore.

private lazy var container = ButtonContentView()

public convenience init() {
Expand All @@ -114,7 +100,6 @@ open class Button: UIView {

self.title = title
self.loadingTitle = loadingTitle

commonInit()
}

Expand All @@ -139,24 +124,56 @@ open class Button: UIView {
UIEdgeInsets(top: 0, left: leftBleedingInsets, bottom: 0, right: rightBleedingInsets)
}

override public var tag: Int {
get { backingButton.tag }
set { backingButton.tag = newValue }
}

override public var intrinsicContentSize: CGSize {
container.intrinsicContentSize
}
}

@objc public extension Button {
func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) {
backingButton.addTarget(target, action: action, for: controlEvents)
public var isLoading = false {
didSet {
didUpdateState()
}
}

override open var isHighlighted: Bool {
didSet {
didUpdateState()
}
}

override open var isSelected: Bool {
didSet {
didUpdateState()
}
}

override open var isEnabled: Bool {
didSet {
didUpdateState()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should modify the isLoading flag when setting one of these to false/true,

E.g. isEnabled=true, isLoading=true -> we set isEnabled=false -> the button won't change. In this scenario, would it make sense to se set isLoading=false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer that changing the value of one property does not affect the others. Enable and disable are different things from loading and not loading. If the button is loading and you want to stop loading, you must set the isLoading property to false, that's it. If you set the isEnabled property to false while loading, the button will be disabled as soon as you set the isLoading property back to false. I do not see anything wrong here.

However, I dot no have a strong opinion here. I will do you what you guys think is better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, it was just something that I found weird, but it can work as you said, thanks!


override open var state: UIControl.State {
if isLoading {
return .loading
} else if !isEnabled {
return .disabled
} else if isSelected || isHighlighted {
return .selected
} else {
return .normal
}
}
Comment on lines +155 to +165
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State is now a computed property.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only thing that bothers me is being able to set invalid combinations, like "isLoading=true" and "isEnabled=true"... or "isSelected=true" and "isEnabled=false", our component doesn't support it so it is what it is...


override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !isLoading else { return nil }
return super.hitTest(point, with: event)
}
}
Comment on lines +167 to +171
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to "disable" the button when it is loading.


@objc public extension Button {
override var accessibilityLabel: String? {
get {
if state == .loading {
if isLoading {
return loadingTitle
} else if overridenAccessibilityLabel != nil {
return overridenAccessibilityLabel
Expand All @@ -168,15 +185,6 @@ open class Button: UIView {
overridenAccessibilityLabel = newValue
}
}

override func accessibilityActivate() -> Bool {
if state.shouldBackingButtonBeEnabled {
backingButton.sendActions(for: .touchUpInside)
return true
} else {
return false
}
}
}
Comment on lines -172 to 188
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. The UIContol handles all this for us, based on the state property.


private extension Button {
Expand Down Expand Up @@ -217,7 +225,6 @@ private extension Button {
func commonInit() {
setUpView()
setUpContainer()
setUpBackingButton()
updateStyle()
}

Expand All @@ -228,33 +235,38 @@ private extension Button {
}
layer.borderWidth = Constants.borderWidth
isAccessibilityElement = true
updateTraits()
}

func setUpContainer() {
container.isUserInteractionEnabled = false
addSubview(withDefaultConstraints: container)
}

func setUpBackingButton() {
updateBackingButtonEnabled()
backingButton.setContentHuggingPriority(UILayoutPriority(rawValue: 1), for: .horizontal)
backingButton.setContentHuggingPriority(UILayoutPriority(rawValue: 1), for: .vertical)
addSubview(withDefaultConstraints: backingButton)
backingButton.isHighlightedDidChangeHandler = { [weak self] in
self?.updateStateBasedOnBackingButton()
func applyStyleColors() {
let stateStyle: StateStyle?

if isLoading {
stateStyle = style.stateStyleByState[.loading]
} else if !isEnabled {
stateStyle = style.stateStyleByState[.disabled]
} else if isSelected || isHighlighted {
stateStyle = style.stateStyleByState[.selected]
} else {
stateStyle = style.stateStyleByState[.normal]
}
}

func applyStyleColors() {
guard let stateStyle = style.stateStyleByState[state] else {
guard stateStyle != nil else {
preconditionFailure("Style \(style) does not have stateStyle for state \(state). Check that the current style is defined properly.")
}
container.textColor = stateStyle.textColor
backgroundColor = stateStyle.backgroundColor
layer.borderColor = stateStyle.borderColor.cgColor

container.textColor = stateStyle!.textColor
backgroundColor = stateStyle!.backgroundColor
layer.borderColor = stateStyle!.borderColor.cgColor
}

func didUpdateState(previousState: State) {
if state == .loading {
func didUpdateState() {
if isLoading {
animator.stopAnimation(true)

// transition to loading
Expand All @@ -265,9 +277,10 @@ private extension Button {
self?.container.transitionToLoading()
self?.applyStyleColors()
}
updateBackingButtonEnabled()
updateTraits()
isShowingLoadingAnimation = true
animator.startAnimation()
} else if previousState == .loading {
} else if isShowingLoadingAnimation {
Comment on lines -270 to +283
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this isShowingLoadingAnimation to remove the concept of previousState.

animator.stopAnimation(true)

// transition to normal
Expand All @@ -278,84 +291,57 @@ private extension Button {
animator.addCompletion { [weak self] _ in
guard let s = self else { return }
UIAccessibility.post(notification: .layoutChanged, argument: s.accessibilityLabel)
s.updateBackingButtonEnabled()
s.updateTraits()
}
isShowingLoadingAnimation = false
animator.startAnimation()
} else {
applyStyleColors()
updateBackingButtonEnabled()
updateTraits()
}
}

func updateBackingButtonEnabled() {
func updateTraits() {
accessibilityTraits = state.accesibilityTraits
backingButton.isEnabled = state.shouldBackingButtonBeEnabled
}

func updateStateBasedOnBackingButton() {
if backingButton.isHighlighted && state == .normal {
state = .selected
} else if !backingButton.isHighlighted && state == .selected {
state = .normal
}
}
}

private extension Button.State {
var shouldBackingButtonBeEnabled: Bool {
switch self {
case .disabled, .loading: return false
case .normal, .selected: return true
}
}

var accesibilityTraits: UIAccessibilityTraits {
if shouldBackingButtonBeEnabled {
return .button
} else {
if contains(.disabled) || contains(.loading) {
return [.button, .notEnabled]
} else {
return .button
}
}
}

// MARK: Dummy button

private class BackingButton: UIButton {
var isHighlightedDidChangeHandler: (() -> Void)?

override var isHighlighted: Bool {
didSet {
isHighlightedDidChangeHandler?()
}
}

init() {
super.init(frame: .zero)
accessibilityElementsHidden = true
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

// MARK: Objective-C API

@objc public extension Button {
func objc_setNormalState() {
state = .normal
isLoading = false
isSelected = false
isEnabled = true
}

func objc_setLoadingState() {
state = .loading
isLoading = true
}

func objc_setDisabledState() {
state = .disabled
isEnabled = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also change "isLoading" to false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not (same reason as here #79 (comment))

}

func objc_setLinkStyle() {
style = .link
}
}

// MARK: Useful extensions

public extension Button.State {
static let loading = UIControl.State(rawValue: 1 << 50) // Arbitrary value
}

extension Button.State: Hashable {}
4 changes: 2 additions & 2 deletions Mistica/Source/Components/Button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You can check out the `ButtonStyle+Toolkit.swift` file to check out the availabl

## State

Set a new value to the property `state` in order to change the button's state.
Set a new value to the properties `isLoading`, `isSelected` or `isEnabled` in order to change the button's state.

* **Normal:** Initial button state, enabled and accepting touches.
* **Selected:** It automatically changes to this state when the button is tapped and will remain like that until the tapped event stops.
Expand All @@ -34,4 +34,4 @@ In order to align the button the system will use the decorator view (button back
In order to change this behavior, for `link` buttons, you can set the view's `contentMode` to `.left` or `.right` to enable bleed alignment for the title.
This means that the text will be left (or right) aligned and the "highlight" background will overflow the button.

![custom](./docs/images/left-content-mode-selected.png)
![custom](./docs/images/left-content-mode-selected.png)
18 changes: 4 additions & 14 deletions Mistica/Source/Components/Cards/DataCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,22 +115,12 @@ public extension DataCard {
}
}

var primaryButtonState: Button.State {
get {
cardBaseView.buttonsView.primaryButtonState
}
set {
cardBaseView.buttonsView.primaryButtonState = newValue
}
var primaryButton: Button {
cardBaseView.buttonsView.primaryButton
}

var linkButtonState: Button.State {
get {
cardBaseView.buttonsView.linkButtonState
}
set {
cardBaseView.buttonsView.linkButtonState = newValue
}
var linkButton: Button {
cardBaseView.buttonsView.linkButton
}
}

Expand Down
Loading