diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-03-22 13:54:16 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-03-23 10:16:14 +0100 |
| commit | c86923e77b54360347202aef32fe6dbde2165946 (patch) | |
| tree | 25f4bc41b19ef04923ffc93b1d16407a182827ff | |
| parent | 599366b9418423b041f725ed60cc097761cb41db (diff) | |
| download | mullvadvpn-c86923e77b54360347202aef32fe6dbde2165946.tar.xz mullvadvpn-c86923e77b54360347202aef32fe6dbde2165946.zip | |
Extract LoginContentView
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 9 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountInputGroupView.swift | 44 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginContentView.swift | 247 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginViewController.swift | 157 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginViewController.xib | 199 |
5 files changed, 345 insertions, 311 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 35431c363f..1928db029c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -143,7 +143,6 @@ 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; }; 58AB9DEC2501040C006C5526 /* ConsentViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 58AB9DEB2501040C006C5526 /* ConsentViewController.xib */; }; - 58AB9DEE25010636006C5526 /* LoginViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 58AB9DED25010636006C5526 /* LoginViewController.xib */; }; 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; @@ -158,6 +157,7 @@ 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; 58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743722B25EAB00015324 /* PacketTunnelSettingsGenerator.swift */; }; 58B9814E24FEA70D00C0D59E /* WireguardKeysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 58B9814D24FEA70D00C0D59E /* WireguardKeysViewController.xib */; }; + 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; }; 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; }; 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB142489139B00095626 /* DisplayChainedError.swift */; }; 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; @@ -352,7 +352,6 @@ 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; }; 58AB9DEB2501040C006C5526 /* ConsentViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConsentViewController.xib; sourceTree = "<group>"; }; - 58AB9DED25010636006C5526 /* LoginViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LoginViewController.xib; sourceTree = "<group>"; }; 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -361,6 +360,7 @@ 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardAssociatedAddresses.swift; sourceTree = "<group>"; }; 58B8743722B25EAB00015324 /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; }; 58B9814D24FEA70D00C0D59E /* WireguardKeysViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WireguardKeysViewController.xib; sourceTree = "<group>"; }; + 58B993B02608A34500BA7811 /* LoginContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContentView.swift; sourceTree = "<group>"; }; 58B9EB122488ED2100095626 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = "<group>"; }; 58B9EB142489139B00095626 /* DisplayChainedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayChainedError.swift; sourceTree = "<group>"; }; 58BA692D23E99EFF009DC256 /* Locking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locking.swift; sourceTree = "<group>"; }; @@ -592,7 +592,6 @@ 58BA692D23E99EFF009DC256 /* Locking.swift */, 5815039F24D6ECF200C9C50E /* Logging */, 58CE5E65224146200008646E /* LoginViewController.swift */, - 58AB9DED25010636006C5526 /* LoginViewController.xib */, 58C3B06624EA768100C0348E /* LogStreamerViewController.swift */, 58CE5E67224146200008646E /* Main.storyboard */, 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */, @@ -645,6 +644,7 @@ 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */, 585CA70E25F8C44600B47C62 /* UIMetrics.swift */, 583DA21325FA4B5C00318683 /* LocationDataSource.swift */, + 58B993B02608A34500BA7811 /* LoginContentView.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -873,7 +873,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 58AB9DEE25010636006C5526 /* LoginViewController.xib in Resources */, + 58D9AF6B2501111800B6FAB5 /* ConnectViewController.xib in Resources */, 58F3C0A624A50157003E76BE /* relays.json in Resources */, 58CE5E6E224146210008646E /* LaunchScreen.storyboard in Resources */, 58CE5E6B224146210008646E /* Assets.xcassets in Resources */, @@ -956,6 +956,7 @@ 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */, 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */, 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, + 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */, diff --git a/ios/MullvadVPN/AccountInputGroupView.swift b/ios/MullvadVPN/AccountInputGroupView.swift index bfff00d4df..f3c2a05d8d 100644 --- a/ios/MullvadVPN/AccountInputGroupView.swift +++ b/ios/MullvadVPN/AccountInputGroupView.swift @@ -8,9 +8,27 @@ import UIKit -@IBDesignable class AccountInputGroupView: UIView { +class AccountInputGroupView: UIView { - @IBOutlet var textField: UITextField! + let textField: AccountTextField = { + let textField = AccountTextField() + textField.font = UIFont.systemFont(ofSize: 20) + textField.translatesAutoresizingMaskIntoConstraints = false + textField.attributedPlaceholder = NSAttributedString( + string: "0000 0000 0000 0000", + attributes: [.foregroundColor: UIColor.lightGray]) + textField.textContentType = .username + textField.clearButtonMode = .never + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.smartDashesType = .no + textField.smartInsertDeleteType = .no + textField.smartQuotesType = .no + textField.spellCheckingType = .no + textField.keyboardType = .numberPad + + return textField + }() enum Style { case normal, error, authenticating @@ -73,9 +91,23 @@ import UIKit // MARK: - View lifecycle - override func awakeFromNib() { - super.awakeFromNib() - setup() + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(textField) + + NSLayoutConstraint.activate([ + textField.topAnchor.constraint(equalTo: topAnchor), + textField.leadingAnchor.constraint(equalTo: leadingAnchor), + textField.trailingAnchor.constraint(equalTo: trailingAnchor), + textField.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } // MARK: - CALayerDelegate @@ -113,7 +145,7 @@ import UIKit // MARK: - Private - private func setup() { + private func setupView() { backgroundColor = UIColor.clear borderLayer.lineWidth = borderWidth diff --git a/ios/MullvadVPN/LoginContentView.swift b/ios/MullvadVPN/LoginContentView.swift new file mode 100644 index 0000000000..ce2bb46a90 --- /dev/null +++ b/ios/MullvadVPN/LoginContentView.swift @@ -0,0 +1,247 @@ +// +// LoginContentView.swift +// MullvadVPN +// +// Created by pronebird on 22/03/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class LoginContentView: UIView { + + lazy var titleLabel: UILabel = { + let textLabel = UILabel() + textLabel.font = UIFont.systemFont(ofSize: 32) + textLabel.textColor = .white + textLabel.translatesAutoresizingMaskIntoConstraints = false + return textLabel + }() + + let messageLabel: UILabel = { + let textLabel = UILabel() + textLabel.font = UIFont.systemFont(ofSize: 17) + textLabel.textColor = UIColor.white.withAlphaComponent(0.6) + textLabel.translatesAutoresizingMaskIntoConstraints = false + return textLabel + }() + + let accountInputGroup: AccountInputGroupView = { + let inputGroup = AccountInputGroupView() + inputGroup.translatesAutoresizingMaskIntoConstraints = false + return inputGroup + }() + + var accountTextField: AccountTextField { + return accountInputGroup.textField + } + + let statusImageView: StatusImageView = { + let imageView = StatusImageView(style: .failure) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.alpha = 0 + return imageView + }() + + let contentContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layoutMargins = UIMetrics.contentLayoutMargins + return view + }() + + let formContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layoutMargins = UIMetrics.contentLayoutMargins + return view + }() + + let footerContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layoutMargins = UIMetrics.contentLayoutMargins + view.backgroundColor = .secondaryColor + return view + }() + + let footerLabel: UILabel = { + let textLabel = UILabel() + textLabel.font = UIFont.systemFont(ofSize: 17) + textLabel.textColor = UIColor.white.withAlphaComponent(0.6) + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.text = NSLocalizedString("Don't have an account number?", comment: "") + return textLabel + }() + + let createAccountButton: AppButton = { + let button = AppButton(style: .default) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString("Create account", comment: ""), for: .normal) + return button + }() + + let activityIndicator: SpinnerActivityIndicatorView = { + let view = SpinnerActivityIndicatorView(style: .large) + view.tintColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private var isStatusImageVisible = false + private var contentContainerBottomConstraint: NSLayoutConstraint? + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .primaryColor + layoutMargins = UIMetrics.contentLayoutMargins + + addSubviews() + addKeyboardHandlers() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setStatusImage(style: StatusImageView.Style?, visible: Bool, animated: Bool) { + if let style = style { + statusImageView.style = style + } + + isStatusImageVisible = visible + updateStatusImageVisibility(animated: animated) + } + + private func updateStatusImageVisibility(animated: Bool) { + let statusImageFrame = statusImageView.convert(statusImageView.bounds, to: self) + let shouldShow = isStatusImageVisible && safeAreaLayoutGuide.layoutFrame.contains(statusImageFrame) + + let actions = { + // Only display the status image if it doesn't overlap the safe area layout guide. + if shouldShow { + self.statusImageView.alpha = 1 + } else { + self.statusImageView.alpha = 0 + } + } + + if animated { + UIView.animate(withDuration: 0.25) { + actions() + } + } else { + actions() + } + } + + private func addSubviews() { + formContainer.addSubview(titleLabel) + formContainer.addSubview(messageLabel) + formContainer.addSubview(accountInputGroup) + + contentContainer.addSubview(activityIndicator) + contentContainer.addSubview(statusImageView) + contentContainer.addSubview(formContainer) + + footerContainer.addSubview(footerLabel) + footerContainer.addSubview(createAccountButton) + + addSubview(contentContainer) + addSubview(footerContainer) + + let contentContainerBottomConstraint = bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor) + self.contentContainerBottomConstraint = contentContainerBottomConstraint + + NSLayoutConstraint.activate([ + contentContainer.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + contentContainer.leadingAnchor.constraint(equalTo: leadingAnchor), + contentContainer.trailingAnchor.constraint(equalTo: trailingAnchor), + contentContainerBottomConstraint, + + footerContainer.leadingAnchor.constraint(equalTo: leadingAnchor), + footerContainer.trailingAnchor.constraint(equalTo: trailingAnchor), + footerContainer.bottomAnchor.constraint(equalTo: bottomAnchor), + + footerLabel.topAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.topAnchor), + footerLabel.leadingAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.leadingAnchor), + footerLabel.trailingAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.trailingAnchor), + + createAccountButton.topAnchor.constraint(equalToSystemSpacingBelow: footerLabel.bottomAnchor, multiplier: 1), + createAccountButton.leadingAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.leadingAnchor), + createAccountButton.trailingAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.trailingAnchor), + createAccountButton.bottomAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.bottomAnchor), + + statusImageView.centerXAnchor.constraint(equalTo: contentContainer.centerXAnchor), + formContainer.topAnchor.constraint(equalTo: statusImageView.bottomAnchor, constant: 30), + formContainer.centerYAnchor.constraint(equalTo: contentContainer.centerYAnchor, constant: -20), + formContainer.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor), + formContainer.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor), + + activityIndicator.centerXAnchor.constraint(equalTo: statusImageView.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: statusImageView.centerYAnchor), + + titleLabel.topAnchor.constraint(equalTo: formContainer.topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), + + messageLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1), + 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), + ]) + } + + private func addKeyboardHandlers() { + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, + selector: #selector(keyboardWillShow(_:)), + name: UIWindow.keyboardWillShowNotification, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(keyboardWillChangeFrame(_:)), + name: UIWindow.keyboardWillChangeFrameNotification, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(keyboardWillHide(_:)), + name: UIWindow.keyboardWillHideNotification, + object: nil) + } + + + // MARK: - Keyboard notifications + + @objc private func keyboardWillShow(_ notification: Notification) { + guard let keyboardFrameValue = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return } + + makeLoginFormVisible(keyboardFrame: keyboardFrameValue.cgRectValue) + } + + @objc private func keyboardWillChangeFrame(_ notification: Notification) { + guard let keyboardFrameValue = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return } + + makeLoginFormVisible(keyboardFrame: keyboardFrameValue.cgRectValue) + } + + @objc private func keyboardWillHide(_ notification: Notification) { + contentContainerBottomConstraint?.constant = 0 + layoutIfNeeded() + updateStatusImageVisibility(animated: false) + } + + private func makeLoginFormVisible(keyboardFrame: CGRect) { + let viewFrame = convert(bounds, to: nil) + let intersection = viewFrame.intersection(keyboardFrame) + + contentContainerBottomConstraint?.constant = intersection.height + + layoutIfNeeded() + updateStatusImageVisibility(animated: false) + } +} diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index a0c676fb33..f15514c6ce 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -28,17 +28,30 @@ protocol LoginViewControllerDelegate: class { class LoginViewController: UIViewController, RootContainment { - @IBOutlet var keyboardToolbar: UIToolbar! - @IBOutlet var keyboardToolbarLoginButton: UIBarButtonItem! - @IBOutlet var accountInputGroup: AccountInputGroupView! - @IBOutlet var accountTextField: AccountTextField! - @IBOutlet var titleLabel: UILabel! - @IBOutlet var messageLabel: UILabel! - @IBOutlet var loginForm: UIView! - @IBOutlet var loginFormWrapperBottomConstraint: NSLayoutConstraint! - @IBOutlet var activityIndicator: SpinnerActivityIndicatorView! - @IBOutlet var statusImageView: StatusImageView! - @IBOutlet var createAccountButton: AppButton! + private lazy var contentView: LoginContentView = { + let view = LoginContentView(frame: self.view.bounds) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private lazy var accountInputAccessoryCancelButton: UIBarButtonItem = { + return UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelLogin)) + }() + + private lazy var accountInputAccessoryLoginButton: UIBarButtonItem = { + return UIBarButtonItem(title: NSLocalizedString("Log in", comment: ""), style: .done, target: self, action: #selector(doLogin)) + }() + + private lazy var accountInputAccessoryToolbar: UIToolbar = { + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 44)) + toolbar.items = [ + self.accountInputAccessoryCancelButton, + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + self.accountInputAccessoryLoginButton + ] + toolbar.sizeToFit() + return toolbar + }() private let logger = Logger(label: "LoginViewController") @@ -65,10 +78,15 @@ class LoginViewController: UIViewController, RootContainment { override func viewDidLoad() { super.viewDidLoad() - accountTextField.inputAccessoryView = keyboardToolbar - accountTextField.attributedPlaceholder = NSAttributedString( - string: "0000 0000 0000 0000", - attributes: [.foregroundColor: UIColor.lightGray]) + view.addSubview(contentView) + NSLayoutConstraint.activate([ + contentView.topAnchor.constraint(equalTo: view.topAnchor), + contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + contentView.accountTextField.inputAccessoryView = self.accountInputAccessoryToolbar updateDisplayedMessage() updateStatusIcon() @@ -76,72 +94,24 @@ class LoginViewController: UIViewController, RootContainment { let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, - selector: #selector(keyboardWillShow(_:)), - name: UIWindow.keyboardWillShowNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(keyboardWillChangeFrame(_:)), - name: UIWindow.keyboardWillChangeFrameNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(keyboardWillHide(_:)), - name: UIWindow.keyboardWillHideNotification, - object: nil) - - notificationCenter.addObserver(self, - selector: #selector(textDidBeginEditing(_:)), - name: UITextField.textDidBeginEditingNotification, - object: accountTextField) - - notificationCenter.addObserver(self, - selector: #selector(textDidEndEditing(_:)), - name: UITextField.textDidEndEditingNotification, - object: accountTextField) + contentView.createAccountButton.addTarget(self, action: #selector(createNewAccount), for: .touchUpInside) notificationCenter.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, - object: accountTextField) + object: contentView.accountTextField) } // MARK: - Public func reset() { loginState = .default - accountTextField.autoformattingText = "" + contentView.accountTextField.autoformattingText = "" updateKeyboardToolbar() } - // MARK: - Keyboard notifications - - @objc private func keyboardWillShow(_ notification: Notification) { - guard let keyboardFrameValue = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return } - - makeLoginFormVisible(keyboardFrame: keyboardFrameValue.cgRectValue) - } - - @objc private func keyboardWillChangeFrame(_ notification: Notification) { - guard let keyboardFrameValue = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return } - - makeLoginFormVisible(keyboardFrame: keyboardFrameValue.cgRectValue) - } - - @objc private func keyboardWillHide(_ notification: Notification) { - loginFormWrapperBottomConstraint.constant = 0 - view.layoutIfNeeded() - } - // MARK: - UITextField notifications - @objc func textDidBeginEditing(_ notification: Notification) { - updateStatusIcon() - } - - @objc func textDidEndEditing(_ notification: Notification) { - updateStatusIcon() - } - @objc func textDidChange(_ notification: Notification) { // Reset the text style as user start typing if case .failure = loginState { @@ -154,12 +124,12 @@ class LoginViewController: UIViewController, RootContainment { // MARK: - Actions - @IBAction func cancelLogin() { + @objc func cancelLogin() { view.endEditing(true) } - @IBAction func doLogin() { - let accountToken = accountTextField.parsedToken + @objc func doLogin() { + let accountToken = contentView.accountTextField.parsedToken beginLogin(method: .existingAccount) @@ -176,16 +146,16 @@ class LoginViewController: UIViewController, RootContainment { } } - @IBAction func createNewAccount() { + @objc func createNewAccount() { beginLogin(method: .newAccount) - accountTextField.autoformattingText = "" + contentView.accountTextField.autoformattingText = "" updateKeyboardToolbar() Account.shared.loginWithNewAccount { (result) in switch result { case .success(let response): - self.accountTextField.autoformattingText = response.token + self.contentView.accountTextField.autoformattingText = response.token self.endLogin(.success(.newAccount)) case .failure(let error): @@ -199,15 +169,15 @@ class LoginViewController: UIViewController, RootContainment { // MARK: - Private private func loginStateDidChange() { - accountInputGroup.loginState = loginState + contentView.accountInputGroup.loginState = loginState // Keep the settings button disabled to prevent user from going to settings while // authentication or during the delay after the successful login and transition to the main // controller. switch loginState { case .authenticating: - activityIndicator.startAnimating() - createAccountButton.isEnabled = false + contentView.activityIndicator.startAnimating() + contentView.createAccountButton.isEnabled = false // Fallthrough to make sure that the settings button is disabled // in .authenticating and .success cases. @@ -218,8 +188,8 @@ class LoginViewController: UIViewController, RootContainment { case .default, .failure: rootContainerController?.setEnableSettingsButton(true) - createAccountButton.isEnabled = true - activityIndicator.stopAnimating() + contentView.createAccountButton.isEnabled = true + contentView.activityIndicator.stopAnimating() } updateDisplayedMessage() @@ -229,22 +199,13 @@ class LoginViewController: UIViewController, RootContainment { private func updateStatusIcon() { switch loginState { case .failure: - let opacity: CGFloat = self.accountTextField.isEditing ? 0 : 1 - statusImageView.style = .failure - animateStatusImage(to: opacity) + contentView.setStatusImage(style: .failure, visible: true, animated: true) case .success: - statusImageView.style = .success - animateStatusImage(to: 1) + contentView.setStatusImage(style: .success, visible: true, animated: true) case .default, .authenticating: - animateStatusImage(to: 0) - } - } - - private func animateStatusImage(to alpha: CGFloat) { - UIView.animate(withDuration: 0.25) { - self.statusImageView.alpha = alpha + contentView.setStatusImage(style: nil, visible: false, animated: true) } } @@ -261,7 +222,7 @@ class LoginViewController: UIViewController, RootContainment { if case .authenticating(.existingAccount) = oldLoginState, case .failure = loginState { - accountTextField.becomeFirstResponder() + contentView.accountTextField.becomeFirstResponder() } else if case .success = loginState { // Navigate to the main view after 1s delay DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { @@ -273,23 +234,15 @@ class LoginViewController: UIViewController, RootContainment { } private func updateDisplayedMessage() { - titleLabel.text = loginState.localizedTitle - messageLabel.text = loginState.localizedMessage + contentView.titleLabel.text = loginState.localizedTitle + contentView.messageLabel.text = loginState.localizedMessage } private func updateKeyboardToolbar() { - let accountTokenLength = accountTextField.parsedToken.count + let accountTokenLength = contentView.accountTextField.parsedToken.count let enableButton = accountTokenLength >= kMinimumAccountTokenLength - keyboardToolbarLoginButton.isEnabled = enableButton - } - - private func makeLoginFormVisible(keyboardFrame: CGRect) { - let convertedKeyboardFrame = view.convert(keyboardFrame, from: nil) - let (_, remainder) = view.frame.divided(atDistance: convertedKeyboardFrame.minY, from: CGRectEdge.minYEdge) - - loginFormWrapperBottomConstraint.constant = remainder.height - view.layoutIfNeeded() + accountInputAccessoryLoginButton.isEnabled = enableButton } } diff --git a/ios/MullvadVPN/LoginViewController.xib b/ios/MullvadVPN/LoginViewController.xib deleted file mode 100644 index e5dd791b1b..0000000000 --- a/ios/MullvadVPN/LoginViewController.xib +++ /dev/null @@ -1,199 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> - <device id="retina6_1" orientation="portrait" appearance="light"/> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/> - <capability name="Named colors" minToolsVersion="9.0"/> - <capability name="Safe area layout guides" minToolsVersion="9.0"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <objects> - <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LoginViewController" customModule="MullvadVPN" customModuleProvider="target"> - <connections> - <outlet property="accountInputGroup" destination="fmY-Fe-bhX" id="lRy-3H-p77"/> - <outlet property="accountTextField" destination="M05-uw-Xgl" id="d7E-mT-gts"/> - <outlet property="activityIndicator" destination="kcs-fP-UuB" id="nTJ-wH-MFm"/> - <outlet property="createAccountButton" destination="yyj-Bk-eOB" id="A6e-QQ-2Hz"/> - <outlet property="keyboardToolbar" destination="eMY-ag-aGA" id="MXd-lD-jRp"/> - <outlet property="keyboardToolbarLoginButton" destination="vJz-hf-rNV" id="IcH-iY-AI6"/> - <outlet property="loginForm" destination="h09-lb-ltN" id="4pC-oh-SiV"/> - <outlet property="loginFormWrapperBottomConstraint" destination="a30-Jy-fnc" id="LXy-zf-zOf"/> - <outlet property="messageLabel" destination="BPK-kZ-9lL" id="eKD-KS-aq8"/> - <outlet property="statusImageView" destination="L2c-40-K8a" id="6xq-lK-xjr"/> - <outlet property="titleLabel" destination="edY-81-swW" id="7HS-3s-d6L"/> - <outlet property="view" destination="mbj-X6-VW9" id="7jT-Iq-dEp"/> - </connections> - </placeholder> - <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> - <view contentMode="scaleToFill" id="mbj-X6-VW9"> - <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> - <subviews> - <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hlu-LB-fs2" userLabel="Container"> - <rect key="frame" x="0.0" y="44" width="414" height="852"/> - <subviews> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kcs-fP-UuB" customClass="SpinnerActivityIndicatorView" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="183" y="265.5" width="48" height="48"/> - <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <constraints> - <constraint firstAttribute="width" constant="48" id="ayU-NN-NND"/> - <constraint firstAttribute="height" constant="48" id="tdi-f8-eAx"/> - </constraints> - </view> - <view clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.0" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L2c-40-K8a" customClass="StatusImageView" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="177" y="259.5" width="60" height="60"/> - </view> - <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="h09-lb-ltN" userLabel="Form"> - <rect key="frame" x="0.0" y="343.5" width="414" height="125.5"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Login" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="edY-81-swW"> - <rect key="frame" x="24" y="0.0" width="366" height="39"/> - <fontDescription key="fontDescription" type="system" pointSize="32"/> - <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter your account number" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BPK-kZ-9lL"> - <rect key="frame" x="24" y="47" width="366" height="20.5"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" white="1" alpha="0.60359589039999995" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fmY-Fe-bhX" customClass="AccountInputGroupView" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="24" y="77.5" width="366" height="48"/> - <subviews> - <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="0000 0000 0000 0000" textAlignment="natural" adjustsFontSizeToFit="NO" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="M05-uw-Xgl" userLabel="Account Text Field" customClass="AccountTextField" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="366" height="48"/> - <accessibility key="accessibilityConfiguration" identifier="LoginTextField"/> - <fontDescription key="fontDescription" type="system" pointSize="20"/> - <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="numberPad" enablesReturnKeyAutomatically="YES" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="username"/> - </textField> - </subviews> - <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <constraints> - <constraint firstItem="M05-uw-Xgl" firstAttribute="top" secondItem="fmY-Fe-bhX" secondAttribute="top" id="9ld-io-34b"/> - <constraint firstAttribute="height" constant="48" placeholder="YES" id="QKg-Me-fbd"/> - <constraint firstAttribute="trailing" secondItem="M05-uw-Xgl" secondAttribute="trailing" id="XbY-CL-4DV"/> - <constraint firstAttribute="bottom" secondItem="M05-uw-Xgl" secondAttribute="bottom" id="qle-Bq-Gwu"/> - <constraint firstItem="M05-uw-Xgl" firstAttribute="leading" secondItem="fmY-Fe-bhX" secondAttribute="leading" id="ti3-Jt-gIj"/> - </constraints> - <connections> - <outlet property="textField" destination="M05-uw-Xgl" id="Stn-EF-XKw"/> - </connections> - </view> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <constraints> - <constraint firstAttribute="trailingMargin" secondItem="edY-81-swW" secondAttribute="trailing" id="1OY-3d-amx"/> - <constraint firstItem="edY-81-swW" firstAttribute="leading" secondItem="h09-lb-ltN" secondAttribute="leadingMargin" id="2EO-AX-dOa"/> - <constraint firstItem="fmY-Fe-bhX" firstAttribute="leading" secondItem="edY-81-swW" secondAttribute="leading" id="6Yg-wS-lQA"/> - <constraint firstItem="edY-81-swW" firstAttribute="top" secondItem="h09-lb-ltN" secondAttribute="top" id="L7R-pW-kDT"/> - <constraint firstItem="BPK-kZ-9lL" firstAttribute="leading" secondItem="edY-81-swW" secondAttribute="leading" id="Luz-tR-dyN"/> - <constraint firstItem="BPK-kZ-9lL" firstAttribute="top" secondItem="edY-81-swW" secondAttribute="bottom" constant="8" id="RQ5-p6-Tcp"/> - <constraint firstItem="fmY-Fe-bhX" firstAttribute="top" secondItem="BPK-kZ-9lL" secondAttribute="bottom" constant="10" id="T6H-O9-mQp"/> - <constraint firstItem="fmY-Fe-bhX" firstAttribute="trailing" secondItem="edY-81-swW" secondAttribute="trailing" id="lqk-t6-mkw"/> - <constraint firstItem="BPK-kZ-9lL" firstAttribute="trailing" secondItem="edY-81-swW" secondAttribute="trailing" id="mit-be-kUU"/> - <constraint firstAttribute="bottom" secondItem="fmY-Fe-bhX" secondAttribute="bottom" id="r57-ni-R5Y"/> - </constraints> - </view> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <constraints> - <constraint firstItem="h09-lb-ltN" firstAttribute="leading" secondItem="Hlu-LB-fs2" secondAttribute="leading" id="6ui-Qt-KHA"/> - <constraint firstItem="L2c-40-K8a" firstAttribute="centerX" secondItem="kcs-fP-UuB" secondAttribute="centerX" id="Bn8-0h-4u1"/> - <constraint firstItem="L2c-40-K8a" firstAttribute="centerY" secondItem="kcs-fP-UuB" secondAttribute="centerY" id="EmV-jB-7A5"/> - <constraint firstItem="kcs-fP-UuB" firstAttribute="centerX" secondItem="Hlu-LB-fs2" secondAttribute="centerX" id="aHt-jx-l4c"/> - <constraint firstItem="h09-lb-ltN" firstAttribute="top" secondItem="kcs-fP-UuB" secondAttribute="bottom" constant="30" id="b9G-Ra-uTL"/> - <constraint firstItem="h09-lb-ltN" firstAttribute="centerY" secondItem="Hlu-LB-fs2" secondAttribute="centerY" constant="-20" id="d3Q-mn-gqk"/> - <constraint firstAttribute="trailing" secondItem="h09-lb-ltN" secondAttribute="trailing" id="hkn-ig-VGd"/> - </constraints> - </view> - <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OWx-up-Gr3" userLabel="Footer"> - <rect key="frame" x="0.0" y="751.5" width="414" height="144.5"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Don't have an account number?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kmh-Zz-WX7"> - <rect key="frame" x="24" y="16" width="366" height="20.5"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" white="1" alpha="0.60327482880000005" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bk-eOB" customClass="AppButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="24" y="44.5" width="366" height="42"/> - <constraints> - <constraint firstAttribute="height" constant="42" placeholder="YES" id="aLH-Os-Yhl"/> - </constraints> - <state key="normal" title="Create account" backgroundImage="DefaultButton"> - <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - </state> - <connections> - <action selector="createNewAccount" destination="-1" eventType="touchUpInside" id="Sat-eK-Sfr"/> - </connections> - </button> - </subviews> - <color key="backgroundColor" name="Secondary"/> - <constraints> - <constraint firstItem="Kmh-Zz-WX7" firstAttribute="top" secondItem="OWx-up-Gr3" secondAttribute="topMargin" id="2gd-XW-al2"/> - <constraint firstAttribute="bottomMargin" secondItem="yyj-Bk-eOB" secondAttribute="bottom" id="5sc-Gk-BLo"/> - <constraint firstItem="yyj-Bk-eOB" firstAttribute="top" secondItem="Kmh-Zz-WX7" secondAttribute="bottom" constant="8" id="6ep-aF-S3v"/> - <constraint firstAttribute="trailingMargin" secondItem="yyj-Bk-eOB" secondAttribute="trailing" id="JZr-eV-Ptz"/> - <constraint firstItem="yyj-Bk-eOB" firstAttribute="leading" secondItem="OWx-up-Gr3" secondAttribute="leadingMargin" id="aF1-ha-Rup"/> - <constraint firstItem="Kmh-Zz-WX7" firstAttribute="leading" secondItem="OWx-up-Gr3" secondAttribute="leadingMargin" id="joi-NK-b3p"/> - <constraint firstAttribute="trailingMargin" secondItem="Kmh-Zz-WX7" secondAttribute="trailing" id="pJv-fq-yMY"/> - </constraints> - <edgeInsets key="layoutMargins" top="16" left="0.0" bottom="24" right="0.0"/> - </view> - </subviews> - <viewLayoutGuide key="safeArea" id="nNG-OH-fpp"/> - <color key="backgroundColor" name="Primary"/> - <constraints> - <constraint firstItem="Hlu-LB-fs2" firstAttribute="top" secondItem="nNG-OH-fpp" secondAttribute="top" id="9Wj-KC-bOY"/> - <constraint firstAttribute="bottom" secondItem="OWx-up-Gr3" secondAttribute="bottom" id="Gtb-JS-xnU"/> - <constraint firstItem="OWx-up-Gr3" firstAttribute="leading" secondItem="mbj-X6-VW9" secondAttribute="leading" id="HGZ-UR-R1R"/> - <constraint firstItem="Hlu-LB-fs2" firstAttribute="leading" secondItem="mbj-X6-VW9" secondAttribute="leading" id="Psd-xu-SF5"/> - <constraint firstAttribute="bottom" secondItem="Hlu-LB-fs2" secondAttribute="bottom" id="a30-Jy-fnc"/> - <constraint firstAttribute="trailing" secondItem="Hlu-LB-fs2" secondAttribute="trailing" id="eE0-5M-Lr5"/> - <constraint firstAttribute="trailing" secondItem="OWx-up-Gr3" secondAttribute="trailing" id="jUk-Gq-dMy"/> - </constraints> - <edgeInsets key="layoutMargins" top="0.0" left="24" bottom="0.0" right="24"/> - <point key="canvasLocation" x="139" y="153"/> - </view> - <toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="eMY-ag-aGA"> - <rect key="frame" x="0.0" y="0.0" width="375" height="44"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> - <items> - <barButtonItem style="plain" systemItem="cancel" id="JeF-aH-YXy"> - <connections> - <action selector="cancelLogin" destination="-1" id="Bil-lr-11g"/> - </connections> - </barButtonItem> - <barButtonItem style="plain" systemItem="flexibleSpace" id="VXP-Mz-kgj"/> - <barButtonItem title="Log in" style="done" id="vJz-hf-rNV"> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="LoginBarButtonItem"/> - </userDefinedRuntimeAttributes> - <connections> - <action selector="doLogin" destination="-1" id="Tif-Kp-Pkt"/> - </connections> - </barButtonItem> - </items> - <point key="canvasLocation" x="138" y="517"/> - </toolbar> - </objects> - <designables> - <designable name="M05-uw-Xgl"> - <size key="intrinsicContentSize" width="207.5" height="25.5"/> - </designable> - <designable name="yyj-Bk-eOB"> - <size key="intrinsicContentSize" width="123" height="22"/> - </designable> - </designables> - <resources> - <image name="DefaultButton" width="9" height="9"/> - <namedColor name="Primary"> - <color red="0.16078431372549021" green="0.30196078431372547" blue="0.45098039215686275" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - </namedColor> - <namedColor name="Secondary"> - <color red="0.098039215686274508" green="0.1803921568627451" blue="0.27058823529411763" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - </namedColor> - </resources> -</document> |
