diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-06-16 11:24:55 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-06-20 11:05:29 +0200 |
| commit | 2c44099ddde23d3f0a6b7c0707634e5da7754183 (patch) | |
| tree | 2488dc71fac1587ff3be5a870d65d63f2fbddf68 /ios | |
| parent | 0f0555df589ae8662ab4c71d8d23106dbf9baf46 (diff) | |
| download | mullvadvpn-2c44099ddde23d3f0a6b7c0707634e5da7754183.tar.xz mullvadvpn-2c44099ddde23d3f0a6b7c0707634e5da7754183.zip | |
Fix constraints and add animations
Diffstat (limited to 'ios')
| -rw-r--r-- | ios/MullvadVPN/AccountInputGroupView.swift | 158 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginContentView.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginViewController.swift | 3 |
3 files changed, 121 insertions, 62 deletions
diff --git a/ios/MullvadVPN/AccountInputGroupView.swift b/ios/MullvadVPN/AccountInputGroupView.swift index e587679f7a..7bc54d236b 100644 --- a/ios/MullvadVPN/AccountInputGroupView.swift +++ b/ios/MullvadVPN/AccountInputGroupView.swift @@ -9,6 +9,8 @@ import UIKit import Logging +private let accountInputGroupViewAnimationDuration: TimeInterval = 0.25 + protocol AccountInputGroupViewDelegate: AnyObject { func accountInputGroupViewShouldRemoveLastUsedAccount(_ view: AccountInputGroupView) -> Bool func accountInputGroupViewShouldAttemptLogin(_ view: AccountInputGroupView) @@ -73,7 +75,6 @@ class AccountInputGroupView: UIView { private let separator: UIView = { let separator = UIView() separator.translatesAutoresizingMaskIntoConstraints = false - separator.backgroundColor = UIColor.AccountTextField.NormalState.borderColor return separator }() @@ -97,9 +98,9 @@ class AccountInputGroupView: UIView { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.titleLabel?.font = accountNumberFont() - button.setTitle("", for: .normal) + button.setTitle(" ", for: .normal) button.contentHorizontalAlignment = .leading - button.titleEdgeInsets = UIEdgeInsets(top: 0, left: UIMetrics.textFieldMargins.left, bottom: 0, right: 0) + button.contentEdgeInsets = UIMetrics.textFieldMargins button.setTitleColor(UIColor.AccountTextField.NormalState.textColor, for: .normal) return button @@ -114,7 +115,7 @@ class AccountInputGroupView: UIView { return button }() - private let contentView: UIView = { + let contentView: UIView = { let view = UIView() view.backgroundColor = .clear view.translatesAutoresizingMaskIntoConstraints = false @@ -127,7 +128,7 @@ class AccountInputGroupView: UIView { private let borderRadius = CGFloat(8) private let borderWidth = CGFloat(2) - private var lastUsedAccount: String = "" + private var lastUsedAccount: String? private var borderColor: UIColor { switch loginState { @@ -170,12 +171,11 @@ class AccountInputGroupView: UIView { } } - private let borderLayer = CAShapeLayer() + private let borderLayer = AccountInputBorderLayer() private let contentLayerMask = CALayer() - var lastUsedAccountViewHeightConstraint: NSLayoutConstraint! - var lastUsedAccountHeightConstraint: NSLayoutConstraint! - var separatorHeightConstraint: NSLayoutConstraint! + private var lastUsedAccountVisibleConstraint: NSLayoutConstraint! + private var lastUsedAccountHiddenConstraint: NSLayoutConstraint! // MARK: - View lifecycle @@ -184,34 +184,40 @@ class AccountInputGroupView: UIView { addSubview(contentView) contentView.addSubview(topRowView) + contentView.addSubview(separator) contentView.addSubview(bottomRowView) topRowView.addSubview(privateTextField) topRowView.addSubview(sendButton) - bottomRowView.addSubview(separator) bottomRowView.addSubview(lastUsedAccountButton) bottomRowView.addSubview(removeLastUsedAccountButton) privateTextField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) sendButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - separatorHeightConstraint = separator.heightAnchor.constraint(equalToConstant: 0) - lastUsedAccountHeightConstraint = lastUsedAccountButton.heightAnchor.constraint(equalToConstant: 0) - lastUsedAccountButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) removeLastUsedAccountButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + lastUsedAccountVisibleConstraint = heightAnchor.constraint(equalTo: contentView.heightAnchor) + lastUsedAccountHiddenConstraint = heightAnchor.constraint(equalTo: topRowView.heightAnchor) + NSLayoutConstraint.activate([ + lastUsedAccountHiddenConstraint, + contentView.topAnchor.constraint(equalTo: topAnchor), contentView.leadingAnchor.constraint(equalTo: leadingAnchor), contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: bottomAnchor), topRowView.topAnchor.constraint(equalTo: contentView.topAnchor), topRowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), topRowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - topRowView.bottomAnchor.constraint(equalTo: bottomRowView.topAnchor), + topRowView.bottomAnchor.constraint(equalTo: separator.topAnchor), + + separator.topAnchor.constraint(equalTo: topRowView.bottomAnchor), + separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: borderWidth), - bottomRowView.topAnchor.constraint(equalTo: topRowView.bottomAnchor), + bottomRowView.topAnchor.constraint(equalTo: separator.bottomAnchor), bottomRowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), bottomRowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), bottomRowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), @@ -226,24 +232,15 @@ class AccountInputGroupView: UIView { sendButton.bottomAnchor.constraint(equalTo: topRowView.bottomAnchor), sendButton.widthAnchor.constraint(equalTo: sendButton.heightAnchor), - separator.topAnchor.constraint(equalTo: bottomRowView.topAnchor), - separator.bottomAnchor.constraint(equalTo: lastUsedAccountButton.topAnchor), - separator.leadingAnchor.constraint(equalTo: bottomRowView.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: bottomRowView.trailingAnchor), - separatorHeightConstraint, - - lastUsedAccountButton.topAnchor.constraint(equalTo: separator.bottomAnchor), + lastUsedAccountButton.topAnchor.constraint(equalTo: bottomRowView.topAnchor), lastUsedAccountButton.bottomAnchor.constraint(equalTo: bottomRowView.bottomAnchor), lastUsedAccountButton.leadingAnchor.constraint(equalTo: bottomRowView.leadingAnchor), lastUsedAccountButton.trailingAnchor.constraint(equalTo: removeLastUsedAccountButton.leadingAnchor), - lastUsedAccountButton.heightAnchor.constraint(lessThanOrEqualTo: privateTextField.heightAnchor), - lastUsedAccountHeightConstraint, - removeLastUsedAccountButton.topAnchor.constraint(equalTo: separator.bottomAnchor), - removeLastUsedAccountButton.leadingAnchor.constraint(equalTo: lastUsedAccountButton.trailingAnchor), - removeLastUsedAccountButton.trailingAnchor.constraint(equalTo: bottomRowView.trailingAnchor), + removeLastUsedAccountButton.topAnchor.constraint(equalTo: bottomRowView.topAnchor), removeLastUsedAccountButton.bottomAnchor.constraint(equalTo: bottomRowView.bottomAnchor), - removeLastUsedAccountButton.widthAnchor.constraint(equalTo: removeLastUsedAccountButton.heightAnchor), + removeLastUsedAccountButton.trailingAnchor.constraint(equalTo: bottomRowView.trailingAnchor), + removeLastUsedAccountButton.widthAnchor.constraint(equalTo: sendButton.widthAnchor), ]) backgroundColor = UIColor.clear @@ -277,7 +274,7 @@ class AccountInputGroupView: UIView { updateAppearance() updateTextFieldEnabled() updateSendButtonAppearance(animated: animated) - updateLastUsedAccount() + updateLastUsedAccountConstraints(animated: animated) } func setOnReturnKey(_ onReturnKey: ((AccountInputGroupView) -> Bool)?) { @@ -302,10 +299,18 @@ class AccountInputGroupView: UIView { updateSendButtonAppearance(animated: false) } - func setLastUsedAccount(_ accountNumber: String) { + func setLastUsedAccount(_ accountNumber: String?, animated: Bool) { + if let accountNumber = accountNumber { + let formattedNumber = StringFormatter.formattedAccountNumber(from: accountNumber) + + UIView.performWithoutAnimation { + self.lastUsedAccountButton.setTitle(formattedNumber, for: .normal) + self.lastUsedAccountButton.layoutIfNeeded() + } + } + lastUsedAccount = accountNumber - lastUsedAccountButton.setTitle(accountNumber, for: .normal) - setLastUsedAccount(isExpanded: true) + updateLastUsedAccountConstraints(animated: animated) } // MARK: - CALayerDelegate @@ -345,20 +350,24 @@ class AccountInputGroupView: UIView { } @objc private func handleSendButton(_ sender: Any) { - self.delegate?.accountInputGroupViewShouldAttemptLogin(self) + delegate?.accountInputGroupViewShouldAttemptLogin(self) } @objc private func didTapLastUsedAccount() { + guard let lastUsedAccount = lastUsedAccount else { + return + } + setAccount(lastUsedAccount) privateTextField.resignFirstResponder() - setLastUsedAccount(isExpanded: false) - self.delegate?.accountInputGroupViewShouldAttemptLogin(self) + + updateLastUsedAccountConstraints(animated: true) + delegate?.accountInputGroupViewShouldAttemptLogin(self) } @objc private func didTapRemoveLastUsedAccount() { - if self.delegate?.accountInputGroupViewShouldRemoveLastUsedAccount(self) ?? false { - clearAccount() - setLastUsedAccount(isExpanded: false) + if delegate?.accountInputGroupViewShouldRemoveLastUsedAccount(self) ?? false { + setLastUsedAccount(nil, animated: true) } } @@ -391,6 +400,7 @@ class AccountInputGroupView: UIView { private func updateAppearance() { borderLayer.strokeColor = borderColor.cgColor + separator.backgroundColor = borderColor topRowView.backgroundColor = backgroundLayerColor privateTextField.textColor = textColor } @@ -405,26 +415,49 @@ class AccountInputGroupView: UIView { } } - private func updateLastUsedAccount() { - guard !lastUsedAccount.isEmpty else { - setLastUsedAccount(isExpanded: false) - return - } - switch self.loginState { - case .authenticating, .success: - setLastUsedAccount(isExpanded: false) - default: - setLastUsedAccount(isExpanded: true) + private func updateLastUsedAccountConstraints(animated: Bool) { + var shouldExpand: Bool + + if lastUsedAccount == nil { + shouldExpand = false + } else { + switch loginState { + case .authenticating, .success: + shouldExpand = false + case .default, .failure: + shouldExpand = true + } } + + updateLastUsedAccountConstraints(isExpanded: shouldExpand, animated: animated) } - private func setLastUsedAccount(isExpanded: Bool) { - lastUsedAccountHeightConstraint.constant = isExpanded ? 50 : 0 - lastUsedAccountButton.alpha = isExpanded ? 1 : 0 - lastUsedAccountButton.isUserInteractionEnabled = isExpanded ? true : false + private func updateLastUsedAccountConstraints(isExpanded: Bool, animated: Bool) { + let actions = { + let constraintToDeactivate: NSLayoutConstraint + let constraintToActivate: NSLayoutConstraint + + if isExpanded { + constraintToActivate = self.lastUsedAccountVisibleConstraint + constraintToDeactivate = self.lastUsedAccountHiddenConstraint + } else { + constraintToActivate = self.lastUsedAccountHiddenConstraint + constraintToDeactivate = self.lastUsedAccountVisibleConstraint + } - separatorHeightConstraint.constant = isExpanded ? 2 : 0 - separator.alpha = isExpanded ? 1 : 0 + constraintToDeactivate.isActive = false + constraintToActivate.isActive = true + } + + if animated { + actions() + UIView.animate(withDuration: accountInputGroupViewAnimationDuration) { + self.layoutIfNeeded() + } + } else { + actions() + setNeedsLayout() + } } private func updateSendButtonAppearance(animated: Bool) { @@ -453,7 +486,7 @@ class AccountInputGroupView: UIView { } if animated { - UIView.animate(withDuration: 0.25) { + UIView.animate(withDuration: accountInputGroupViewAnimationDuration) { actions() } } else { @@ -493,3 +526,16 @@ class AccountInputGroupView: UIView { } } + +private class AccountInputBorderLayer: CAShapeLayer { + override class func defaultAction(forKey event: String) -> CAAction? { + if event == "path" { + let action = CABasicAnimation(keyPath: event) + action.duration = accountInputGroupViewAnimationDuration + action.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + return action + } + return super.defaultAction(forKey: event) + } +} diff --git a/ios/MullvadVPN/LoginContentView.swift b/ios/MullvadVPN/LoginContentView.swift index 18f3e5df08..661ab8401e 100644 --- a/ios/MullvadVPN/LoginContentView.swift +++ b/ios/MullvadVPN/LoginContentView.swift @@ -35,6 +35,12 @@ class LoginContentView: UIView { return inputGroup }() + let accountInputGroupWrapper: UIView = { + let wrapperView = UIView() + wrapperView.translatesAutoresizingMaskIntoConstraints = false + return wrapperView + }() + let statusImageView: StatusImageView = { let imageView = StatusImageView(style: .failure) imageView.translatesAutoresizingMaskIntoConstraints = false @@ -156,7 +162,8 @@ class LoginContentView: UIView { private func addSubviews() { formContainer.addSubview(titleLabel) formContainer.addSubview(messageLabel) - formContainer.addSubview(accountInputGroup) + formContainer.addSubview(accountInputGroupWrapper) + accountInputGroupWrapper.addSubview(accountInputGroup) contentContainer.addSubview(activityIndicator) contentContainer.addSubview(statusImageView) @@ -195,6 +202,7 @@ class LoginContentView: UIView { formContainer.centerYAnchor.constraint(equalTo: contentContainer.centerYAnchor, constant: -20), formContainer.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor), formContainer.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor), + formContainer.bottomAnchor.constraint(equalTo: accountInputGroupWrapper.bottomAnchor), activityIndicator.centerXAnchor.constraint(equalTo: statusImageView.centerXAnchor), activityIndicator.centerYAnchor.constraint(equalTo: statusImageView.centerYAnchor), @@ -207,10 +215,14 @@ class LoginContentView: UIView { messageLabel.leadingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), messageLabel.trailingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), - accountInputGroup.topAnchor.constraint(equalToSystemSpacingBelow: messageLabel.bottomAnchor, multiplier: 1), - accountInputGroup.leadingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), - accountInputGroup.trailingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), - accountInputGroup.bottomAnchor.constraint(equalTo: formContainer.bottomAnchor), + accountInputGroupWrapper.topAnchor.constraint(equalToSystemSpacingBelow: messageLabel.bottomAnchor, multiplier: 1), + accountInputGroupWrapper.leadingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), + accountInputGroupWrapper.trailingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), + accountInputGroupWrapper.heightAnchor.constraint(equalTo: accountInputGroup.contentView.heightAnchor), + + accountInputGroup.topAnchor.constraint(equalTo: accountInputGroupWrapper.topAnchor), + accountInputGroup.leadingAnchor.constraint(equalTo: accountInputGroupWrapper.leadingAnchor), + accountInputGroup.trailingAnchor.constraint(equalTo: accountInputGroupWrapper.trailingAnchor), ]) } diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index 0c356fec68..bdffad84b2 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -224,7 +224,8 @@ class LoginViewController: UIViewController, RootContainment { private func updateLastUsedAccount() { do { let accountNumber = try SettingsManager.getLastUsedAccount() - contentView.accountInputGroup.setLastUsedAccount(StringFormatter.formattedAccountNumber(from: accountNumber)) + + contentView.accountInputGroup.setLastUsedAccount(accountNumber, animated: false) } catch { logger.error(chainedError: AnyChainedError(error), message: "Failed to update last used account.") |
