1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
//
// SpinnerActivityIndicatorView.swift
// MullvadVPN
//
// Created by pronebird on 15/05/2019.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import UIKit
@MainActor
class SpinnerActivityIndicatorView: UIView {
private static let rotationAnimationKey = "rotation"
private static let animationDuration = 0.6
@MainActor
enum Style {
case small, medium, large, custom
var intrinsicSize: CGSize {
switch self {
case .small:
return CGSize(width: 16, height: 16)
case .medium:
return CGSize(width: 20, height: 20)
case .large:
return CGSize(width: 60, height: 60)
case .custom:
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
}
}
private let imageView = UIImageView(image: .spinner)
private(set) var isAnimating = false
private(set) var style = Style.large
private var sceneActivationObserver: Any?
override var intrinsicContentSize: CGSize {
style.intrinsicSize
}
init(style: Style) {
self.style = style
let size = style == .custom ? .zero : style.intrinsicSize
super.init(frame: CGRect(origin: .zero, size: size))
addSubview(imageView)
isHidden = true
backgroundColor = UIColor.clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
MainActor.assumeIsolated {
unregisterSceneActivationObserver()
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if window == nil {
unregisterSceneActivationObserver()
} else {
registerSceneActivationObserver()
restartAnimationIfNeeded()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let size = style == .custom ? frame.size : style.intrinsicSize
imageView.frame = CGRect(origin: .zero, size: size)
}
func startAnimating() {
guard !isAnimating else { return }
isAnimating = true
isHidden = false
addAnimation()
}
func stopAnimating() {
guard isAnimating else { return }
isAnimating = false
isHidden = true
removeAnimation()
}
private func addAnimation() {
layer.add(createAnimation(), forKey: Self.rotationAnimationKey)
}
private func removeAnimation() {
layer.removeAnimation(forKey: Self.rotationAnimationKey)
}
private func registerSceneActivationObserver() {
unregisterSceneActivationObserver()
sceneActivationObserver = NotificationCenter.default.addObserver(
forName: UIScene.willEnterForegroundNotification,
object: window?.windowScene,
queue: .main,
using: { [weak self] _ in
MainActor.assumeIsolated {
self?.restartAnimationIfNeeded()
}
}
)
}
private func unregisterSceneActivationObserver() {
if let sceneActivationObserver {
NotificationCenter.default.removeObserver(sceneActivationObserver)
self.sceneActivationObserver = nil
}
}
private func restartAnimationIfNeeded() {
let animation = layer.animation(forKey: Self.rotationAnimationKey)
if isAnimating, animation == nil {
removeAnimation()
addAnimation()
}
}
private func createAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.toValue = NSNumber(value: Double.pi * 2)
animation.duration = Self.animationDuration
animation.repeatCount = Float.infinity
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.timeOffset = layer.convertTime(CACurrentMediaTime(), from: nil)
return animation
}
}
|