diff --git a/Projects/Presentation/Sources/Common/Extension/Date+.swift b/Projects/Presentation/Sources/Common/Extension/Date+.swift index 84151db5..69671e98 100644 --- a/Projects/Presentation/Sources/Common/Extension/Date+.swift +++ b/Projects/Presentation/Sources/Common/Extension/Date+.swift @@ -22,6 +22,7 @@ extension Date { case dayOfWeek case date case amPmTime + case amPmTimeShort var formatString: String { switch self { @@ -30,6 +31,7 @@ extension Date { case .dayOfWeek: "E" case .date: "d" case .amPmTime: "a HH:mm" + case .amPmTimeShort: "a h:mm" } } } diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/DatePickerView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/DatePickerView.swift new file mode 100644 index 00000000..2651ead6 --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/Component/DatePickerView.swift @@ -0,0 +1,72 @@ +// +// DatePickerViewController.swift +// Presentation +// +// Created by 이동현 on 7/27/25. +// + +import SnapKit +import UIKit + +protocol DatePickerViewDelegate: AnyObject { + func datePickerView(_ pickerView: DatePickerView, didSelectTime time: Date) +} + +final class DatePickerView: UIViewController { + private enum Layout { + static let datePickerHeight: CGFloat = 195 + static let horizontalSpacing: CGFloat = 20 + static let registerButtonHeight: CGFloat = 54 + static let registerButtonVerticalSpacing: CGFloat = 14 + } + + private let datePicker = UIDatePicker() + private let registerButton = UIButton() + weak var delegate: DatePickerViewDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + configureAttribute() + configureLayout() + } + + private func configureAttribute() { + datePicker.preferredDatePickerStyle = .wheels + datePicker.datePickerMode = .time + datePicker.locale = Locale(identifier: "en_US") + datePicker.backgroundColor = .white + datePicker.tintColor = .black + + registerButton.layer.cornerRadius = 12 + registerButton.layer.masksToBounds = true + registerButton.backgroundColor = BitnagilColor.navy500 + registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font + registerButton.setTitle("등록하기", for: .normal) + registerButton.setTitleColor(.white, for: .normal) + registerButton.addAction( + UIAction { [weak self] _ in + guard let self else { return } + self.delegate?.datePickerView(self, didSelectTime: datePicker.date) + dismiss(animated: true) + }, + for: .touchUpInside) + } + + private func configureLayout() { + let safeArea = view.safeAreaLayoutGuide + view.addSubview(datePicker) + view.addSubview(registerButton) + + datePicker.snp.makeConstraints { make in + make.top.horizontalEdges.equalToSuperview() + make.height.equalTo(Layout.datePickerHeight) + } + + registerButton.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + make.top.equalTo(datePicker.snp.bottom).offset(Layout.registerButtonVerticalSpacing) + make.bottom.equalTo(safeArea.snp.bottom).offset(-Layout.registerButtonVerticalSpacing) + make.height.equalTo(Layout.registerButtonHeight) + } + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift index 78582920..66a7df6d 100644 --- a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift +++ b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift @@ -44,6 +44,7 @@ final class RoutineCreationView: BaseViewController { static let registerButtonTopSpacing: CGFloat = 54 static let registerButtonHeight: CGFloat = 54 static let registerButtonBottomSpacing: CGFloat = 14 + static let datePickerBottomSheetHeight: CGFloat = 347 } private let scrollView = UIScrollView() @@ -70,6 +71,7 @@ final class RoutineCreationView: BaseViewController { private let startTimeAsterisk = UIImageView() private let timePickerButton = RoutineTimePickerButton() private let allDayButton = UIButton() + private let allDayLabelButton = UIButton() private let allDayLabel = UILabel() private let weekdaysStackView = UIStackView() @@ -201,6 +203,7 @@ final class RoutineCreationView: BaseViewController { contentView.addSubview(startTimeTitleLabel) contentView.addSubview(startTimeAsterisk) contentView.addSubview(allDayButton) + contentView.addSubview(allDayLabelButton) contentView.addSubview(allDayLabel) contentView.addSubview(timePickerButton) @@ -341,6 +344,10 @@ final class RoutineCreationView: BaseViewController { make.centerY.equalTo(startTimeTitleLabel) } + allDayLabelButton.snp.makeConstraints { make in + make.edges.equalTo(allDayLabel) + } + timePickerButton.snp.makeConstraints { make in make.top.equalTo(startTimeTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing) make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) @@ -436,10 +443,9 @@ final class RoutineCreationView: BaseViewController { viewModel.output.executionTimePublisher .receive(on: DispatchQueue.main) - .sink { [weak self] executionTime in - self?.timePickerButton.configure(title: executionTime.description) - - let allDayButtonImage = executionTime == .allDay + .sink { [weak self] executionType in + self?.timePickerButton.configure(title: executionType.description) + let allDayButtonImage = executionType == "하루종일" ? BitnagilIcon.checkedIcon : BitnagilIcon.uncheckedIcon self?.allDayButton.setImage(allDayButtonImage, for: .normal) @@ -498,9 +504,19 @@ final class RoutineCreationView: BaseViewController { }, for: .touchUpInside) - allDayButton.addAction( + [allDayButton, allDayLabelButton].forEach { + $0.addAction( + UIAction { [weak self] _ in + self?.viewModel.action(input: .configureExecution(type: .allDay)) + }, + for: .touchUpInside) + } + + timePickerButton.addAction( UIAction { [weak self] _ in - self?.viewModel.action(input: .configureExecution(type: .allDay)) + let datePickerView = DatePickerView() + datePickerView.delegate = self + self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.datePickerBottomSheetHeight) }, for: .touchUpInside) } @@ -607,3 +623,9 @@ extension RoutineCreationView: RoutineCreationInputViewDelegate { viewModel.action(input: .configureSubRoutine(name: text, index: index)) } } + +extension RoutineCreationView: DatePickerViewDelegate { + func datePickerView(_ pickerView: DatePickerView, didSelectTime time: Date) { + viewModel.action(input: .configureExecution(type: .time(startAt: time))) + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift b/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift index fb97fc54..b14c377b 100644 --- a/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift +++ b/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift @@ -5,6 +5,7 @@ // Created by 이동현 on 7/20/25. // import Combine +import Foundation final class RoutineCreationViewModel: ViewModel { enum RepeatType { @@ -20,14 +21,14 @@ final class RoutineCreationViewModel: ViewModel { } enum ExecutionType: Comparable { - case time(startAt: String) + case time(startAt: Date) case allDay case none var description: String { switch self { case .time(let time): - return time + return time.convertToString(dateType: .amPmTimeShort) case .allDay: return "하루종일" case .none: @@ -53,7 +54,7 @@ final class RoutineCreationViewModel: ViewModel { let subRoutinesPublisher: AnyPublisher<[String], Never> let repeatTypePublisher: AnyPublisher let weekDayPublisher: AnyPublisher, Never> - let executionTimePublisher: AnyPublisher + let executionTimePublisher: AnyPublisher let isRoutineValid: AnyPublisher } @@ -71,7 +72,9 @@ final class RoutineCreationViewModel: ViewModel { subRoutinesPublisher: subRoutinesSubject.eraseToAnyPublisher(), repeatTypePublisher: repeatTypeSubject.eraseToAnyPublisher(), weekDayPublisher: weekDaySubject.eraseToAnyPublisher(), - executionTimePublisher: executionTimeSubject.eraseToAnyPublisher(), + executionTimePublisher: executionTimeSubject + .map{ $0.description } + .eraseToAnyPublisher(), isRoutineValid: checkRoutinePublisher.eraseToAnyPublisher()) } @@ -90,9 +93,9 @@ final class RoutineCreationViewModel: ViewModel { case .toggleRepeatDay(let weekDay): configureWeekDay(weekDay: weekDay) case .toggleRepeatAllDay: - configurExecutionTime(time: .allDay) + configureExecutionTime(type: .allDay) case .configureExecution(let startTime): - configurExecutionTime(time: startTime) + configureExecutionTime(type: startTime) case .registerRoutine: registerRoutine() } @@ -159,16 +162,16 @@ final class RoutineCreationViewModel: ViewModel { weekDaySubject.send(weekDays) } - private func configurExecutionTime(time: ExecutionType) { + private func configureExecutionTime(type: ExecutionType) { if - time == .allDay, + type == .allDay, executionTimeSubject.value == .allDay { executionTimeSubject.send(.none) return } - executionTimeSubject.send(time) + executionTimeSubject.send(type) } private func updateIsRoutineValid() {