-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHomeViewModel.swift
More file actions
424 lines (358 loc) · 14.1 KB
/
Copy pathHomeViewModel.swift
File metadata and controls
424 lines (358 loc) · 14.1 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//
// HomeViewModel.swift
// BeforeGoing
//
// Created by APPLE on 9/23/25.
//
import CoreLocation
import Foundation
import UIKit
protocol HomeOutput {}
final class HomeViewModel: ViewModeling {
private static let seperator = ", "
private let todayMissionLimit = 20
private let getMemberNameUseCase: GetMemberNameType
private let weatherUseCase: FetchWeatherType
private let getMissionsUseCase: FetchMissionsType
private let checkMissionUseCase: CheckMissionType
private let addTodayMissionUseCase: AddTodayMissionType
private let deleteTodayMissionUseCase: DeleteTodayMissionType
private var missions: [(
missionID: Int,
content: String,
initState: ListItemState,
state: ListItemState,
isChecked: Bool
)] = []
init(
getMemberNameUseCase: GetMemberNameType,
weatherUseCase: FetchWeatherType,
getMissionsUseCase: FetchMissionsType,
checkMissionUseCase: CheckMissionType,
addTodayMissionUseCase: AddTodayMissionType,
deleteTodayMissionUseCase: DeleteTodayMissionType
) {
self.getMemberNameUseCase = getMemberNameUseCase
self.weatherUseCase = weatherUseCase
self.getMissionsUseCase = getMissionsUseCase
self.checkMissionUseCase = checkMissionUseCase
self.addTodayMissionUseCase = addTodayMissionUseCase
self.deleteTodayMissionUseCase = deleteTodayMissionUseCase
}
enum Input {
case requestName
case requestDate
case requestWeather(
date: Date,
memberName: String,
latitude: CLLocationDegrees,
longitude: CLLocationDegrees
)
case scenarioDidTap(
scenarioID: Int,
date: String
)
case missionChecked(
missionID: Int,
date: String,
willBeChecked: Bool
)
case addTodayMissionButtonDidTap(
scenarioID: Int,
date: String,
content: String
)
case deleteTodayMissionButtonDidTap(missionID: Int)
}
typealias Output = HomeOutput
struct MemberNameOutput: HomeOutput {
let memberName: String
}
struct DateOutput: HomeOutput {
let date: String
}
struct WeatherOutput: HomeOutput {
let weatherResult: Result<NSMutableAttributedString, Error>
}
struct MissionsOutput: HomeOutput {
let missionsResult: Result<MissionsEntity, Error>
}
struct TodayMissionOutput: HomeOutput {
let todayMissionResult: Result<TodayMissionEntity, Error>
}
struct DeleteTodayMissionOutput: HomeOutput {
let deleteTodayMissionResult: Result<Void, Error>
}
struct EmptyOutput: HomeOutput {}
func action(input: Input) async throws -> Output {
switch input {
case .requestName:
let memberName = getMemberNameUseCase.execute()
return MemberNameOutput(memberName: memberName)
case .requestDate:
let date = DateUtil.getCurrentDate(format: "yyyy년 MM월 dd일")
return DateOutput(date: date)
case .requestWeather(let date, let memberName, let latitude, let longitude) :
let administrativeArea: NSMutableAttributedString
do {
administrativeArea = try await getAdministrativeArea(
latitude: latitude,
longitude: longitude
)
} catch (let error) {
return WeatherOutput(weatherResult: .failure(error))
}
do {
let result = try await requestWeatherResult(
date: date,
latitude: latitude,
longitude: longitude
)
let weatherResult = convertWeatherResult(
memberName: memberName,
date: date,
administrativeArea: administrativeArea,
result: result
)
return WeatherOutput(weatherResult: .success(weatherResult))
} catch (let error) {
return WeatherOutput(weatherResult: .failure(error))
}
case .scenarioDidTap(let scenarioID, let date):
do {
let result = try await getMissionsUseCase.execute(
scenarioID: scenarioID,
date: date
)
missions.removeAll()
result.todayMissions.forEach {
if $0.isChecked {
addMissionContent($0.missionId, $0.content, .today, .completed, $0.isChecked)
return
}
addMissionContent($0.missionId, $0.content, .today, .today, $0.isChecked)
}
result.basicMissions.forEach {
if $0.isChecked {
addMissionContent($0.missionId, $0.content, .normal, .completed, $0.isChecked)
return
}
addMissionContent($0.missionId, $0.content, .normal, .normal, $0.isChecked)
}
sortMissions()
return MissionsOutput(missionsResult: .success(result))
} catch {
BeforeGoingLogger.error(error)
return MissionsOutput(missionsResult: .failure(error))
}
case .missionChecked(let missionID, let date, let willBeChecked):
do {
try await checkMissionUseCase.execute(
missionID: missionID,
date: date,
isChecked: willBeChecked
)
if let index = missions.firstIndex(where: { $0.missionID == missionID }) {
willBeChecked ? completeMission(at: index) : cancelMission(at: index)
}
} catch {
BeforeGoingLogger.error(error)
}
return EmptyOutput()
case .addTodayMissionButtonDidTap(let scenarioID, let date, let content):
if isLimitTodayMissions {
return TodayMissionOutput(todayMissionResult: .failure(BeforeGoingError.missionLimitError))
}
do {
let result = try await addTodayMissionUseCase.execute(
scenarioID: scenarioID,
date: date,
content: content
)
missions.insert(
(
missionID: result.missionId,
content: result.content,
initState: .today,
state: .today,
isChecked: result.isChecked
),
at: 0
)
sortMissions()
return TodayMissionOutput(todayMissionResult: .success(result))
} catch {
BeforeGoingLogger.error(error)
return TodayMissionOutput(todayMissionResult: .failure(error))
}
case .deleteTodayMissionButtonDidTap(let missionID):
do {
try await deleteTodayMissionUseCase.execute(missionID: missionID)
guard let index = missions.firstIndex(where: { $0.missionID == missionID }) else {
return DeleteTodayMissionOutput(
deleteTodayMissionResult: .failure(BeforeGoingError.missionNotFound)
)
}
missions.remove(at: index)
return DeleteTodayMissionOutput(deleteTodayMissionResult: .success(Void()))
} catch {
BeforeGoingLogger.error(error)
return DeleteTodayMissionOutput(deleteTodayMissionResult: .failure(error))
}
}
}
func isExistMission(content: String) -> Bool {
missions.contains(where: { mission in
mission.content == content
})
}
private func getAdministrativeArea(
latitude: CLLocationDegrees,
longitude: CLLocationDegrees
) async throws -> NSMutableAttributedString {
let location = CLLocation(latitude: latitude, longitude: longitude)
let placemarks = try await CLGeocoder().reverseGeocodeLocation(location)
let administrativeArea = placemarks.first?.administrativeArea ?? ""
return administrativeArea.customText(rangedText: administrativeArea)
}
private func requestWeatherResult(
date: Date,
latitude: CLLocationDegrees,
longitude: CLLocationDegrees
) async throws -> WeatherEntity? {
let timezone = TimeZone.current.identifier
let result = try await weatherUseCase.execute(
date: date,
timezone: timezone,
location: CLLocation(latitude: latitude, longitude: longitude)
)
return result
}
private func convertWeatherResult(
memberName: String,
date: Date,
administrativeArea: NSMutableAttributedString,
result: WeatherEntity?
) -> NSMutableAttributedString {
var weatherResult = NSMutableAttributedString()
guard let monthAndDay = DateUtil.toMonthAndDay(date: date) else {
return weatherResult
}
guard let result else {
let scenarioIntroduce = "\(memberName)님의 \(monthAndDay) 시나리오예요!".customText(
rangedText: "\(memberName)",
color: UIColor.blue700.cgColor
)
if date < DateUtil.getCurrentDate() {
weatherResult.append(NSAttributedString(string: "지난 날짜의 기상 정보는 제공하지 않아요"))
} else {
weatherResult.append(NSAttributedString(string: "해당 날짜의 기상 정보는 확인하기 어려워요"))
}
weatherResult.append(NSAttributedString(string: "\n"))
weatherResult.append(scenarioIntroduce)
return weatherResult
}
let weatherInformation = makeWeatherString(result)
let supplies = makeSupplyString(result)
weatherResult.do {
$0.append(administrativeArea)
$0.append(NSAttributedString(string: "는 지금 "))
$0.append(weatherInformation)
if let supplies,
!supplies.string.isEmpty {
$0.append(NSAttributedString(string: "\n"))
$0.append(supplies)
$0.append(NSAttributedString(string: " 챙겨보세요!"))
}
}
return weatherResult
}
private func makeWeatherString(_ weather: WeatherEntity) -> NSMutableAttributedString {
let weatherDescription = weather.weatherCondition.description
let uvDescription = weather.uvIndex.description
let weatherString = "\(weatherDescription), 자외선 \(uvDescription)!"
let customedWeatherString = weatherString.customText(rangedText: "\(weatherDescription), 자외선")
return customedWeatherString
}
private func makeSupplyString(_ weather: WeatherEntity) -> NSMutableAttributedString? {
let supplies = [
weather.weatherCondition.supply,
weather.uvIndex.supply
].compactMap { $0 }
guard !supplies.isEmpty else {
return nil
}
let supplyString = supplies.joined(separator: ", ")
let customedSupplyString = supplyString.customText(
rangedText: supplyString,
color: UIColor.blue700.cgColor
)
return customedSupplyString
}
private func addMissionContent(
_ missionID: Int,
_ content: String,
_ beforeState: ListItemState,
_ state: ListItemState,
_ isChecked: Bool
) {
if !isExistMission(content: content) {
self.missions.append((missionID, content, beforeState, state, isChecked))
}
}
private var isLimitTodayMissions: Bool {
missions.filter({ $0.initState == .today }).count >= todayMissionLimit
}
}
extension HomeViewModel {
var missionsCount: Int {
missions.count
}
func getMissionTitle(at index: Int) -> String {
missions[index].content
}
func getMissionState(at index: Int) -> ListItemState {
missions[index].state
}
func getBeforeMissionState(at index: Int) -> ListItemState {
missions[index].initState
}
func isTodayMission(at index: Int) -> Bool {
missions[index].state == .today
}
func removeMission(at index: Int) {
missions.remove(at: index)
}
func getMissionID(at index: Int) -> Int {
missions[index].missionID
}
private func completeMission(at index: Int) {
missions[index].state = .completed
missions[index].isChecked = true
let removed = missions.remove(at: index)
missions.append(removed)
sortMissions()
}
private func cancelMission(at index: Int) {
missions[index].state = missions[index].initState
missions[index].isChecked = false
let removed = missions.remove(at: index)
missions.insert(removed, at: 0)
sortMissions()
}
private func sortMissions() {
missions.sort { mission1, mission2 in
let isChecked1 = mission1.isChecked
let isChecked2 = mission2.isChecked
if isChecked1 != isChecked2 {
return !isChecked1
}
let isBasicMission1 = mission1.initState == .normal
let isBasicMission2 = mission2.initState == .normal
if isBasicMission1 != isBasicMission2 {
return !isBasicMission1
}
return mission1.missionID < mission2.missionID
}
}
}