I assume you can find out how to setup a SwiftUI project using Xcode. So, I will jump right into how to build calendar views and load data from SQLite.
App Views
We will make a very simple app with only 2 views: MainView for displaying current month and DetailView for displaying current date and week.
View hierarchy
MainView
MainView.swift
import SwiftUI
struct MainView: View {
@Binding var currentDate: CalendarDay
var onSwipeUp: (() -> Void)?
var onToday: () -> Void
var body: some View {
VStack(spacing: 20) {
CalendarView(
currentDate: $currentDate,
onNavigate: changeMonth,
formatTitle: monthYearString,
onSwipeUp: onSwipeUp,
onSwipeLeft: { withAnimation { nextMonth() } },
onSwipeRight: { withAnimation { prevMonth() } }
) {
let days = Calendar.current.monthDays(for: currentDate.solar)
MonthView(days: days, currentDate: $currentDate)
}
Spacer()
MenuView(
mode: .month,
onToday: onToday
)
}
}
private func changeMonth(by value: Int) {
if let newMonth = Calendar.current.date(byAdding: .month, value: value, to: currentDate.solar) {
currentDate = Calendar.current.lunarDay(for: newMonth)
}
}
private func monthYearString(for date: CalendarDay) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
return formatter.string(from: date.solar)
}
private func nextMonth() {
// swipe left → next month
let next = Calendar.current.date(byAdding: .month, value: 1, to: currentDate.solar)!
currentDate = Calendar.current.lunarDay(for: next)
}
private func prevMonth() {
// swipe right → previous month
let prev = Calendar.current.date(byAdding: .month, value: -1, to: currentDate.solar)!
currentDate = Calendar.current.lunarDay(for: prev)
}
}
This view consists of two nested shared views: CalendarView and MenuView. They are organized in a vertical stack VStack
, separated by a Spacer. Spacer is just a way to fulfill empty space between views.
This MainView is responsible for displaying calendar of current month, and navigating to next/previous month. We can swipe left/right or click left/right buttons next to month name to navigate.
The MenuView currently has only one menu: Today for going back to Today while navigating.
DetailView
DetailView.swift
import SwiftUI
struct DetailView: View {
@Binding var currentDate: CalendarDay
var onToday: () -> Void
@Environment(\.dismiss) private var dismiss
var body: some View {
// Header with today’s date
VStack {
Text(currentDate.solarFormatted())
.font(.headline)
.foregroundColor(.gray)
Text("Lunar: \(currentDate.lunarFormatted())")
.font(.headline)
.fontWeight(.semibold)
.padding(.vertical, 5)
CalendarView(
currentDate: $currentDate,
onNavigate: changeWeek,
formatTitle: formatWeek,
onSwipeLeft: nextWeek,
onSwipeRight: prevWeek,
onSwipeDown: { dismiss() }
) {
let days = Calendar.current.weekDays(for: currentDate.solar)
WeekView(days: days, currentDate: $currentDate)
if currentDate.isHoliday {
HolidayView(currentDate: $currentDate)
}
}
Spacer()
// Bottom Navigation
MenuView(
mode: .home,
onToday: onToday
)
}
.padding()
.contentShape(Rectangle())
}
private func changeWeek(_ by: Int) {
if let newDate = Calendar.current.date(byAdding: .weekOfYear, value: by, to: currentDate.solar) {
currentDate = Calendar.current.lunarDay(for: newDate)
}
}
private func formatWeek(_ date: CalendarDay) -> String {
let week = Calendar.current.dateComponents([.weekOfYear, .year], from: date.solar)
let prefix = String(localized: "week")
return "\(prefix) \(week.weekOfYear!) \(week.year!)"
}
private func nextWeek() {
// swipe left → next week
let next = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: currentDate.solar)!
currentDate = Calendar.current.lunarDay(for: next)
}
private func prevWeek() {
// swipe right → previous week
let prev = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: currentDate.solar)!
currentDate = Calendar.current.lunarDay(for: prev)
}
}
This DetailView is actually similar with MainView with some additional components for displaying current date in both solar and lunar(Vietnamese) format, following by CalendarView and MenuView.
However, this time the swipe actions and buttons will allow you to navigate between weeks instead of months.
The HomeView
Actually, the app will first load another view as entry instead of the two views above. I call this view HomeView. It is where we store the app state like current date and which views to show based on the state.
HomeView.swift
import SwiftUI
struct HomeView: View {
@State private var currentDate = Calendar.current.lunarDay(for: Date())
@State private var showDetailView = false
@State private var showSettings = false
var body: some View {
NavigationStack {
MainView(
currentDate: $currentDate,
onSwipeUp: showDayView,
onToday: onToday
)
}
.contentShape(Rectangle())
.sheet(isPresented: $showDetailView) {
DetailView(
currentDate: $currentDate,
onToday: onToday
)
}
}
private func showDayView() {
showDetailView = true
}
private func onToday() {
currentDate = Calendar.current.lunarDay()
}
}
The CalendarView
So far, we don't see yet where and how the swipe events are being handled. Yes, you're right. That's the shared CalendarView.
CalendarView.swift
import SwiftUI
struct CalendarView<Content: View>: View {
@Binding var currentDate: CalendarDay
var onNavigate: (_ by: Int) -> Void
var formatTitle: (_ date: CalendarDay) -> String
var onSwipeUp: (() -> Void)?
var onSwipeLeft: (() -> Void)?
var onSwipeRight: (() -> Void)?
var onSwipeDown: (() -> Void)?
@ViewBuilder var content: () -> Content
var body: some View {
VStack {
HStack {
Button(action: { onNavigate(-1) }) {
Image(systemName: "chevron.left")
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
Spacer()
Text(formatTitle(currentDate))
.font(.title2)
.fontWeight(.bold)
Spacer()
Button(action: { onNavigate(1) }) {
Image(systemName: "chevron.right")
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
}
.padding()
Divider()
content()
Spacer()
}
.contentShape(Rectangle()) // make whole area hittable
.gesture(
DragGesture()
.onEnded { value in
if value.translation.height < -50 { // swipe up
onSwipeUp?()
} else if value.translation.width < -50 { // swipe left
onSwipeLeft?()
} else if value.translation.width > 50 { // swipe right
onSwipeRight?()
} else if value.translation.height > 50 { // swipe down
onSwipeDown?()
}
}
)
}
}
This view is responsible for capturing swift events and calling the event handlers which are passed from parent view of this CalendarView. This view is being reused across MainView and DetailView. Each views is providing different handlers for the swipe events. That's swiping on MainView will allow you to navigate between although on DetailView you will be navigated between weeks.
You might also see the onNavigate handler. Yes, this is the handler for left/right buttons next to the title of the CalendarView. This title is also flexible, depends on what the parent view provided.
About the onToday
handler, it is used for handling the Today button's click event. And you might guess it is the MenuView's responsibility to catch the event and map it with the corresponding handler.
Conclusion
You can use VStack and HStack for organizing views. You can capture events using gestures or actions(events). You can pass event handlers via View constructor. You can store state at the root/entry view and bind state using @Binding annotation. Which binding you can modify the state at anywhere.
Next, I will continue with part 2 - loading lunar data from SQLite.
You can see the source code of this app here.
Top comments (0)