-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLoadingFeature.swift
More file actions
168 lines (151 loc) · 5.25 KB
/
Copy pathLoadingFeature.swift
File metadata and controls
168 lines (151 loc) · 5.25 KB
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//
// LoadingFeature.swift
// DevLogPresentation
//
// Created by opfic on 6/11/26.
//
import ComposableArchitecture
@Reducer
struct LoadingFeature {
@ObservableState
struct State: Equatable {
var isLoading = false
var immediateCountByTarget: [Target: Int] = [:]
var delayedCountByTarget: [Target: Int] = [:]
var scheduledDelayedTargets = Set<Target>()
var visibleDelayedTargets = Set<Target>()
var visibleTargets = Set<Target>()
}
struct Target: Hashable, Sendable {
static let `default` = Self("default")
let id: String
init(_ id: String) {
self.id = id
}
}
enum Mode: Equatable, Sendable {
case immediate
case delayed
}
enum Action: Equatable {
case begin(target: Target, mode: Mode)
case end(target: Target, mode: Mode)
case delayedLoadingDidBecomeVisible(target: Target)
}
private enum CancelID: Hashable {
case delayedLoading(Target)
}
@Dependency(\.continuousClock) var clock
private let delay = Duration.seconds(0.3)
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .begin(let target, let mode):
return begin(target: target, mode: mode, state: &state)
case .end(let target, let mode):
return end(target: target, mode: mode, state: &state)
case .delayedLoadingDidBecomeVisible(let target):
return delayedLoadingDidBecomeVisible(target: target, state: &state)
}
}
}
}
private extension LoadingFeature {
func begin(
target: Target,
mode: Mode,
state: inout State
) -> Effect<Action> {
switch mode {
case .immediate:
state.immediateCountByTarget[target, default: 0] += 1
state.setVisibilityIfNeeded(for: target, isVisible: true)
return .none
case .delayed:
state.delayedCountByTarget[target, default: 0] += 1
return scheduleDelayedLoadingIfNeeded(for: target, state: &state)
}
}
func end(
target: Target,
mode: Mode,
state: inout State
) -> Effect<Action> {
switch mode {
case .immediate:
let count = state.immediateCountByTarget[target, default: 0]
state.immediateCountByTarget[target] = max(0, count - 1)
case .delayed:
let count = state.delayedCountByTarget[target, default: 0]
state.delayedCountByTarget[target] = max(0, count - 1)
}
return updateLoadingVisibility(for: target, state: &state)
}
func delayedLoadingDidBecomeVisible(
target: Target,
state: inout State
) -> Effect<Action> {
state.scheduledDelayedTargets.remove(target)
guard 0 < state.delayedCountByTarget[target, default: 0] else { return .none }
state.visibleDelayedTargets.insert(target)
if state.immediateCountByTarget[target, default: 0] == 0 {
state.setVisibilityIfNeeded(for: target, isVisible: true)
}
return .none
}
func scheduleDelayedLoadingIfNeeded(
for target: Target,
state: inout State
) -> Effect<Action> {
guard !state.scheduledDelayedTargets.contains(target),
!state.visibleDelayedTargets.contains(target),
0 < state.delayedCountByTarget[target, default: 0] else { return .none }
state.scheduledDelayedTargets.insert(target)
return .run { [clock, delay] send in
try await clock.sleep(for: delay)
await send(.delayedLoadingDidBecomeVisible(target: target))
}
.cancellable(id: CancelID.delayedLoading(target), cancelInFlight: true)
}
func updateLoadingVisibility(
for target: Target,
state: inout State
) -> Effect<Action> {
if 0 < state.immediateCountByTarget[target, default: 0] {
state.setVisibilityIfNeeded(for: target, isVisible: true)
return .none
}
if state.visibleDelayedTargets.contains(target) {
if state.delayedCountByTarget[target, default: 0] == 0 {
state.visibleDelayedTargets.remove(target)
state.setVisibilityIfNeeded(for: target, isVisible: false)
} else {
state.setVisibilityIfNeeded(for: target, isVisible: true)
}
return .none
}
if 0 < state.delayedCountByTarget[target, default: 0] {
state.setVisibilityIfNeeded(
for: target,
isVisible: state.visibleTargets.contains(target)
)
return scheduleDelayedLoadingIfNeeded(for: target, state: &state)
}
state.scheduledDelayedTargets.remove(target)
state.setVisibilityIfNeeded(for: target, isVisible: false)
return .cancel(id: CancelID.delayedLoading(target))
}
}
private extension LoadingFeature.State {
mutating func setVisibilityIfNeeded(
for target: LoadingFeature.Target,
isVisible: Bool
) {
if isVisible {
visibleTargets.insert(target)
} else {
visibleTargets.remove(target)
}
isLoading = !visibleTargets.isEmpty
}
}