Files
Reflect/Feels Watch App/ContentView.swift
Trey t 224c00423a Add Apple Watch companion app with complications and WCSession sync
- Add watchOS app target with mood voting UI (5 mood buttons)
- Add WidgetKit complications (circular, corner, inline, rectangular)
- Add WatchConnectivityManager for bidirectional sync between iOS and watch
- iOS app acts as central coordinator - all mood logging flows through MoodLogger
- Watch votes send to iPhone via WCSession, iPhone logs and notifies watch back
- Widget votes use openAppWhenRun=true to run MoodLogger in main app process
- Add #if !os(watchOS) guards to Mood.swift and Random.swift for compatibility
- Update SKStoreReviewController to AppStore.requestReview (iOS 18 deprecation fix)
- Watch reads user's moodImages preference from GroupUserDefaults for emoji style

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 17:19:17 -06:00

192 lines
5.5 KiB
Swift

//
// ContentView.swift
// Feels Watch App
//
// Main voting interface for logging moods on Apple Watch.
//
import SwiftUI
import WatchKit
struct ContentView: View {
@State private var showConfirmation = false
@State private var selectedMood: Mood?
var body: some View {
ZStack {
VStack(spacing: 8) {
Text("How do you feel?")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.secondary)
// Top row: Great, Good, Average
HStack(spacing: 8) {
MoodButton(mood: .great, action: { logMood(.great) })
MoodButton(mood: .good, action: { logMood(.good) })
MoodButton(mood: .average, action: { logMood(.average) })
}
// Bottom row: Bad, Horrible
HStack(spacing: 8) {
MoodButton(mood: .bad, action: { logMood(.bad) })
MoodButton(mood: .horrible, action: { logMood(.horrible) })
}
}
.opacity(showConfirmation ? 0.3 : 1)
// Confirmation overlay
if showConfirmation {
ConfirmationView(mood: selectedMood)
}
}
}
private func logMood(_ mood: Mood) {
selectedMood = mood
// Haptic feedback
WKInterfaceDevice.current().play(.success)
let date = Date()
// Send to iPhone for centralized logging (iOS handles all side effects)
// Also save locally as fallback and for immediate complication updates
Task { @MainActor in
// Always save locally for immediate complication display
WatchDataProvider.shared.addMood(mood, forDate: date)
// Send to iPhone - it will handle HealthKit, Live Activity, etc.
_ = WatchConnectivityManager.shared.sendMoodToPhone(mood: mood.rawValue, date: date)
}
// Show confirmation
withAnimation(.easeInOut(duration: 0.2)) {
showConfirmation = true
}
// Hide confirmation after delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.easeInOut(duration: 0.2)) {
showConfirmation = false
}
}
}
}
// MARK: - Mood Button
struct MoodButton: View {
let mood: Mood
let action: () -> Void
var body: some View {
Button(action: action) {
Text(mood.watchEmoji)
.font(.system(size: 28))
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(mood.watchColor.opacity(0.3))
.cornerRadius(12)
}
.buttonStyle(.plain)
}
}
// MARK: - Confirmation View
struct ConfirmationView: View {
let mood: Mood?
var body: some View {
VStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 40))
.foregroundColor(.green)
Text("Logged!")
.font(.system(size: 18, weight: .semibold))
if let mood = mood {
Text(mood.watchEmoji)
.font(.system(size: 24))
}
}
}
}
// MARK: - Watch Mood Image Provider
/// Provides the appropriate emoji based on user's selected mood image style
enum WatchMoodImageStyle: Int {
case fontAwesome = 0
case emoji = 1
case handEmoji = 2
static var current: WatchMoodImageStyle {
// Use optional chaining for preview safety - App Group may not exist in canvas
guard let defaults = UserDefaults(suiteName: Constants.currentGroupShareId) else {
return .emoji
}
let rawValue = defaults.integer(forKey: "moodImages")
return WatchMoodImageStyle(rawValue: rawValue) ?? .emoji
}
func emoji(for mood: Mood) -> String {
switch self {
case .fontAwesome:
// FontAwesome uses face icons - map to similar emoji
switch mood {
case .great: return "😁"
case .good: return "🙂"
case .average: return "😐"
case .bad: return "🙁"
case .horrible: return "😫"
case .missing, .placeholder: return ""
}
case .emoji:
switch mood {
case .great: return "😀"
case .good: return "🙂"
case .average: return "😑"
case .bad: return "😕"
case .horrible: return "💩"
case .missing, .placeholder: return ""
}
case .handEmoji:
switch mood {
case .great: return "🙏"
case .good: return "👍"
case .average: return "🖖"
case .bad: return "👎"
case .horrible: return "🖕"
case .missing, .placeholder: return ""
}
}
}
}
// MARK: - Watch-Specific Mood Extensions
extension Mood {
/// Emoji representation for watch display based on user's selected style
var watchEmoji: String {
WatchMoodImageStyle.current.emoji(for: self)
}
/// Color for watch UI (simplified palette)
var watchColor: Color {
switch self {
case .great: return .green
case .good: return .mint
case .average: return .yellow
case .bad: return .orange
case .horrible: return .red
case .missing, .placeholder: return .gray
}
}
}
#Preview {
ContentView()
}