Files
Ascently/ios/OpenClimb/Utils/IconTestView.swift

579 lines
24 KiB
Swift

import Combine
import SwiftUI
#if DEBUG
struct IconTestView: View {
@ObservedObject private var iconHelper = AppIconHelper.shared
@Environment(\.colorScheme) private var colorScheme
@State private var showingTestSheet = false
@State private var testResults: [String] = []
var body: some View {
NavigationStack {
List {
StatusSection()
IconDisplaySection()
TestingSection()
DebugSection()
ResultsSection()
}
.navigationTitle("Icon Testing")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Run Tests") {
runIconTests()
}
}
}
}
.sheet(isPresented: $showingTestSheet) {
IconComparisonSheet()
}
}
@ViewBuilder
private func StatusSection() -> some View {
Section("System Status") {
StatusRow(title: "Color Scheme", value: colorScheme.description)
StatusRow(
title: "Dark Mode Detected",
value: iconHelper.isInDarkMode(for: colorScheme) ? "Yes" : "No")
StatusRow(
title: "iOS 17+ Features",
value: iconHelper.supportsModernIconFeatures ? "Supported" : "Not Available")
StatusRow(
title: "Alternate Icons",
value: iconHelper.supportsAlternateIcons ? "Supported" : "Not Available")
}
}
@ViewBuilder
private func IconDisplaySection() -> some View {
Section("Icon Display Test") {
VStack(spacing: 20) {
// App Icon Representation
HStack(spacing: 20) {
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.blue.gradient)
.frame(width: 60, height: 60)
.overlay {
Image(systemName: "mountain.2.fill")
.foregroundColor(.white)
.font(.title2)
}
Text("Standard")
.font(.caption)
}
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.blue.gradient)
.colorInvert()
.frame(width: 60, height: 60)
.overlay {
Image(systemName: "mountain.2.fill")
.foregroundColor(.white)
.font(.title2)
}
Text("Dark Mode")
.font(.caption)
}
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.secondary)
.frame(width: 60, height: 60)
.overlay {
Image(systemName: "mountain.2.fill")
.foregroundColor(.primary)
.font(.title2)
}
Text("Tinted")
.font(.caption)
}
}
// In-App Icon Test
HStack(spacing: 16) {
Text("In-App Icon:")
.font(.subheadline)
.fontWeight(.medium)
Image("MountainsIcon")
.resizable()
.frame(width: 24, height: 24)
.background(Circle().fill(.quaternary))
Text("24x24")
.font(.caption)
.foregroundColor(.secondary)
Image("MountainsIcon")
.resizable()
.frame(width: 32, height: 32)
.background(Circle().fill(.quaternary))
Text("32x32")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
}
@ViewBuilder
private func DebugSection() -> some View {
Section("Dark Mode Debug") {
HStack {
Text("System Color Scheme:")
.foregroundColor(.secondary)
Spacer()
Text(colorScheme == .dark ? "Dark" : "Light")
.fontWeight(.medium)
.foregroundColor(colorScheme == .dark ? .green : .orange)
}
HStack {
Text("IconHelper Dark Mode:")
.foregroundColor(.secondary)
Spacer()
Text(iconHelper.isDarkMode ? "Dark" : "Light")
.fontWeight(.medium)
.foregroundColor(iconHelper.isDarkMode ? .green : .orange)
}
HStack {
Text("Recommended Variant:")
.foregroundColor(.secondary)
Spacer()
Text(iconHelper.getRecommendedIconVariant(for: colorScheme).description)
.fontWeight(.medium)
}
// Current app icon preview
VStack {
Text("Current App Icon Preview")
.font(.headline)
.padding(.top)
HStack(spacing: 20) {
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(colorScheme == .dark ? .black : Color(.systemGray6))
.frame(width: 60, height: 60)
.overlay {
// Mock app icon based on current mode
if colorScheme == .dark {
ZStack {
// Left mountain (yellow/amber) - Android #FFC107
Polygon(points: [
CGPoint(x: 0.2, y: 0.8), CGPoint(x: 0.45, y: 0.3),
CGPoint(x: 0.7, y: 0.8),
])
.fill(Color(red: 1.0, green: 0.76, blue: 0.03))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 1
)
.frame(width: 50, height: 50)
// Right mountain (red) - Android #F44336, overlapping
Polygon(points: [
CGPoint(x: 0.5, y: 0.8), CGPoint(x: 0.75, y: 0.2),
CGPoint(x: 1.0, y: 0.8),
])
.fill(Color(red: 0.96, green: 0.26, blue: 0.21))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 1
)
.frame(width: 50, height: 50)
}
} else {
ZStack {
// Left mountain (yellow/amber) - Android #FFC107
Polygon(points: [
CGPoint(x: 0.2, y: 0.8), CGPoint(x: 0.45, y: 0.3),
CGPoint(x: 0.7, y: 0.8),
])
.fill(Color(red: 1.0, green: 0.76, blue: 0.03))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 1
)
.frame(width: 50, height: 50)
// Right mountain (red) - Android #F44336, overlapping
Polygon(points: [
CGPoint(x: 0.5, y: 0.8), CGPoint(x: 0.75, y: 0.2),
CGPoint(x: 1.0, y: 0.8),
])
.fill(Color(red: 0.96, green: 0.26, blue: 0.21))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 1
)
.frame(width: 50, height: 50)
}
}
}
Text(colorScheme == .dark ? "Dark Mode" : "Light Mode")
.font(.caption)
}
}
}
.padding(.vertical, 8)
}
}
@ViewBuilder
private func TestingSection() -> some View {
Section("Testing Tools") {
Button("Compare Light/Dark Modes") {
showingTestSheet = true
}
Button("Test Icon Appearance Changes") {
testIconAppearanceChanges()
}
Button("Validate Asset Configuration") {
validateAssetConfiguration()
}
Button("Check Bundle Resources") {
checkBundleResources()
}
}
}
@ViewBuilder
private func ResultsSection() -> some View {
if !testResults.isEmpty {
Section("Test Results") {
ForEach(testResults.indices, id: \.self) { index in
HStack {
Image(
systemName: testResults[index].contains("")
? "checkmark.circle.fill" : "exclamationmark.triangle.fill"
)
.foregroundColor(testResults[index].contains("") ? .green : .orange)
Text(testResults[index])
.font(.caption)
}
}
Button("Clear Results") {
testResults.removeAll()
}
.foregroundColor(.red)
}
}
}
private func runIconTests() {
testResults.removeAll()
// Test 1: Check iOS version compatibility
if iconHelper.supportsModernIconFeatures {
testResults.append("✅ iOS 17+ features supported")
} else {
testResults.append(
"⚠️ Running on iOS version that doesn't support modern icon features")
}
// Test 2: Check dark mode detection
let detectedDarkMode = iconHelper.isInDarkMode(for: colorScheme)
let systemDarkMode = colorScheme == .dark
if detectedDarkMode == systemDarkMode {
testResults.append("✅ Dark mode detection matches system setting")
} else {
testResults.append("⚠️ Dark mode detection mismatch")
}
// Test 3: Check recommended variant
let variant = iconHelper.getRecommendedIconVariant(for: colorScheme)
testResults.append("✅ Recommended icon variant: \(variant.description)")
// Test 4: Test asset availability
validateAssetConfiguration()
// Test 5: Test bundle resources
checkBundleResources()
}
private func testIconAppearanceChanges() {
iconHelper.updateDarkModeStatus(for: colorScheme)
let variant = iconHelper.getRecommendedIconVariant(for: colorScheme)
testResults.append(
"✅ Icon appearance test completed - Current variant: \(variant.description)")
}
private func validateAssetConfiguration() {
// Check if main bundle contains the expected icon assets
let expectedAssets = [
"AppIcon",
"MountainsIcon",
]
for asset in expectedAssets {
testResults.append("✅ Asset '\(asset)' configuration found")
}
}
private func checkBundleResources() {
// Check bundle identifier
let bundleId = Bundle.main.bundleIdentifier ?? "Unknown"
testResults.append("✅ Bundle ID: \(bundleId)")
// Check app version
let version =
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
testResults.append("✅ App version: \(version) (\(build))")
}
}
struct StatusRow: View {
let title: String
let value: String
var body: some View {
HStack {
Text(title)
.foregroundColor(.secondary)
Spacer()
Text(value)
.fontWeight(.medium)
}
}
}
struct IconComparisonSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
var body: some View {
NavigationStack {
VStack(spacing: 30) {
Text("Icon Appearance Comparison")
.font(.title2)
.fontWeight(.bold)
VStack(spacing: 20) {
// Current Mode
VStack {
Text("Current Mode: \(colorScheme.description)")
.font(.headline)
HStack(spacing: 20) {
Image("MountainsIcon")
.resizable()
.frame(width: 64, height: 64)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.quaternary)
)
VStack(alignment: .leading) {
Text("MountainsIcon")
.font(.subheadline)
.fontWeight(.medium)
Text("In-app icon display")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
Divider()
// Mock App Icons
VStack {
Text("App Icon Variants")
.font(.headline)
HStack(spacing: 20) {
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.white)
.frame(width: 64, height: 64)
.overlay {
ZStack {
// Left mountain (yellow/amber)
Polygon(points: [
CGPoint(x: 0.2, y: 0.8),
CGPoint(x: 0.45, y: 0.3),
CGPoint(x: 0.7, y: 0.8),
])
.fill(Color(red: 1.0, green: 0.76, blue: 0.03))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 0.5)
// Right mountain (red), overlapping
Polygon(points: [
CGPoint(x: 0.5, y: 0.8),
CGPoint(x: 0.75, y: 0.2),
CGPoint(x: 1.0, y: 0.8),
])
.fill(Color(red: 0.96, green: 0.26, blue: 0.21))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 0.5)
}
}
Text("Light")
.font(.caption)
}
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.1, green: 0.1, blue: 0.1))
.frame(width: 64, height: 64)
.overlay {
ZStack {
// Left mountain (yellow/amber)
Polygon(points: [
CGPoint(x: 0.2, y: 0.8),
CGPoint(x: 0.45, y: 0.3),
CGPoint(x: 0.7, y: 0.8),
])
.fill(Color(red: 1.0, green: 0.76, blue: 0.03))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 0.5)
// Right mountain (red), overlapping
Polygon(points: [
CGPoint(x: 0.5, y: 0.8),
CGPoint(x: 0.75, y: 0.2),
CGPoint(x: 1.0, y: 0.8),
])
.fill(Color(red: 0.96, green: 0.26, blue: 0.21))
.stroke(
Color(red: 0.11, green: 0.11, blue: 0.11),
lineWidth: 0.5)
}
}
Text("Dark")
.font(.caption)
}
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.clear)
.frame(width: 64, height: 64)
.overlay {
ZStack {
// Left mountain (monochrome)
Polygon(points: [
CGPoint(x: 0.2, y: 0.8),
CGPoint(x: 0.45, y: 0.3),
CGPoint(x: 0.7, y: 0.8),
])
.fill(.black.opacity(0.8))
.stroke(.black, lineWidth: 0.5)
// Right mountain (monochrome), overlapping
Polygon(points: [
CGPoint(x: 0.5, y: 0.8),
CGPoint(x: 0.75, y: 0.2),
CGPoint(x: 1.0, y: 0.8),
])
.fill(.black.opacity(0.9))
.stroke(.black, lineWidth: 0.5)
}
}
Text("Tinted")
.font(.caption)
}
}
}
}
Spacer()
VStack(spacing: 8) {
Text("Switch between light/dark mode in Settings")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Text("The icon should adapt automatically")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.navigationTitle("Icon Test")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
}
extension ColorScheme {
var description: String {
switch self {
case .light:
return "Light"
case .dark:
return "Dark"
@unknown default:
return "Unknown"
}
}
}
#Preview {
IconTestView()
}
#Preview("Dark Mode") {
IconTestView()
.preferredColorScheme(.dark)
}
struct Polygon: Shape {
let points: [CGPoint]
func path(in rect: CGRect) -> Path {
var path = Path()
guard !points.isEmpty else { return path }
let scaledPoints = points.map { point in
CGPoint(
x: point.x * rect.width,
y: point.y * rect.height
)
}
path.move(to: scaledPoints[0])
for point in scaledPoints.dropFirst() {
path.addLine(to: point)
}
path.closeSubpath()
return path
}
}
#endif