Add MoonWatch app, widget extension, shared module, and .gitignore
Adds the real MoonWatch implementation on top of the initial Xcode template: new SwiftUI views (MoonDiskView, StarFieldView), the MoonWatchWidget extension, and a Shared module containing the moon phase calculator, shadow shape, and shared asset catalog. Also adds a Swift/Xcode/macOS .gitignore and untracks per-user xcuserdata files. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
29
MoonWatchWidget/Info.plist
Normal file
29
MoonWatchWidget/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>MoonWatch Widget</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
203
MoonWatchWidget/MoonWatchWidget.swift
Normal file
203
MoonWatchWidget/MoonWatchWidget.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// MoonWatchWidget.swift
|
||||
// MoonWatchWidget
|
||||
//
|
||||
// Home Screen + Lock Screen widget showing the current moon phase.
|
||||
// Pulls phase math + the photographic moon disk from the Shared module.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct MoonPhaseEntry: TimelineEntry, Sendable {
|
||||
let date: Date
|
||||
let snapshot: MoonPhaseSnapshot
|
||||
}
|
||||
|
||||
struct MoonPhaseProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> MoonPhaseEntry {
|
||||
let now = Date()
|
||||
return MoonPhaseEntry(date: now, snapshot: MoonPhaseCalculator.snapshot(for: now))
|
||||
}
|
||||
|
||||
func getSnapshot(
|
||||
in context: Context,
|
||||
completion: @escaping @Sendable (MoonPhaseEntry) -> Void
|
||||
) {
|
||||
let now = Date()
|
||||
completion(MoonPhaseEntry(date: now, snapshot: MoonPhaseCalculator.snapshot(for: now)))
|
||||
}
|
||||
|
||||
func getTimeline(
|
||||
in context: Context,
|
||||
completion: @escaping @Sendable (Timeline<MoonPhaseEntry>) -> Void
|
||||
) {
|
||||
var entries: [MoonPhaseEntry] = []
|
||||
let now = Date()
|
||||
let cal = Calendar.current
|
||||
// 48 hourly entries covers two full days; .atEnd triggers a fresh
|
||||
// reload once the last entry is consumed, so the widget is always current.
|
||||
for hourOffset in 0..<48 {
|
||||
let date = cal.date(byAdding: .hour, value: hourOffset, to: now) ?? now
|
||||
let snap = MoonPhaseCalculator.snapshot(for: date)
|
||||
entries.append(MoonPhaseEntry(date: date, snapshot: snap))
|
||||
}
|
||||
completion(Timeline(entries: entries, policy: .atEnd))
|
||||
}
|
||||
}
|
||||
|
||||
struct MoonWatchWidgetEntryView: View {
|
||||
var entry: MoonPhaseEntry
|
||||
@Environment(\.widgetFamily) private var family
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
smallView
|
||||
case .systemMedium:
|
||||
mediumView
|
||||
case .systemLarge:
|
||||
largeView
|
||||
case .accessoryCircular:
|
||||
circularAccessory
|
||||
case .accessoryRectangular:
|
||||
rectangularAccessory
|
||||
case .accessoryInline:
|
||||
Text("\(Image(systemName: "moon.fill")) \(entry.snapshot.namedPhase.rawValue)")
|
||||
default:
|
||||
smallView
|
||||
}
|
||||
}
|
||||
|
||||
private var smallView: some View {
|
||||
VStack(spacing: 6) {
|
||||
MoonImageDisk(normalizedPhase: entry.snapshot.normalizedPhase)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
Text(entry.snapshot.namedPhase.rawValue)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
Text("\(Int((entry.snapshot.illuminatedFraction * 100).rounded()))% lit")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
private var mediumView: some View {
|
||||
HStack(spacing: 14) {
|
||||
MoonImageDisk(normalizedPhase: entry.snapshot.normalizedPhase)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.frame(maxWidth: 130)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.snapshot.namedPhase.rawValue)
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
Text("\(Int((entry.snapshot.illuminatedFraction * 100).rounded()))% illuminated")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white.opacity(0.75))
|
||||
Text("Age: \(String(format: "%.1f", entry.snapshot.moonAgeDays)) days")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private var largeView: some View {
|
||||
VStack(spacing: 12) {
|
||||
MoonImageDisk(normalizedPhase: entry.snapshot.normalizedPhase)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
VStack(spacing: 4) {
|
||||
Text(entry.snapshot.namedPhase.rawValue)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
Text(
|
||||
"\(Int((entry.snapshot.illuminatedFraction * 100).rounded()))% lit · age \(String(format: "%.1f", entry.snapshot.moonAgeDays)) days"
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var circularAccessory: some View {
|
||||
ZStack {
|
||||
AccessoryWidgetBackground()
|
||||
MoonImageDisk(normalizedPhase: entry.snapshot.normalizedPhase)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
|
||||
private var rectangularAccessory: some View {
|
||||
HStack(spacing: 8) {
|
||||
MoonImageDisk(normalizedPhase: entry.snapshot.normalizedPhase)
|
||||
.frame(width: 32, height: 32)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(entry.snapshot.namedPhase.rawValue)
|
||||
.font(.caption.weight(.semibold))
|
||||
Text(
|
||||
"\(Int((entry.snapshot.illuminatedFraction * 100).rounded()))% · \(String(format: "%.1f", entry.snapshot.moonAgeDays))d"
|
||||
)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MoonWatchWidget: Widget {
|
||||
let kind: String = "MoonWatchWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: MoonPhaseProvider()) { entry in
|
||||
MoonWatchWidgetEntryView(entry: entry)
|
||||
.containerBackground(for: .widget) {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.04, green: 0.04, blue: 0.14),
|
||||
Color(red: 0.06, green: 0.04, blue: 0.18),
|
||||
Color(red: 0.01, green: 0.02, blue: 0.06),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("Moon Phase")
|
||||
.description("See the current phase of the moon at a glance.")
|
||||
.supportedFamilies([
|
||||
.systemSmall,
|
||||
.systemMedium,
|
||||
.systemLarge,
|
||||
.accessoryCircular,
|
||||
.accessoryRectangular,
|
||||
.accessoryInline,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct MoonWatchWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
MoonWatchWidget()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
MoonWatchWidget()
|
||||
} timeline: {
|
||||
MoonPhaseEntry(date: .now, snapshot: MoonPhaseCalculator.snapshot(for: .now))
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
MoonWatchWidget()
|
||||
} timeline: {
|
||||
MoonPhaseEntry(date: .now, snapshot: MoonPhaseCalculator.snapshot(for: .now))
|
||||
}
|
||||
|
||||
#Preview(as: .accessoryRectangular) {
|
||||
MoonWatchWidget()
|
||||
} timeline: {
|
||||
MoonPhaseEntry(date: .now, snapshot: MoonPhaseCalculator.snapshot(for: .now))
|
||||
}
|
||||
Reference in New Issue
Block a user