Files
Reflect/Shared/Views/Sharing/SharingStylePickerView.swift
Trey T ed8205cd88 Complete accessibility identifier coverage across all 152 project files
Exhaustive file-by-file audit of every Swift file in the project (iOS app,
Watch app, Widget extension). Every interactive UI element — buttons, toggles,
pickers, links, menus, tap gestures, text editors, color pickers, photo
pickers — now has an accessibilityIdentifier for XCUITest automation.

46 files changed across Shared/, Onboarding/, Watch App/, and Widget targets.
Added ~100 new ID definitions covering settings debug controls, export/photo
views, sharing templates, customization subviews, onboarding flows, tip
modals, widget voting buttons, and watch mood buttons.
2026-03-26 08:34:56 -05:00

274 lines
9.5 KiB
Swift

//
// SharingStylePickerView.swift
// Reflect
//
// A horizontal pager that lets users swipe between design variations
// for a sharing template, then export the selected design.
//
import SwiftUI
struct SharingDesign: Identifiable {
let id = UUID()
let name: String
let shareView: AnyView
let image: () -> UIImage
}
struct SharePickerData: Identifiable {
let id = UUID()
let title: String
let designs: [SharingDesign]
}
struct SharingStylePickerView: View {
let title: String
let designs: [SharingDesign]
@State private var selectedIndex = 0
@StateObject private var shareImage = ShareImageStateViewModel()
@Environment(\.presentationMode) var presentationMode
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
private var textColor: Color { theme.currentTheme.labelColor }
private var safeIndex: Int {
guard !designs.isEmpty else { return 0 }
return min(selectedIndex, designs.count - 1)
}
var body: some View {
if designs.isEmpty {
Text("No designs available")
.foregroundColor(textColor)
} else {
VStack(spacing: 0) {
// Title bar
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Exit")
.font(.headline)
.foregroundColor(.red)
}
.accessibilityIdentifier(AccessibilityID.Sharing.exitButton)
Spacer()
Text(title)
.font(.headline)
.foregroundColor(textColor)
Spacer()
// Invisible placeholder for symmetry
Text("Exit")
.font(.headline)
.hidden()
}
.padding(.horizontal)
.padding(.vertical, 12)
// Pager showing scaled-down shareViews
TabView(selection: $selectedIndex) {
ForEach(Array(designs.enumerated()), id: \.offset) { index, design in
design.shareView
.scaleEffect(0.45, anchor: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
// Design name label
Text(designs[safeIndex].name)
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(textColor)
.padding(.top, 8)
.animation(.none, value: selectedIndex)
// Share button
Button(action: {
let img = designs[safeIndex].image()
shareImage.selectedShareImage = img
shareImage.showSheet = true
}) {
Text("Share")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.green)
.cornerRadius(14)
}
.accessibilityIdentifier(AccessibilityID.Sharing.shareButton)
.padding(.horizontal, 24)
.padding(.top, 12)
.padding(.bottom, 24)
}
.background(theme.currentTheme.bg.edgesIgnoringSafeArea(.all))
.sheet(isPresented: $shareImage.showSheet) {
if let uiImage = shareImage.selectedShareImage {
ShareSheet(photo: uiImage)
}
}
}
}
}
// MARK: - Longest Streak Picker (mood selection + style picker)
struct LongestStreakPickerView: View {
let startDate: Date
let endDate: Date
@State private var selectedMood: Mood = .great
@State private var streakEntries: [MoodEntryModel] = []
@State private var selectedIndex = 0
@StateObject private var shareImage = ShareImageStateViewModel()
@Environment(\.presentationMode) var presentationMode
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
private var textColor: Color { theme.currentTheme.labelColor }
private var designs: [SharingDesign] {
[
SharingDesign(
name: "Gradient Bar",
shareView: AnyView(LongestStreakV2(streakEntries: streakEntries, selectedMood: selectedMood)),
image: { LongestStreakV2(streakEntries: streakEntries, selectedMood: selectedMood).image }
),
SharingDesign(
name: "Dark Badge",
shareView: AnyView(LongestStreakV3(streakEntries: streakEntries, selectedMood: selectedMood)),
image: { LongestStreakV3(streakEntries: streakEntries, selectedMood: selectedMood).image }
),
]
}
var body: some View {
VStack(spacing: 0) {
// Title bar with mood picker
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Exit")
.font(.headline)
.foregroundColor(.red)
}
.accessibilityIdentifier(AccessibilityID.Sharing.exitButton)
Spacer()
Menu {
ForEach(Mood.allValues) { mood in
Button(mood.strValue) {
selectedMood = mood
recomputeStreak()
}
.accessibilityIdentifier(AccessibilityID.Sharing.moodMenuButton(mood.strValue))
}
} label: {
HStack(spacing: 6) {
Text(selectedMood.strValue)
.font(.headline)
.foregroundColor(textColor)
Image(systemName: "chevron.down")
.font(.caption)
.foregroundColor(textColor.opacity(0.6))
}
}
.accessibilityIdentifier(AccessibilityID.Sharing.moodMenu)
Spacer()
Text("Exit")
.font(.headline)
.hidden()
}
.padding(.horizontal)
.padding(.vertical, 12)
// Pager
TabView(selection: $selectedIndex) {
ForEach(Array(designs.enumerated()), id: \.offset) { index, design in
design.shareView
.scaleEffect(0.45, anchor: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
// Design name
Text(designs[selectedIndex].name)
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(textColor)
.padding(.top, 8)
.animation(.none, value: selectedIndex)
// Share button
Button(action: {
let img = designs[selectedIndex].image()
shareImage.selectedShareImage = img
shareImage.showSheet = true
}) {
Text("Share")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.green)
.cornerRadius(14)
}
.accessibilityIdentifier(AccessibilityID.Sharing.shareButton)
.padding(.horizontal, 24)
.padding(.top, 12)
.padding(.bottom, 24)
}
.background(theme.currentTheme.bg.edgesIgnoringSafeArea(.all))
.sheet(isPresented: $shareImage.showSheet) {
if let uiImage = shareImage.selectedShareImage {
ShareSheet(photo: uiImage)
}
}
.onAppear { recomputeStreak() }
}
@MainActor
private func recomputeStreak() {
let allEntries = DataController.shared.getData(
startDate: startDate,
endDate: endDate,
includedDays: [1, 2, 3, 4, 5, 6, 7]
)
var splitArrays = createSubArrays(fromMoodEntries: allEntries, splitOn: selectedMood)
splitArrays.sort { $0.count > $1.count }
streakEntries = splitArrays.first ?? []
}
private func createSubArrays(fromMoodEntries: [MoodEntryModel], splitOn: Mood) -> [[MoodEntryModel]] {
var splitArrays = [[MoodEntryModel]]()
var currentSplit = [MoodEntryModel]()
for entry in fromMoodEntries {
if entry.mood == splitOn {
currentSplit.append(entry)
} else {
splitArrays.append(currentSplit)
currentSplit.removeAll()
}
}
splitArrays.append(currentSplit)
return splitArrays
}
}