diff --git a/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift b/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift index 850fba7..33cd7d9 100644 --- a/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift @@ -10,17 +10,19 @@ import Foundation final class TodoDetailViewModel: Store { struct State { var todo: Todo? - var isLoading = false - var showEditor = false - var showAlert = false - var alertTitle = "" - var alertMessage = "" + var isLoading: Bool = false + var showAlert: Bool = false + var showEditor: Bool = false + var showInfo: Bool = false + var alertTitle: String = "" + var alertMessage: String = "" } enum Action { case onAppear case setAlert(Bool) case setShowEditor(Bool) + case setShowInfo(Bool) case setTodo(Todo) case setLoading(Bool) case upsertTodo(Todo) @@ -57,6 +59,8 @@ final class TodoDetailViewModel: Store { setAlert(&state, isPresented: isPresented) case .setShowEditor(let isPresented): state.showEditor = isPresented + case .setShowInfo(let presented): + state.showInfo = presented case .setTodo(let todo): state.todo = todo case .setLoading(let value): diff --git a/DevLog/UI/Common/Componeent/SheetToolbar.swift b/DevLog/UI/Common/Componeent/SheetToolbar.swift deleted file mode 100644 index b42e149..0000000 --- a/DevLog/UI/Common/Componeent/SheetToolbar.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SheetToolbar.swift -// DevLog -// -// Created by 최윤진 on 2/27/26. -// - -import SwiftUI - -struct SheetToolbar: ToolbarContent { - let onCancel: () -> Void - let onConfirm: () -> Void - let isConfirmEnabled: Bool - - init( - onCancel: @escaping () -> Void, - onConfirm: @escaping () -> Void, - isConfirmEnabled: Bool = true - ) { - self.onCancel = onCancel - self.onConfirm = onConfirm - self.isConfirmEnabled = isConfirmEnabled - } - - var body: some ToolbarContent { - if #available(iOS 26.0, *) { - ToolbarItem(placement: .topBarLeading) { - Button(role: .cancel) { - onCancel() - } - } - - ToolbarItem(placement: .topBarTrailing) { - Button(role: .confirm) { - onConfirm() - } - .disabled(!isConfirmEnabled) - } - } else { - ToolbarItem(placement: .topBarLeading) { - Button { - onCancel() - } label: { - Text("취소") - } - } - - ToolbarItem(placement: .topBarTrailing) { - Button { - onConfirm() - } label: { - Text("확인") - .bold() - } - .disabled(!isConfirmEnabled) - } - } - } -} diff --git a/DevLog/UI/Common/Componeent/ToolbarButton+.swift b/DevLog/UI/Common/Componeent/ToolbarButton+.swift new file mode 100644 index 0000000..00c89da --- /dev/null +++ b/DevLog/UI/Common/Componeent/ToolbarButton+.swift @@ -0,0 +1,62 @@ +// +// ToolbarButton+.swift +// DevLog +// +// Created by 최윤진 on 3/1/26. +// + +import SwiftUI + +struct ToolbarLeadingButton: ToolbarContent { + var action: (() -> Void)? + + var body: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { + if #available(iOS 26.0, *) { + Button(role: .cancel) { + action?() + } + } else { + Button { + action?() + } label: { + Text("취소") + } + } + } + } +} + +struct ToolbarTrailingButton: ToolbarContent { + var action: (() -> Void)? + private var isDisabled: Bool = false + + init(action: (() -> Void)? = nil) { + self.action = action + } + + func disabled(_ isDisabled: Bool) -> ToolbarTrailingButton { + var copy = self + copy.isDisabled = isDisabled + return copy + } + + var body: some ToolbarContent { + ToolbarItem(placement: .topBarTrailing) { + if #available(iOS 26.0, *) { + Button(role: .confirm) { + action?() + } + .disabled(isDisabled) + } else { + Button { + action?() + } label: { + Text("확인") + .bold() + } + .disabled(isDisabled) + } + } + } +} diff --git a/DevLog/UI/Home/TodoDetailView.swift b/DevLog/UI/Home/TodoDetailView.swift index 575301c..52faa11 100644 --- a/DevLog/UI/Home/TodoDetailView.swift +++ b/DevLog/UI/Home/TodoDetailView.swift @@ -18,35 +18,8 @@ struct TodoDetailView: View { ScrollView { LazyVStack(alignment: .leading, spacing: 10) { Text(todo.title) - .font(.title3) + .font(.title3.bold()) .padding(.horizontal) - if let date = todo.dueDate { - Divider() - HStack { - Text("마감일") - Spacer() - Text(date.formatted(date: .long, time: .omitted)) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color.gray.opacity(0.2)) - ) - } - .padding(.horizontal) - } - Divider() - HStack { - Text("태그") - Divider() - ScrollView(.horizontal) { - HStack { - ForEach(todo.tags, id: \.self) { tag in - Tag(tag, isEditing: false) - } - } - } - .scrollIndicators(.never) - } - .padding(.horizontal) Divider() Markdown(todo.content) .padding(.horizontal) @@ -57,6 +30,12 @@ struct TodoDetailView: View { } } .onAppear { viewModel.send(.onAppear) } + .sheet(isPresented: Binding( + get: { viewModel.state.showInfo }, + set: { viewModel.send(.setShowInfo($0)) } + )) { + sheetContent + } .fullScreenCover(isPresented: Binding( get: { viewModel.state.showEditor }, set: { viewModel.send(.setShowEditor($0)) } @@ -68,12 +47,84 @@ struct TodoDetailView: View { ) } } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - viewModel.send(.setShowEditor(true)) - } label: { - Text("수정") + .toolbar { toolbarContent } + } + + @ToolbarContentBuilder + private var toolbarContent: some ToolbarContent { + ToolbarItem(placement: .topBarTrailing) { + Button { + viewModel.send(.setShowInfo(true)) + } label: { + Image(systemName: "info.circle") + } + } + if #available(iOS 26.0, *) { + ToolbarSpacer(.fixed, placement: .topBarTrailing) + } + ToolbarItem(placement: .topBarTrailing) { + Button { + viewModel.send(.setShowEditor(true)) + } label: { + Text("수정") + } + } + } + + private var sheetContent: some View { + NavigationStack { + ScrollView { + LazyVStack(spacing: 32) { + VStack { + HStack { + Text("마감일") + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + } + HStack(spacing: 8) { + Image(systemName: "calendar") + .foregroundStyle(.secondary) + Text( + viewModel.state.todo?.dueDate? + .formatted(date: .abbreviated, time: .omitted) + ?? "마감일 없음" + ) + .foregroundStyle( + viewModel.state.todo?.dueDate == nil ? .secondary : .primary + ) + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(.tertiarySystemFill)) + ) + Divider() + } + VStack { + HStack { + Text("태그") + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + } + Divider() + if let tags = viewModel.state.todo?.tags, !tags.isEmpty { + TagLayout { + ForEach(tags, id: \.self) { tag in + Tag(tag, isEditing: false) + } + } + } + } + } + .padding(.horizontal) + } + .toolbar { + ToolbarLeadingButton { + viewModel.send(.setShowInfo(false)) } } } diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index e5f9505..b716611 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -47,14 +47,14 @@ struct TodoEditorView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.background, for: .navigationBar) .toolbar { - SheetToolbar( - onCancel: { dismiss() }, - onConfirm: { - onSubmit?(viewModel.upsertTodo()) - dismiss() - }, - isConfirmEnabled: viewModel.state.isValidToSave - ) + ToolbarLeadingButton { + dismiss() + } + ToolbarTrailingButton { + onSubmit?(viewModel.upsertTodo()) + dismiss() + } + .disabled(!viewModel.state.isValidToSave) } } } diff --git a/DevLog/UI/Setting/PushNotificationSettingsView.swift b/DevLog/UI/Setting/PushNotificationSettingsView.swift index 643ed3a..41e8c74 100644 --- a/DevLog/UI/Setting/PushNotificationSettingsView.swift +++ b/DevLog/UI/Setting/PushNotificationSettingsView.swift @@ -89,10 +89,12 @@ struct PushNotificationSettingsView: View { .onAppear { UIDatePicker.appearance().minuteInterval = 5 } .onDisappear { UIDatePicker.appearance().minuteInterval = 1 /* 기본값으로 복원 */ } .toolbar { - SheetToolbar( - onCancel: { viewModel.send(.rollbackUpdate) }, - onConfirm: { viewModel.send(.confirmUpdate) } - ) + ToolbarLeadingButton { + viewModel.send(.rollbackUpdate) + } + ToolbarTrailingButton { + viewModel.send(.confirmUpdate) + } } .background( GeometryReader { geometry in