Files
Sportstime/SportsTime/Export/Views/SharePreviewView.swift
Trey t d63d311cab feat: add WCAG AA accessibility app-wide, fix CloudKit container config, remove debug logs
- Add VoiceOver labels, hints, and element grouping across all 60+ views
- Add Reduce Motion support (Theme.Animation.prefersReducedMotion) to all animations
- Replace fixed font sizes with semantic Dynamic Type styles
- Hide decorative elements from VoiceOver with .accessibilityHidden(true)
- Add .minimumHitTarget() modifier ensuring 44pt touch targets
- Add AccessibilityAnnouncer utility for VoiceOver announcements
- Improve color contrast values in Theme.swift for WCAG AA compliance
- Extract CloudKitContainerConfig for explicit container identity
- Remove PostHog debug console log from AnalyticsManager

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 09:27:23 -06:00

262 lines
8.2 KiB
Swift

//
// SharePreviewView.swift
// SportsTime
//
// Unified preview and customization UI for all shareable content.
//
import SwiftUI
struct SharePreviewView<Content: ShareableContent>: View {
let content: Content
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@State private var selectedTheme: ShareTheme
@State private var generatedImage: UIImage?
@State private var isGenerating = false
@State private var error: String?
@State private var showCopiedToast = false
init(content: Content) {
self.content = content
_selectedTheme = State(initialValue: ShareThemePreferences.theme(for: content.cardType))
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: Theme.Spacing.lg) {
// Preview
previewSection
// Theme selector
themeSelector
// Action buttons
actionButtons
}
.padding(Theme.Spacing.md)
}
.themedBackground()
.navigationTitle("Share")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
.alert("Error", isPresented: .constant(error != nil)) {
Button("OK") { error = nil }
} message: {
Text(error ?? "")
}
.overlay {
if showCopiedToast {
copiedToast
}
}
.task {
await generatePreview()
}
.onChange(of: selectedTheme) { _, _ in
Task { await generatePreview() }
}
}
}
// MARK: - Preview Section
private var previewSection: some View {
VStack(spacing: Theme.Spacing.sm) {
Text("Preview")
.font(.subheadline)
.foregroundStyle(Theme.textMuted(colorScheme))
if let image = generatedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(9/16, contentMode: .fit)
.frame(maxHeight: 400)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: 5)
.accessibilityLabel("Share card preview")
} else if isGenerating {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(Theme.cardBackground(colorScheme))
.aspectRatio(9/16, contentMode: .fit)
.frame(maxHeight: 400)
.overlay {
ProgressView()
}
}
}
}
// MARK: - Theme Selector
private var themeSelector: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Theme")
.font(.subheadline)
.foregroundStyle(Theme.textMuted(colorScheme))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.sm) {
ForEach(ShareTheme.all) { theme in
themeButton(theme)
}
}
}
}
}
private func themeButton(_ theme: ShareTheme) -> some View {
Button {
withAnimation {
selectedTheme = theme
ShareThemePreferences.setTheme(theme, for: content.cardType)
}
} label: {
VStack(spacing: 4) {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colors: theme.gradientColors,
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 50, height: 50)
Circle()
.fill(theme.accentColor)
.frame(width: 16, height: 16)
}
.overlay {
if selectedTheme.id == theme.id {
RoundedRectangle(cornerRadius: 8)
.stroke(Theme.warmOrange, lineWidth: 3)
}
}
Text(theme.name)
.font(.caption2)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
.buttonStyle(.plain)
.accessibilityLabel("Select \(theme.name) theme")
.accessibilityHint(selectedTheme.id == theme.id ? "Currently selected" : "Double-tap to select this theme")
.accessibilityAddTraits(selectedTheme.id == theme.id ? .isSelected : [])
}
// MARK: - Action Buttons
private var actionButtons: some View {
VStack(spacing: Theme.Spacing.sm) {
// Primary: Share to Instagram
Button {
shareToInstagram()
} label: {
HStack {
Image(systemName: "camera.fill")
.accessibilityHidden(true)
Text("Share to Instagram")
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.disabled(generatedImage == nil)
// Copy Image
Button {
copyImage()
} label: {
HStack {
Image(systemName: "doc.on.doc")
.accessibilityHidden(true)
Text("Copy to Clipboard")
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.cardBackgroundElevated(colorScheme))
.foregroundStyle(Theme.textPrimary(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.disabled(generatedImage == nil)
}
}
// MARK: - Copied Toast
private var copiedToast: some View {
VStack {
Spacer()
HStack {
Image(systemName: "checkmark.circle.fill")
.accessibilityHidden(true)
Text("Copied to clipboard")
}
.padding()
.background(.ultraThinMaterial)
.clipShape(Capsule())
.padding(.bottom, 100)
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
// MARK: - Actions
private func generatePreview() async {
isGenerating = true
do {
generatedImage = try await content.render(theme: selectedTheme)
} catch {
self.error = error.localizedDescription
}
isGenerating = false
}
private func shareToInstagram() {
guard let image = generatedImage else { return }
if !ShareService.shared.shareToInstagram(image: image) {
// Fallback to system share
showSystemShare()
}
}
private func copyImage() {
guard let image = generatedImage else { return }
ShareService.shared.copyToClipboard(image: image)
withAnimation {
showCopiedToast = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
showCopiedToast = false
}
}
}
private func showSystemShare() {
guard let image = generatedImage,
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else { return }
ShareService.shared.presentShareSheet(image: image, from: rootVC)
}
}