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.
274 lines
9.5 KiB
Swift
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
|
|
}
|
|
}
|