DEV Community

Chanh Le
Chanh Le

Posted on • Edited on

Make a Calendar app with SwiftUI - part 2

In part 1, I did show you how to build a simple Calendar App with SwiftUI. This part 2 will continue to show how to load data from SQLite so we can see the lunar date of each solar date.

Add SQLite package to Xcode project

I am using this package for working with SQLite on Swift. You just need to add it to Xcode like this.

After adding SQLite, we add this file for connecting SQLite lunar_calendar.db.
LunarDatabase.swift

import Foundation
import SQLite

struct LunarDate: Hashable {
    let solarDate: String
    let year: Int
    let month: Int
    let day: Int
    let isLeapMonth: Bool
}

class LunarDatabase {
    private let db: Connection


    init?() {
        guard let dbPath = Bundle.main.path(forResource: "lunar_calendar", ofType: "db") else {
            print("❌ Database file not found in bundle")
            return nil
        }

        do {
            db = try Connection(dbPath, readonly: true)
        } catch {
            print("❌ Failed to open DB: \(error)")
            return nil
        }
    }

    func query(for date: Date) -> LunarDate? {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        let dateStr = formatter.string(from: date)

        do {
            // Raw SQL with parameter binding
            let sql = "SELECT solar_date, lunar_year, lunar_month, lunar_day, is_leap_month FROM lunar_dates WHERE solar_date = ? LIMIT 1"
            let stmt = try db.prepare(sql)

            // Run the statement with dateStr as parameter
            for row in try stmt.run(dateStr) {
                return LunarDate(
                    solarDate: row[0] as! String,
                    year: Int(row[1] as! Int64),   // SQLite returns Int64
                    month: Int(row[2] as! Int64),
                    day: Int(row[3] as! Int64),
                    isLeapMonth: (row[4] as! Int64) == 1
                )
            }
        } catch {
            print("Query error: \(error)")
        }
        return nil
    }

Enter fullscreen mode Exit fullscreen mode

We have a table called lunar_dates with schema like this:

lunar_dates schema

We now can extend the Date by adding toLunar method to load lunar date from SQLite.
Date+Extension.swift


let lunarDB = LunarDatabase()

extension Date {
    func toLunar() -> LunarDate? {
        return lunarDB?.query(for: self)
    }
}

Enter fullscreen mode Exit fullscreen mode

Now, let's extend Calendar by adding methods to collect week days, month days like below:
Calendar+Extension.swift


struct CalendarDay: Identifiable, Hashable {
    var id: Date { solar }   // unique per day
    let solar: Date
    let lunar: LunarDate
    let gregorianDay: Int
    let lunarDay: String
    let isToday: Bool
    let isHoliday: Bool
    let isSpecialDay: Bool
}

extension Calendar {

    func lunarDay(for solar: Date = Date()) -> CalendarDay {
        let lunar = solar.toLunar()!
        let lunarDay = lunar.day
        var lunarDayStr = "\(lunarDay)"
        if lunarDay == 1 {
            let lunarMonth = lunar.month
            lunarDayStr = "\(lunarDay)/\(lunarMonth)"
        }

        return CalendarDay(
            solar: solar,
            lunar: lunar,
            gregorianDay: component(.day, from: solar),
            lunarDay: lunarDayStr,
            isToday: isDateInToday(solar),
            isHoliday: holiday(solar, lunar) != nil,
            isSpecialDay: specialDay(solar, lunar) != nil
        )
    }

    func weekDays(for date: Date) -> [CalendarDay] {
        let startOfWeek = self.startOfWeek(for: date)
        return (0..<7).map { offset in
            let d = self.date(byAdding: .day, value: offset, to: startOfWeek)!
            return self.lunarDay(for: d)
        }
    }

    func monthDays(for date: Date) -> [CalendarDay] {
        guard let monthInterval = self.dateInterval(of: .month, for: date),
              let monthFirstWeek = self.dateInterval(of: .weekOfMonth, for: monthInterval.start),
              let monthLastWeek = self.dateInterval(of: .weekOfMonth, for: monthInterval.end.addingTimeInterval(-1))
        else { return [] }

        // Correct end: subtract 1 day because `end` is exclusive
        let lastVisibleDay = self.date(byAdding: .day, value: -1, to: monthLastWeek.end)!
        let fullRange = monthFirstWeek.start...lastVisibleDay

        var days: [CalendarDay] = []
        var current = fullRange.lowerBound

        while current <= fullRange.upperBound {
            days.append(self.lunarDay(for: current))
            current = self.date(byAdding: .day, value: 1, to: current)!
        }

        return days
    }

    func weekNumber(for date: Date) -> Int {
        return self.component(.weekOfYear, from: date)
    }

    func startOfWeek(for date: Date) -> Date {
        let comps = dateComponents([.yearForWeekOfYear, .weekOfYear], from: date)
        return self.date(from: comps)!
    }

}

Enter fullscreen mode Exit fullscreen mode

These added methods will be used for producing input for MonthView and WeekView of the app.

You can check full source code here.

Next time, I will show you how to generate the lunar_calendar.db using lunardate Python package.

Top comments (0)