-
Notifications
You must be signed in to change notification settings - Fork 1k
Vertical mode floating button #5938
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
base: master
Are you sure you want to change the base?
Changes from all commits
6c4eb89
de3611a
7d0a376
4188aef
76dfb3e
2ea9b44
effb077
d354c73
7f50090
d1bb7cc
523b63a
8da2e74
94c73ea
f41d2e9
c92d45e
b372a6f
9d367a2
5126af6
48f2542
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 |
|---|---|---|
|
|
@@ -149,6 +149,14 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| private lazy var errorLabel: UILabel = { | ||
| ElementsUI.makeErrorLabel(theme: configuration.appearance.asElementsTheme) | ||
| }() | ||
| private lazy var bottomSpacer: UIView = { | ||
| let spacer = UIView() | ||
| spacer.translatesAutoresizingMaskIntoConstraints = false | ||
| let buttonHeight = primaryButton.bounds.height | ||
| let totalBottomPadding = buttonHeight + buttonSpacing - 20 // account for stackView spacing | ||
|
Collaborator
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. Where does this |
||
| spacer.heightAnchor.constraint(equalToConstant: totalBottomPadding).isActive = true | ||
| return spacer | ||
| }() | ||
| let stackView: UIStackView = UIStackView() | ||
|
|
||
| // MARK: - Initializers | ||
|
|
@@ -275,6 +283,7 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| updatePrimaryButton() | ||
| updateMandate() | ||
| updateError() | ||
| updateFloatingButton() | ||
| } | ||
|
|
||
| func updatePrimaryButton() { | ||
|
|
@@ -329,9 +338,7 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| self.mandateView.setHiddenIfNecessary(newMandateText == nil) | ||
| let hasLabelInStackView = newMandateText != nil || self.errorLabel.text != nil | ||
| if self.isViewLoaded, hadLabelInStackView != hasLabelInStackView { | ||
| self.primaryButtonTopAnchorConstraint.isActive = false | ||
| self.primaryButtonTopAnchorConstraint = self.stackView.bottomAnchor.constraint(equalTo: self.primaryButton.topAnchor, constant: hasLabelInStackView ? -20 : -32) | ||
| self.primaryButtonTopAnchorConstraint.isActive = true | ||
| self.primaryButtonTopAnchorConstraint.constant = -self.buttonSpacing | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -353,6 +360,80 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| }) | ||
| } | ||
|
|
||
| func updateFloatingButton() { | ||
| guard isViewLoaded, view.window != nil else { | ||
| return | ||
| } | ||
| // Skip floating button updates when the keyboard is visible | ||
| guard !isKeyboardVisible else { | ||
| return | ||
| } | ||
|
|
||
| if shouldButtonFloat { | ||
| activateFloatingButton() | ||
| } else { | ||
| deactivateFloatingButton() | ||
| } | ||
| } | ||
|
|
||
| private func activateFloatingButton() { | ||
| guard !isButtonFloating, let bottomSheet = bottomSheetController else { | ||
| return | ||
| } | ||
|
|
||
| NSLayoutConstraint.deactivate([ | ||
| primaryButtonTopAnchorConstraint, | ||
| primaryButtonBottomConstraint, | ||
| ]) | ||
|
|
||
| // Create floating constraint | ||
| primaryButtonFloatingBottomConstraint = primaryButton.bottomAnchor.constraint(equalTo: bottomSheet.view.safeAreaLayoutGuide.bottomAnchor) | ||
| stackViewBottomConstraint = stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor) | ||
| stackViewBottomConstraint.priority = .defaultLow | ||
|
Comment on lines
+391
to
+392
Collaborator
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. Why is this constraint needed and why |
||
|
|
||
| NSLayoutConstraint.activate([ | ||
| primaryButtonFloatingBottomConstraint, | ||
| stackViewBottomConstraint, | ||
| ]) | ||
|
|
||
| // Add bottom spacer to allow scrolling past content | ||
| stackView.addArrangedSubview(bottomSpacer) | ||
|
|
||
| UIView.animate(withDuration: 0.3) { | ||
| bottomSheet.view.layoutIfNeeded() | ||
|
Collaborator
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. Why in activate and deactivate floating button are we animating different views? |
||
| } | ||
|
|
||
| isButtonFloating = true | ||
| } | ||
|
|
||
| private func deactivateFloatingButton() { | ||
| guard isButtonFloating else { | ||
| return | ||
| } | ||
|
|
||
| // Remove bottom spacer when not floating | ||
| stackView.removeArrangedSubview(bottomSpacer) | ||
|
|
||
| // Move button back to normal position | ||
| NSLayoutConstraint.deactivate([ | ||
| primaryButtonFloatingBottomConstraint, | ||
| stackViewBottomConstraint, | ||
| ]) | ||
|
|
||
| primaryButtonTopAnchorConstraint.constant = -buttonSpacing | ||
|
|
||
| NSLayoutConstraint.activate([ | ||
| primaryButtonTopAnchorConstraint, | ||
| primaryButtonBottomConstraint, | ||
| ]) | ||
|
|
||
| UIView.animate(withDuration: 0.3) { | ||
| self.view.layoutIfNeeded() | ||
| } | ||
|
|
||
| isButtonFloating = false | ||
| } | ||
|
|
||
| /// Returns the default selected row in the vertical list - the previous payment option, the last VC's selection, or the customer's default. | ||
| func calculateInitialSelection() -> RowButtonType? { | ||
| if let previousPaymentOption { | ||
|
|
@@ -494,7 +575,33 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| var primaryButtonTopAnchorConstraint: NSLayoutConstraint! | ||
| private var stackViewBottomConstraint: NSLayoutConstraint! | ||
| private var primaryButtonTopAnchorConstraint: NSLayoutConstraint! | ||
| private var primaryButtonBottomConstraint: NSLayoutConstraint! | ||
| private var primaryButtonFloatingBottomConstraint: NSLayoutConstraint! | ||
| private var isButtonFloating: Bool = false | ||
| private var isKeyboardVisible: Bool = false | ||
| private var shouldButtonFloat: Bool { | ||
| // Only float when on the payment method list screen | ||
| guard let scrollView = bottomSheetController?.scrollView, let paymentMethodListViewController, children.contains(paymentMethodListViewController) else { | ||
| return false | ||
| } | ||
|
|
||
| // Only float if the top of the button in its natural position would not be visible at all | ||
| let contentHeight = scrollView.contentSize.height | ||
| let scrollViewVisibleHeight = scrollView.bounds.height | ||
|
|
||
| // Calculate where the button's top would be in its natural (non-floating) position | ||
| let topOfButtonInNaturalPosition = isButtonFloating ? | ||
| contentHeight - bottomSpacer.bounds.height + buttonSpacing : | ||
| contentHeight - primaryButton.bounds.height - configuration.appearance.formInsets.bottom | ||
|
|
||
| return topOfButtonInNaturalPosition > scrollViewVisibleHeight | ||
| } | ||
| private var buttonSpacing: CGFloat { | ||
| mandateView.attributedText == nil && errorLabel.text == nil ? 32 : 20 | ||
| } | ||
|
|
||
| // MARK: - UIViewController Methods | ||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
|
|
@@ -517,7 +624,9 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| subview.translatesAutoresizingMaskIntoConstraints = false | ||
| view.addSubview(subview) | ||
| } | ||
| primaryButtonTopAnchorConstraint = stackView.bottomAnchor.constraint(equalTo: primaryButton.topAnchor, constant: mandateView.attributedText == nil && errorLabel.text == nil ? -32 : -20) | ||
| primaryButtonTopAnchorConstraint = stackView.bottomAnchor.constraint(equalTo: primaryButton.topAnchor, constant: -buttonSpacing) | ||
| primaryButtonBottomConstraint = primaryButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -configuration.appearance.formInsets.bottom) | ||
|
|
||
| NSLayoutConstraint.activate([ | ||
| stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | ||
| stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | ||
|
|
@@ -526,8 +635,20 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
|
|
||
| stackView.topAnchor.constraint(equalTo: view.topAnchor), | ||
| primaryButtonTopAnchorConstraint, | ||
| primaryButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -configuration.appearance.formInsets.bottom), | ||
| primaryButtonBottomConstraint, | ||
| ]) | ||
|
|
||
| // Observe keyboard to prevent floating state changes when the keyboard is visible | ||
| NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) | ||
| NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) | ||
| } | ||
|
|
||
| @objc private func keyboardWillShow() { | ||
| isKeyboardVisible = true | ||
| } | ||
|
|
||
| @objc private func keyboardWillHide() { | ||
| isKeyboardVisible = false | ||
| } | ||
|
|
||
| private var canPresentLinkOnPrimaryButton: Bool { | ||
|
|
@@ -580,6 +701,12 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo | |
| logInitialDisplayedPaymentMethods() | ||
| isLinkWalletButtonSelected = false | ||
| linkConfirmOption = nil | ||
| updateFloatingButton() | ||
| } | ||
|
|
||
| override func viewWillDisappear(_ animated: Bool) { | ||
| deactivateFloatingButton() | ||
| super.viewWillDisappear(animated) | ||
|
Comment on lines
+707
to
+709
Collaborator
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. Why is this needed? Also nit: but can we move the |
||
| } | ||
|
|
||
| private func logInitialDisplayedPaymentMethods() { | ||
|
|
||
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.
This captures the height of the primary button when
bottomSpaceris first accessed, if theprimaryButtonhasn't been laid out yet the height will be 0 and the spacer will have the wrong height. Can you confirm thebottomSpaceris first accessed after theprimaryButtonhas been laid out? If you're not sure, maybe try recalculating this dynamically or using a constraint approach.