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:
2026-05-07 21:05:16 -07:00
parent 1698e3ce15
commit 87f8c7173c
17 changed files with 1211 additions and 24 deletions

View File

@@ -8,14 +8,195 @@
import SwiftUI
struct ContentView: View {
@AppStorage("moonSouthernHemisphere") private var southernHemisphere = false
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
TimelineView(.periodic(from: .now, by: 60)) { context in
let snapshot = MoonPhaseCalculator.snapshot(for: context.date)
ZStack {
Color.black
.ignoresSafeArea()
NightSkyBackground()
ScrollView {
VStack(spacing: 22) {
FloatingMoon(normalizedPhase: snapshot.normalizedPhase)
.frame(maxWidth: 320, maxHeight: 320)
.padding(.top, 32)
.padding(.bottom, 4)
VStack(spacing: 5) {
Text(snapshot.namedPhase.rawValue)
.font(.title.weight(.semibold))
.foregroundStyle(.white)
.multilineTextAlignment(.center)
Text(detailLine(snapshot))
.font(.subheadline)
.foregroundStyle(.white.opacity(0.75))
.multilineTextAlignment(.center)
Text(nextPhaseLine(snapshot))
.font(.caption)
.foregroundStyle(.white.opacity(0.55))
.multilineTextAlignment(.center)
Text(dateLine(snapshot.date))
.font(.caption2)
.foregroundStyle(.white.opacity(0.40))
.multilineTextAlignment(.center)
}
.padding(.horizontal, 28)
.padding(.vertical, 18)
.glassEffect(
.regular,
in: RoundedRectangle(cornerRadius: 28, style: .continuous)
)
.padding(.horizontal, 24)
HemisphereSettingsCard(
southernHemisphere: $southernHemisphere
)
.padding(.horizontal, 24)
}
.frame(maxWidth: .infinity)
.padding(.bottom, 40)
}
.scrollIndicators(.hidden)
.scrollContentBackground(.hidden)
.background(Color.clear)
}
}
.padding()
.environment(\.moonSouthernHemisphere, southernHemisphere)
.preferredColorScheme(.dark)
}
private func detailLine(_ s: MoonPhaseSnapshot) -> String {
let pct = Int((s.illuminatedFraction * 100).rounded())
let fmt = NumberFormatter()
fmt.minimumFractionDigits = 1
fmt.maximumFractionDigits = 1
let age = fmt.string(from: NSNumber(value: s.moonAgeDays))
?? String(format: "%.1f", s.moonAgeDays)
return "\(pct)% illuminated · \(age) days old"
}
private func nextPhaseLine(_ s: MoonPhaseSnapshot) -> String {
let days = s.daysToNextPhase
if days < 1 {
let hours = Int((days * 24).rounded())
return "Next: \(s.nextPhaseName.rawValue) in ~\(hours)h"
} else {
let d = String(format: "%.1f", days)
return "Next: \(s.nextPhaseName.rawValue) in ~\(d) days"
}
}
private func dateLine(_ date: Date) -> String {
let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .short
return "Updated \(df.string(from: date))"
}
}
private struct FloatingMoon: View {
var normalizedPhase: Double
var body: some View {
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { ctx in
let t = ctx.date.timeIntervalSinceReferenceDate
let dy = CGFloat(sin(t * 0.55) * 7)
let dx = CGFloat(sin(t * 0.37 + 1.2) * 4)
MoonDiskView(normalizedPhase: normalizedPhase)
.offset(x: dx, y: dy)
}
}
}
private struct HemisphereSettingsCard: View {
@Binding var southernHemisphere: Bool
var body: some View {
Toggle(isOn: $southernHemisphere) {
Label {
VStack(alignment: .leading, spacing: 2) {
Text("Southern hemisphere")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white)
Text("Mirror waxing/waning orientation")
.font(.caption)
.foregroundStyle(.white.opacity(0.55))
}
} icon: {
Image(systemName: "globe.americas.fill")
.foregroundStyle(.white.opacity(0.85))
}
}
.tint(Color(red: 0.55, green: 0.62, blue: 1.0))
.padding(.horizontal, 20)
.padding(.vertical, 16)
.glassEffect(
.regular,
in: RoundedRectangle(cornerRadius: 24, style: .continuous)
)
}
}
private struct NightSkyBackground: View {
var body: some View {
ZStack {
LinearGradient(
colors: [
Color(red: 0.02, green: 0.02, blue: 0.06),
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.01, blue: 0.04),
],
startPoint: .top,
endPoint: .bottom
)
RadialGradient(
colors: [
Color(red: 0.40, green: 0.18, blue: 0.55).opacity(0.45),
Color(red: 0.30, green: 0.12, blue: 0.45).opacity(0.18),
Color.clear,
],
center: UnitPoint(x: 0.85, y: 0.15),
startRadius: 0,
endRadius: 520
)
.blendMode(.screen)
.blur(radius: 40)
RadialGradient(
colors: [
Color(red: 0.10, green: 0.35, blue: 0.70).opacity(0.40),
Color(red: 0.06, green: 0.20, blue: 0.50).opacity(0.16),
Color.clear,
],
center: UnitPoint(x: 0.05, y: 0.90),
startRadius: 0,
endRadius: 540
)
.blendMode(.screen)
.blur(radius: 40)
RadialGradient(
colors: [
Color(red: 0.78, green: 0.42, blue: 0.85).opacity(0.20),
Color.clear,
],
center: UnitPoint(x: 0.6, y: 0.55),
startRadius: 0,
endRadius: 380
)
.blendMode(.screen)
.blur(radius: 60)
StarFieldView(starCount: 200)
}
.ignoresSafeArea()
}
}