summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-06-16 11:24:55 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-06-20 11:05:29 +0200
commit2c44099ddde23d3f0a6b7c0707634e5da7754183 (patch)
tree2488dc71fac1587ff3be5a870d65d63f2fbddf68 /ios
parent0f0555df589ae8662ab4c71d8d23106dbf9baf46 (diff)
downloadmullvadvpn-2c44099ddde23d3f0a6b7c0707634e5da7754183.tar.xz
mullvadvpn-2c44099ddde23d3f0a6b7c0707634e5da7754183.zip
Fix constraints and add animations
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadVPN/AccountInputGroupView.swift158
-rw-r--r--ios/MullvadVPN/LoginContentView.swift22
-rw-r--r--ios/MullvadVPN/LoginViewController.swift3
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.")