// // ConnectionLogo.swift // PrivadoVPN // // Created by Lizaveta Malinouskaya on 18.11.21. // Copyright © 2021 Privado LLC. All rights reserved. // import UIKit protocol ConnectionLogoInput { func update(state: ConnectionViewColorState) func didTapPastClickZone() } class ConnectionLogo: UIView, ConnectionLogoInput { enum Constants { enum Localized { static let tap = NSLocalizedString("connection.lable.tap", comment: "Tap here") } enum Font { static let label = UIFont(name: "Roboto-Medium", size: 14 * Constants.Geometry.multiplier) } enum AnimationKeys { static let opacity = "opacity" static let tint = "contentsMultiplyColor" static let opacity1 = "OpacityAnimation1" static let opacity2 = "OpacityAnimation2" static let opacity3 = "OpacityAnimation3" static let rotation = "rotationAnimation" static let color = "ColorAnimation" } enum AnimationDuration { static let animation: CFTimeInterval = 3 * fadeInOut static let fadeInOut: CFTimeInterval = 3 static let connected: CFTimeInterval = 0.5 static let color: CFTimeInterval = 8 static let rotation: CFTimeInterval = 10 static let disconnect: CFTimeInterval = 0.5 } enum Geometry { static let lock: CGFloat = 85 * multiplier static let background: CGFloat = 233 * multiplier static let innerCircle: CGFloat = 117 * multiplier static let middleCircle: CGFloat = 145 * multiplier static let outerCircle: CGFloat = 173 * multiplier static let rectHeight: CGFloat = 26 * multiplier static let rectWidth: CGFloat = 85 * multiplier static let labelY: CGFloat = 32 * multiplier static let multiplier = DeviceInfoProvider.isIPad ? 0.85 : 0.75 } enum Image { static let lockDisconnected = UIImage(named: "ipad.connection.lock.disconnected.new") static let background = UIImage(named: "ipad.connection.background.ellipse") static let innerCircle = UIImage(named: "ipad.connection.circle.inner") static let middleCircle = UIImage(named: "ipad.connection.circle.middle") static let outerCircle = UIImage(named: "ipad.connection.circle.out") static let innerLarge = UIImage(named: "ipad.connection.circle.inner.large") static let middleLarge = UIImage(named: "ipad.connection.circle.middle.large") static let outerLarge = UIImage(named: "ipad.connection.circle.out.large") static let innerMedium = UIImage(named: "ipad.connection.circle.inner.medium") static let middleMedium = UIImage(named: "ipad.connection.circle.middle.medium") static let outerMedium = UIImage(named: "ipad.connection.circle.out.medium") static let innerSmall = UIImage(named: "ipad.connection.circle.inner.small") static let outerSmall = UIImage(named: "ipad.connection.circle.out.small") } enum Color { static let clickBackground = Colors.AnimationLock.clickBackground.color static let clickLock = Colors.AnimationLock.clickLock.color static let clickRings = Colors.AnimationLock.clickRings.color static let clickText = Colors.AnimationLock.clickText.color static let clickWhite = Colors.basicText.color static let backgroundBlue = Colors.AnimationLock.blue.color static let connected = Colors.AnimationLock.green.color static let disconnected = Colors.AnimationLock.red.color static let progress = Colors.AnimationLock.yellow.color static let white = Colors.AnimationLock.white.color } } private var state: ConnectionViewColorState? { willSet { if newValue == self.state { self.didEnterTwice = true } else { self.didEnterTwice = false } } } private var connectionProgressColor = Constants.Color.progress private var didEnterTwice = false private var tapLabel: UIView? private var lockView: UIImageView? private var innerRingView: UIImageView? private var middleRingView: UIImageView? private var outerRingView: UIImageView? private var backgroundView: UIImageView? private var innerRingViews: [UIImageView] = [] private var middleRingViews: [UIImageView] = [] private var outerRingViews: [UIImageView] = [] private var foregroundImageView: UIImageView? private var didReturnFromBackground = false private var clickZoneAnimationFinished = true // MARK: - Init init() { super.init(frame: .zero) NotificationCenter.default.addObserver(self, selector: #selector(self.applicationDidBecomeActive), name: Notification.Name(PrivadoConstants.Application.didBecomeActive), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.applicationDidBecomeActive), name: Notification.Name(PrivadoConstants.Notification.serversViewDidAppear), object: nil) self.createUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func didMoveToSuperview() { super.didMoveToSuperview() let a = "adfdf" } // MARK: - selectors // Set animations after application have been closed @objc func applicationDidBecomeActive() { if self.didReturnFromBackground { guard let state = self.state else { return } self.update(state: state) self.setRotationAnimations() } self.didReturnFromBackground = true } @objc func didTapPastClickZone() { if self.state == .disconnected { if self.clickZoneAnimationFinished { UIView.animate(withDuration: Constants.AnimationDuration.disconnect, delay: 0, options: .autoreverse, animations: { self.changeRingsColor(to: Constants.Color.clickRings) self.tapLabel?.layer.opacity = 0 self.tapLabel?.layer.opacity = 1 self.lockView?.tintColor = Constants.Color.clickLock self.backgroundView?.tintColor = Constants.Color.clickBackground self.clickZoneAnimationFinished = false }, completion: { _ in self.tapLabel?.layer.opacity = 0 self.changeRingsColor(to: Constants.Color.white) self.lockView?.tintColor = Constants.Color.disconnected self.backgroundView?.tintColor = Constants.Color.backgroundBlue self.clickZoneAnimationFinished = true }) } } } // MARK: - ConnectionLogoInput func update(state: ConnectionViewColorState) { self.state = state if !didEnterTwice { switch state { case .connected: self.stopProgressAnimation() self.setRotationSpeed(isConnecting: false) self.addConnectedAnimation() case .connecting: self.setRotationSpeed(isConnecting: true) self.addProgressAnimation() self.changeRingsColor(to: Constants.Color.white) case .disconnecting: self.stopProgressAnimation() self.stopConnectedAnimation() UIView.animate(withDuration: Constants.AnimationDuration.disconnect, animations: { self.changeRingsColor(to: Constants.Color.disconnected) self.lockView?.tintColor = Constants.Color.disconnected }) case .disconnected: self.stopProgressAnimation() UIView.animate(withDuration: Constants.AnimationDuration.disconnect, animations: { self.changeRingsColor(to: Constants.Color.white) }) self.lockView?.tintColor = Constants.Color.disconnected case .error: self.setRotationSpeed(isConnecting: false) self.stopProgressAnimation() } } } // MARK: - UI private func createUI() { self.translatesAutoresizingMaskIntoConstraints = false self.backgroundView = self.initializeImage(from: self, image: Constants.Image.background, width: Constants.Geometry.background) self.backgroundView?.tintColor = Constants.Color.backgroundBlue self.lockView = self.initializeImage(from: self, image: Constants.Image.lockDisconnected, width: Constants.Geometry.lock) self.innerRingView = self.initializeImage(from: self, image: Constants.Image.innerCircle, width: Constants.Geometry.innerCircle) self.innerRingViews = [self.initializeImage(from: self, image: Constants.Image.innerLarge, width: Constants.Geometry.innerCircle), self.initializeImage(from: self, image: Constants.Image.innerMedium, width: Constants.Geometry.innerCircle), self.initializeImage(from: self, image: Constants.Image.innerSmall, width: Constants.Geometry.innerCircle)] self.middleRingView = self.initializeImage(from: self, image: Constants.Image.middleCircle, width: Constants.Geometry.middleCircle) self.middleRingViews = [self.initializeImage(from: self, image: Constants.Image.middleLarge, width: Constants.Geometry.middleCircle), self.initializeImage(from: self, image: Constants.Image.middleMedium, width: Constants.Geometry.middleCircle)] self.outerRingView = self.initializeImage(from: self, image: Constants.Image.outerCircle, width: Constants.Geometry.outerCircle) self.outerRingViews = [self.initializeImage(from: self, image: Constants.Image.outerLarge, width: Constants.Geometry.outerCircle), self.initializeImage(from: self, image: Constants.Image.outerMedium, width: Constants.Geometry.outerCircle), self.initializeImage(from: self, image: Constants.Image.outerSmall, width: Constants.Geometry.outerCircle)] self.tapLabel = self.initializeTapLabel(from: self) self.setRotationAnimations() self.setColorViewsOpacity() self.changeRingsColor(to: Constants.Color.white) } private func initializeImage(from container: UIView, image: UIImage?, width: CGFloat) -> UIImageView { let imageView = UIImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(imageView) imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate) NSLayoutConstraint.activate([ imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor), imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor), imageView.widthAnchor.constraint(equalToConstant: width), imageView.heightAnchor.constraint(equalToConstant: width) ]) return imageView } private func initializeTapLabel(from container: UIView) -> UIView { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = Constants.Color.clickWhite view.layer.cornerRadius = 10 let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = Constants.Font.label label.text = Constants.Localized.tap label.textColor = Constants.Color.backgroundBlue view.addSubview(label) view.layer.opacity = 0 self.addSubview(view) NSLayoutConstraint.activate([ view.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: Constants.Geometry.labelY), view.centerXAnchor.constraint(equalTo: self.centerXAnchor), view.heightAnchor.constraint(equalToConstant: Constants.Geometry.rectHeight), view.widthAnchor.constraint(equalToConstant: Constants.Geometry.rectWidth), label.centerXAnchor.constraint(equalTo: view.centerXAnchor), label.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) return view } // MARK: - animations private func setRotationSpeed(isConnecting: Bool) { let speed: Float = isConnecting ? 2 : 1.1 self.layer.timeOffset = self.layer.convertTime(CACurrentMediaTime(), from: nil) self.layer.beginTime = CACurrentMediaTime() self.layer.speed = speed } private func setRotationAnimations() { self.innerRingView?.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation) self.outerRingView?.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation) self.middleRingView?.rotate(clockwise: true, duration: Constants.AnimationDuration.rotation) for view in self.innerRingViews { view.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation) } for view in self.outerRingViews { view.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation) } for view in self.middleRingViews { view.rotate(clockwise: true, duration: Constants.AnimationDuration.rotation) } } private func addProgressAnimation() { self.setColorShiftAnimation(fromColor: Constants.Color.disconnected, toColor: Constants.Color.progress, duration: Constants.AnimationDuration.color) self.addInnerCircleOpacityAnimation(isConnecting: true) self.addMidleCircleOpacityAnimation(isConnecting: true) self.addOuterCircleOpacityAnimation(isConnecting: true) } private func addConnectedAnimation() { self.setColorShiftAnimation(fromColor: self.connectionProgressColor, toColor: Constants.Color.connected, duration: Constants.AnimationDuration.color / 2) self.addInnerCircleOpacityAnimation(isConnecting: false) self.addMidleCircleOpacityAnimation(isConnecting: false) self.addOuterCircleOpacityAnimation(isConnecting: false) } private func setColorShiftAnimation(fromColor: UIColor, toColor: UIColor, duration: CFTimeInterval) { self.addColorShiftAnimation(to: self.lockView, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration) self.lockView?.tintColor = toColor for view in self.innerRingViews { self.addColorShiftAnimation(to: view, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration) view.tintColor = toColor } for view in self.outerRingViews { self.addColorShiftAnimation(to: view, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration) view.tintColor = toColor } for view in self.middleRingViews { self.addColorShiftAnimation(to: view, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration) view.tintColor = toColor } } private func changeRingsColor(to color: UIColor) { self.innerRingView?.tintColor = color self.middleRingView?.tintColor = color self.outerRingView?.tintColor = color } private func addOuterCircleOpacityAnimation(isConnecting: Bool) { if isConnecting { // large - large - small - all else { var animations: [CABasicAnimation] = [] animations.append(contentsOf: self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut)) animations.append(contentsOf: self.getOpacityAnimations(beginTime: Constants.AnimationDuration.fadeInOut, duration: Constants.AnimationDuration.fadeInOut)) let group = CAAnimationGroup() group.animations = animations group.duration = Constants.AnimationDuration.animation group.beginTime = CACurrentMediaTime() group.repeatCount = Float.infinity self.outerRingViews[safe: 0]?.layer.add(group, forKey: Constants.AnimationKeys.opacity1) // small part group.beginTime = CACurrentMediaTime() + 2 * Constants.AnimationDuration.fadeInOut group.animations = getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut) self.outerRingViews[safe: 2]?.layer.add(group, forKey: Constants.AnimationKeys.opacity2) } else { UIView.animate(withDuration: Constants.AnimationDuration.connected, delay: 2 * Constants.AnimationDuration.connected, options: .autoreverse, animations: { for view in self.outerRingViews { view.layer.opacity = 0 view.layer.opacity = 1 } }, completion: {_ in for view in self.outerRingViews { view.layer.opacity = 0 } }) } } private func addMidleCircleOpacityAnimation(isConnecting: Bool) { if isConnecting { // large - medium - medium - all else { let group = CAAnimationGroup() group.repeatCount = Float.infinity group.duration = Constants.AnimationDuration.animation group.beginTime = CACurrentMediaTime() group.animations = self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut) self.middleRingViews[safe: 0]?.layer.add(group, forKey: Constants.AnimationKeys.opacity1) var animations: [CABasicAnimation] = [] animations.append(contentsOf: self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut)) animations.append(contentsOf: self.getOpacityAnimations(beginTime: Constants.AnimationDuration.fadeInOut, duration: Constants.AnimationDuration.fadeInOut)) group.beginTime = CACurrentMediaTime() + Constants.AnimationDuration.fadeInOut group.animations = animations self.middleRingViews[safe: 1]?.layer.add(group, forKey: Constants.AnimationKeys.opacity2) } else { UIView.animate(withDuration: Constants.AnimationDuration.connected, delay: Constants.AnimationDuration.connected, options: .autoreverse, animations: { for view in self.middleRingViews { view.layer.opacity = 0 view.layer.opacity = 1 } }, completion: {_ in for view in self.middleRingViews { view.layer.opacity = 0 } }) } } private func addInnerCircleOpacityAnimation(isConnecting: Bool) { if isConnecting { // medium - medium - small // medium part 2 times var animations: [CABasicAnimation] = [] animations.append(contentsOf: self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut)) animations.append(contentsOf: self.getOpacityAnimations(beginTime: Constants.AnimationDuration.fadeInOut, duration: Constants.AnimationDuration.fadeInOut)) let group = CAAnimationGroup() group.animations = animations group.duration = Constants.AnimationDuration.animation group.beginTime = CACurrentMediaTime() group.repeatCount = Float.infinity self.innerRingViews[safe: 0]?.layer.add(group, forKey: Constants.AnimationKeys.opacity1) // small part group.beginTime = CACurrentMediaTime() + 2 * Constants.AnimationDuration.fadeInOut group.animations = getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut) self.innerRingViews[safe: 2]?.layer.add(group, forKey: Constants.AnimationKeys.opacity2) } else { UIView.animate(withDuration: Constants.AnimationDuration.connected, delay: 0, options: .autoreverse, animations: { for view in self.innerRingViews { view.layer.opacity = 0 view.layer.opacity = 1 } }, completion: {_ in for view in self.innerRingViews { view.layer.opacity = 0 } }) } } private func getOpacityAnimations(beginTime: CFTimeInterval, duration: CFTimeInterval) -> [CABasicAnimation] { let fromZeroAnimation = CABasicAnimation(keyPath: Constants.AnimationKeys.opacity) fromZeroAnimation.fromValue = 0 fromZeroAnimation.toValue = 1 fromZeroAnimation.duration = duration / 2 fromZeroAnimation.beginTime = beginTime let toZeroAnimation = CABasicAnimation(keyPath: Constants.AnimationKeys.opacity) toZeroAnimation.fromValue = 1 toZeroAnimation.toValue = 0 toZeroAnimation.beginTime = beginTime + duration / 2 toZeroAnimation.duration = duration / 2 return [fromZeroAnimation, toZeroAnimation] } private func addColorShiftAnimation(to view: UIView?, fromColor: CGColor, toColor: CGColor, duration: CFTimeInterval) { let disconnectedToProgressAnimation = CABasicAnimation(keyPath: Constants.AnimationKeys.tint) disconnectedToProgressAnimation.fromValue = fromColor disconnectedToProgressAnimation.toValue = toColor disconnectedToProgressAnimation.duration = duration view?.layer.add(disconnectedToProgressAnimation, forKey: Constants.AnimationKeys.color) } private func stopConnectedAnimation() { self.lockView?.layer.removeAnimation(forKey: Constants.AnimationKeys.color) self.setColorViewsOpacity() } private func stopProgressAnimation() { self.lockView?.layer.removeAnimation(forKey: Constants.AnimationKeys.color) let removeAnimations = { (view: UIView) in view.layer.removeAnimation(forKey: Constants.AnimationKeys.opacity1) view.layer.removeAnimation(forKey: Constants.AnimationKeys.opacity2) view.layer.removeAnimation(forKey: Constants.AnimationKeys.opacity3) } for view in self.innerRingViews { removeAnimations(view) } for view in self.middleRingViews { removeAnimations(view) } for view in self.outerRingViews { removeAnimations(view) } } private func setColorViewsOpacity() { for view in self.middleRingViews { view.layer.opacity = 0 } for view in self.outerRingViews { view.layer.opacity = 0 } for view in self.innerRingViews { view.layer.opacity = 0 } } } extension UIView { func rotate(clockwise: Bool, duration: CFTimeInterval) { let rotation = CABasicAnimation(keyPath: "transform.rotation.z") var value: Double = 0 if clockwise { value = Double.pi * 2 } else { value = -Double.pi * 2 } rotation.toValue = NSNumber(value: value) rotation.duration = duration rotation.isCumulative = true rotation.repeatCount = Float.greatestFiniteMagnitude self.layer.add(rotation, forKey: "RotationAnimation") } }