Skip to content

Commit 20aebe5

Browse files
authored
[Feat] 날짜 선택가능한 DatePickerView 구현
* feat: DatePickerView 구현 * fix: 하루 종일 설정 버튼 추가 - 하루 종일 UILabel을 눌러도 하루 종일 버튼이 눌리도록 수정 * refactor: 코드 리뷰 반영
1 parent 86392db commit 20aebe5

4 files changed

Lines changed: 114 additions & 15 deletions

File tree

Projects/Presentation/Sources/Common/Extension/Date+.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extension Date {
2222
case dayOfWeek
2323
case date
2424
case amPmTime
25+
case amPmTimeShort
2526

2627
var formatString: String {
2728
switch self {
@@ -30,6 +31,7 @@ extension Date {
3031
case .dayOfWeek: "E"
3132
case .date: "d"
3233
case .amPmTime: "a HH:mm"
34+
case .amPmTimeShort: "a h:mm"
3335
}
3436
}
3537
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// DatePickerViewController.swift
3+
// Presentation
4+
//
5+
// Created by 이동현 on 7/27/25.
6+
//
7+
8+
import SnapKit
9+
import UIKit
10+
11+
protocol DatePickerViewDelegate: AnyObject {
12+
func datePickerView(_ pickerView: DatePickerView, didSelectTime time: Date)
13+
}
14+
15+
final class DatePickerView: UIViewController {
16+
private enum Layout {
17+
static let datePickerHeight: CGFloat = 195
18+
static let horizontalSpacing: CGFloat = 20
19+
static let registerButtonHeight: CGFloat = 54
20+
static let registerButtonVerticalSpacing: CGFloat = 14
21+
}
22+
23+
private let datePicker = UIDatePicker()
24+
private let registerButton = UIButton()
25+
weak var delegate: DatePickerViewDelegate?
26+
27+
override func viewDidLoad() {
28+
super.viewDidLoad()
29+
configureAttribute()
30+
configureLayout()
31+
}
32+
33+
private func configureAttribute() {
34+
datePicker.preferredDatePickerStyle = .wheels
35+
datePicker.datePickerMode = .time
36+
datePicker.locale = Locale(identifier: "en_US")
37+
datePicker.backgroundColor = .white
38+
datePicker.tintColor = .black
39+
40+
registerButton.layer.cornerRadius = 12
41+
registerButton.layer.masksToBounds = true
42+
registerButton.backgroundColor = BitnagilColor.navy500
43+
registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font
44+
registerButton.setTitle("등록하기", for: .normal)
45+
registerButton.setTitleColor(.white, for: .normal)
46+
registerButton.addAction(
47+
UIAction { [weak self] _ in
48+
guard let self else { return }
49+
self.delegate?.datePickerView(self, didSelectTime: datePicker.date)
50+
dismiss(animated: true)
51+
},
52+
for: .touchUpInside)
53+
}
54+
55+
private func configureLayout() {
56+
let safeArea = view.safeAreaLayoutGuide
57+
view.addSubview(datePicker)
58+
view.addSubview(registerButton)
59+
60+
datePicker.snp.makeConstraints { make in
61+
make.top.horizontalEdges.equalToSuperview()
62+
make.height.equalTo(Layout.datePickerHeight)
63+
}
64+
65+
registerButton.snp.makeConstraints { make in
66+
make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing)
67+
make.top.equalTo(datePicker.snp.bottom).offset(Layout.registerButtonVerticalSpacing)
68+
make.bottom.equalTo(safeArea.snp.bottom).offset(-Layout.registerButtonVerticalSpacing)
69+
make.height.equalTo(Layout.registerButtonHeight)
70+
}
71+
}
72+
}

Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
4444
static let registerButtonTopSpacing: CGFloat = 54
4545
static let registerButtonHeight: CGFloat = 54
4646
static let registerButtonBottomSpacing: CGFloat = 14
47+
static let datePickerBottomSheetHeight: CGFloat = 347
4748
}
4849

4950
private let scrollView = UIScrollView()
@@ -70,6 +71,7 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
7071
private let startTimeAsterisk = UIImageView()
7172
private let timePickerButton = RoutineTimePickerButton()
7273
private let allDayButton = UIButton()
74+
private let allDayLabelButton = UIButton()
7375
private let allDayLabel = UILabel()
7476
private let weekdaysStackView = UIStackView()
7577

@@ -201,6 +203,7 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
201203
contentView.addSubview(startTimeTitleLabel)
202204
contentView.addSubview(startTimeAsterisk)
203205
contentView.addSubview(allDayButton)
206+
contentView.addSubview(allDayLabelButton)
204207
contentView.addSubview(allDayLabel)
205208
contentView.addSubview(timePickerButton)
206209

@@ -341,6 +344,10 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
341344
make.centerY.equalTo(startTimeTitleLabel)
342345
}
343346

347+
allDayLabelButton.snp.makeConstraints { make in
348+
make.edges.equalTo(allDayLabel)
349+
}
350+
344351
timePickerButton.snp.makeConstraints { make in
345352
make.top.equalTo(startTimeTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing)
346353
make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset)
@@ -436,10 +443,9 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
436443

437444
viewModel.output.executionTimePublisher
438445
.receive(on: DispatchQueue.main)
439-
.sink { [weak self] executionTime in
440-
self?.timePickerButton.configure(title: executionTime.description)
441-
442-
let allDayButtonImage = executionTime == .allDay
446+
.sink { [weak self] executionType in
447+
self?.timePickerButton.configure(title: executionType.description)
448+
let allDayButtonImage = executionType == "하루종일"
443449
? BitnagilIcon.checkedIcon
444450
: BitnagilIcon.uncheckedIcon
445451
self?.allDayButton.setImage(allDayButtonImage, for: .normal)
@@ -498,9 +504,19 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
498504
},
499505
for: .touchUpInside)
500506

501-
allDayButton.addAction(
507+
[allDayButton, allDayLabelButton].forEach {
508+
$0.addAction(
509+
UIAction { [weak self] _ in
510+
self?.viewModel.action(input: .configureExecution(type: .allDay))
511+
},
512+
for: .touchUpInside)
513+
}
514+
515+
timePickerButton.addAction(
502516
UIAction { [weak self] _ in
503-
self?.viewModel.action(input: .configureExecution(type: .allDay))
517+
let datePickerView = DatePickerView()
518+
datePickerView.delegate = self
519+
self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.datePickerBottomSheetHeight)
504520
},
505521
for: .touchUpInside)
506522
}
@@ -607,3 +623,9 @@ extension RoutineCreationView: RoutineCreationInputViewDelegate {
607623
viewModel.action(input: .configureSubRoutine(name: text, index: index))
608624
}
609625
}
626+
627+
extension RoutineCreationView: DatePickerViewDelegate {
628+
func datePickerView(_ pickerView: DatePickerView, didSelectTime time: Date) {
629+
viewModel.action(input: .configureExecution(type: .time(startAt: time)))
630+
}
631+
}

Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by 이동현 on 7/20/25.
66
//
77
import Combine
8+
import Foundation
89

910
final class RoutineCreationViewModel: ViewModel {
1011
enum RepeatType {
@@ -20,14 +21,14 @@ final class RoutineCreationViewModel: ViewModel {
2021
}
2122

2223
enum ExecutionType: Comparable {
23-
case time(startAt: String)
24+
case time(startAt: Date)
2425
case allDay
2526
case none
2627

2728
var description: String {
2829
switch self {
2930
case .time(let time):
30-
return time
31+
return time.convertToString(dateType: .amPmTimeShort)
3132
case .allDay:
3233
return "하루종일"
3334
case .none:
@@ -53,7 +54,7 @@ final class RoutineCreationViewModel: ViewModel {
5354
let subRoutinesPublisher: AnyPublisher<[String], Never>
5455
let repeatTypePublisher: AnyPublisher<RepeatType?, Never>
5556
let weekDayPublisher: AnyPublisher<Set<Week>, Never>
56-
let executionTimePublisher: AnyPublisher<ExecutionType, Never>
57+
let executionTimePublisher: AnyPublisher<String, Never>
5758
let isRoutineValid: AnyPublisher<Bool, Never>
5859
}
5960

@@ -71,7 +72,9 @@ final class RoutineCreationViewModel: ViewModel {
7172
subRoutinesPublisher: subRoutinesSubject.eraseToAnyPublisher(),
7273
repeatTypePublisher: repeatTypeSubject.eraseToAnyPublisher(),
7374
weekDayPublisher: weekDaySubject.eraseToAnyPublisher(),
74-
executionTimePublisher: executionTimeSubject.eraseToAnyPublisher(),
75+
executionTimePublisher: executionTimeSubject
76+
.map{ $0.description }
77+
.eraseToAnyPublisher(),
7578
isRoutineValid: checkRoutinePublisher.eraseToAnyPublisher())
7679
}
7780

@@ -90,9 +93,9 @@ final class RoutineCreationViewModel: ViewModel {
9093
case .toggleRepeatDay(let weekDay):
9194
configureWeekDay(weekDay: weekDay)
9295
case .toggleRepeatAllDay:
93-
configurExecutionTime(time: .allDay)
96+
configureExecutionTime(type: .allDay)
9497
case .configureExecution(let startTime):
95-
configurExecutionTime(time: startTime)
98+
configureExecutionTime(type: startTime)
9699
case .registerRoutine:
97100
registerRoutine()
98101
}
@@ -159,16 +162,16 @@ final class RoutineCreationViewModel: ViewModel {
159162
weekDaySubject.send(weekDays)
160163
}
161164

162-
private func configurExecutionTime(time: ExecutionType) {
165+
private func configureExecutionTime(type: ExecutionType) {
163166
if
164-
time == .allDay,
167+
type == .allDay,
165168
executionTimeSubject.value == .allDay
166169
{
167170
executionTimeSubject.send(.none)
168171
return
169172
}
170173

171-
executionTimeSubject.send(time)
174+
executionTimeSubject.send(type)
172175
}
173176

174177
private func updateIsRoutineValid() {

0 commit comments

Comments
 (0)