커스텀 달력
내가 직접 만든 달력! 기존의 DatePicker 나 UICalendarView 는 제약되어 있는게 너무 많아서 하나하나 커스텀 하기 위해서 직접 달력을 만들었다.
넣은 기능
- TabView 로 만들어 page 형태로 드래그가 가능하다.
- 우측 상단의 < > 버튼을 통해서도 자연스럽게 이동이 가능하다.
- 오늘 버튼을 누르게 되면 이번달로 이동하면서 오늘이 선택된다.
- 날짜 선택이 가능하고 다시 선택하면 날짜 선택이 해제된다.
- 날짜마다 아래쪽에 내가 원하는 View를 넣을 수 있다.
- Frame 이 일정하다. 어느 달로 가더라도 4주, 5주를 계산하여 높이가 320 으로 일정하다.
코드
CalendarView, Date+Extension 두가지 파일로 구성했다. CalendarView 의 경우 172줄로 좀 긴 감이 있지만... 찢어놓는게 더 이해하기 힘들것 같아서 우선 붙여두었다.
CalendarView
생각보다 TabView 쪽이 까다로워서 좀 애를 먹었는데, 0, 1, 2 페이지가 있고 0 페이지로 간다면 0 페이지의 앞의 달을 생성해주고 2 페이지에 해당하는 달을 months 배열에서 빼준다. 그렇게 되면 이전달로 계속 스크롤이 가능해진다!
import SwiftUI
struct CalendarView: View {
@State private var selectedDate: Date?
@State private var months: [[Month]] = [[],[],[]]
@State private var selection = 1
private let dayOfWeek: [String] = ["일", "월", "화", "수", "목", "금", "토"]
private let columns = Array(repeating: GridItem(.flexible()), count: 7)
private let calendarHeight: CGFloat = 320
var body: some View {
NavigationStack {
ScrollView {
VStack {
CalendarHeader()
CalendarBody()
}
}
.onAppear {
fetchMonths()
}
.navigationTitle("달력")
}
}
@ViewBuilder
private func CalendarHeader() -> some View {
HStack(spacing: 16) {
VStack(alignment: .leading) {
if let date = months[1].first?.date {
Text(date.format("YYYY"))
.font(.footnote)
.fontWeight(.semibold)
Text(date.format("MMMM"))
.font(.title.bold())
}
}
Spacer()
Button("오늘") {
fetchMonths()
selectedDate = Date()
}
.font(.callout)
.buttonStyle(.bordered)
Button {
selectedDate = nil
withAnimation {
selection = 0
}
} label: {
Image(systemName: "chevron.left")
.font(.title2)
}
Button {
selectedDate = nil
withAnimation {
selection = 2
}
} label: {
Image(systemName: "chevron.right")
.font(.title2)
}
}
.padding()
HStack(spacing: 0) {
ForEach(dayOfWeek, id: \.self) { day in
Text(day)
.font(.callout)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
}
}
.padding(.horizontal, -5)
}
@ViewBuilder
private func CalendarBody() -> some View {
TabView(selection: $selection) {
ForEach(months.indices, id: \.self) { index in
let month = months[index]
DateGrid(month)
.tag(index)
.onDisappear {
paginateMonth()
}
.onAppear {
selection = 0
selection = 1
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(height: calendarHeight)
}
@ViewBuilder
private func DateGrid(_ month: [Month]) -> some View {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(month) { value in
DateCell(value: value)
.onTapGesture {
if selectedDate == value.date {
selectedDate = nil
} else {
selectedDate = value.date
}
}
}
}
}
@ViewBuilder
private func DateCell(value: Month) -> some View {
VStack(spacing: 0) {
if value.day != -1 {
VStack {
Text("\(value.day)")
.fontWeight(value.date.isToday ? .bold : .semibold)
.foregroundStyle(value.date.isToday ? .blue : .gray.opacity(0.8))
Circle()
.foregroundStyle(.clear)
}
.background {
if let selectedDate {
Capsule()
.foregroundStyle(.blue.opacity(0.2))
.opacity(value.date.isSameDay(selectedDate) ? 1 : 0)
.frame(width: 36)
.padding(.vertical, value.date.numberOfWeeks == 6 ? 1 : 2)
.offset(y: -2)
}
}
}
}
.frame(height: calendarHeight/value.date.numberOfWeeks, alignment: .top)
}
private func fetchMonths() {
months[0] = Date().createPreviousMonth()
months[1] = Date().createMonth()
months[2] = Date().createNextMonth()
}
private func paginateMonth() {
guard let date = months[selection].first?.date else { return }
if selection == 0 {
selectedDate = nil
months.insert(date.createPreviousMonth(), at: 0)
months.removeLast()
selection = 1
}
if selection == 2 {
selectedDate = nil
months.append(date.createNextMonth())
months.removeFirst()
selection = 1
}
}
}
struct Month: Identifiable {
var id = UUID().uuidString
var day: Int
var date: Date
}
Date+Extension
간단하게 사용하기 위해서 Extension 을 활용하였다. createMonth() 가 여기서의 메인 함수라고 볼 수 있는데, 날짜를 받아서 그 날짜에 해당되는 달의 날을 반환해주는 함수이다.
import SwiftUI
extension Date {
func format(_ format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: self)
}
func isSameDay(_ date: Date) -> Bool {
Calendar.current.isDate(self, inSameDayAs: date)
}
var isToday: Bool {
Calendar.current.isDateInToday(self)
}
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
var startOfMonth: Date {
let calendar = Calendar.current
return calendar.date(from: calendar.dateComponents([.year, .month], from: self)) ?? Date()
}
var numberOfWeeks: CGFloat {
guard let range = Calendar.current.range(of: .weekOfMonth, in: .month, for: self) else { return 0 }
return CGFloat(range.count)
}
func createMonth(_ date: Date = Date()) -> [Month] {
let calendar = Calendar.current
guard let startDate = calendar.date(from: calendar.dateComponents([.year, .month], from: date)) else { return [] }
guard let range = calendar.range(of: .day, in: .month, for: startDate) else { return [] }
var days = range.compactMap { day -> Month in
let date = calendar.date(byAdding: .day, value: day - 1, to: startDate)!
return Month(day: calendar.component(.day, from: date), date: date)
}
let firstWeekday = calendar.component(.weekday, from: days.first?.date ?? Date())
for _ in 0..<(firstWeekday - calendar.firstWeekday) {
days.insert(Month(day: -1, date: date), at: 0)
}
return days
}
func createPreviousMonth() -> [Month] {
let calendar = Calendar.current
let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: self)) ?? Date()
guard let startDate = calendar.date(byAdding: .month, value: -1, to: startOfMonth) else { return [] }
return createMonth(startDate)
}
func createNextMonth() -> [Month] {
let calendar = Calendar.current
let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: self)) ?? Date()
guard let startDate = calendar.date(byAdding: .month, value: +1, to: startOfMonth) else { return [] }
return createMonth(startDate)
}
}
'→ Swift Archive' 카테고리의 다른 글
[Swift] 정규표현식 사용하여 문자열 검사하기 (0) | 2024.04.15 |
---|---|
[SwiftUI] 스크롤 위치 추적하기 (1) | 2024.02.16 |
[SwiftUI] TabView 이상한 현상 발견?! (0) | 2024.02.03 |
[SwiftUI] 앱의 실행, 종료 알기 (1) | 2024.01.27 |
[SwiftUI] 앱이 background 에서 다시 돌아왔을 때 알기 (0) | 2024.01.27 |