vds_ios/VDS/Components/Loader/Loader.swift
Matt Bruce 7b2e7f3c4f moved to 60 seconds
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2023-12-15 14:07:35 -06:00

127 lines
4.3 KiB
Swift

//
// Loader.swift
// VDS
//
// Created by Matt Bruce on 7/5/23.
//
import Foundation
import UIKit
import VDSColorTokens
/// A loader is an indicator that uses animation to show customers that there is an indefinite amount of wait time while a task is ongoing, e.g. a page is loading, a form is being submitted. The component disappears when the task is complete.
@objc(VDSLoader)
open class Loader: View {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero)
}
public override init(frame: CGRect) {
super.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var icon = Icon().with { $0.name = .loader }
private var opacity: CGFloat = 0.8
private var iconColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
private var loadingTimer: Timer?
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Loader will be active if 'active' prop is passed.
open var isActive: Bool = true { didSet { setNeedsUpdate() } }
/// The Int used to determine the height and width of the Loader
open var size: Int = 40 {
didSet {
setNeedsUpdate();
invalidateIntrinsicContentSize()
}
}
/// The natural size for the receiving view, considering only properties of the view itself.
open override var intrinsicContentSize: CGSize { .init(width: size, height: size) }
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
addSubview(icon)
isAccessibilityElement = true
icon.isAccessibilityElement = false
icon
.pinTopGreaterThanOrEqualTo()
.pinLeadingGreaterThanOrEqualTo()
.pinTrailingLessThanOrEqualTo()
.pinBottomLessThanOrEqualTo()
NSLayoutConstraint.activate([
icon.centerXAnchor.constraint(equalTo: centerXAnchor),
icon.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
icon.color = iconColorConfiguration.getColor(self)
icon.customSize = size
if isActive {
startAnimating()
} else {
stopAnimating()
}
}
open override func updateAccessibility() {
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private let rotationLayerName = "rotationAnimation"
private func startAnimating() {
accessibilityLabel = "Loading"
icon.layer.remove(layerName: rotationLayerName)
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 0.5
rotation.repeatCount = .infinity
icon.layer.add(rotation, forKey: rotationLayerName)
// Focus VoiceOver on this view
UIAccessibility.post(notification: .layoutChanged, argument: self)
loadingTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
self?.accessibilityLabel = "Still Loading"
UIAccessibility.post(notification: .announcement, argument: "Still Loading")
}
}
private func stopAnimating() {
icon.layer.removeAnimation(forKey: rotationLayerName)
loadingTimer?.invalidate()
loadingTimer = nil
}
}
extension Icon.Name {
static let loader = Icon.Name(name: "loader")
}