Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers, StoreKit product IDs, data store filenames, URL schemes, logger subsystems, Swift identifiers, user-facing strings (7 languages), file names, directory names, Xcode project, schemes, assets, and documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BIN
Reflect Watch App/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
BIN
Reflect Watch App/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 936 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
Reflect Watch App/Assets.xcassets/Contents.json
Normal file
6
Reflect Watch App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
236
Reflect Watch App/ContentView.swift
Normal file
236
Reflect Watch App/ContentView.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Reflect 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?
|
||||
@State private var todaysMood: Mood?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let mood = todaysMood ?? selectedMood, showConfirmation || todaysMood != nil {
|
||||
// Show "already rated" view
|
||||
AlreadyRatedView(mood: mood)
|
||||
} else {
|
||||
// Show voting UI
|
||||
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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
checkTodaysEntry()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkTodaysEntry() {
|
||||
let entry = ExtensionDataProvider.shared.getTodayEntry()
|
||||
if let entry = entry, entry.mood != .missing && entry.mood != .placeholder {
|
||||
todaysMood = entry.mood
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
ExtensionDataProvider.shared.add(mood: mood, forDate: date, entryType: .watch)
|
||||
|
||||
// Send to iPhone - it will handle HealthKit, Live Activity, etc.
|
||||
_ = WatchConnectivityManager.shared.sendMoodToPhone(mood: mood.rawValue, date: date)
|
||||
}
|
||||
|
||||
// Show confirmation and keep it (user already rated)
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
showConfirmation = true
|
||||
todaysMood = mood
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Already Rated View
|
||||
|
||||
struct AlreadyRatedView: View {
|
||||
let mood: Mood
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
Text(mood.watchEmoji)
|
||||
.font(.system(size: 50))
|
||||
|
||||
Text("Logged!")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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: - 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
|
||||
case weather = 3
|
||||
case garden = 4
|
||||
case hearts = 5
|
||||
case cosmic = 6
|
||||
|
||||
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 "❓"
|
||||
}
|
||||
case .weather:
|
||||
switch mood {
|
||||
case .great: return "☀️"
|
||||
case .good: return "⛅"
|
||||
case .average: return "☁️"
|
||||
case .bad: return "🌧️"
|
||||
case .horrible: return "⛈️"
|
||||
case .missing: return "🌫️"
|
||||
case .placeholder: return "❓"
|
||||
}
|
||||
case .garden:
|
||||
switch mood {
|
||||
case .great: return "🌸"
|
||||
case .good: return "🌿"
|
||||
case .average: return "🌱"
|
||||
case .bad: return "🍂"
|
||||
case .horrible: return "🥀"
|
||||
case .missing: return "🕳️"
|
||||
case .placeholder: return "❓"
|
||||
}
|
||||
case .hearts:
|
||||
switch mood {
|
||||
case .great: return "💖"
|
||||
case .good: return "🩷"
|
||||
case .average: return "🤍"
|
||||
case .bad: return "🩶"
|
||||
case .horrible: return "💔"
|
||||
case .missing: return "🖤"
|
||||
case .placeholder: return "❓"
|
||||
}
|
||||
case .cosmic:
|
||||
switch mood {
|
||||
case .great: return "⭐"
|
||||
case .good: return "🌕"
|
||||
case .average: return "🌓"
|
||||
case .bad: return "🌑"
|
||||
case .horrible: return "🕳️"
|
||||
case .missing: return "✧"
|
||||
case .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()
|
||||
}
|
||||
20
Reflect Watch App/Reflect Watch App.entitlements
Normal file
20
Reflect Watch App/Reflect Watch App.entitlements
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.88oakapps.reflect</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.88oakapps.reflect</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
20
Reflect Watch App/Reflect Watch AppDebug.entitlements
Normal file
20
Reflect Watch App/Reflect Watch AppDebug.entitlements
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.88oakapps.reflect.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.88oakapps.reflect.debug</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
232
Reflect Watch App/ReflectComplication.swift
Normal file
232
Reflect Watch App/ReflectComplication.swift
Normal file
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// ReflectComplication.swift
|
||||
// Reflect Watch App
|
||||
//
|
||||
// WidgetKit complications for Apple Watch.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Timeline Provider
|
||||
|
||||
struct ReflectTimelineProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> ReflectEntry {
|
||||
ReflectEntry(date: Date(), mood: nil, streak: 0)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (ReflectEntry) -> Void) {
|
||||
Task { @MainActor in
|
||||
let entry = createEntry()
|
||||
completion(entry)
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<ReflectEntry>) -> Void) {
|
||||
Task { @MainActor in
|
||||
let entry = createEntry()
|
||||
|
||||
// Refresh at midnight for the next day
|
||||
let tomorrow = Calendar.current.startOfDay(
|
||||
for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!
|
||||
)
|
||||
|
||||
let timeline = Timeline(entries: [entry], policy: .after(tomorrow))
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createEntry() -> ReflectEntry {
|
||||
let todayEntry = ExtensionDataProvider.shared.getTodayEntry()
|
||||
let streak = ExtensionDataProvider.shared.getCurrentStreak()
|
||||
|
||||
return ReflectEntry(
|
||||
date: Date(),
|
||||
mood: todayEntry?.mood,
|
||||
streak: streak
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline Entry
|
||||
|
||||
struct ReflectEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let mood: Mood?
|
||||
let streak: Int
|
||||
}
|
||||
|
||||
// MARK: - Complication Views
|
||||
|
||||
struct ReflectComplicationEntryView: View {
|
||||
var entry: ReflectEntry
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .accessoryCircular:
|
||||
CircularView(entry: entry)
|
||||
case .accessoryCorner:
|
||||
CornerView(entry: entry)
|
||||
case .accessoryInline:
|
||||
InlineView(entry: entry)
|
||||
case .accessoryRectangular:
|
||||
RectangularView(entry: entry)
|
||||
default:
|
||||
CircularView(entry: entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Circular Complication
|
||||
|
||||
struct CircularView: View {
|
||||
let entry: ReflectEntry
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AccessoryWidgetBackground()
|
||||
|
||||
if let mood = entry.mood {
|
||||
Text(mood.watchEmoji)
|
||||
.font(.system(size: 24))
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
Image(systemName: "face.smiling")
|
||||
.font(.system(size: 18))
|
||||
Text("Log")
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Corner Complication
|
||||
|
||||
struct CornerView: View {
|
||||
let entry: ReflectEntry
|
||||
|
||||
var body: some View {
|
||||
if let mood = entry.mood {
|
||||
Text(mood.watchEmoji)
|
||||
.font(.system(size: 20))
|
||||
.widgetLabel {
|
||||
Text(mood.widgetDisplayName)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "face.smiling")
|
||||
.font(.system(size: 20))
|
||||
.widgetLabel {
|
||||
Text("Log mood")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Inline Complication
|
||||
|
||||
struct InlineView: View {
|
||||
let entry: ReflectEntry
|
||||
|
||||
var body: some View {
|
||||
if entry.streak > 0 {
|
||||
Label("\(entry.streak) day streak", systemImage: "flame.fill")
|
||||
} else if let mood = entry.mood {
|
||||
Text("\(mood.watchEmoji) \(mood.widgetDisplayName)")
|
||||
} else {
|
||||
Label("Log your mood", systemImage: "face.smiling")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Rectangular Complication
|
||||
|
||||
struct RectangularView: View {
|
||||
let entry: ReflectEntry
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let mood = entry.mood {
|
||||
Text(mood.watchEmoji)
|
||||
.font(.system(size: 28))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Today")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
|
||||
if entry.streak > 1 {
|
||||
Label("\(entry.streak) days", systemImage: "flame.fill")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "face.smiling")
|
||||
.font(.system(size: 24))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Reflect")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
Text("Tap to log mood")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Configuration
|
||||
|
||||
struct ReflectComplication: Widget {
|
||||
let kind: String = "ReflectComplication"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: ReflectTimelineProvider()) { entry in
|
||||
ReflectComplicationEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("Reflect")
|
||||
.description("See today's mood and streak.")
|
||||
.supportedFamilies([
|
||||
.accessoryCircular,
|
||||
.accessoryCorner,
|
||||
.accessoryInline,
|
||||
.accessoryRectangular
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Circular - Mood") {
|
||||
CircularView(entry: ReflectEntry(date: Date(), mood: .great, streak: 5))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
}
|
||||
|
||||
#Preview("Circular - Empty") {
|
||||
CircularView(entry: ReflectEntry(date: Date(), mood: nil, streak: 0))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
}
|
||||
|
||||
#Preview("Rectangular - Mood") {
|
||||
RectangularView(entry: ReflectEntry(date: Date(), mood: .good, streak: 7))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
|
||||
}
|
||||
|
||||
#Preview("Inline - Streak") {
|
||||
InlineView(entry: ReflectEntry(date: Date(), mood: .great, streak: 5))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryInline))
|
||||
}
|
||||
|
||||
#Preview("Corner - Mood") {
|
||||
CornerView(entry: ReflectEntry(date: Date(), mood: .average, streak: 3))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCorner))
|
||||
}
|
||||
23
Reflect Watch App/ReflectWatchApp.swift
Normal file
23
Reflect Watch App/ReflectWatchApp.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// ReflectWatchApp.swift
|
||||
// Reflect Watch App
|
||||
//
|
||||
// Entry point for the Apple Watch companion app.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct ReflectWatchApp: App {
|
||||
|
||||
init() {
|
||||
// Initialize Watch Connectivity for cross-device widget updates
|
||||
_ = WatchConnectivityManager.shared
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user