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.
190 lines
7.6 KiB
Swift
190 lines
7.6 KiB
Swift
//
|
|
// VotingLayoutPickerView.swift
|
|
// Reflect (iOS)
|
|
//
|
|
// Created by Claude Code on 12/9/24.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct VotingLayoutPickerView: View {
|
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
|
@AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0
|
|
|
|
private var textColor: Color { theme.currentTheme.labelColor }
|
|
|
|
private var currentLayout: VotingLayoutStyle {
|
|
VotingLayoutStyle(rawValue: votingLayoutStyle) ?? .horizontal
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Voting Layout")
|
|
.font(.headline)
|
|
.foregroundColor(textColor)
|
|
.padding(.horizontal)
|
|
.padding(.top)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
|
Button(action: {
|
|
if UIAccessibility.isReduceMotionEnabled {
|
|
votingLayoutStyle = layout.rawValue
|
|
} else {
|
|
withAnimation(.easeInOut(duration: 0.2)) {
|
|
votingLayoutStyle = layout.rawValue
|
|
}
|
|
}
|
|
AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName))
|
|
}) {
|
|
VStack(spacing: 6) {
|
|
layoutIcon(for: layout)
|
|
.frame(width: 44, height: 44)
|
|
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6))
|
|
|
|
Text(layout.displayName)
|
|
.font(.caption)
|
|
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8))
|
|
}
|
|
.frame(width: 70)
|
|
.padding(.vertical, 12)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityIdentifier(AccessibilityID.Customize.votingLayoutButton(layout.displayName))
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
.padding(.bottom)
|
|
}
|
|
.background(theme.currentTheme.secondaryBGColor)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func layoutIcon(for layout: VotingLayoutStyle) -> some View {
|
|
switch layout {
|
|
case .horizontal:
|
|
HStack(spacing: 4) {
|
|
ForEach(0..<5) { _ in
|
|
Circle()
|
|
.frame(width: 6, height: 6)
|
|
}
|
|
}
|
|
case .cards:
|
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 3) {
|
|
ForEach(0..<6) { _ in
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.frame(width: 10, height: 12)
|
|
}
|
|
}
|
|
case .stacked:
|
|
VStack(spacing: 3) {
|
|
ForEach(0..<4) { _ in
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.frame(width: 32, height: 6)
|
|
}
|
|
}
|
|
case .aura:
|
|
// Glowing orbs in 2 rows
|
|
VStack(spacing: 4) {
|
|
HStack(spacing: 6) {
|
|
ForEach(0..<3, id: \.self) { _ in
|
|
ZStack {
|
|
Circle()
|
|
.fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8))
|
|
.frame(width: 14, height: 14)
|
|
Circle()
|
|
.fill(.green)
|
|
.frame(width: 8, height: 8)
|
|
}
|
|
}
|
|
}
|
|
HStack(spacing: 10) {
|
|
ForEach(0..<2, id: \.self) { _ in
|
|
ZStack {
|
|
Circle()
|
|
.fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8))
|
|
.frame(width: 14, height: 14)
|
|
Circle()
|
|
.fill(.green)
|
|
.frame(width: 8, height: 8)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case .orbit:
|
|
// Center core with orbiting planets
|
|
ZStack {
|
|
// Orbital ring
|
|
Circle()
|
|
.stroke(Color.primary.opacity(0.2), lineWidth: 1)
|
|
.frame(width: 32, height: 32)
|
|
// Center core
|
|
Circle()
|
|
.fill(Color.primary.opacity(0.8))
|
|
.frame(width: 8, height: 8)
|
|
// Orbiting planets
|
|
ForEach(0..<5, id: \.self) { index in
|
|
Circle()
|
|
.fill(Color.accentColor)
|
|
.frame(width: 6, height: 6)
|
|
.offset(orbitOffset(index: index, total: 5, radius: 16))
|
|
}
|
|
}
|
|
case .neon:
|
|
// Equalizer bars
|
|
ZStack {
|
|
// Grid background hint
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.black)
|
|
.frame(width: 36, height: 36)
|
|
|
|
// Equalizer bars
|
|
HStack(spacing: 2) {
|
|
ForEach(0..<5, id: \.self) { index in
|
|
let heights: [CGFloat] = [24, 18, 14, 10, 8]
|
|
RoundedRectangle(cornerRadius: 1)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.frame(width: 4, height: heights[index])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func orbitOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
|
|
// Start from top (-π/2) and go clockwise
|
|
let startAngle = -Double.pi / 2
|
|
let angleStep = (2 * Double.pi) / Double(total)
|
|
let angle = startAngle + angleStep * Double(index)
|
|
return CGSize(
|
|
width: radius * CGFloat(cos(angle)),
|
|
height: radius * CGFloat(sin(angle))
|
|
)
|
|
}
|
|
}
|
|
|
|
struct VotingLayoutPickerView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
VotingLayoutPickerView()
|
|
.padding()
|
|
}
|
|
}
|