Skip to content

Instantly share code, notes, and snippets.

@theoknock
Last active October 30, 2025 13:04
Show Gist options
  • Select an option

  • Save theoknock/af2b060a70a7bf1bb70d7651bdd2be37 to your computer and use it in GitHub Desktop.

Select an option

Save theoknock/af2b060a70a7bf1bb70d7651bdd2be37 to your computer and use it in GitHub Desktop.
A Swift/SwiftUI app that calculates planetary hour data.
//
// TempSwiftUIView.swift
// PlanetaryHours2025-Claude
//
// Created by Xcode Developer on 10/30/25.
//
import SwiftUI
import WeatherKit
import CoreLocation
import Observation
import Combine
// MARK: - Main View
struct ContentView: View {
@State private var viewModel = PlanetaryHoursViewModel()
@State private var locationManager = LocationManager()
@State private var location: CLLocation = CLLocation()
var body: some View {
NavigationView {
VStack(spacing: 0) {
Text("Planetary Hour Calculator")
.font(.title2)
.foregroundStyle(Color(.label))
HStack {
// Spacer()
Text("JAMES ALAN BUSH")
// .multilineTextAlignment(TextAlignment.leading)
// .font(.caption2)
// .foregroundStyle(Color(.tertiaryLabel))
// .scaledToFill()
// Spacer()
Text("VERSION 0.1")
// .multilineTextAlignment(TextAlignment.center)
// .font(.caption2)
// .foregroundStyle(Color(.label))
// .scaledToFill()
// Spacer()
Text("BUILD 1")
// .multilineTextAlignment(TextAlignment.trailing)
// .font(.caption2)
// .foregroundStyle(Color(.label))
// .scaledToFill()
// Spacer()
}
.padding(.vertical)
.foregroundStyle(.tertiary)
.font(.caption2)
.frame(maxWidth: .infinity)
// Input Section
Form {
Section(header: Text("Location")) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Current Location")
.font(.subheadline)
.foregroundColor(.secondary)
if let location = locationManager.location {
Text(viewModel.locationName)
.font(.caption)
.foregroundColor(.blue)
Text("Lat: \(location.coordinate.latitude, specifier: "%.4f"), Lon: \(location.coordinate.longitude, specifier: "%.4f")")
.font(.caption2)
.foregroundColor(.gray)
} else {
Text("Location not available")
.font(.caption)
.foregroundColor(.gray)
}
}
Spacer()
Button(action: {
locationManager.requestLocation()
}) {
Image(systemName: "location.fill")
.foregroundColor(.blue)
}
.buttonStyle(.borderless)
}
if let error = locationManager.locationError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
if locationManager.authorizationStatus == .denied ||
locationManager.authorizationStatus == .restricted {
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
.font(.caption)
}
}
Section(header: Text("Date")) {
HStack(alignment: VerticalAlignment.center, spacing: 10.0, content: {
DatePicker(
"",
selection: $viewModel.selectedDate,
in: dateRange, // <-- This restricts the selectable dates
displayedComponents: .date
)
Button {
guard let location = locationManager.location else {
viewModel.errorMessage = "Please enable location services first"
return
}
Task {
await viewModel.calculatePlanetaryHours(for: location)
}
} label: {
Text(viewModel.isLoading ? "Calculating..." : "Recalculate")
.background(Color.blue.opacity(0.1)).cornerRadius(15)
}
})
}
// Section {
// Button(action: {
// guard let location = locationManager.location else {
// viewModel.errorMessage = "Please enable location services first"
// return
// }
// Task {
// await viewModel.calculatePlanetaryHours(for: location)
// }
// }) {
// // HStack {
// // VStack {
// // Spacer()
// if viewModel.isLoading {
// ProgressView()
// // .padding()
// }
// Text(viewModel.isLoading ? "Calculating..." : "Recalculate")
// .background(Color.blue.opacity(0.1)).cornerRadius(15)
// // Spacer()
// // }
// // }
//// }
//
//
//
// // .border(.blue)
// .disabled(viewModel.isLoading || locationManager.location == nil)
// }
// // .font(.caption)
//// .padding(.horizontal)
//// .multilineTextAlignment(TextAlignment.center)
//
// }
}
// DatePicker("Select Date", selection: $viewModel.selectedDate, displayedComponents: .date)
}
.background(Color.gray.opacity(0.1))
// Section {
// Button(action: {
// guard let location = locationManager.location else {
// viewModel.errorMessage = "Please enable location services first"
// return
// }
// Task {
// await viewModel.calculatePlanetaryHours(for: location)
// }
// }) {
// HStack {
// Spacer()
// if viewModel.isLoading {
// ProgressView()
// .padding(.trailing, 8)
// }
// Text(viewModel.isLoading ? "Calculating..." : "Calculate Planetary Hours")
// Spacer()
// }
// }
// .disabled(viewModel.isLoading || locationManager.location == nil)
// }
}
.task {
locationManager.requestLocation()
if let location = locationManager.location {
await viewModel.calculatePlanetaryHours(for: location)
}
}
.onAppear {
locationManager.requestLocation()
}
.onChange(of: viewModel.selectedDate) { oldValue, newValue in
guard let location = locationManager.location else { return }
Task { await viewModel.calculatePlanetaryHours(for: location) }
}
// Results Section
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.padding()
}
if !viewModel.planetaryHours.isEmpty {
PlanetaryHoursTableView(hours: viewModel.planetaryHours)
// .frame(height: UIScreen.main.bounds.height
}
// Spacer()
}
}
// .navigationTitle("Planetary Hours")
// .task {
// locationManager.requestLocation()
// if let location = locationManager.location {
// await viewModel.calculatePlanetaryHours(for: location)
// }
// }
// .onAppear {
// locationManager.requestLocation()
// }
// .onChange(of: viewModel.selectedDate) { oldValue, newValue in
// guard let location = locationManager.location else { return }
// Task { await viewModel.calculatePlanetaryHours(for: location) }
// }
// }
// .ignoresSafeArea()
// MARK: - Planetary Rulers (Names, Symbols, Colors)
// Selectable planetary orders for hour-by-hour rotation
private enum PlanetaryOrder {
case chaldean
case babylonian
var sequence: [String] {
switch self {
case .chaldean:
return ["Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon"]
case .babylonian:
// Geocentric (Babylonian) order from nearest to farthest with Sun between Venus and Mars
return ["Moon", "Mercury", "Venus", "Sun", "Mars", "Jupiter", "Saturn"]
}
}
}
// Choose which order to use in the app
private let planetaryOrder: PlanetaryOrder = .babylonian
// Exact symbols provided by the user (Moon: ☽, Mars: ♂︎, Venus: ♀︎)
private let planetSymbols: [String: String] = [
"Saturn": "♄",
"Jupiter": "♃",
"Mars": "♂︎",
"Sun": "☉",
"Venus": "♀︎",
"Mercury": "☿",
"Moon": "☽"
]
// Display names
private let planetNames: [String: String] = [
"Saturn": "Saturn",
"Jupiter": "Jupiter",
"Mars": "Mars",
"Sun": "Sun",
"Venus": "Venus",
"Mercury": "Mercury",
"Moon": "Moon"
]
// --- Index-based models (Sun/Sunday start) ---
// 0: Sun, 1: Moon, 2: Mars, 3: Mercury, 4: Jupiter, 5: Venus, 6: Saturn
private let PLANETS: [String] = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
private let PLANET_SYMBOLS_LIST: [String] = ["☉", "☽", "♂︎", "☿", "♃", "♀︎", "♄"]
private let WEEKDAYS: [String] = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
// Babylonian forward stepping cycle expressed as planet INDEXES into PLANETS
// Sun → Venus → Mercury → Moon → Saturn → Jupiter → Mars → (repeat)
// With PLANETS indexing, that is: [0, 5, 3, 1, 6, 4, 2]
private let BABYLONIAN_CYCLE: [Int] = [0, 5, 3, 1, 6, 4, 2]
// Calendar weekday helpers: 1=Sunday ... 7=Saturday
private func previousWeekday(_ weekday: Int) -> Int {
return weekday == 1 ? 7 : (weekday - 1)
}
// Map civil weekday to planetary day ruler start index (Sunday=Sun, Monday=Moon, ... Saturday=Saturn)
private func dayRulerIndex(for weekday: Int) -> Int {
let order = planetaryOrder.sequence
// Planet that rules the civil weekday name
let dayRuler: String
switch weekday {
case 1: dayRuler = "Sun" // Sunday
case 2: dayRuler = "Moon" // Monday
case 3: dayRuler = "Mars" // Tuesday
case 4: dayRuler = "Mercury" // Wednesday
case 5: dayRuler = "Jupiter" // Thursday
case 6: dayRuler = "Venus" // Friday
case 7: dayRuler = "Saturn" // Saturday
default: dayRuler = "Sun"
}
return order.firstIndex(of: dayRuler) ?? 0
}
// Return the ruling planet *symbol* for the given planetary hour
private func planetarySymbol(for hour: PlanetaryHour) -> String {
// Determine civil weekday index: Apple: 1=Sunday..7=Saturday → 0..6
let wk = Calendar.current.component(.weekday, from: hour.startTime) - 1
let dayStartPlanetIndex = wk
if hour.isDaytime && hour.hourNumber == 1 {
return PLANET_SYMBOLS_LIST[dayStartPlanetIndex]
}
guard let cycleStart = BABYLONIAN_CYCLE.firstIndex(of: dayStartPlanetIndex) else {
return PLANET_SYMBOLS_LIST[dayStartPlanetIndex]
}
let offsetWithinDay = (hour.isDaytime ? (hour.hourNumber - 1) : (12 + hour.hourNumber - 1))
let planetIndex = BABYLONIAN_CYCLE[(cycleStart + offsetWithinDay) % BABYLONIAN_CYCLE.count]
return PLANET_SYMBOLS_LIST[planetIndex]
}
// Return the ruling planet *name* for the given planetary hour
private func planetaryName(for hour: PlanetaryHour) -> String {
// Determine civil weekday index: Apple: 1=Sunday..7=Saturday → 0..6
let wk = Calendar.current.component(.weekday, from: hour.startTime) - 1
let dayStartPlanetIndex = wk // index-aligned: Sunday→Sun(0), Monday→Moon(1), ...
// First DAY hour resets to the weekday ruler
if hour.isDaytime && hour.hourNumber == 1 {
return PLANETS[dayStartPlanetIndex]
}
// Find where the dayStartPlanet sits inside the Babylonian forward cycle
guard let cycleStart = BABYLONIAN_CYCLE.firstIndex(of: dayStartPlanetIndex) else {
return PLANETS[dayStartPlanetIndex]
}
// Hour offset within the civil day: 0..23 (0 for Day Hour 1)
let offsetWithinDay = (hour.isDaytime ? (hour.hourNumber - 1) : (12 + hour.hourNumber - 1))
// Advance forward within the Babylonian cycle
let planetIndex = BABYLONIAN_CYCLE[(cycleStart + offsetWithinDay) % BABYLONIAN_CYCLE.count]
return PLANETS[planetIndex]
}
// SwiftUI color mapping equivalent to the Objective‑C UIColor function
private func colorForPlanetarySymbol(_ symbol: String) -> Color {
switch symbol {
case "☉": return .yellow
case "☽": return .white
case "♂︎": return .red
case "☿": return .brown
case "♃": return .orange
case "♀︎": return .green
case "♄": return .gray
default: return .white
}
}
// MARK: - Table View
struct PlanetaryHoursTableView: View {
let hours: [PlanetaryHour]
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
}()
var body: some View {
ScrollView {
VStack(spacing: 0) {
// Day Hours Section
Text("DAY")
.font(.headline)
.frame(maxWidth: .infinity)
.background(Color.yellow.opacity(0.1))
.cornerRadius(10)
ForEach(hours.filter { $0.isDaytime }) { hour in
HourRowView(hour: hour, timeFormatter: timeFormatter)
}
}
// .font(.default)
.frame(maxWidth: .infinity)
.background(Color.orange.opacity(0.1))
.cornerRadius(10)
.padding([.horizontal])
VStack(spacing: 0) {
// Night Hours Section
Text("NIGHT")
.font(.headline)
.frame(maxWidth: .infinity)
.background(Color.blue.opacity(0.2))
.padding()
ForEach(hours.filter { !$0.isDaytime }) { hour in
HourRowView(hour: hour, timeFormatter: timeFormatter)
}
}
// .font(.default)
.frame(maxWidth: .infinity)
.background(Color.indigo.opacity(0.1))
.cornerRadius(10)
}
}
}
// MARK: - Hour Row View
struct HourRowView: View {
let hour: PlanetaryHour
let timeFormatter: DateFormatter
var body: some View {
HStack(spacing: 12) {
let sym = planetarySymbol(for: hour)
let name = planetaryName(for: hour)
HStack(spacing: 6) {
Text("Hour \(hour.hourNumber)")
Text(sym)
.foregroundStyle(colorForPlanetarySymbol(sym))
}
.font(.system(.body, design: .monospaced))
.frame(width: 140, alignment: .leading)
Text(name)
.font(.system(.body, design: .monospaced))
.frame(width: 90, alignment: .leading)
VStack(alignment: .leading, spacing: 4) {
Text(timeFormatter.string(from: hour.startTime))
.font(.caption)
Text(timeFormatter.string(from: hour.endTime))
.font(.caption)
}
.frame(width: 140, alignment: .leading)
Text(hour.durationString)
.font(.system(.body, design: .monospaced))
.frame(width: 90, alignment: .trailing)
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
// .background(hour.isDaytime ? Color.yellow.opacity(0.1) : Color.indigo.opacity(0.1))
}
}
private var dateRange: ClosedRange<Date> {
let calendar = Calendar.current
let today = Date()
let sevenDaysBefore = calendar.date(byAdding: .day, value: -7, to: today)!
let sevenDaysAfter = calendar.date(byAdding: .day, value: 7, to: today)!
return sevenDaysBefore ... sevenDaysAfter
}
#Preview {
ContentView()
.preferredColorScheme(.dark)
.dynamicTypeSize(DynamicTypeSize.medium)
}
//
// PlanetaryHours2025_ClaudeApp.swift
// PlanetaryHours2025-Claude
//
// Created by Xcode Developer on 10/29/25.
//
import SwiftUI
import WeatherKit
import CoreLocation
import Observation
import Combine
// MARK: - Models
struct PlanetaryHour: Identifiable {
let id = UUID()
let hourNumber: Int
let startTime: Date
let endTime: Date
let isDaytime: Bool
var duration: TimeInterval {
endTime.timeIntervalSince(startTime)
}
var durationString: String {
let minutes = Int(duration / 60)
let seconds = Int(duration.truncatingRemainder(dividingBy: 60))
return String(format: "%d:%02d", minutes, seconds)
}
}
// MARK: - Location Manager
@Observable class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var location: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
var locationError: String?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
authorizationStatus = manager.authorizationStatus
}
func requestLocation() {
locationError = nil
switch authorizationStatus {
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .authorizedWhenInUse,
.authorizedAlways:
manager.requestLocation()
case .denied,
.restricted:
locationError = "Location access denied. Please enable location services in Settings."
@unknown default:
locationError = "Unknown authorization status"
}
}
// CLLocationManagerDelegate methods
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
if authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways {
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationError = "Failed to get location: \(error.localizedDescription)"
}
}
// MARK: - View Model
@MainActor
@Observable
class PlanetaryHoursViewModel {
var planetaryHours: [PlanetaryHour] = []
var isLoading = false
var errorMessage: String?
var selectedDate = Date()
var locationName: String = "Unknown Location"
private let weatherService = WeatherService.shared
private let geocoder = CLGeocoder()
func calculatePlanetaryHours(for location: CLLocation) async {
isLoading = true
errorMessage = nil
// Get location name
await getLocationName(for: location)
do {
// Get daily forecast for the selected date
let startOfDay = Calendar.current.startOfDay(for: selectedDate)
let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
let dayWeather = try await weatherService.weather(
for: location,
including: .daily(startDate: startOfDay, endDate: endOfDay)
)
guard let todayForecast = dayWeather.first else {
errorMessage = "No weather data available for this date"
isLoading = false
return
}
// Get sunrise and sunset times
guard let sunrise = todayForecast.sun.sunrise,
let sunset = todayForecast.sun.sunset else {
errorMessage = "Sunrise/sunset data not available for this location and date"
isLoading = false
return
}
// Calculate planetary hours
var hours: [PlanetaryHour] = []
// Daytime hours (12 segments from sunrise to sunset)
let dayDuration = sunset.timeIntervalSince(sunrise)
let dayHourLength = dayDuration / 12.0
for i in 0 ..< 12 {
let startTime = sunrise.addingTimeInterval(Double(i) * dayHourLength)
let endTime = sunrise.addingTimeInterval(Double(i + 1) * dayHourLength)
hours.append(PlanetaryHour(
hourNumber: i + 1,
startTime: startTime,
endTime: endTime,
isDaytime: true
))
}
// Nighttime hours (12 segments from sunset to next sunrise)
// Get next day's sunrise
let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
let dayAfter = Calendar.current.date(byAdding: .day, value: 2, to: startOfDay)!
let nextDayWeather = try await weatherService.weather(
for: location,
including: .daily(startDate: nextDay, endDate: dayAfter)
)
guard let tomorrowForecast = nextDayWeather.first,
let nextSunrise = tomorrowForecast.sun.sunrise else {
errorMessage = "Could not get next day's sunrise"
isLoading = false
return
}
let nightDuration = nextSunrise.timeIntervalSince(sunset)
let nightHourLength = nightDuration / 12.0
for i in 0 ..< 12 {
let startTime = sunset.addingTimeInterval(Double(i) * nightHourLength)
let endTime = sunset.addingTimeInterval(Double(i + 1) * nightHourLength)
hours.append(PlanetaryHour(
hourNumber: i + 1,
startTime: startTime,
endTime: endTime,
isDaytime: false
))
}
planetaryHours = hours
} catch {
errorMessage = "Error fetching weather data: \(error.localizedDescription)"
}
isLoading = false
}
private func getLocationName(for location: CLLocation) async {
do {
let placemarks = try await geocoder.reverseGeocodeLocation(location)
if let placemark = placemarks.first {
var components: [String] = []
if let city = placemark.locality {
components.append(city)
}
if let state = placemark.administrativeArea {
components.append(state)
}
if let country = placemark.country {
components.append(country)
}
locationName = components.isEmpty ? "Unknown Location" : components.joined(separator: ", ")
}
} catch {
locationName = "Lat: \(String(format: "%.4f", location.coordinate.latitude)), Lon: \(String(format: "%.4f", location.coordinate.longitude))"
}
}
}
// MARK: - App Entry Point
@main
struct PlanetaryHoursApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@theoknock
Copy link
Author

ScreenRecording_10-30-2025.08-39-07_1.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment