Skip to content

Commit 6510233

Browse files
authored
[#578] TodayView에 TCA를 적용한다 (#606)
* feat: Store 1차 구현 * refactor: 헬퍼 메서드를 피쳐 쪽으로 이동 * refactor: 불필요 리듀서 분리 수정 * refactor: 코디네이터에서 캐싱하는 Store들은 Bindable 처리 * refactor: 불필요 컴포넌트화 제거 * refactor: Picker에 BindingAction 처리 * refactor: Toggle에 BindingAction 처리 * refactor: BindingAction으로 인한 기존 액션 제거 * refactor: now 시점을 최대한 상위 위치로 옮겨 시간을 일차화
1 parent 9e1c5c3 commit 6510233

10 files changed

Lines changed: 1542 additions & 49 deletions

File tree

Application/DevLogCore/Sources/TodayDisplayOptions.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public struct TodayDisplayOptions: Equatable {
2121

2222
public var dueDateVisibility: DueDateVisibility
2323
public var focusVisibility: FocusVisibility
24+
public var isFocusedOnly: Bool {
25+
get { focusVisibility == .focusedOnly }
26+
set { focusVisibility = newValue ? .focusedOnly : .all }
27+
}
2428

2529
public init(
2630
dueDateVisibility: DueDateVisibility,

Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ struct PushNotificationListView: View {
1515
@ScaledMetric(relativeTo: .largeTitle) private var labelWidth = 34
1616
@State private var headerOffset: CGFloat = 0
1717
@State private var isScrollTrackingEnabled = false
18-
@State private var store: StoreOf<PushNotificationListFeature>
18+
@Bindable var store: StoreOf<PushNotificationListFeature>
1919
let coordinator: PushNotificationListViewCoordinator
2020
let isCompactLayout: Bool
21+
2122
init(
2223
coordinator: PushNotificationListViewCoordinator,
2324
isCompactLayout: Bool
2425
) {
2526
self.coordinator = coordinator
2627
self.isCompactLayout = isCompactLayout
27-
self._store = State(initialValue: coordinator.store)
28+
self.store = coordinator.store
2829
}
2930

3031
var body: some View {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//
2+
// TodayFeature+State.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/14/26.
6+
//
7+
8+
import DevLogCore
9+
import Foundation
10+
11+
extension TodayFeature {
12+
static func summaryValue(
13+
for scope: SectionScope,
14+
todos: [TodayTodoItem],
15+
displayOptions: TodayDisplayOptions,
16+
now: Date
17+
) -> Int {
18+
let displayedTodos = displayedTodos(
19+
todos: todos,
20+
displayOptions: displayOptions
21+
)
22+
23+
switch scope {
24+
case .all:
25+
return displayedTodos.count
26+
case .focused:
27+
return displayedTodos.filter(\.isPinned).count
28+
case .overdue:
29+
return displayedTodos.filter { isOverdue($0, now: now) }.count
30+
case .dueSoon:
31+
return displayedTodos.filter { isDueSoon($0, now: now) }.count
32+
}
33+
}
34+
35+
static func displayedTodos(
36+
todos: [TodayTodoItem],
37+
displayOptions: TodayDisplayOptions
38+
) -> [TodayTodoItem] {
39+
let dueDateFilteredTodos: [TodayTodoItem]
40+
switch displayOptions.dueDateVisibility {
41+
case .all:
42+
dueDateFilteredTodos = todos
43+
case .withDueDateOnly:
44+
dueDateFilteredTodos = todos.filter { $0.dueDate != nil }
45+
case .withoutDueDateOnly:
46+
dueDateFilteredTodos = todos.filter { $0.dueDate == nil }
47+
}
48+
49+
switch displayOptions.focusVisibility {
50+
case .all:
51+
return dueDateFilteredTodos
52+
case .focusedOnly:
53+
return dueDateFilteredTodos.filter(\.isPinned)
54+
}
55+
}
56+
57+
static func groupedSectionItems(
58+
from items: [TodayTodoItem],
59+
now: Date
60+
) -> TodayFeature.SectionCollection {
61+
let calendar = Calendar.current
62+
let startOfToday = calendar.startOfDay(for: now)
63+
guard let windowEnd = calendar.date(
64+
byAdding: .day,
65+
value: TodayFeature.upcomingWindowDays,
66+
to: startOfToday
67+
) else {
68+
return TodayFeature.SectionCollection(
69+
focused: items.filter(\.isPinned),
70+
unscheduled: items.filter { !$0.isPinned && $0.dueDate == nil }
71+
)
72+
}
73+
74+
var collection = TodayFeature.SectionCollection()
75+
76+
for item in items {
77+
if item.isPinned {
78+
collection.focused.append(item)
79+
continue
80+
}
81+
82+
guard let dueDate = item.dueDate else {
83+
collection.unscheduled.append(item)
84+
continue
85+
}
86+
87+
let dueDay = calendar.startOfDay(for: dueDate)
88+
if dueDay < startOfToday {
89+
collection.overdue.append(item)
90+
} else if dueDay <= windowEnd {
91+
collection.dueSoon.append(item)
92+
} else {
93+
collection.later.append(item)
94+
}
95+
}
96+
97+
return collection
98+
}
99+
100+
static func isOverdue(_ item: TodayTodoItem, now: Date) -> Bool {
101+
guard let dueDate = item.dueDate else { return false }
102+
let calendar = Calendar.current
103+
return calendar.startOfDay(for: dueDate) < calendar.startOfDay(for: now)
104+
}
105+
106+
static func isDueSoon(_ item: TodayTodoItem, now: Date) -> Bool {
107+
guard let dueDate = item.dueDate else { return false }
108+
let calendar = Calendar.current
109+
let startOfToday = calendar.startOfDay(for: now)
110+
guard let windowEnd = calendar.date(
111+
byAdding: .day,
112+
value: TodayFeature.upcomingWindowDays,
113+
to: startOfToday
114+
) else {
115+
return false
116+
}
117+
let dueDay = calendar.startOfDay(for: dueDate)
118+
return startOfToday <= dueDay && dueDay <= windowEnd
119+
}
120+
121+
static func makeSection(
122+
category: SectionCategory,
123+
title: String,
124+
items: [TodayTodoItem]
125+
) -> [SectionContent] {
126+
guard !items.isEmpty else { return [] }
127+
return [SectionContent(category: category, title: title, items: items)]
128+
}
129+
}

0 commit comments

Comments
 (0)