Files
2021-10-18 10:52:27 +03:00

398 lines
19 KiB
Swift

//
// CredentialsController.swift
// PrivadoVPN
//
// Created by Zhandos Bolatbekov on 13.10.2021.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import UIKit
final class CredentialsController: UIViewController {
private enum Constants {
enum Font {
static let headerTitle = UIFont.primary(size: 30, weight: .semibold)
static let headerSubtitle = UIFont.primary(size: 21.3, weight: .semibold)
static let email = UIFont.primary(size: 20.7, weight: .medium)
static let credentials = UIFont.primary(size: 20, weight: .medium)
static let prompt = UIFont.primary(size: 14.7, weight: .medium)
static let buttonTitle = UIFont.primary(size: 21.3, weight: .bold)
}
enum LocalizedString {
static let headerTitle = "credentials.header.title"
static let headerSubtitle = "credentials.header.subtitle"
static let username = "credentials.username"
static let password = "credentials.password"
static let prompt = "credentials.prompt"
static let buttonTutorial = "credentials.button.tutorial"
static let buttonUseApp = "credentials.button.useApp"
}
enum Color {
static let headerTitle = Colors.basicText.color
static let headerSubtitle = Colors.SystemCTA.lightPurple.color
static let emailLabel = Colors.SystemCTA.lightPurple.color
static let credentialName = Colors.basicText.color
static let credentialValue = Colors.SystemCTA.lightPurple.color
static let copyButton = Colors.SystemCTA.lightPurple.color
static let prompt = Colors.SystemCTA.lightPurple.color
static let background = UIColor(rgb: 0x202350)
static let credentialItemBackground = UIColor(rgb: 0x202864)
static let buttonBackground = UIColor(rgb: 0x3E449C)
static let buttonsContainerBackground = UIColor(rgb: 0x202864)
static let buttonTitle = UIColor.white
}
enum Geometry {
static let credentialLabelIndentX: CGFloat = 21
static let credentialLabelTop: CGFloat = 26.7
static let credentialLabelBottom: CGFloat = 22
static let credentialItemsIndentY: CGFloat = 6
static let credentialsSeparatorHeight: CGFloat = 3.3
static let credentialsContainerCornerRadius: CGFloat = 16.7
static let headerTitleTop: CGFloat = 66.7
static let commonIndentX: CGFloat = 30
static let headerSubtitleTop: CGFloat = 10
static let emailTop: CGFloat = 36.7
static let credentialsTop: CGFloat = 20
static let promptTop: CGFloat = 25
static let promptIndentX: CGFloat = 40
static let promptBottom: CGFloat = 40
static let promptLineSpacing: CGFloat = 5
static let copyButtonSize: CGFloat = 32
static let emailMinScaleFactor: CGFloat = 0.8
static let credentialMinScaleFactor: CGFloat = 0.8
static let buttonsIndentY: CGFloat = 36
static let buttonsIndentX: CGFloat = 31
static let buttonsSpacing: CGFloat = 20
static let buttonHeight: CGFloat = 60
static let buttonsContainerShadowOffset = CGSize(width: 0, height: -1.7)
static let buttonsContainerShadowRadius: CGFloat = 10
}
static let buttonsContainerShadowOpacity: Float = 0.33
static let copyButtonImageName = "copy"
static let credentialSeparator = " "
}
// MARK: - Properties
private let output: CredentialsControllerOutput
private var buttonsContainer: UIView?
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
private var credentialLabels = [UILabel]()
// MARK: - Init
init(output: CredentialsControllerOutput) {
self.output = output
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { nil }
// MARK: - Life-cycle
override func viewDidLoad() {
super.viewDidLoad()
self.createUI()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.buttonsContainer?.dropShadow(shadowColor: .black,
fillColor: Constants.Color.buttonsContainerBackground,
opacity: Constants.buttonsContainerShadowOpacity,
offset: Constants.Geometry.buttonsContainerShadowOffset,
shadowRadius: Constants.Geometry.buttonsContainerShadowRadius)
self.syncCredentialsFontSize()
}
// MARK: - Private
private func syncCredentialsFontSize() {
let minFontSize: CGFloat = self.credentialLabels.reduce(into: Constants.Font.credentials.pointSize) { result, label in
if let size = label.getFontSizeForLabel() {
result = min(result, size)
}
}
for i in 0..<self.credentialLabels.count {
self.credentialLabels[i].font = self.credentialLabels[i].font.withSize(minFontSize)
}
}
}
// MARK: - UI
extension CredentialsController {
private func createUI() {
self.view.backgroundColor = Constants.Color.background
let headerTitle = self.createHeaderTitle()
let headerSubtitle = self.createHeaderSubtitle()
let emailLabel = self.createEmailLabel()
let credentials = self.createCredentialsContainer()
let promptLabel = self.createPromptLabel()
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
[headerTitle, headerSubtitle, emailLabel, credentials, promptLabel]
.forEach(contentView.addSubview(_:))
let scrollView = UIScrollView()
scrollView.alwaysBounceVertical = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
self.view.addSubview(scrollView)
let buttons = self.createButtons()
self.view.addSubview(buttons)
self.buttonsContainer = buttons
NSLayoutConstraint.activate([
headerTitle.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.Geometry.headerTitleTop),
headerTitle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
headerTitle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
headerSubtitle.topAnchor.constraint(equalTo: headerTitle.bottomAnchor, constant: Constants.Geometry.headerSubtitleTop),
headerSubtitle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
headerSubtitle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
emailLabel.topAnchor.constraint(equalTo: headerSubtitle.bottomAnchor, constant: Constants.Geometry.emailTop),
emailLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
emailLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
credentials.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: Constants.Geometry.credentialsTop),
credentials.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
credentials.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
promptLabel.topAnchor.constraint(equalTo: credentials.bottomAnchor, constant: Constants.Geometry.promptTop),
promptLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.promptIndentX),
promptLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.promptIndentX),
promptLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constants.Geometry.promptBottom),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
buttons.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
buttons.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
buttons.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
private func createCredentialsContainer() -> UIView {
let view = UIView()
view.layer.cornerRadius = Constants.Geometry.credentialsContainerCornerRadius
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
let stackView = UIStackView(arrangedSubviews: [
self.createCredentialItemContainer(name: NSLocalizedString(Constants.LocalizedString.username, comment: "") + Constants.credentialSeparator,
value: self.output.username),
self.createCredentialItemContainer(name: NSLocalizedString(Constants.LocalizedString.password, comment: "") + Constants.credentialSeparator,
value: self.output.password)
])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fillEqually
stackView.axis = .vertical
stackView.spacing = Constants.Geometry.credentialsSeparatorHeight
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.topAnchor),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
return view
}
private func createCredentialItemContainer(name: String, value: String?) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = Constants.Color.credentialItemBackground
let label = self.createCredentialLabel(name: name, value: value)
self.credentialLabels.append(label)
view.addSubview(label)
let copyButton = self.createCopyButton(for: label)
view.addSubview(copyButton)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Geometry.credentialLabelIndentX),
label.trailingAnchor.constraint(equalTo: copyButton.leadingAnchor, constant: -Constants.Geometry.credentialLabelIndentX / 2),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.Geometry.credentialLabelTop),
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Constants.Geometry.credentialLabelBottom),
copyButton.centerYAnchor.constraint(equalTo: label.centerYAnchor),
copyButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Geometry.credentialLabelIndentX),
copyButton.heightAnchor.constraint(equalToConstant: Constants.Geometry.copyButtonSize),
copyButton.widthAnchor.constraint(equalToConstant: Constants.Geometry.copyButtonSize)
])
return view
}
private func createHeaderTitle() -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Constants.Font.headerTitle
label.textAlignment = .center
label.textColor = Constants.Color.headerTitle
label.numberOfLines = 0
label.text = NSLocalizedString(Constants.LocalizedString.headerTitle, comment: "")
return label
}
private func createHeaderSubtitle() -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Constants.Font.headerSubtitle
label.textAlignment = .center
label.textColor = Constants.Color.headerSubtitle
label.numberOfLines = 0
label.text = NSLocalizedString(Constants.LocalizedString.headerSubtitle, comment: "")
return label
}
private func createEmailLabel() -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Constants.Font.email
label.textAlignment = .center
label.textColor = Constants.Color.emailLabel
label.numberOfLines = 1
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = Constants.Geometry.emailMinScaleFactor
label.lineBreakMode = .byTruncatingTail
label.text = self.output.email
return label
}
private func createCredentialLabel(name: String, value: String?) -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Constants.Font.credentials
label.numberOfLines = 1
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = Constants.Geometry.credentialMinScaleFactor
label.lineBreakMode = .byTruncatingTail
label.tag = self.credentialLabels.count
let attributedString = NSMutableAttributedString(string: name,
attributes: [.foregroundColor: Constants.Color.credentialName])
let suffix = NSAttributedString(string: value ?? "",
attributes: [.foregroundColor: Constants.Color.credentialValue])
attributedString.append(suffix)
label.attributedText = attributedString
return label
}
private func createCopyButton(for label: UILabel) -> UIButton {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = Constants.Color.copyButton
button.setImage(UIImage(imageLiteralResourceName: Constants.copyButtonImageName), for: .normal)
button.tag = label.tag
button.addTarget(self, action: #selector(self.didTapCopyButton(sender:)), for: .touchUpInside)
return button
}
private func createPromptLabel() -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Constants.Font.prompt
label.textColor = Constants.Color.prompt
label.numberOfLines = 0
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = Constants.Geometry.promptLineSpacing
label.attributedText = NSAttributedString(
string: NSLocalizedString(Constants.LocalizedString.prompt, comment: ""),
attributes: [.paragraphStyle: paragraphStyle]
)
return label
}
private func createButtons() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = Constants.Color.buttonsContainerBackground
let tutorialButton = self.createActionButton(title: NSLocalizedString(Constants.LocalizedString.buttonTutorial, comment: ""))
tutorialButton.addTarget(self, action: #selector(self.didTapTutorialButton), for: .touchUpInside)
let skipButton = self.createActionButton(title: NSLocalizedString(Constants.LocalizedString.buttonUseApp, comment: ""))
skipButton.addTarget(self, action: #selector(self.didTapSkipButton), for: .touchUpInside)
let stackView = UIStackView(arrangedSubviews: [skipButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = Constants.Geometry.buttonsSpacing
stackView.axis = .vertical
stackView.distribution = .fillEqually
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.Geometry.buttonsIndentY),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Constants.Geometry.buttonsIndentY),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Geometry.buttonsIndentX),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Geometry.buttonsIndentX)
])
return view
}
private func createActionButton(title: String) -> UIButton {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = Constants.Color.buttonBackground
button.layer.cornerRadius = Constants.Geometry.buttonHeight / 2
let title = NSAttributedString(
string: title,
attributes: [.font: Constants.Font.buttonTitle,
.foregroundColor: Constants.Color.buttonTitle]
)
button.setAttributedTitle(title, for: .normal)
button.heightAnchor.constraint(equalToConstant: Constants.Geometry.buttonHeight).isActive = true
return button
}
@objc
private func didTapTutorialButton() {
self.output.didTapTutorial()
}
@objc
private func didTapSkipButton() {
self.output.didTapSkip()
}
@objc
private func didTapCopyButton(sender: UIButton) {
guard let label = self.credentialLabels.first(where: { $0.tag == sender.tag }),
let text = label.attributedText?.string,
let value = text.split(separator: String.Element(Constants.credentialSeparator)).last else { return }
UINotificationFeedbackGenerator().notificationOccurred(.success)
UIPasteboard.general.string = String(value)
}
}