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