Files
Reflect/Shared/Services/WatchExporter.swift
Trey t 0442eab1f8 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>
2026-02-26 11:47:16 -06:00

251 lines
10 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
/// 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)
try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true)
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)
try? FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true)
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() {
try? data.write(to: url)
}
}
}
}
#endif