// // 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(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