1.0.0 for iOS is ready to ship
This commit is contained in:
579
ios/OpenClimb/Utils/IconTestView.swift
Normal file
579
ios/OpenClimb/Utils/IconTestView.swift
Normal file
@@ -0,0 +1,579 @@
|
||||
|
||||
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 {
|
||||
NavigationView {
|
||||
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 {
|
||||
NavigationView {
|
||||
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
|
||||
Reference in New Issue
Block a user