// // 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 } }