summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-03-22 13:54:16 +0100
committerAndrej Mihajlov <and@mullvad.net>2021-03-23 10:16:14 +0100
commitc86923e77b54360347202aef32fe6dbde2165946 (patch)
tree25f4bc41b19ef04923ffc93b1d16407a182827ff
parent599366b9418423b041f725ed60cc097761cb41db (diff)
downloadmullvadvpn-c86923e77b54360347202aef32fe6dbde2165946.tar.xz
mullvadvpn-c86923e77b54360347202aef32fe6dbde2165946.zip
Extract LoginContentView
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj9
-rw-r--r--ios/MullvadVPN/AccountInputGroupView.swift44
-rw-r--r--ios/MullvadVPN/LoginContentView.swift247
-rw-r--r--ios/MullvadVPN/LoginViewController.swift157
-rw-r--r--ios/MullvadVPN/LoginViewController.xib199
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>