Files
2021-12-06 10:53:47 +00:00

427 lines
19 KiB
Swift

//
// ServersControllerIPAD.swift
// PrivadoVPN
//
// Created by Zhandos Bolatbekov on 29.06.2021.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import UIKit
enum ServersControllerComponentIPAD {
case serverList
case connection
case connectionStatus
case menu
case notificationsBell
case trafficCounter
case location
case message
}
enum ServersControllerModalComponentIPAD {
case notifications
case upgrade
}
protocol ServersControllerOutputIPAD {
func preferencesClosed()
func viewDidAppear()
}
final class ServersControllerIPAD: BaseViewController,
ServersControllerInput,
ViewModuleControllerType {
typealias ComponentCreationClosure = (ServersControllerComponentIPAD) -> UIView
typealias ModalCreationClosure = (ServersControllerModalComponentIPAD) -> UIViewController
// swiftlint:disable nesting
private enum Constants {
enum ImageName {
static let mapBackground = "ipad.map.background"
static let logo = "logo.main"
}
enum Geometry {
enum Portrait {
static let messageViewTop: CGFloat = 144
static let trafficCounterTop: CGFloat = 126
static let menuViewWidth: CGFloat = 319
static let connectionBottom: CGFloat = 22
static let mapImageTop: CGFloat = 81
static let logoTop: CGFloat = 62
static let menuTop: CGFloat = 66
static let bellTop: CGFloat = 61
static let bellRight: CGFloat = 24
static let serverListWidthMultiplier: CGFloat = 0.57
}
enum Landscape {
static let messageViewTop: CGFloat = 102
static let trafficCounterTop: CGFloat = 108
static let preferenceViewWidth: CGFloat = 288
static let preferenceHorizontalOffset: CGFloat = 28
static let preferenceTopOffset: CGFloat = 51
static let preferenceBottomOffset: CGFloat = 29
static let connectionBottom: CGFloat = 22
static let mapImageTop: CGFloat = -51
static let logoTop: CGFloat = 49
static let menuTop: CGFloat = 53
static let bellTop: CGFloat = 47
static let bellRight: CGFloat = 31
static let serverListWidthMultiplier: CGFloat = 0.45
}
static let connectionStatusTop: CGFloat = 12
static let menuLeading: CGFloat = 29
static let serverListHeightMultiplier: CGFloat = 0.75
}
enum Color {
static let backGradientTop = UIColor(rgb: 0x2A2F54)
static let backGradientBottom = UIColor(rgb: 0x2A2E4B)
static let backGradientTopHighlighted = UIColor(rgb: 0x5058C8)
static let backGradientBottomHighlighted = UIColor(rgb: 0x141850)
static let connectionStatusText = Colors.SystemCTA.lightPurple.color
}
enum Font {
static let connectionStatusText = UIFont.primary(size: 14, weight: .bold)
}
static let mapImageOpacity: CGFloat = 0.05
}
// swiftlint:enable nesting
private let output: ServersControllerOutputIPAD
private var backgroundGradientView: GradientView?
private var connectionStatusLabel: UILabel?
public var preferenceViewController: UIViewController? {
didSet {
self.setupPreferences()
}
}
private var notificationViewController: UIViewController?
private var preferenceBG: UIView?
private let componentCreationClosure: ComponentCreationClosure
private let modalCreationClosure: ModalCreationClosure
// MARK: - Init
init(output: ServersControllerOutputIPAD,
componentCreation: @escaping ComponentCreationClosure,
modalCreation: @escaping ModalCreationClosure) {
self.output = output
self.componentCreationClosure = componentCreation
self.modalCreationClosure = modalCreation
super.init()
}
required init?(coder: NSCoder) { nil }
// MARK: - Lifecycle
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent // .default
}
override func viewDidLoad() {
super.viewDidLoad()
self.configureUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let superview = self.view.superview else { return }
self.output.viewDidAppear()
}
// MARK: - ServersControllerInput
func upgradeWarning(show: Bool) { }
func changeMessageVisibility(isHidden: Bool) { }
func updateBackground(isHighlighted: Bool) {
self.backgroundGradientView?.stops = isHighlighted
? [.init(color: Constants.Color.backGradientTopHighlighted, location: 0),
.init(color: Constants.Color.backGradientBottomHighlighted, location: 1)]
: [.init(color: Constants.Color.backGradientTop, location: 0),
.init(color: Constants.Color.backGradientBottom, location: 1)]
}
func updateConnectionStatus(title: String?) {
self.connectionStatusLabel?.text = title
}
func changeMenuState() {
guard let preferences = preferenceViewController?.view else { return }
guard let notifications = self.notificationViewController?.view else { return }
if preferences.isAnimating() { return }
if notifications.isHidden == false {
notifications.fade(fadeIn: false, onCompletion: { notifications.isHidden = true })
}
if preferences.isHidden {
preferences.slide(out: false)
preferenceBG?.fade(fadeIn: true, 0.3)
} else {
preferences.slide(
out: true,
onCompletion: { [weak self] in
self?.output.preferencesClosed()
preferences.isHidden = true
}
)
preferenceBG?.fade(fadeIn: false, 0.3)
}
}
func changeNotificationsState() {
guard let notifications = self.notificationViewController?.view else { return }
if notifications.isHidden {
notifications.removeFromSuperview()
self.view.addSubview(notifications)
self.sharedConstraints.append(contentsOf: [
notifications.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.preferenceTopOffset),
notifications.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Landscape.preferenceHorizontalOffset)
])
let landscape = DeviceInfoProvider.isLandscapeOrienation && self.traitCollection.horizontalSizeClass == .regular
self.layoutEmitter.invoke(landscape)
notifications.fade(fadeIn: true)
} else {
notifications.fade(fadeIn: false, onCompletion: { notifications.isHidden = true })
}
}
func changeTutorialState(show: Bool) {
// MARK: - TODO
}
func scrollServers(direction: ServersScrollDirection) {
}
// MARK: - Private
// swiftlint:disable function_body_length
private func configureUI() {
let backgroundGradientView = self.initializeBackgroundGradient()
let mapImageView = self.initializeMapImage()
let connectionView = self.componentCreationClosure(.connection)
self.view.addSubview(connectionView)
let logoView = self.initializeLogoView()
let connectionStatusView = self.componentCreationClosure(.connectionStatus)
self.view.addSubview(connectionStatusView)
guard let serverList = self.componentCreationClosure(.serverList) as? (UIView & ServerListPullable) else {
return
}
self.view.addSubview(serverList)
let trafficView = self.componentCreationClosure(.trafficCounter)
self.view.addSubview(trafficView)
let messageView = self.componentCreationClosure(.message)
self.view.addSubview(messageView)
let location = self.componentCreationClosure(.location)
self.view.addSubview(location)
let upgradeController = self.modalCreationClosure(.upgrade)
upgradeController.view.isHidden = true
self.addChild(upgradeController)
self.view.addSubview(upgradeController.view)
let menuView = self.componentCreationClosure(.menu)
self.view.addSubview(menuView)
let notificationBell = self.componentCreationClosure(.notificationsBell)
self.view.addSubview(notificationBell)
let notification = self.modalCreationClosure(.notifications)
notification.view.isHidden = true
self.notificationViewController = notification
self.addChild(notification)
self.view.addSubview(notification.view)
self.portraitConstraints = [
trafficView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Portrait.trafficCounterTop),
messageView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Portrait.messageViewTop),
mapImageView.topAnchor.constraint(equalTo: self.view.topAnchor,
constant: Constants.Geometry.Portrait.mapImageTop),
logoView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Portrait.logoTop),
menuView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Portrait.menuTop),
serverList.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: Constants.Geometry.Portrait.serverListWidthMultiplier),
notificationBell.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Portrait.bellRight),
notificationBell.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Portrait.bellTop)
]
self.landscapeConstraints = [
trafficView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.trafficCounterTop),
messageView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.messageViewTop),
mapImageView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor,
constant: Constants.Geometry.Landscape.mapImageTop),
logoView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.logoTop),
menuView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.menuTop),
serverList.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: Constants.Geometry.Landscape.serverListWidthMultiplier),
notificationBell.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Landscape.bellRight),
notificationBell.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.bellTop)
]
let serverListHeightConstraint = serverList.heightAnchor.constraint(equalToConstant: serverList.minHeight)
serverListHeightConstraint.priority = .defaultHigh
let serverListHeightMaxConstraint = serverList.heightAnchor.constraint(lessThanOrEqualTo: self.view.heightAnchor,
multiplier: Constants.Geometry.serverListHeightMultiplier)
serverListHeightMaxConstraint.priority = .required
self.sharedConstraints = [
messageView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
messageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
trafficView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
trafficView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
location.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
location.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
location.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
notification.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.preferenceTopOffset),
notification.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Landscape.preferenceHorizontalOffset),
connectionView.centerXAnchor.constraint(equalTo: mapImageView.centerXAnchor),
connectionView.centerYAnchor.constraint(equalTo: mapImageView.centerYAnchor),
mapImageView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
mapImageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
logoView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
connectionStatusView.topAnchor.constraint(equalTo: logoView.bottomAnchor,
constant: Constants.Geometry.connectionStatusTop),
connectionStatusView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
menuView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.menuLeading),
backgroundGradientView.topAnchor.constraint(equalTo: self.view.topAnchor),
backgroundGradientView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
backgroundGradientView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
backgroundGradientView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
serverList.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
serverListHeightConstraint,
serverListHeightMaxConstraint,
serverList.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
serverList.topAnchor.constraint(greaterThanOrEqualTo: self.view.safeAreaLayoutGuide.topAnchor),
upgradeController.view.topAnchor.constraint(equalTo: self.view.topAnchor),
upgradeController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
upgradeController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
upgradeController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
]
serverList.heightConstraint = serverListHeightConstraint
serverList.isHidden = true
let landscape = DeviceInfoProvider.isLandscapeOrienation && self.traitCollection.horizontalSizeClass == .regular
self.layoutEmitter.invoke(landscape)
}
// swiftlint:enable function_body_length
private func setupPreferences() {
guard let preferencesViewController = self.preferenceViewController else {
return
}
preferencesViewController.view.isHidden = true
let bg = self.initializePreferenceBG()
self.addChild(preferencesViewController)
self.view.addSubview(preferencesViewController.view)
self.landscapeConstraints.append(contentsOf: [
preferencesViewController.view.widthAnchor.constraint(equalToConstant: Constants.Geometry.Landscape.preferenceViewWidth),
preferencesViewController.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Landscape.preferenceTopOffset),
preferencesViewController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -Constants.Geometry.Landscape.preferenceBottomOffset),
preferencesViewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.Landscape.preferenceHorizontalOffset),
preferencesViewController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Landscape.preferenceHorizontalOffset)
])
self.portraitConstraints.append(contentsOf: [
preferencesViewController.view.widthAnchor.constraint(equalToConstant: Constants.Geometry.Portrait.menuViewWidth),
preferencesViewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
preferencesViewController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
preferencesViewController.view.topAnchor.constraint(equalTo: self.view.topAnchor),
preferencesViewController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
self.sharedConstraints.append(contentsOf: [
bg.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
bg.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
bg.topAnchor.constraint(equalTo: self.view.topAnchor),
bg.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
let landscape = DeviceInfoProvider.isLandscapeOrienation && self.traitCollection.horizontalSizeClass == .regular
self.layoutEmitter.invoke(landscape)
}
private func initializeMapImage() -> UIImageView {
let image = UIImage(imageLiteralResourceName: Constants.ImageName.mapBackground)
let imageView = ImageView(image: image)
imageView.contentMode = .scaleAspectFill
imageView.alpha = Constants.mapImageOpacity
self.view.addSubview(imageView)
return imageView
}
private func initializeBackgroundGradient() -> UIView {
let gradientView = GradientView()
gradientView.translatesAutoresizingMaskIntoConstraints = false
gradientView.horizontalMode = false
gradientView.stops = [
.init(color: Constants.Color.backGradientTop, location: 0),
.init(color: Constants.Color.backGradientBottom, location: 1)
]
self.view.addSubview(gradientView)
self.backgroundGradientView = gradientView
return gradientView
}
private func initializeLogoView() -> UIImageView {
let image = UIImage(imageLiteralResourceName: Constants.ImageName.logo)
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(imageView)
return imageView
}
private func initializePreferenceBG() -> UIView {
let bg = UIView()
bg.translatesAutoresizingMaskIntoConstraints = false
bg.backgroundColor = Colors.Settings.bgMain.color
bg.isHidden = true
self.preferenceBG = bg
self.view.addSubview(bg)
return bg
}
}