146 lines
4.9 KiB
Swift
146 lines
4.9 KiB
Swift
//
|
|
// Loader.swift
|
|
// VDS
|
|
//
|
|
// Created by Matt Bruce on 7/5/23.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import VDSCoreTokens
|
|
|
|
|
|
/// 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 { isActive ? .init(width: size, height: size) : .zero }
|
|
|
|
//--------------------------------------------------
|
|
// 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 && isVisibleOnScreen {
|
|
startAnimating()
|
|
} else {
|
|
stopAnimating()
|
|
}
|
|
invalidateIntrinsicContentSize()
|
|
}
|
|
|
|
open override func updateAccessibility() {
|
|
super.updateAccessibility()
|
|
|
|
// check to make sure VoiceOver is running
|
|
guard UIAccessibility.isVoiceOverRunning, isActive else {
|
|
loadingTimer?.invalidate()
|
|
loadingTimer = nil
|
|
return
|
|
}
|
|
|
|
// Focus VoiceOver on this view
|
|
UIAccessibility.post(notification: .layoutChanged, argument: self)
|
|
|
|
// setup timer for post
|
|
loadingTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
|
|
guard let self, self.isActive, self.isVisibleOnScreen else { return }
|
|
self.accessibilityLabel = "Still Loading"
|
|
UIAccessibility.post(notification: .announcement, argument: "Still Loading")
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Methods
|
|
//--------------------------------------------------
|
|
private let rotationLayerName = "rotationAnimation"
|
|
private func startAnimating() {
|
|
accessibilityLabel = "Loading"
|
|
isAccessibilityElement = true
|
|
icon.isHidden = false
|
|
|
|
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)
|
|
}
|
|
|
|
private func stopAnimating() {
|
|
isAccessibilityElement = false
|
|
icon.isHidden = true
|
|
icon.layer.removeAnimation(forKey: rotationLayerName)
|
|
loadingTimer?.invalidate()
|
|
loadingTimer = nil
|
|
}
|
|
|
|
deinit {
|
|
stopAnimating()
|
|
}
|
|
}
|
|
|
|
extension Icon.Name {
|
|
static let loader = Icon.Name(name: "loader")
|
|
}
|