mycode/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift
songyc macbook dd95dcd28f fix(widget): resolve invisible content bug by applying proper containerBackground and dynamic contrast for transparent/tinted themes
On a tinted/transparent (or iOS 18 accented) home screen, the Action Run
widget turned into a solid white block with invisible content, while the
other widgets rendered fine. Cause: only the Action Run cell paints an opaque
tag-color background (WidgetTintedCellBackground) with hardcoded
.foregroundStyle(.white) text on top. In any non-.fullColor rendering mode
the system re-interprets that opaque fill toward white and the white text
disappears — classic white-on-white. The other widgets already use the
adaptive AppTheme.surface plus semantic .primary/.secondary text, which is
why they were unaffected.

Fix, confined to ActionRunCellView:
- Read @Environment(\.widgetRenderingMode). In .fullColor keep the tag-color
  fill + white text (unchanged). In accented/vibrant, drop the opaque fill to
  a faint translucent tint and render content with the semantic .primary color
  so the system's tint keeps a clear foreground/background contrast.
- No hardcoded .white survives on the tinted path; the container background is
  still owned by widgetAppTheme()'s .containerBackground(for: .widget) (clear
  in non-fullColor so the system glass shows through), so this composes safely
  with iOS 18 home-screen customization.

The Live Activity's white-on-tint icon (HaruDanimWidgets) is intentionally
left as-is — Live Activities always render full color and aren't subject to
home-screen widgetRenderingMode.

Verified in the in-app widget preview with a new DEBUG affordance
(-widgetRenderMode accented|vibrant, added to WidgetPreviewScreen): forcing
accented mode, the Action Run cells now show legible dark icons/labels/values
on a faint tint instead of blank white. Build succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-12 23:35:09 +09:00

200 lines
8.2 KiB
Swift

//
// WidgetPreviewScreen.swift
// Haru_Danim
//
// DEBUG : .
// `-widgetPreview YES`( ) / `-widgetPreview lock`() .
// ( )
//
#if DEBUG
import SwiftUI
import WidgetKit
struct WidgetPreviewScreen: View {
let showLock: Bool
@State private var actionEntry: ActionRunEntry?
@State private var goalBarsEntry: GoalBarsEntry?
@State private var questRingEntry: QuestRingEntry?
@State private var gridEntry: GoalQuestGridEntry?
@State private var statsEntrySmall: StatsChartEntry?
@State private var statsEntryMedium: StatsChartEntry?
@State private var lockEntries: [(String, LockGoalEntry)] = []
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if showLock {
lockSection
} else {
homeSection
}
}
.padding()
}
.background {
Color(white: 0.9).ignoresSafeArea()
}
.onAppear {
load()
// -widgetPreviewScroll B|C|D|S
if let anchor = UserDefaults.standard.string(forKey: "widgetPreviewScroll") {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation { proxy.scrollTo(anchor, anchor: .top) }
}
}
}
}
}
private func load() {
// -premium PremiumGate
_ = PremiumManager.shared
if showLock {
var gauge = LockGoalConfigIntent()
gauge.style = .gauge
var number = LockGoalConfigIntent()
number.style = .number
var dots = LockGoalConfigIntent()
dots.style = .dots
lockEntries = [
("gauge", LockGoalProvider.makeEntry(gauge)),
("number", LockGoalProvider.makeEntry(number)),
("dots", LockGoalProvider.makeEntry(dots)),
]
} else {
actionEntry = ActionRunProvider.makeEntry(ActionRunConfigIntent())
goalBarsEntry = GoalBarsProvider.makeEntry(GoalBarsConfigIntent())
questRingEntry = QuestRingProvider.makeEntry(QuestRingConfigIntent())
gridEntry = GoalQuestGridProvider.makeEntry(GoalQuestGridConfigIntent())
let statsConfig = StatsChartConfigIntent()
statsEntrySmall = StatsChartProvider.makeEntry(statsConfig, family: .systemSmall)
statsEntryMedium = StatsChartProvider.makeEntry(statsConfig, family: .systemMedium)
}
}
// MARK:
@ViewBuilder
private var homeSection: some View {
if let entry = actionEntry {
row("A 행동 실행 · 소형/중형") {
widgetBox(.systemSmall) { ActionRunWidgetView(previewFamily: .systemSmall, entry: entry) }
widgetBox(.systemMedium) { ActionRunWidgetView(previewFamily: .systemMedium, entry: entry) }
}
row("A 행동 실행 · 대형") {
widgetBox(.systemLarge) { ActionRunWidgetView(previewFamily: .systemLarge, entry: entry) }
}
}
if let entry = goalBarsEntry {
row("B 목표 진행률 · 소형/중형") {
widgetBox(.systemSmall) { GoalBarsWidgetView(previewFamily: .systemSmall, entry: entry) }
widgetBox(.systemMedium) { GoalBarsWidgetView(previewFamily: .systemMedium, entry: entry) }
}
.id("B")
}
if let entry = questRingEntry {
row("C 다짐 진행률 · 소형/중형") {
widgetBox(.systemSmall) { QuestRingWidgetView(previewFamily: .systemSmall, entry: entry) }
widgetBox(.systemMedium) { QuestRingWidgetView(previewFamily: .systemMedium, entry: entry) }
}
.id("C")
}
if let entry = gridEntry {
row("D 다짐 현황 · 소형/중형") {
widgetBox(.systemSmall) { GoalQuestGridWidgetView(previewFamily: .systemSmall, entry: entry) }
widgetBox(.systemMedium) { GoalQuestGridWidgetView(previewFamily: .systemMedium, entry: entry) }
}
.id("D")
row("D 다짐 현황 · 대형") {
widgetBox(.systemLarge) { GoalQuestGridWidgetView(previewFamily: .systemLarge, entry: entry) }
}
}
if let small = statsEntrySmall, let medium = statsEntryMedium {
row("통계 · 소형/중형") {
widgetBox(.systemSmall) { StatsChartWidgetView(previewFamily: .systemSmall, entry: small) }
widgetBox(.systemMedium) { StatsChartWidgetView(previewFamily: .systemMedium, entry: medium) }
}
.id("S")
}
}
// MARK:
@ViewBuilder
private var lockSection: some View {
ForEach(lockEntries, id: \.0) { name, entry in
row("잠금화면 · \(name)") {
ZStack {
Color.black
LockGoalWidgetView(previewFamily: .accessoryCircular, entry: entry)
.foregroundStyle(.white)
}
.frame(width: 72, height: 72)
.clipShape(Circle())
ZStack {
Color.black
LockGoalWidgetView(previewFamily: .accessoryRectangular, entry: entry)
.foregroundStyle(.white)
.padding(6)
}
.frame(width: 172, height: 76)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
if let entry = lockEntries.first?.1 {
row("잠금화면 · inline") {
ZStack {
Color.black
LockGoalWidgetView(previewFamily: .accessoryInline, entry: entry)
.foregroundStyle(.white)
}
.frame(width: 230, height: 32)
.clipShape(Capsule())
}
}
}
// MARK:
private func row(_ title: String, @ViewBuilder content: () -> some View) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 12, content: content)
}
}
/// -widgetRenderMode accented|vibrant /
/// white-on-white ( fullColor)
private var forcedRenderingMode: WidgetRenderingMode {
switch UserDefaults.standard.string(forKey: "widgetRenderMode") {
case "accented": return .accented
case "vibrant": return .vibrant
default: return .fullColor
}
}
private func widgetBox(_ family: WidgetFamily, @ViewBuilder content: () -> some View) -> some View {
let size: CGSize = switch family {
case .systemMedium: CGSize(width: 338, height: 158)
case .systemLarge: CGSize(width: 338, height: 354)
default: CGSize(width: 158, height: 158)
}
// containerBackground
// /
// contentMarginsDisabled
return content()
.frame(width: size.width, height: size.height)
.background(AppTheme.background)
.environment(\.colorScheme, WidgetAppearance.appScheme)
.environment(\.widgetRenderingMode, forcedRenderingMode)
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
.shadow(color: .black.opacity(0.08), radius: 6, y: 2)
}
}
#endif