diff --git a/Source/DDS/Component/Modal/DatePicker/DatePickerPresenter.swift b/Source/DDS/Component/Modal/DatePicker/DatePickerPresenter.swift new file mode 100644 index 0000000..4c790a6 --- /dev/null +++ b/Source/DDS/Component/Modal/DatePicker/DatePickerPresenter.swift @@ -0,0 +1,225 @@ +import SwiftUI + +public struct DodamDatePickerPresenter: ModalViewProtocol { + @StateObject private var provider: DatePickerProvider + @State private var size: CGSize = .zero + @State private var monthDate: Date = .now + private let calendar = { + var calendar = Calendar.current + calendar.locale = Locale(identifier: "ko-KR") + return calendar + }() + private let weekdays = ["일", "월", "화", "수", "목", "금", "토"] + let content: () -> C + + init( + provider: DatePickerProvider, + @ViewBuilder content: @escaping () -> C + ) { + self._provider = .init(wrappedValue: provider) + self.content = content + } + + func dismiss() { + provider.isPresent = false + } + + private var range: Int? { + calendar.range(of: .day, in: .month, for: monthDate)?.count + } + + private var weeks: [[Date?]] { + // 해당 월의 첫째 날 + var components = calendar.dateComponents([.year, .month], from: monthDate) + components.day = 1 + let firstDayOfMonth = calendar.date(from: components)! + + // 첫째 날의 요일 (일요일 = 1, 월요일 = 2, ..., 토요일 = 7) + let firstWeekday = calendar.component(.weekday, from: firstDayOfMonth) + + // 날짜 배열 생성 + var days: [Date?] = Array(repeating: nil, count: firstWeekday - 1) + days += Array(1...(range ?? 0)).compactMap { + components.day = $0 + return calendar.date(from: components) + } + days += Array(repeating: nil, count: (7 - days.count % 7) % 7) + + // 주 단위로 배열을 나눔 + return stride(from: 0, to: days.count, by: 7).map { + Array(days[$0..<$0 + 7]) + } + } + + private func isEnabled(_ date: Date, between startDate: Date, and endDate: Date) -> Bool { + let startComponents = calendar.dateComponents([.year, .month, .day], from: startDate) + let endComponents = calendar.dateComponents([.year, .month, .day], from: endDate) + let dateComponents = calendar.dateComponents([.year, .month, .day], from: date) + + if let start = calendar.date(from: startComponents), + let end = calendar.date(from: endComponents), + let target = calendar.date(from: dateComponents) { + return (start...end).contains(target) + } + + return false + } + + public var body: some View { + BaseModal( + isPresent: $provider.isPresent, + content: content + ) { + VStack(spacing: 16) { + header + calendarView + HStack { + Spacer() + DodamTextButton.large(title: "선택", color: DodamColor.Primary.normal) { + provider.action() + dismiss() + } + } + } + .padding(24) + .frame(width: 328) + .clipShape(.extraLarge) + } + .animation(.spring, value: monthDate) + } + + @ViewBuilder + private var header: some View { + VStack(spacing: 4) { + Text(provider.title) + .heading2(.bold) + .foreground(DodamColor.Label.strong) + .frame(maxWidth: .infinity, alignment: .leading) + HStack(spacing: 8) { + Text( + String(calendar.dateComponents([.year], from: monthDate).year ?? 0) + + "년 " + + String(calendar.dateComponents([.month], from: monthDate).month ?? 0) + + "월") + .body1(.medium) + .foreground(DodamColor.Label.strong) + Spacer() + Button { + if let date = calendar.date(byAdding: .month, value: -1, to: monthDate) { + self.monthDate = date + } + } label: { + Image(icon: .chevronLeft) + .resizable() + .foreground(DodamColor.Primary.normal) + .frame(width: 20, height: 20) + .padding(8) + } + Button { + if let date = calendar.date(byAdding: .month, value: 1, to: monthDate) { + self.monthDate = date + } + } label: { + Image(icon: .chevronRight) + .resizable() + .foreground(DodamColor.Primary.normal) + .frame(width: 20, height: 20) + .padding(8) + } + } + } + .animation(.none, value: monthDate) + } + + @ViewBuilder + private var calendarView: some View { + VStack(spacing: 0) { + // header + HStack(spacing: 0) { + ForEach(weekdays, id: \.self) { week in + Text(week) + .label(.regular) + .foreground(DodamColor.Label.alternative) + .frame(maxWidth: .infinity) + } + } + // days + ForEach(weeks, id: \.self) { week in + HStack(spacing: 0) { + ForEach(week, id: \.self) { day in + let enabled = isEnabled( + day ?? .now, + between: provider.startDate ?? .now, + and: provider.endDate ?? .now + ) + let selected = day == provider.date + Button { + provider.date = day ?? .now + } label: { + Text(day == nil ? "" : "\(calendar.component(.day, from: day!))") + .headline(.medium) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + .opacity({ + guard day != nil else { + return 0 + } + return enabled ? 1 : 0.5 + }()) + .foreground( + selected + ? DodamColor.Static.white + : DodamColor.Label.alternative + ) + .background { + if selected { + Rectangle() + .dodamFill(DodamColor.Primary.normal) + .frame(width: 38, height: 38) + .clipShape(.small) + } + } + } + .disabled(!enabled) + } + } + } + } + .animation(.none, value: monthDate) + } +} + +private struct DatePickerPreview: View { + @StateObject private var provider = DatePickerProvider() + @State var hour = 0 + @State var minute = 0 + var body: some View { + DodamDatePickerPresenter(provider: provider) { + VStack { + Button("Show") { + provider.present( + "외출 일시", + startDate: .now, + endDate: { + var d = Date.now + d = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: d)! + return d + }() + ) { + print("Hello") + } + } + } + } + .registerPretendard() + } +} + +#Preview { + DatePickerPreview() +} + +#Preview { + DatePickerPreview() + .preferredColorScheme(.dark) +} diff --git a/Source/DDS/Component/Modal/DatePicker/DatePickerProvider.swift b/Source/DDS/Component/Modal/DatePicker/DatePickerProvider.swift new file mode 100644 index 0000000..8da47f2 --- /dev/null +++ b/Source/DDS/Component/Modal/DatePicker/DatePickerProvider.swift @@ -0,0 +1,32 @@ +// +// File.swift +// +// +// Created by hhhello0507 on 7/27/24. +// + +import Foundation + +public final class DatePickerProvider: ModalProvider { + @Published var isPresent = false + @Published public var date: Date = .now + + @Published var title: String = "" + @Published var startDate: Date? + @Published var endDate: Date? + @Published var action: () -> Void = {} + + public func present( + _ title: String, + startDate: Date?, + endDate: Date?, + action: @escaping () -> Void + ) { + self.date = .now + self.title = title + self.startDate = startDate + self.endDate = endDate + self.action = action + self.isPresent = true + } +} diff --git a/Source/DDS/Component/Modal/Dialog/DialogPresenter.swift b/Source/DDS/Component/Modal/Dialog/DialogPresenter.swift index e74676b..5f86036 100644 --- a/Source/DDS/Component/Modal/Dialog/DialogPresenter.swift +++ b/Source/DDS/Component/Modal/Dialog/DialogPresenter.swift @@ -3,7 +3,6 @@ import Combine public struct DodamDialogPresenter: ModalViewProtocol { - public typealias P = DialogProvider @StateObject private var provider: DialogProvider let content: () -> C diff --git a/Source/DDS/Component/Modal/Dialog/DialogProvider.swift b/Source/DDS/Component/Modal/Dialog/DialogProvider.swift index b31659f..180ebbd 100644 --- a/Source/DDS/Component/Modal/Dialog/DialogProvider.swift +++ b/Source/DDS/Component/Modal/Dialog/DialogProvider.swift @@ -1,8 +1,8 @@ import Foundation public final class DialogProvider: ObservableObject, ModalProvider { + @Published var isPresent = false - @Published public var isPresent = false @Published var title: String = "" @Published var message: String? @Published var secondaryButton: DialogButton? diff --git a/Source/DDS/Component/Modal/DodamModalPresenter.swift b/Source/DDS/Component/Modal/DodamModalPresenter.swift index 200a784..a7e3848 100644 --- a/Source/DDS/Component/Modal/DodamModalPresenter.swift +++ b/Source/DDS/Component/Modal/DodamModalPresenter.swift @@ -10,20 +10,32 @@ import SwiftUI public struct DodamModalProvider: View { private let dialogProvider: DialogProvider + private let datePickerProvider: DatePickerProvider + private let timePickerProvider: TimePickerProvider private let content: () -> C public init( dialogProvider: DialogProvider, + datePickerProvider: DatePickerProvider, + timePickerProvider: TimePickerProvider, @ViewBuilder content: @escaping () -> C ) { self.dialogProvider = dialogProvider + self.datePickerProvider = datePickerProvider + self.timePickerProvider = timePickerProvider self.content = content } public var body: some View { DodamDialogPresenter(provider: dialogProvider) { - content() - .environmentObject(dialogProvider) + DodamDatePickerPresenter(provider: datePickerProvider) { + DodamTimePickerPresenter(provider: timePickerProvider) { + content() + .environmentObject(dialogProvider) + .environmentObject(datePickerProvider) + .environmentObject(timePickerProvider) + } + } } } } diff --git a/Source/DDS/Component/Modal/ModalViewProtocol.swift b/Source/DDS/Component/Modal/ModalViewProtocol.swift index 4feb6f5..a583fdf 100644 --- a/Source/DDS/Component/Modal/ModalViewProtocol.swift +++ b/Source/DDS/Component/Modal/ModalViewProtocol.swift @@ -1,7 +1,6 @@ import SwiftUI protocol ModalViewProtocol: View { - associatedtype P: ModalProvider associatedtype C: View var content: () -> C { get } diff --git a/Source/DDS/Component/Modal/TimePicker/TimePickerPresenter.swift b/Source/DDS/Component/Modal/TimePicker/TimePickerPresenter.swift index 02eb21c..ae0d987 100644 --- a/Source/DDS/Component/Modal/TimePicker/TimePickerPresenter.swift +++ b/Source/DDS/Component/Modal/TimePicker/TimePickerPresenter.swift @@ -2,14 +2,13 @@ import SwiftUI public struct DodamTimePickerPresenter: ModalViewProtocol { - public typealias P = TimePickerProvider + private let hours = Array(0..<24) + private let minutes = Array(0..<60) + @StateObject private var provider: TimePickerProvider @State private var size: CGSize = .zero let content: () -> C - private let hours = Array(0..<24) - private let minutes = Array(0..<60) - init( provider: TimePickerProvider, @ViewBuilder content: @escaping () -> C diff --git a/Source/DDS/Component/Modal/TimePicker/TimePickerProvider.swift b/Source/DDS/Component/Modal/TimePicker/TimePickerProvider.swift index a932fce..252b548 100644 --- a/Source/DDS/Component/Modal/TimePicker/TimePickerProvider.swift +++ b/Source/DDS/Component/Modal/TimePicker/TimePickerProvider.swift @@ -9,7 +9,8 @@ import Foundation import SwiftUI public final class TimePickerProvider: ModalProvider { - @Published public var isPresent: Bool = false + @Published var isPresent: Bool = false + @Published var title: String = "" @Published var hour: Int = 0 @Published var minute: Int = 0