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>
This commit is contained in:
191
Feels Watch App/ContentView.swift
Normal file
191
Feels Watch App/ContentView.swift
Normal file
@@ -0,0 +1,191 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user