-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from all commits
ab86371
8c04d9e
1e78bf9
a101b3e
b0a91c8
91ccc56
9f948fb
2301586
dbfebb0
9683768
f20c65b
124e5e5
6adf977
49fe990
21752af
bbb791b
dc4d49f
1fa7a0e
e233172
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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] | ||
|
@@ -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 | ||
|
@@ -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() | ||
private lazy var container = ButtonContentView() | ||
|
||
public convenience init() { | ||
|
@@ -114,7 +100,6 @@ open class Button: UIView { | |
|
||
self.title = title | ||
self.loadingTitle = loadingTitle | ||
|
||
commonInit() | ||
} | ||
|
||
|
@@ -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() | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 However, I dot no have a strong opinion here. I will do you what you guys think is better. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. State is now a computed property. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed. The |
||
|
||
private extension Button { | ||
|
@@ -217,7 +225,6 @@ private extension Button { | |
func commonInit() { | ||
setUpView() | ||
setUpContainer() | ||
setUpBackingButton() | ||
updateStyle() | ||
} | ||
|
||
|
@@ -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 | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added this |
||
animator.stopAnimation(true) | ||
|
||
// transition to normal | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this also change "isLoading" to false? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 {} |
There was a problem hiding this comment.
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.