Skip to content

Commit 1aa65e6

Browse files
authored
[Fix/#42] TodoEditorView에 확인된 버그를 해결한다 (#47)
* fix: X버튼과 추가 버튼을 탭해도 창이 내려가지 않는 현상 해결 * refactor: 뷰 코드 정리 * refactor: title 문자열 수정 * feat: 여백 부분을 탭하면 설명 필드에 키보드 포커싱이 되도록 추가 * ui: 디자인 수정 * style: 띄어쓰기 추가 * refactor: adaptiveButtonStyle 수정 * feat: 좌측정렬 레이아웃 구현 * feat: 태그 컴포넌트 구현 * refactor: 동일한 태그명일 시 추가되지 않도록 개선 * feat: 마감일 피커 구현 * chore: Tag 관련 컴포넌트 분리 * refactor: 태그 입력 및 확인을 sheet 버전으로 변경 * style: Preview 제거 * refactor: focus 변수 제거 * feat: 시트가 내려갈 때 입력했던 문자열 초기화 * feat: spacing 변수로 일관화 * style: 코드 위치 이동 및 주석 제거 * feat: 추가할 태그 문자열이 없거나 포함되어 있다면 탭 이펙트 차단 * feat: 마감일 체크박스 추가 * refactor: 불필요 ignoresSafeArea 제거 * fix: 스크롤 시 툴바 배경에 내용과 header과 어우러지지 않아 수정 * fix: Todo 작성 시 kind가 etc로만 적용되는 현상 해결 * fix: DatePicker 높이보다 sheet의 safeArea로 인해 Picker이 가려지는 현상 해결
1 parent 3d159d3 commit 1aa65e6

10 files changed

Lines changed: 592 additions & 193 deletions

File tree

DevLog/Presentation/Extension/View+.swift

Lines changed: 0 additions & 24 deletions
This file was deleted.

DevLog/Presentation/ViewModel/TodoEditorViewModel.swift

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
//
77

88
import Foundation
9+
import OrderedCollections
910

1011
final class TodoEditorViewModel: Store {
1112
struct State {
1213
var title: String = ""
1314
var content: String = ""
1415
var dueDate: Date?
15-
var tags: [String] = []
16+
var tags: OrderedSet<String> = []
1617
var tagText: String = ""
1718
var focusOnEditor: Bool = false
1819
var hasDueDate: Bool { return dueDate != nil }
@@ -28,10 +29,10 @@ final class TodoEditorViewModel: Store {
2829
}
2930

3031
enum Action {
31-
case addTag
32+
case addTag(String)
3233
case removeTag(String)
3334
case setContent(String)
34-
case setDueDate(Date)
35+
case setDueDate(Date?)
3536
case setTabViewTag(Tag)
3637
case setTagText(String)
3738
case setTitle(String)
@@ -50,39 +51,47 @@ final class TodoEditorViewModel: Store {
5051
private let createdAt: Date?
5152
private let kind: TodoKind
5253

53-
init(title: String, todo: Todo? = nil) {
54-
self.navigationTitle = title
55-
self.id = todo?.id ?? UUID().uuidString
56-
self.isPinned = todo?.isPinned ?? false
57-
self.isCompleted = todo?.isCompleted ?? false
58-
self.isChecked = todo?.isChecked ?? false
59-
self.createdAt = todo?.createdAt ?? nil
60-
self.kind = todo?.kind ?? .etc
61-
if let todo {
62-
state.title = todo.title
63-
state.content = todo.content
64-
state.dueDate = todo.dueDate
65-
state.tags = todo.tags
66-
}
54+
// 새로운 Todo 생성용 생성자
55+
init(kind: TodoKind) {
56+
self.navigationTitle = "\(kind.localizedName) 추가"
57+
self.id = UUID().uuidString
58+
self.isPinned = false
59+
self.isCompleted = false
60+
self.isChecked = false
61+
self.createdAt = nil
62+
self.kind = kind
63+
}
64+
65+
// 기존 Todo 편집용 생성자
66+
init(todo: Todo) {
67+
self.navigationTitle = "편집"
68+
self.id = todo.id
69+
self.isPinned = todo.isPinned
70+
self.isCompleted = todo.isCompleted
71+
self.isChecked = todo.isChecked
72+
self.createdAt = todo.createdAt
73+
self.kind = todo.kind
74+
state.title = todo.title
75+
state.content = todo.content
76+
state.dueDate = todo.dueDate
77+
state.tags = OrderedSet(todo.tags)
6778
}
6879

6980
func reduce(with action: Action) -> [SideEffect] {
7081
switch action {
71-
case .addTag:
72-
let tagText = state.tagText
73-
if !state.tags.contains(tagText) && !tagText.isEmpty {
74-
state.tags.append(tagText)
75-
state.tagText = ""
76-
}
82+
case .addTag(let tag):
83+
if !tag.isEmpty { state.tags.append(tag) }
7784
case .removeTag(let tagText):
7885
state.tags.removeAll { $0 == tagText }
7986
case .setContent(let stringValue),
8087
.setTagText(let stringValue),
8188
.setTitle(let stringValue):
8289
handleStringAction(action, stringValue: stringValue)
8390
case .setDueDate(let dueDate):
84-
if let tomorrowDate = calendar.date(byAdding: .day, value: 1, to: Date()) {
91+
if let tomorrowDate = calendar.date(byAdding: .day, value: 1, to: Date()), let dueDate {
8592
state.dueDate = max(dueDate, tomorrowDate)
93+
} else {
94+
state.dueDate = nil
8695
}
8796
case .setTabViewTag(let tag):
8897
state.tabViewTag = tag
@@ -125,7 +134,7 @@ extension TodoEditorViewModel {
125134
createdAt: self.createdAt ?? date,
126135
updatedAt: date,
127136
dueDate: state.dueDate,
128-
tags: state.tags,
137+
tags: state.tags.map { $0 },
129138
kind: self.kind
130139
)
131140
}

DevLog/Resource/Localizable.xcstrings

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,6 @@
250250
},
251251
"계정 연동" : {
252252

253-
},
254-
"내용을 입력하세요" : {
255-
256253
},
257254
"네트워크 문제" : {
258255

@@ -289,6 +286,9 @@
289286
},
290287
"생성" : {
291288

289+
},
290+
"설명(선택 사항)" : {
291+
292292
},
293293
"설정" : {
294294

@@ -352,6 +352,9 @@
352352
},
353353
"태그" : {
354354

355+
},
356+
"태그 입력" : {
357+
355358
},
356359
"테마" : {
357360

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
// Tag+.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/6/26.
6+
//
7+
8+
import SwiftUI
9+
10+
struct Tag: View {
11+
@Environment(\.colorScheme) private var colorScheme
12+
@State private var height: CGFloat = 0
13+
private let name: String
14+
private let isEditing: Bool
15+
private var action: (() -> Void)?
16+
17+
init(_ name: String, isEditing: Bool, action: (() -> Void)? = nil) {
18+
self.name = name
19+
self.isEditing = isEditing
20+
self.action = action
21+
}
22+
23+
var body: some View {
24+
HStack(spacing: 4) {
25+
Text(name)
26+
.foregroundStyle(.blue)
27+
.bold()
28+
.lineLimit(1)
29+
.fixedSize()
30+
.padding(.vertical, 4)
31+
.padding(.leading, 8)
32+
.padding(.trailing, isEditing ? 0 : 8)
33+
.background {
34+
GeometryReader { geo in
35+
Color.clear
36+
.onAppear {
37+
height = geo.size.height
38+
}
39+
}
40+
}
41+
42+
if isEditing {
43+
Button {
44+
action?()
45+
} label: {
46+
Image(systemName: "xmark.circle.fill")
47+
.resizable()
48+
.aspectRatio(contentMode: .fit)
49+
.frame(width: height, height: height)
50+
.symbolRenderingMode(.palette)
51+
.foregroundStyle(
52+
.blue,
53+
.black.opacity(colorScheme == .light ? 0 : 0.4)
54+
)
55+
56+
}
57+
}
58+
}
59+
.background {
60+
Capsule()
61+
.fill(.blue.opacity(0.2))
62+
}
63+
}
64+
}
65+
66+
struct TagLayout: Layout {
67+
var verticalSpacing: CGFloat = 8
68+
var horizontalSpacing: CGFloat = 8
69+
70+
func sizeThatFits(
71+
proposal: ProposedViewSize,
72+
subviews: Subviews,
73+
cache: inout ()
74+
) -> CGSize {
75+
let maxWidth = proposal.width ?? .infinity
76+
let rows = computeRows(maxWidth: maxWidth, subviews: subviews)
77+
let height =
78+
rows.reduce(0) { $0 + $1.maxHeight }
79+
+ CGFloat(max(0, rows.count - 1)) * verticalSpacing
80+
return CGSize(width: proposal.width ?? 0, height: height)
81+
}
82+
83+
func placeSubviews(
84+
in bounds: CGRect,
85+
proposal: ProposedViewSize,
86+
subviews: Subviews,
87+
cache: inout ()
88+
) {
89+
let rows = computeRows(maxWidth: bounds.width, subviews: subviews)
90+
var minY = bounds.minY
91+
92+
for row in rows {
93+
var minX = bounds.minX
94+
95+
for index in row.indices {
96+
let size = subviews[index].sizeThatFits(.unspecified)
97+
subviews[index].place(
98+
at: CGPoint(x: minX, y: minY),
99+
proposal: ProposedViewSize(size)
100+
)
101+
minX += size.width + horizontalSpacing
102+
}
103+
104+
minY += row.maxHeight + verticalSpacing
105+
}
106+
}
107+
108+
private func computeRows(
109+
maxWidth: CGFloat,
110+
subviews: Subviews
111+
) -> [Row] {
112+
let availableWidth = maxWidth > 0 ? maxWidth : .infinity
113+
var rows: [Row] = []
114+
var currentRow = Row()
115+
var currentWidth: CGFloat = 0
116+
117+
for (index, subview) in subviews.enumerated() {
118+
let size = subview.sizeThatFits(.unspecified)
119+
120+
if currentWidth + size.width > availableWidth && !currentRow.indices.isEmpty {
121+
rows.append(currentRow)
122+
currentRow = Row()
123+
currentWidth = 0
124+
}
125+
126+
currentRow.indices.append(index)
127+
currentRow.maxHeight = max(currentRow.maxHeight, size.height)
128+
currentWidth += size.width + horizontalSpacing
129+
}
130+
131+
if !currentRow.indices.isEmpty {
132+
rows.append(currentRow)
133+
}
134+
135+
return rows
136+
}
137+
138+
private struct Row {
139+
var indices: [Int] = []
140+
var maxHeight: CGFloat = 0
141+
}
142+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// EnvironmentValues+.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/6/26.
6+
//
7+
8+
import SwiftUI
9+
10+
extension EnvironmentValues {
11+
12+
var safeAreaInsets: EdgeInsets {
13+
self[SafeAreaInsetsKey.self]
14+
}
15+
16+
private struct SafeAreaInsetsKey: EnvironmentKey {
17+
static var defaultValue: EdgeInsets {
18+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
19+
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else {
20+
return EdgeInsets()
21+
}
22+
return window.safeAreaInsets.insets
23+
}
24+
}
25+
}
26+
27+
extension UIEdgeInsets {
28+
var insets: EdgeInsets {
29+
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
30+
}
31+
}

DevLog/UI/Extension/View+.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// View+.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 11/22/25.
6+
//
7+
8+
import SwiftUI
9+
10+
extension View {
11+
var sceneWidth: CGFloat {
12+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
13+
else { return UIScreen.main.bounds.width }
14+
15+
return windowScene.screen.bounds.width
16+
}
17+
18+
var sceneHeight: CGFloat {
19+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
20+
else { return UIScreen.main.bounds.height }
21+
22+
return windowScene.screen.bounds.height
23+
}
24+
25+
var safeAreaInsets: UIEdgeInsets {
26+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
27+
let window = windowScene.windows.first
28+
else { return UIEdgeInsets.zero }
29+
30+
return window.safeAreaInsets
31+
}
32+
33+
@ViewBuilder
34+
func adaptiveButtonStyle(_ color: Color? = nil) -> some View {
35+
if #available(iOS 26.0, *), color == nil {
36+
self.buttonStyle(.glass)
37+
} else {
38+
self.foregroundStyle(Color(.label))
39+
.font(.footnote)
40+
.padding(.vertical, 8)
41+
.padding(.horizontal, 16)
42+
.background {
43+
Capsule()
44+
.fill(.ultraThinMaterial)
45+
.background {
46+
Capsule()
47+
.fill(color ?? Color.clear)
48+
}
49+
.overlay {
50+
Capsule()
51+
.stroke(Color.white.opacity(0.2), lineWidth: 1)
52+
}
53+
}
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)