Skip to content

Commit c25b9c9

Browse files
committed
feat: 로딩 상태를 뷰모델마다 관리할 수 있는 객체 구현
1 parent e97c12a commit c25b9c9

1 file changed

Lines changed: 150 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//
2+
// LoadingState.swift
3+
// DevLog
4+
//
5+
// Created by opfic on 3/16/26.
6+
//
7+
8+
import Foundation
9+
10+
@MainActor
11+
final class LoadingState {
12+
private enum DefaultTarget: Hashable {
13+
case value
14+
}
15+
16+
enum Mode {
17+
case immediate
18+
case delayed
19+
}
20+
21+
private let delay: Duration
22+
private var immediateCountByTarget: [AnyHashable: Int] = [:]
23+
private var delayedCountByTarget: [AnyHashable: Int] = [:]
24+
private var delayedTaskByTarget: [AnyHashable: Task<Void, Never>] = [:]
25+
private var visibleDelayedTargets = Set<AnyHashable>()
26+
27+
init(delay: Duration = .milliseconds(500)) {
28+
self.delay = delay
29+
}
30+
31+
func begin(
32+
mode: Mode,
33+
update: @escaping @MainActor (Bool) -> Void
34+
) {
35+
begin(target: DefaultTarget.value, mode: mode) { _, isLoading in
36+
update(isLoading)
37+
}
38+
}
39+
40+
func begin<T: Hashable>(
41+
target: T,
42+
mode: Mode,
43+
update: @escaping @MainActor (T, Bool) -> Void
44+
) {
45+
let hashableTarget = AnyHashable(target)
46+
begin(target: hashableTarget, mode: mode) { isLoading in
47+
update(target, isLoading)
48+
}
49+
}
50+
51+
func end(
52+
mode: Mode,
53+
update: @escaping @MainActor (Bool) -> Void
54+
) {
55+
end(target: DefaultTarget.value, mode: mode) { _, isLoading in
56+
update(isLoading)
57+
}
58+
}
59+
60+
func end<T: Hashable>(
61+
target: T,
62+
mode: Mode,
63+
update: @escaping @MainActor (T, Bool) -> Void
64+
) {
65+
let hashableTarget = AnyHashable(target)
66+
end(target: hashableTarget, mode: mode) { isLoading in
67+
update(target, isLoading)
68+
}
69+
}
70+
71+
private func begin(
72+
target: AnyHashable,
73+
mode: Mode,
74+
update: @escaping @MainActor (Bool) -> Void
75+
) {
76+
switch mode {
77+
case .immediate:
78+
immediateCountByTarget[target, default: 0] += 1
79+
update(true)
80+
case .delayed:
81+
delayedCountByTarget[target, default: 0] += 1
82+
scheduleDelayedLoadingIfNeeded(for: target, update: update)
83+
}
84+
}
85+
86+
private func end(
87+
target: AnyHashable,
88+
mode: Mode,
89+
update: @escaping @MainActor (Bool) -> Void
90+
) {
91+
switch mode {
92+
case .immediate:
93+
let count = immediateCountByTarget[target, default: 0]
94+
immediateCountByTarget[target] = max(0, count - 1)
95+
case .delayed:
96+
let count = delayedCountByTarget[target, default: 0]
97+
delayedCountByTarget[target] = max(0, count - 1)
98+
}
99+
updateLoadingVisibility(for: target, update: update)
100+
}
101+
102+
private func scheduleDelayedLoadingIfNeeded(
103+
for target: AnyHashable,
104+
update: @escaping @MainActor (Bool) -> Void
105+
) {
106+
guard delayedTaskByTarget[target] == nil,
107+
!visibleDelayedTargets.contains(target),
108+
0 < delayedCountByTarget[target, default: 0] else { return }
109+
delayedTaskByTarget[target] = Task { [weak self] in
110+
guard let self else { return }
111+
try? await Task.sleep(for: delay)
112+
if Task.isCancelled { return }
113+
await MainActor.run {
114+
self.delayedTaskByTarget[target] = nil
115+
guard 0 < self.delayedCountByTarget[target, default: 0] else { return }
116+
self.visibleDelayedTargets.insert(target)
117+
if self.immediateCountByTarget[target, default: 0] == 0 {
118+
update(true)
119+
}
120+
}
121+
}
122+
}
123+
124+
private func updateLoadingVisibility(
125+
for target: AnyHashable,
126+
update: @escaping @MainActor (Bool) -> Void
127+
) {
128+
if 0 < immediateCountByTarget[target, default: 0] {
129+
update(true)
130+
return
131+
}
132+
if visibleDelayedTargets.contains(target) {
133+
if delayedCountByTarget[target, default: 0] == 0 {
134+
visibleDelayedTargets.remove(target)
135+
update(false)
136+
} else {
137+
update(true)
138+
}
139+
return
140+
}
141+
if 0 < delayedCountByTarget[target, default: 0] {
142+
update(false)
143+
scheduleDelayedLoadingIfNeeded(for: target, update: update)
144+
return
145+
}
146+
delayedTaskByTarget[target]?.cancel()
147+
delayedTaskByTarget[target] = nil
148+
update(false)
149+
}
150+
}

0 commit comments

Comments
 (0)