Skip to content

Commit 468b380

Browse files
authored
[#124] 캐시 디렉토리의 용량을 보여주고 제거하는 UI를 구성한다 (#129)
* style: delete -> cancel * feat: FileManager의 캐시 디렉토리 크기를 보여주고 탭 하면 얼럿이 뜨도록 추가 * refactor: NumberFormatter 대신 formatted 사용 * feat: 캐시 디렉토리 제거 로직 구현 * ui: 캐시 디렉토리가 비어 있으면 버튼 탭을 막고 비활성화 형태로 개선 * refactor: 디렉토리를 새로 생성하지 않고, 있는지만 확인하도록 개선 * refactor: 일일이 제거하는 대신 디렉토리를 제거 후 생성하는 형태로 개선 * refactor: 실제 파일 크기의 단위 증가에 더 맞도록 개선 * rollback: 캐시 디렉토리는 임의로 지울 수 없기 때문에 하나하나 지워야 함 * fix: 에러가 발생했을 때 에러가 나면 얼럿이 떠야 하므로 미리 얼럿을 끔
1 parent 37a9100 commit 468b380

3 files changed

Lines changed: 125 additions & 12 deletions

File tree

DevLog/Presentation/ViewModel/SettingViewModel.swift

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Foundation
1010
final class SettingViewModel: Store {
1111
struct State {
1212
var theme = ""
13+
var dirSize: Int64 = 0
1314
var isLoading = false
1415
var showAlert: Bool = false
1516
var alertTitle: String = ""
@@ -23,8 +24,11 @@ final class SettingViewModel: Store {
2324
case setAlert(isPresented: Bool, type: AlertType? = nil)
2425
case setLoading(Bool)
2526
case setTheme(String)
27+
case updateDirSize
2628
case tapDeleteAuthButton
2729
case tapSignOutButton
30+
case tapRemoveCacheButton
31+
case confirmRemoveCache
2832
}
2933

3034
enum SideEffect {
@@ -33,15 +37,14 @@ final class SettingViewModel: Store {
3337
}
3438

3539
enum AlertType {
36-
case signOut, delete, error
40+
case signOut, deleteAuth, error, removeCache
3741
}
3842

43+
@Published private(set) var state = State()
3944
private let deleteAuthuseCase: DeleteAuthUseCase
4045
private let signOutUseCase: SignOutUseCase
4146
private let sessionUseCase: AuthSessionUseCase
4247

43-
@Published private(set) var state = State()
44-
4548
init(
4649
deleteAuthUseCase: DeleteAuthUseCase,
4750
signOutUseCase: SignOutUseCase,
@@ -60,10 +63,22 @@ final class SettingViewModel: Store {
6063
state.isLoading = value
6164
case .setTheme(let value):
6265
state.theme = value
66+
case .updateDirSize:
67+
state.dirSize = dirSizeInBytes()
6368
case .tapDeleteAuthButton:
6469
return [.deleteAuth]
6570
case .tapSignOutButton:
6671
return [.signOut]
72+
case .tapRemoveCacheButton:
73+
setAlert(&state, isPresented: true, type: .removeCache)
74+
case .confirmRemoveCache:
75+
do {
76+
setAlert(&state, isPresented: false)
77+
try clearCacheDirectory()
78+
state.dirSize = dirSizeInBytes()
79+
} catch {
80+
setAlert(&state, isPresented: true, type: .error)
81+
}
6782
}
6883
return []
6984
}
@@ -101,23 +116,81 @@ private extension SettingViewModel {
101116
func setAlert(
102117
_ state: inout State,
103118
isPresented: Bool,
104-
type: AlertType?
119+
type: AlertType? = nil
105120
) {
106121
switch type {
107122
case .signOut:
108123
state.alertTitle = "로그아웃"
109124
state.alertMessage = "로그아웃 하시겠습니까?"
110-
case .delete:
125+
case .deleteAuth:
111126
state.alertTitle = "정말 탈퇴하시겠습니까?"
112127
state.alertMessage = "회원 탈퇴가 진행되면 모든 데이터가 지워지고 복구할 수 없습니다."
113128
case .error:
114129
state.alertTitle = "오류"
115130
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
131+
case .removeCache:
132+
state.alertTitle = "임시 데이터 삭제"
133+
state.alertMessage = "임시 데이터를 삭제하고 정리합니다.\n계속하시겠습니까?"
116134
case .none:
117135
state.alertTitle = ""
118136
state.alertMessage = ""
119137
}
120138
state.showAlert = isPresented
121139
state.alertType = type
122140
}
141+
142+
func dirSizeInBytes() -> Int64 {
143+
do {
144+
let cachesDir = try FileManager.default.url(
145+
for: .cachesDirectory,
146+
in: .userDomainMask,
147+
appropriateFor: nil,
148+
create: false
149+
)
150+
guard FileManager.default.fileExists(atPath: cachesDir.path) else { return 0 }
151+
return directorySize(at: cachesDir)
152+
} catch {
153+
return 0
154+
}
155+
}
156+
157+
private func directorySize(at url: URL) -> Int64 {
158+
guard FileManager.default.fileExists(atPath: url.path) else { return 0 }
159+
guard let enumerator = FileManager.default.enumerator(
160+
at: url,
161+
includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey],
162+
options: [.skipsHiddenFiles]
163+
) else {
164+
return 0
165+
}
166+
167+
var total: Int64 = 0
168+
for case let fileURL as URL in enumerator {
169+
guard let resourceValues = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]),
170+
resourceValues.isRegularFile == true,
171+
let fileSize = resourceValues.fileSize else {
172+
continue
173+
}
174+
total += Int64(fileSize)
175+
}
176+
return total
177+
}
178+
179+
private func clearCacheDirectory() throws {
180+
let cachesDir = try FileManager.default.url(
181+
for: .cachesDirectory,
182+
in: .userDomainMask,
183+
appropriateFor: nil,
184+
create: false
185+
)
186+
guard FileManager.default.fileExists(atPath: cachesDir.path) else { return }
187+
let contents = try FileManager.default.contentsOfDirectory(
188+
at: cachesDir,
189+
includingPropertiesForKeys: nil,
190+
options: [.skipsHiddenFiles]
191+
)
192+
for url in contents {
193+
try FileManager.default.removeItem(at: url)
194+
}
195+
}
123196
}

DevLog/Resource/Localizable.xcstrings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@
358358
},
359359
"읽지 않음" : {
360360

361+
},
362+
"임시 데이터 삭제" : {
363+
361364
},
362365
"작년" : {
363366

DevLog/UI/Setting/SettingView.swift

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ struct SettingView: View {
1212
@Environment(\.diContainer) var container: DIContainer
1313
@StateObject var viewModel: SettingViewModel
1414
@EnvironmentObject var router: NavigationRouter
15-
@State private var navigationPath: Path?
1615

1716
var body: some View {
1817
Form {
@@ -28,16 +27,27 @@ struct SettingView: View {
2827
.foregroundStyle(Color.gray)
2928
}
3029
}
31-
.onAppear {
32-
viewModel.send(.setTheme(theme.localizedName))
33-
}
3430

3531
Button {
3632
router.push(Path.pushNotification)
3733
} label: {
3834
Text("알림")
3935
.foregroundStyle(Color.primary)
4036
}
37+
38+
let dirSize = viewModel.state.dirSize
39+
Button {
40+
viewModel.send(.tapRemoveCacheButton)
41+
} label: {
42+
HStack {
43+
Text("임시 데이터 삭제")
44+
.foregroundStyle(dirSize == 0 ? Color.secondary : .primary)
45+
Spacer()
46+
Text(formatFileSize(bytes: dirSize))
47+
.foregroundStyle(Color.secondary.opacity(dirSize == 0 ? 0 : 1))
48+
}
49+
}
50+
.disabled(dirSize == 0)
4151
}
4252

4353
Section {
@@ -79,7 +89,6 @@ struct SettingView: View {
7989
Section {
8090
Button {
8191
router.push(Path.account)
82-
8392
} label: {
8493
Text("계정 연동")
8594
}
@@ -93,7 +102,7 @@ struct SettingView: View {
93102
HStack {
94103
Spacer()
95104
Button(role: .destructive, action: {
96-
viewModel.send(.setAlert(isPresented: true, type: .delete))
105+
viewModel.send(.setAlert(isPresented: true, type: .deleteAuth))
97106
}) {
98107
Text("회원 탈퇴")
99108
.font(.headline)
@@ -139,6 +148,10 @@ struct SettingView: View {
139148
LoadingView()
140149
}
141150
}
151+
.onAppear {
152+
viewModel.send(.setTheme(theme.localizedName))
153+
viewModel.send(.updateDirSize)
154+
}
142155
}
143156

144157
private enum Path: Hashable {
@@ -155,17 +168,41 @@ struct SettingView: View {
155168
Button("확인", role: .destructive) {
156169
viewModel.send(.tapSignOutButton)
157170
}
158-
case .delete:
171+
case .deleteAuth:
159172
Button("취소", role: .cancel) {
160173
viewModel.send(.setAlert(isPresented: false))
161174
}
162175
Button("탈퇴", role: .destructive) {
163176
viewModel.send(.tapDeleteAuthButton)
164177
}
178+
case .removeCache:
179+
Button("취소", role: .cancel) {
180+
viewModel.send(.setAlert(isPresented: false))
181+
}
182+
Button("확인", role: .destructive) {
183+
viewModel.send(.confirmRemoveCache)
184+
}
165185
case .error, .none:
166186
Button("확인", role: .cancel) {
167187
viewModel.send(.setAlert(isPresented: false))
168188
}
169189
}
170190
}
191+
192+
private func formatFileSize(bytes: Int64) -> String {
193+
let units = ["B", "KB", "MB", "GB"]
194+
var value = Double(max(bytes, 0))
195+
var unitIndex = 0
196+
197+
while 1024.0 <= value && unitIndex < units.count - 1 {
198+
value /= 1024.0
199+
unitIndex += 1
200+
}
201+
202+
let truncated = floor(value * 100.0) / 100.0
203+
let numberString = truncated.formatted(
204+
.number.precision(.fractionLength(0...2))
205+
)
206+
return "\(numberString)\(units[unitIndex])"
207+
}
171208
}

0 commit comments

Comments
 (0)