mycode/myApp/HaruDanim/Widgets/WidgetTheme.swift
songyc macbook ac40e14cfe feat(ux): UX 감사 반영 배치 — 시스템 테마·온보딩·일기 잠금·기간 비교 외
2026-07-23 사용성 감사(A그룹 5건 + 지시 추가분)를 한 번에 반영.

[테마] '시스템 설정 따름' 추가 + 기본값 light→system. "system"은 스킴을
  강제하지 않아(preferredColorScheme nil) 앱·위젯('앱 일치' 옵션,
  WidgetThemeOption.scheme: ColorScheme?)·DEBUG 위젯 미리보기까지
  시스템 라이트/다크를 그대로 따른다
[온보딩] 첫 실행 3장 소개(OnboardingView — 행동→목표·다짐→정리).
  데이터가 하나도 없고 본 적 없을 때만 1회(건너뛰기 포함 재표시 없음,
  onboarding.done), 시드·마케팅 촬영 플로우는 자동 건너뜀.
  설정 → 지원 '앱 소개 다시 보기'로 재열람
[일기 잠금] 설정 → 일기(iPad 전용 표시) 토글 — DiaryLock이
  .deviceOwnerAuthentication으로 Face ID/Touch ID/Optic ID+암호 폴백을
  기기별 분기 없이 처리. 일기 탭 진입 게이트(DiaryLockGateView),
  백그라운드 재잠금, 토글 변경 시 인증 요구, 잠글 수단 없는 기기는
  통과(영구 잠김 방지). NSFaceIDUsageDescription 추가(Info.plist+
  InfoPlist.xcstrings ko/en/ja)
[통계] 이전 기간 비교 카드(맨 위) — 총 시간·횟수를 어제/지난주/지난달과
  비교(▲▼%, 이전 기록 없으면 상태 문구). 이전 기간은 관계 기반
  Aggregator로 직접 집계, 필터 반영. 화면 전용(내보내기 미포함 의도)
[스플래시] 1.2→0.7초 단축 (하루에도 여러 번 여는 앱)
[워치] 행동 실행 탭 시 WKInterfaceDevice.play(.click) 햅틱 — 화면을
  안 보고 탭해도 접수 확인
[컴플리케이션] rectangular 빈 상태에 해결 안내("목표 편집에서
  '애플워치에서 보기'를 켜면 나타나요") — 옵트인 발견성 보완
[문구] 행동 편집기 꼬리표 색 규칙(여러 개면 가장 먼저 만든 꼬리표),
  새로고침 버튼 accessibilityHint

- 도움말 3주제 추가(새로고침 버튼·일기 잠금·이전 기간과 비교)
- 신규 문구 31키 en/ja 완역 + 워치 카탈로그 2키, missing/stale 0
- CLAUDE.md §6·§6.6·§6.7·§6.8·§10·§11·§14 갱신, DEBUG 인자
  -showOnboarding·-diaryLockScreen 추가
- 검증: Debug/워치/Store 빌드, 온보딩·설정·비교 카드·잠금 게이트·
  컴플리케이션 스크린샷

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 02:02:38 +09:00

150 lines
5.6 KiB
Swift

//
// WidgetTheme.swift
// Haru_DanimWidgets
//
// (CLAUDE.md §8.1)
// - (Intent) [ / / ] .
// - containerBackground(iOS 17+) , /( )
// ·
// (Color.clear)
// (white-on-white) .
//
import SwiftUI
import WidgetKit
import AppIntents
// MARK: - ( )
enum WidgetThemeOption: String, AppEnum {
case matchApp
case light
case dark
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "테마")
static let caseDisplayRepresentations: [WidgetThemeOption: DisplayRepresentation] = [
.matchApp: "앱 설정과 일치",
.light: "라이트 모드 고정",
.dark: "다크 모드 고정",
]
/// (nil = )
var scheme: ColorScheme? {
switch self {
case .matchApp: return WidgetAppearance.appScheme
case .light: return .light
case .dark: return .dark
}
}
}
enum WidgetAppearance {
/// ("system" | "light" | "dark") .
/// "system"() nil
static var appScheme: ColorScheme? {
let raw = AppGroup.defaults.string(forKey: SettingsKeys.theme)
?? UserDefaults.standard.string(forKey: SettingsKeys.theme)
switch raw {
case "dark": return .dark
case "light": return .light
default: return nil
}
}
}
// MARK: -
struct WidgetThemeModifier: ViewModifier {
@Environment(\.widgetRenderingMode) private var renderingMode
let theme: WidgetThemeOption
func body(content: Content) -> some View {
if renderingMode != .fullColor {
// /( ) : ·
// ()
content
.containerBackground(for: .widget) { Color.clear }
} else if let scheme = theme.scheme {
content
.environment(\.colorScheme, scheme)
.containerBackground(for: .widget) {
// containerBackground ( )
//
AppTheme.background
.environment(\.colorScheme, scheme)
}
} else {
// ( "system"):
// (AppTheme)
content
.containerBackground(for: .widget) { AppTheme.background }
}
}
}
extension View {
/// ( + containerBackground )
func widgetAppTheme(_ theme: WidgetThemeOption = .matchApp) -> some View {
modifier(WidgetThemeModifier(theme: theme))
}
}
// MARK: -
/// ().
/// fullBleed = (ContainerRelativeShape)
/// /( )
/// white-on-white ,
/// ( 0.2 ).
struct WidgetCellBackground: View {
@Environment(\.widgetRenderingMode) private var renderingMode
var fullBleed = false
private var fill: AnyShapeStyle {
renderingMode == .fullColor
? AnyShapeStyle(AppTheme.surface)
: AnyShapeStyle(Color.white.opacity(0.14))
}
var body: some View {
if fullBleed {
ContainerRelativeShape().fill(fill)
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous).fill(fill)
}
}
}
///
struct WidgetTintedCellBackground: View {
let color: Color
var fullBleed = false
var body: some View {
if fullBleed {
ContainerRelativeShape().fill(color)
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous).fill(color)
}
}
}
/// / ( )
struct WidgetBlankCellView: View {
var fullBleed = false
var body: some View {
Group {
if fullBleed {
ContainerRelativeShape()
.strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [5, 4]))
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [5, 4]))
}
}
.foregroundStyle(.quaternary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}