TM-SGNL-iOS/Signal/AppLaunch/LoadingViewController.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

266 lines
9.1 KiB
Swift

//
// 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.
class LoadingViewController: UIViewController {
private var logoView: UIImageView!
private var topLabel: UILabel!
private var bottomLabel: UILabel!
private var progressView = UIProgressView()
private lazy var percentCompleteLabel = UILabel()
private lazy var unitCountLabel = UILabel()
private let labelStack = UIStackView()
private var topLabelTimer: Timer?
private var bottomLabelTimer: Timer?
private var progressObserver: NSKeyValueObservation?
var progress: Progress? {
didSet {
self.progressObserver = progress?
.observe(\.fractionCompleted) { [weak self] progress, _ in
self?.updateProgress(progress)
}
}
}
override 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."
) + "\n" + OWSLocalizedString(
"LOADING_VIEW_CONTROLLER_DONT_CLOSE_APP",
comment: "Shown to users while the app is loading, asking them not to close the app."
)
bottomLabel.textAlignment = .center
labelStack.addArrangedSubview(bottomLabel)
labelStack.setCustomSpacing(20, after: bottomLabel)
progressView.setProgress(0.1, animated: false)
progressView.alpha = 0
labelStack.addArrangedSubview(progressView)
labelStack.setCustomSpacing(16, after: progressView)
progressView.autoPinWidthToSuperview(withMargin: 20, relation: .lessThanOrEqual)
progressView.autoSetDimension(.width, toSize: 330).priority = .defaultLow
percentCompleteLabel.alpha = 0
percentCompleteLabel.font = {
let metrics = UIFontMetrics(forTextStyle: .body)
let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
let font = UIFont.monospacedDigitSystemFont(ofSize: desc.pointSize, weight: .regular)
return metrics.scaledFont(for: font)
}()
percentCompleteLabel.textColor = .Signal.secondaryLabel
labelStack.addArrangedSubview(percentCompleteLabel)
labelStack.setCustomSpacing(6, after: percentCompleteLabel)
unitCountLabel.alpha = 0
unitCountLabel.font = .dynamicTypeBody.monospaced()
unitCountLabel.textColor = .Signal.secondaryLabel
labelStack.addArrangedSubview(unitCountLabel)
labelStack.axis = .vertical
labelStack.alignment = .center
labelStack.spacing = 8
view.addSubview(labelStack)
labelStack.autoPinEdge(.top, to: .bottom, of: logoView, withOffset: 40)
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 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 = 10
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.3) {
self.bottomLabel.alpha = 1
self.progress.map(self.updateProgress(_:))
}
}
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
}
private func updateProgress(_ progress: Progress) {
let percentComplete = Float(progress.fractionCompleted)
let unitCountToComplete = 0
let unitCountCompleted = Int(Double(unitCountToComplete) * progress.fractionCompleted)
progressView.setProgress(percentComplete, animated: true)
percentCompleteLabel.text = String(
format: OWSLocalizedString(
"LINK_NEW_DEVICE_SYNC_PROGRESS_PERCENT",
comment: "On a progress modal indicating the percent complete the sync process is. Embeds {{ formatted percentage }}"
),
percentComplete.formatted(.percent.precision(.fractionLength(0)))
)
unitCountLabel.text = "\(unitCountCompleted.formatted(.number)) / \(unitCountToComplete.formatted(.number))"
if percentComplete > 0 {
percentCompleteLabel.alpha = bottomLabel.alpha
progressView.alpha = bottomLabel.alpha
} else {
percentCompleteLabel.alpha = 0
progressView.alpha = 0
}
// if unitCountToComplete > 0 {
// unitCountLabel.alpha = bottomLabel.alpha
// } else {
// unitCountLabel.alpha = 0
// }
}
// MARK: Orientation
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return UIDevice.current.isIPad ? .all : .portrait
}
private func buildLabel() -> UILabel {
let label = UILabel()
label.textColor = .Signal.label
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
return label
}
}
#if DEBUG
@available(iOS 17, *)
#Preview {
let progress = Progress()
progress.totalUnitCount = 100
let viewController = LoadingViewController()
viewController.progress = progress
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
progress.completedUnitCount += Int64.random(in: 2...8)
if progress.fractionCompleted >= 1 {
progress.completedUnitCount = 100
timer.invalidate()
}
}
return viewController
}
#endif