// // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit import SignalUI // The initial presentation is intended to be indistinguishable from the Launch Screen. // After a delay we present some "loading" UI so the user doesn't think the app is frozen. public class LoadingViewController: UIViewController { var logoView: UIImageView! var topLabel: UILabel! var bottomLabel: UILabel! let labelStack = UIStackView() var topLabelTimer: Timer? var bottomLabelTimer: Timer? override public func loadView() { self.view = UIView() view.backgroundColor = Theme.launchScreenBackgroundColor self.logoView = UIImageView(image: #imageLiteral(resourceName: "signal-logo-128-launch-screen")) view.addSubview(logoView) logoView.autoCenterInSuperview() logoView.autoSetDimensions(to: CGSize(square: 128)) self.topLabel = buildLabel() topLabel.alpha = 0 topLabel.font = UIFont.dynamicTypeTitle2 topLabel.text = OWSLocalizedString("DATABASE_VIEW_OVERLAY_TITLE", comment: "Title shown while the app is updating its database.") labelStack.addArrangedSubview(topLabel) self.bottomLabel = buildLabel() bottomLabel.alpha = 0 bottomLabel.font = UIFont.dynamicTypeBody bottomLabel.text = OWSLocalizedString("DATABASE_VIEW_OVERLAY_SUBTITLE", comment: "Subtitle shown while the app is updating its database.") labelStack.addArrangedSubview(bottomLabel) labelStack.axis = .vertical labelStack.alignment = .center labelStack.spacing = 8 view.addSubview(labelStack) labelStack.autoPinEdge(.top, to: .bottom, of: logoView, withOffset: 20) labelStack.autoPinLeadingToSuperviewMargin() labelStack.autoPinTrailingToSuperviewMargin() labelStack.setCompressionResistanceHigh() labelStack.setContentHuggingHigh() NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: .OWSApplicationDidBecomeActive, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: .OWSApplicationDidEnterBackground, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeDidChange, object: nil) } override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // We only show the "loading" UI if it's a slow launch. Otherwise this ViewController // should be indistinguishable from the launch screen. let kTopLabelThreshold: TimeInterval = 5 topLabelTimer = Timer.scheduledTimer(withTimeInterval: kTopLabelThreshold, repeats: false) { [weak self] _ in self?.showTopLabel() } let kBottomLabelThreshold: TimeInterval = 15 bottomLabelTimer = Timer.scheduledTimer(withTimeInterval: kBottomLabelThreshold, repeats: false) { [weak self] _ in self?.showBottomLabelAnimated() } } // UIStackView removes hidden subviews from the layout. // UIStackView considers views with a sufficiently low // alpha to be "hidden". This can cause layout to glitch // briefly when returning from background. Therefore we // use a "min" alpha value when fading in labels that is // high enough to avoid this UIStackView behavior. private let kMinAlpha: CGFloat = 0.1 private func showBottomLabelAnimated() { bottomLabel.layer.removeAllAnimations() bottomLabel.alpha = kMinAlpha UIView.animate(withDuration: 0.1) { self.bottomLabel.alpha = 1 } } private func showTopLabel() { topLabel.layer.removeAllAnimations() topLabel.alpha = 0.2 UIView.animate(withDuration: 0.9, delay: 0, options: [.autoreverse, .repeat, .curveEaseInOut], animations: { self.topLabel.alpha = 1.0 }, completion: nil) } private func showBottomLabel() { bottomLabel.layer.removeAllAnimations() self.bottomLabel.alpha = 1 } // MARK: - @objc private func didBecomeActive() { AssertIsOnMainThread() guard viewHasEnteredBackground else { // If the app is returning from background, skip any // animations and show the top and bottom labels. return } topLabelTimer?.invalidate() topLabelTimer = nil bottomLabelTimer?.invalidate() bottomLabelTimer = nil showTopLabel() showBottomLabel() labelStack.layoutSubviews() view.layoutSubviews() } private var viewHasEnteredBackground = false @objc private func didEnterBackground() { AssertIsOnMainThread() viewHasEnteredBackground = true } @objc private func themeDidChange() { view.backgroundColor = Theme.launchScreenBackgroundColor } // MARK: Orientation override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { return UIDevice.current.isIPad ? .all : .portrait } // MARK: private func buildLabel() -> UILabel { let label = UILabel() label.textColor = .white label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping return label } }