- Wrap 30+ production print() statements in #if DEBUG guards across 18 files - Add VoiceOver labels, hints, and traits to Watch app, Live Activities, widgets - Add .accessibilityAddTraits(.isButton) to 15+ onTapGesture views - Add text alternatives for color-only indicators (progress dots, mood circles) - Localize raw string literals in NoteEditorView, EntryDetailView, widgets - Replace 25+ silent try? with do/catch + AppLogger error logging - Replace hardcoded font sizes with semantic Dynamic Type fonts - Fix FIXME in IconPickerView (log icon change errors) - Extract magic animation delays to named constants across 8 files - Add widget empty state "Log your first mood!" messaging - Hide decorative images from VoiceOver, add labels to ColorPickers - Remove stale TODO in Color+Codable (alpha change deferred for migration) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
266 lines
11 KiB
Swift
266 lines
11 KiB
Swift
//
|
|
// WatchExporter.swift
|
|
// Reflect
|
|
//
|
|
// Debug utility to export all watch view previews to PNG files.
|
|
// Uses the exportable watch views from ExportableWatchViews.swift.
|
|
//
|
|
|
|
#if DEBUG
|
|
import SwiftUI
|
|
import UIKit
|
|
import os.log
|
|
|
|
/// Exports watch view previews to PNG files for App Store screenshots
|
|
@MainActor
|
|
class WatchExporter {
|
|
|
|
// MARK: - Watch Sizes (Apple Watch Series 9 45mm @ 2x)
|
|
|
|
/// Main watch app screen size (45mm watch)
|
|
static let watchAppSize = CGSize(width: 198, height: 242)
|
|
|
|
/// Complication sizes
|
|
enum ComplicationSize {
|
|
case circular // 50x50 pt
|
|
case corner // 40x40 pt content area
|
|
case inline // 230x26 pt
|
|
case rectangular // 180x70 pt
|
|
|
|
var pointSize: CGSize {
|
|
switch self {
|
|
case .circular: return CGSize(width: 50, height: 50)
|
|
case .corner: return CGSize(width: 100, height: 40)
|
|
case .inline: return CGSize(width: 230, height: 26)
|
|
case .rectangular: return CGSize(width: 180, height: 70)
|
|
}
|
|
}
|
|
|
|
var name: String {
|
|
switch self {
|
|
case .circular: return "circular"
|
|
case .corner: return "corner"
|
|
case .inline: return "inline"
|
|
case .rectangular: return "rectangular"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Available Theme Combinations (same as WidgetExporter)
|
|
|
|
/// All available tint options for export
|
|
static let allTints: [(name: String, tint: MoodTintable.Type)] = [
|
|
("Default", DefaultMoodTint.self),
|
|
("Neon", NeonMoodTint.self),
|
|
("Pastel", PastelTint.self),
|
|
("Monochrome", MonoChromeTint.self)
|
|
]
|
|
|
|
/// All available icon options for export
|
|
static let allIcons: [(name: String, images: MoodImagable.Type)] = [
|
|
("Emoji", EmojiMoodImages.self),
|
|
("FontAwesome", FontAwesomeMoodImages.self),
|
|
("Weather", WeatherMoodImages.self),
|
|
("Garden", GardenMoodImages.self),
|
|
("Hearts", HeartsMoodImages.self),
|
|
("Cosmic", CosmicMoodImages.self),
|
|
("HandEmoji", HandEmojiMoodImages.self)
|
|
]
|
|
|
|
// MARK: - Export All Watch Views
|
|
|
|
/// Exports all watch view variations to disk
|
|
/// - Returns: URL to the export directory, or nil if failed
|
|
static func exportAllWatchViews() async -> URL? {
|
|
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let exportPath = documentsPath.appendingPathComponent("WatchExports", isDirectory: true)
|
|
|
|
// Clean and create export directory
|
|
try? FileManager.default.removeItem(at: exportPath)
|
|
do {
|
|
try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true)
|
|
} catch {
|
|
AppLogger.export.error("Failed to create watch export directory: \(error)")
|
|
return nil
|
|
}
|
|
|
|
var totalExported = 0
|
|
|
|
// Export all tint + icon combinations
|
|
for tintOption in allTints {
|
|
for iconOption in allIcons {
|
|
let folderName = "\(tintOption.name)_\(iconOption.name)"
|
|
let variantPath = exportPath.appendingPathComponent(folderName, isDirectory: true)
|
|
do {
|
|
try FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true)
|
|
} catch {
|
|
AppLogger.export.error("Failed to create watch variant directory '\(folderName)': \(error)")
|
|
continue
|
|
}
|
|
|
|
let config = WatchExportConfig(
|
|
moodTint: tintOption.tint,
|
|
moodImages: iconOption.images
|
|
)
|
|
|
|
let count = await exportWatchViewsForConfig(config: config, to: variantPath)
|
|
totalExported += count
|
|
print(" Exported \(count) watch images to \(folderName)/")
|
|
}
|
|
}
|
|
|
|
print("⌚ Total \(totalExported) watch views exported to: \(exportPath.path)")
|
|
return exportPath
|
|
}
|
|
|
|
/// Exports watch views for a single tint/icon configuration
|
|
private static func exportWatchViewsForConfig(config: WatchExportConfig, to folder: URL) async -> Int {
|
|
var count = 0
|
|
|
|
for colorScheme in [ColorScheme.light, ColorScheme.dark] {
|
|
let schemeName = colorScheme == .light ? "light" : "dark"
|
|
|
|
// Watch App - Voting View
|
|
await exportWatchVotingView(config: config, colorScheme: colorScheme, to: folder, name: "watch_voting_\(schemeName)")
|
|
count += 1
|
|
|
|
// Watch App - Already Rated (all moods)
|
|
for mood in Mood.allValues {
|
|
await exportWatchAlreadyRatedView(mood: mood, config: config, colorScheme: colorScheme, to: folder, name: "watch_logged_\(schemeName)_\(mood.strValue.lowercased())")
|
|
count += 1
|
|
}
|
|
|
|
// Complications - Empty state
|
|
await exportCircularComplication(mood: nil, config: config, colorScheme: colorScheme, to: folder, name: "complication_circular_\(schemeName)_empty")
|
|
await exportCornerComplication(mood: nil, config: config, colorScheme: colorScheme, to: folder, name: "complication_corner_\(schemeName)_empty")
|
|
await exportInlineComplication(mood: nil, streak: 0, config: config, colorScheme: colorScheme, to: folder, name: "complication_inline_\(schemeName)_empty")
|
|
await exportRectangularComplication(mood: nil, streak: 0, config: config, colorScheme: colorScheme, to: folder, name: "complication_rectangular_\(schemeName)_empty")
|
|
count += 4
|
|
|
|
// Complications - With streak
|
|
await exportInlineComplication(mood: nil, streak: 45, config: config, colorScheme: colorScheme, to: folder, name: "complication_inline_\(schemeName)_streak")
|
|
count += 1
|
|
|
|
// Complications - All moods
|
|
for mood in Mood.allValues {
|
|
let moodName = mood.strValue.lowercased()
|
|
await exportCircularComplication(mood: mood, config: config, colorScheme: colorScheme, to: folder, name: "complication_circular_\(schemeName)_\(moodName)")
|
|
await exportCornerComplication(mood: mood, config: config, colorScheme: colorScheme, to: folder, name: "complication_corner_\(schemeName)_\(moodName)")
|
|
await exportInlineComplication(mood: mood, streak: 0, config: config, colorScheme: colorScheme, to: folder, name: "complication_inline_\(schemeName)_\(moodName)")
|
|
await exportRectangularComplication(mood: mood, streak: 45, config: config, colorScheme: colorScheme, to: folder, name: "complication_rectangular_\(schemeName)_\(moodName)")
|
|
count += 4
|
|
}
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
// MARK: - Export Watch App Views
|
|
|
|
private static func exportWatchVotingView(config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
|
let content = ExportableWatchVotingView(config: config)
|
|
|
|
let view = ExportableWatchContainer(
|
|
width: watchAppSize.width,
|
|
height: watchAppSize.height,
|
|
colorScheme: colorScheme
|
|
) {
|
|
content
|
|
}
|
|
|
|
await renderAndSave(view: view, size: watchAppSize, to: folder, name: name)
|
|
}
|
|
|
|
private static func exportWatchAlreadyRatedView(mood: Mood, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
|
let content = ExportableWatchAlreadyRatedView(mood: mood, config: config)
|
|
|
|
let view = ExportableWatchContainer(
|
|
width: watchAppSize.width,
|
|
height: watchAppSize.height,
|
|
colorScheme: colorScheme
|
|
) {
|
|
content
|
|
}
|
|
|
|
await renderAndSave(view: view, size: watchAppSize, to: folder, name: name)
|
|
}
|
|
|
|
// MARK: - Export Complications
|
|
|
|
private static func exportCircularComplication(mood: Mood?, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
|
let size = ComplicationSize.circular.pointSize
|
|
let content = ExportableCircularComplication(mood: mood, config: config)
|
|
|
|
let view = ExportableComplicationContainer(
|
|
size: size,
|
|
colorScheme: colorScheme,
|
|
isCircular: true
|
|
) {
|
|
content
|
|
}
|
|
|
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
|
}
|
|
|
|
private static func exportCornerComplication(mood: Mood?, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
|
let size = ComplicationSize.corner.pointSize
|
|
let content = ExportableCornerComplication(mood: mood, config: config)
|
|
|
|
let view = ExportableComplicationContainer(
|
|
size: size,
|
|
colorScheme: colorScheme
|
|
) {
|
|
content
|
|
}
|
|
|
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
|
}
|
|
|
|
private static func exportInlineComplication(mood: Mood?, streak: Int, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
|
let size = ComplicationSize.inline.pointSize
|
|
let content = ExportableInlineComplication(mood: mood, streak: streak, config: config)
|
|
|
|
let view = ExportableComplicationContainer(
|
|
size: size,
|
|
colorScheme: colorScheme
|
|
) {
|
|
content
|
|
}
|
|
|
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
|
}
|
|
|
|
private static func exportRectangularComplication(mood: Mood?, streak: Int, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
|
let size = ComplicationSize.rectangular.pointSize
|
|
let content = ExportableRectangularComplication(mood: mood, streak: streak, config: config)
|
|
|
|
let view = ExportableComplicationContainer(
|
|
size: size,
|
|
colorScheme: colorScheme
|
|
) {
|
|
content
|
|
}
|
|
|
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
|
}
|
|
|
|
// MARK: - Render and Save
|
|
|
|
private static func renderAndSave<V: View>(view: V, size: CGSize, to folder: URL, name: String) async {
|
|
let renderer = ImageRenderer(content: view.frame(width: size.width, height: size.height))
|
|
renderer.scale = 3.0 // 3x for high res
|
|
|
|
if let image = renderer.uiImage {
|
|
let url = folder.appendingPathComponent("\(name).png")
|
|
if let data = image.pngData() {
|
|
do {
|
|
try data.write(to: url)
|
|
} catch {
|
|
AppLogger.export.error("Failed to write watch image '\(name)': \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|