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

[NT-394] - Loading Button #892

Merged
merged 18 commits into from
Oct 18, 2019
Merged

[NT-394] - Loading Button #892

merged 18 commits into from
Oct 18, 2019

Conversation

dusi
Copy link
Contributor

@dusi dusi commented Oct 11, 2019

πŸ“² What

Adds a LoadingButton class that could be used to display a button that has a loading state.

πŸ€” Why

Our CTA buttons will be performing network requests so this came up as a design tweak to our checkout workflow.

πŸ›  How

The button has a Bool property isLoading that could be toggled ON/OFF in order to show the activity indicator or hide it. This in addition requires storing titles for couple states into a temporary dictionary so that these could be restored later when the loading is stopped.

πŸ‘€ See

Pledge

pledge

Update pledge

update

♿️ Accessibility

  • When the spinner is visible, its accessibility label says Loading and it's localized (this could be verified by slowing down your connection using Network Link Conditioner and using VoiceOver to tap on the button
  • After the spinner is stopped, its accessibility label is using the title again (this could be verified by manually disabling push to thank you screen or the pop back on update manage pledge screen) and using VoiceOver to tap on the button

βœ… Acceptance criteria

  • The Pledge button shows a spinner when tapped and during the network call on the pledge screen
  • The Confirm button shows a spinner when tapped and during the network call on the update pledge screen
  • Button's user interaction is disabled when loading is ON
  • Button's user interaction is re-enabled when loading finishes (this could be verified by manually disabling push to thank you screen or the pop back on update manage pledge screen)

@@ -25,7 +25,7 @@ final class PledgePaymentMethodsViewController: UIViewController {
private lazy var cardsStackView: UIStackView = { UIStackView(frame: .zero) }()
internal weak var delegate: PledgePaymentMethodsViewControllerDelegate?
internal weak var messageDisplayingDelegate: PledgeViewControllerMessageDisplaying?
private lazy var pledgeButton: UIButton = { UIButton.init(type: .custom) }()
private(set) lazy var pledgeButton: LoadingButton = { LoadingButton(frame: .zero) }()
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 can expose this another way but thought this was simplest for what we need to do (RAC binding)

@@ -11,7 +11,7 @@ protocol PledgeViewControllerDelegate: AnyObject {
final class PledgeViewController: UIViewController, MessageBannerViewControllerPresenting {
// MARK: - Properties

private lazy var confirmButton: UIButton = { UIButton(type: .custom) }()
private lazy var confirmButton: LoadingButton = { LoadingButton(frame: .zero) }()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing to LoadingButton as well as using designated initializer

// MARK: - Properties

private lazy var activityIndicator: UIActivityIndicatorView = {
UIActivityIndicatorView(style: .white)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Style is something that could potentially be injected in the initializer in the future but for now I think this style will suffice


private lazy var activityIndicator: UIActivityIndicatorView = {
UIActivityIndicatorView(style: .white)
|> \.hidesWhenStopped .~ true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the magic prop that does not require us to add/remove it from the view hierarchy...it will automatically show/hide the component depending on the isAnimating property.

self.activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}

required init?(coder: NSCoder) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Init with coder so that this supports XIBs


override func setTitle(_ title: String?, for state: UIControl.State) {
// Do not allow changing the title while the activity indicator is animating
guard !self.activityIndicator.isAnimating else { return }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting a title while the activity indicator is loading would display both the indicator and the title so I think this is the right approach here..we can potentially add logic to stop animating if we're setting title but I think due to our RAC bindings it could cause some flickering

self.setTitle(nil, for: normalState)
self.setTitle(nil, for: selectedState)

_ = self
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This makes sure that during the loading phase it's still accessible..

self.originalTitles[normalState.rawValue] = nil
self.originalTitles[selectedState.rawValue] = nil

_ = self
Copy link
Contributor Author

Choose a reason for hiding this comment

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

And this ensures that after loading is done it will again use title for the accessibility label

Copy link
Contributor

@justinswart justinswart left a comment

Choose a reason for hiding this comment

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

I think this all seems legit, it might just be more in line with our other views to put the hiding/showing logic behind a VM?

}
}

private var originalTitles: [UInt: String] = [:]
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason you chose to do this without a view model?

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 simply tried to go with the simplest approach but can back this by a VM to be more consistent with the rest of the codebase πŸ‘

@dusi dusi requested a review from justinswart October 18, 2019 18:17
let normalState = UIControl.State.normal
let selectedState = UIControl.State.selected

if let disabledTitle = self.title(for: disabledState) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracting the title first will make sure that if for some reason we want to start the label with a loading state, we don't just clean the titles, but rather only cache previous titles if there were any.

Copy link
Contributor

@justinswart justinswart left a comment

Choose a reason for hiding this comment

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

Approved with suggestions!


// MARK: - Titles

private func removeTitle() {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about something like this for DRYness:

  private func removeTitle() {
    let states: [UIControl.State] = [.disabled, .highlighted, .normal, .selected]

    states.compactMap { state -> (String, UIControl.State)? in
      guard let title = self.title(for: state) else { return nil }
      return (title, state)
    }
    .forEach { title, state in
      self.originalTitles[state.rawValue] = title
      self.setTitle(nil, for: state)
    }

    _ = self
      |> \.accessibilityLabel %~ { _ in Strings.Loading() }

    UIAccessibility.post(notification: .layoutChanged, argument: self)
  }

UIAccessibility.post(notification: .layoutChanged, argument: self)
}

private func restoreTitle() {
Copy link
Contributor

Choose a reason for hiding this comment

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

You could probably do this here too.

@justinswart
Copy link
Contributor

PS. Thanks for everything! πŸŽ‰

@dusi dusi merged commit 870a778 into master Oct 18, 2019
@dusi dusi deleted the loading-button branch October 18, 2019 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants