mycode/myApp/HaruDanim/IOS/Views/GoalViews.swift
songyc macbook 6880983b54 feat(watch): per-goal opt-in filter for complication list
- 목표 편집에 '애플워치에서 보기' 토글 추가 (iPhone 전용 — 워치가 iPhone과 페어링되므로 iPad에서는 숨김·무의미, 기본 꺼짐) — 목표·다짐이 늘면 컴플리케이션 목록이 폭증하는 문제의 옵트인 해법 (사용자 요청)
- 저장은 기기 로컬 LocalPrefs `local.watchGoals` (§15-3 노출 설정 원칙, pinnedGoals 선례) — CloudKit 스키마 무변경·페이로드 무변경으로 리스크 최소화
- 필터는 WatchSyncManager.makeSnapshot 한 곳: 스냅숏의 목표 = 진행 중 ∩ 체크됨 → 목록(recommendations)·표시 모두 자동 반영, 완료 목표는 기존 진행 중 조건으로 자동 제외. 행동·측정 중 상태(워치 앱 화면·현재 현황·iPad 기록의 동기화 경로)는 필터와 무관 — 지연 추가 0
- 검증(페어링 시뮬레이터 E2E): -watchGoals 1 → 워치 컴플리케이션에 토익 목표+영어 공부 다짐만 표시, -watchGoals 0 → '목표 없음'/'다짐 없음' 빈 상태로 우아하게 처리(크래시 없음), 현재 현황(독서 타이머)은 필터와 무관하게 정상 수신. 아이폰 편집기 토글 렌더 확인. Debug·Store·워치 3스킴 빌드 성공
- 도움말 컴플리케이션 항목 갱신 + 신규 문구 en/ja 완비(카탈로그 766키 클린), §14에 -watchGoals 인자, §5.2/§6.4/§11 문서화
- 주의: 기본 꺼짐이라 업데이트 직후 기존에 페이스에 올린 목표·다짐 컴플리케이션은 빈 상태가 됨 — 목표 편집에서 원하는 목표를 한 번 켜 주면 복구

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-16 23:48:59 +09:00

773 lines
31 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// GoalViews.swift
// Haru_Danim
//
// : // + (CLAUDE.md §6.4)
//
import SwiftUI
import SwiftData
// MARK: -
struct GoalListView: View {
@Environment(\.modelContext) private var context
@Query(sort: [SortDescriptor(\Goal.sortOrder), SortDescriptor(\Goal.createdAt)]) private var goals: [Goal]
private let premium = PremiumManager.shared
@State private var showingAdd: Bool = {
#if DEBUG
// : -goalShowEditor YES
if UserDefaults.standard.bool(forKey: "goalShowEditor") { return true }
#endif
return false
}()
@State private var showLimitAlert = false
@State private var showingPremiumSheet = false
@State private var isReordering = false
@State private var editMode: EditMode = .inactive
#if DEBUG
/// (-questShowEditor): ( , )
@State private var debugEditingQuest: Quest?
#endif
// (CloudKit , LocalPrefs )
@AppStorage(LocalPrefsKeys.collapsedGoals, store: AppGroup.defaults) private var collapsedGoalsRaw = ""
/// ( )
private var activeGoals: [Goal] { goals.filter { $0.status == .inProgress } }
/// /
private var finishedGoals: [Goal] { goals.filter { $0.status != .inProgress } }
var body: some View {
#if DEBUG
// : -goalShowFinished YES ( )
if UserDefaults.standard.bool(forKey: "goalShowFinished") {
FinishedGoalListView()
} else {
goalList
}
#else
goalList
#endif
}
private var goalList: some View {
ScrollViewReader { proxy in
goalListContent(proxy: proxy)
}
}
private func goalListContent(proxy: ScrollViewProxy) -> some View {
List {
if isReordering {
Section {
ForEach(activeGoals) { goal in
reorderRow(goal)
}
.onMove(perform: moveGoals)
} footer: {
Text("오른쪽 핸들을 끌어서 순서를 바꾼 뒤 ‘완료’를 누르세요. 순서는 저장되어 유지돼요.")
}
} else {
ForEach(activeGoals) { goal in
Section {
NavigationLink {
GoalDetailView(goal: goal)
} label: {
GoalRow(goal: goal)
}
if !LocalPrefs.contains(goal.uuid, in: collapsedGoalsRaw) {
ForEach(goal.sortedQuests) { quest in
QuestRow(quest: quest)
}
}
} header: {
if !goal.quests.isEmpty {
collapseHeader(goal)
}
}
}
if activeGoals.isEmpty {
ContentUnavailableView(
"진행 중인 목표가 없어요",
systemImage: "flag.checkered",
description: Text(goals.isEmpty
? "큰 목표를 세우고, 그 안에 다짐을 추가해 보세요."
: "새 목표를 세우거나, 아래에서 완료된 목표를 확인해 보세요.")
)
}
if !finishedGoals.isEmpty {
Section {
NavigationLink {
FinishedGoalListView()
} label: {
HStack {
Label("완료된 목표 보기", systemImage: "checkmark.seal")
.foregroundStyle(.primary)
Spacer()
Text("\(finishedGoals.count)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.id("finishedLink")
}
}
}
}
.scrollContentBackground(.hidden)
.background(AppTheme.background)
.navigationTitle("목표")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if activeGoals.count > 1 {
Button(isReordering ? String(localized: "완료") : String(localized: "순서")) {
withAnimation {
isReordering.toggle()
editMode = isReordering ? .active : .inactive
}
}
}
}
ToolbarItem(placement: .topBarTrailing) {
if !isReordering {
Button {
if !premium.canAddGoal(currentCount: goals.count) {
showLimitAlert = true
} else {
showingAdd = true
}
} label: {
Image(systemName: "plus")
}
.accessibilityLabel(Text("목표 추가"))
}
}
}
.environment(\.editMode, $editMode)
.sheet(isPresented: $showingAdd) {
GoalEditorView(goal: nil)
}
#if DEBUG
.sheet(item: $debugEditingQuest) { quest in
if let goal = quest.goal {
QuestEditorView(goal: goal, quest: quest)
}
}
#endif
.alert("무료 사용 한도", isPresented: $showLimitAlert) {
Button("프리미엄 알아보기") { showingPremiumSheet = true }
Button("확인", role: .cancel) {}
} message: {
Text("무료 버전에서는 목표를 최대 \(FreeLimits.goals)개까지 만들 수 있어요. 프리미엄으로 업그레이드하면 무제한으로 사용할 수 있습니다.")
}
.sheet(isPresented: $showingPremiumSheet) {
PremiumSheetView()
}
.onAppear {
let math = DayMath()
var statusChanged = false
for goal in goals {
let before = goal.status
goal.evaluateIfEnded(math: math)
if goal.status != before { statusChanged = true }
}
if statusChanged { DataChange.commit(context: context) }
#if DEBUG
// : -goalReorder YES , -goalScrollBottom YES
if UserDefaults.standard.bool(forKey: "goalReorder") {
isReordering = true
editMode = .active
}
if UserDefaults.standard.bool(forKey: "goalScrollBottom") {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation { proxy.scrollTo("finishedLink", anchor: .bottom) }
}
}
// : -questShowEditor YES ( )
if UserDefaults.standard.bool(forKey: "questShowEditor") {
let quests = goals.flatMap(\.sortedQuests)
debugEditingQuest = quests.first { $0.deadlineMinutes >= 0 } ?? quests.first
}
#endif
}
}
///
private func reorderRow(_ goal: Goal) -> some View {
HStack(spacing: 10) {
Image(systemName: goal.symbolName)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 28, height: 28)
.background(goal.color, in: RoundedRectangle(cornerRadius: 7, style: .continuous))
Text(goal.title)
.font(.subheadline.weight(.medium))
.lineLimit(1)
}
}
private func moveGoals(from source: IndexSet, to destination: Int) {
var ordered = activeGoals
ordered.move(fromOffsets: source, toOffset: destination)
for (index, goal) in ordered.enumerated() {
goal.sortOrder = index
}
DataChange.commit(context: context)
}
/// /
private func collapseHeader(_ goal: Goal) -> some View {
let collapsed = LocalPrefs.contains(goal.uuid, in: collapsedGoalsRaw)
return Button {
withAnimation(.spring(duration: 0.3)) {
collapsedGoalsRaw = LocalPrefs.toggling(goal.uuid, in: collapsedGoalsRaw)
}
} label: {
HStack(spacing: 4) {
Text("다짐 \(goal.quests.count)")
Image(systemName: collapsed ? "chevron.down" : "chevron.up")
.font(.caption2.weight(.semibold))
}
.font(.caption)
.foregroundStyle(AppTheme.green)
}
.buttonStyle(.plain)
.textCase(nil)
}
}
// MARK: - ( )
struct GoalRow: View {
@Environment(\.modelContext) private var context
let goal: Goal
@State private var showingConfirm = false
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 10) {
Image(systemName: goal.symbolName)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 34, height: 34)
.background(goal.color, in: RoundedRectangle(cornerRadius: 9, style: .continuous))
VStack(alignment: .leading, spacing: 2) {
Text(goal.title)
.font(.body.weight(.semibold))
Text(periodLabel)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
statusBadge
}
if goal.needsConfirmation() {
Button {
showingConfirm = true
} label: {
Label("달성했나요? 탭해서 선택", systemImage: "questionmark.circle.fill")
.font(.subheadline.weight(.semibold))
.foregroundStyle(AppTheme.yellow)
}
.buttonStyle(.plain)
} else if goal.status == .inProgress, let progress = goal.dateProgress() {
VStack(alignment: .leading, spacing: 3) {
ProgressView(value: progress)
.tint(goal.color)
Text("기간 진행률 \(Format.percent(progress))")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 2)
.alert("목표 확인", isPresented: $showingConfirm) {
Button("달성했어요") {
goal.status = .achieved
DataChange.commit(context: context)
}
Button("달성하지 못했어요", role: .destructive) {
goal.status = .notAchieved
DataChange.commit(context: context)
}
Button("취소", role: .cancel) {}
} message: {
Text("\(goal.title) 목표를 달성했나요?")
}
}
private var periodLabel: String {
if let end = goal.endDate {
return "\(Format.shortDate(goal.startDate)) ~ \(Format.shortDate(end))"
}
return String(localized: "\(Format.shortDate(goal.startDate)) 시작 · 종료일 없음")
}
@ViewBuilder
private var statusBadge: some View {
let (text, color): (String, Color) = {
if goal.needsConfirmation() { return (String(localized: "확인 필요"), AppTheme.yellow) }
switch goal.status {
case .inProgress: return (String(localized: "진행 중"), AppTheme.green)
case .achieved: return (String(localized: "달성"), AppTheme.green)
case .notAchieved: return (String(localized: "미달성"), Color.secondary)
}
}()
Text(text)
.font(.caption2.weight(.bold))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(color.opacity(0.15), in: Capsule())
.foregroundStyle(color)
}
}
// MARK: - (// )
struct QuestRow: View {
let quest: Quest
/// (1 )
private var streakLabel: String? {
guard let streak = QuestProgress(quest: quest).streak(), streak.count > 0 else { return nil }
return streak.label
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Image(systemName: quest.targetSymbol)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 24, height: 24)
.background(quest.targetColor, in: RoundedRectangle(cornerRadius: 6, style: .continuous))
Text(quest.targetName)
.font(.subheadline.weight(.medium))
if let streakLabel {
HStack(spacing: 2) {
Image(systemName: "flame.fill")
.font(.system(size: 9, weight: .bold))
Text(streakLabel)
.font(.caption2.weight(.semibold))
}
.foregroundStyle(AppTheme.yellow)
.lineLimit(1)
.layoutPriority(1)
}
Spacer()
Text("\(quest.scheduleLabel) · \(quest.targetValueLabel) \(quest.direction.label)")
.font(.caption2)
.foregroundStyle(.secondary)
}
HStack(spacing: 10) {
ForEach(StatSpan.allCases) { span in
spanGauge(span)
}
}
}
.padding(.leading, 8)
.padding(.vertical, 2)
}
private func spanGauge(_ span: StatSpan) -> some View {
let progress = QuestProgress(quest: quest)
// (· , )
// / " "
let restDay = span == .day && !progress.isScheduled(on: .now)
let result = progress.spanProgress(span)
let over = !restDay && quest.direction == .atMost && result.value > result.target
return VStack(alignment: .leading, spacing: 2) {
Text(span.label)
.font(.caption2)
.foregroundStyle(.secondary)
ProgressView(value: restDay ? 0 : result.ratio)
.tint(over ? .red : quest.targetColor)
.opacity(restDay ? 0.4 : 1)
if restDay {
Text("수행일 아님")
.font(.caption2)
.foregroundStyle(.tertiary)
} else {
Text(Format.percent(result.displayRatio))
.font(.caption2.weight(.semibold).monospacedDigit())
.foregroundStyle(over ? .red : .primary)
}
}
.frame(maxWidth: .infinity)
}
}
// MARK: - (/ )
struct FinishedGoalListView: View {
@Query(
filter: #Predicate<Goal> { $0.statusRaw != "inProgress" },
sort: \Goal.createdAt, order: .reverse
) private var goals: [Goal]
var body: some View {
List {
ForEach(goals) { goal in
Section {
NavigationLink {
GoalDetailView(goal: goal)
} label: {
GoalRow(goal: goal)
}
}
}
if goals.isEmpty {
ContentUnavailableView(
"완료된 목표가 없어요",
systemImage: "checkmark.seal",
description: Text("달성하거나 종료한 목표가 여기에 모여요.")
)
}
}
.scrollContentBackground(.hidden)
.background(AppTheme.background)
.navigationTitle("완료된 목표")
.navigationBarTitleDisplayMode(.inline)
.toolbar(.hidden, for: .tabBar)
}
}
// MARK: -
struct GoalDetailView: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
private let premium = PremiumManager.shared
let goal: Goal
@State private var showingEdit = false
@State private var showingDelete = false
@State private var showingAddQuest = false
@State private var showQuestLimitAlert = false
@State private var showingPremiumSheet = false
@State private var showingManualFinish = false
@State private var askManualResult = false
@State private var editingQuest: Quest?
@State private var editMode: EditMode = .inactive
var body: some View {
List {
Section("목표") {
GoalRow(goal: goal)
}
Section {
ForEach(goal.sortedQuests) { quest in
Button {
editingQuest = quest
} label: {
QuestRow(quest: quest)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions {
Button("삭제", role: .destructive) {
context.delete(quest)
DataChange.commit(context: context)
}
}
}
.onMove { source, destination in
var ordered = goal.sortedQuests
ordered.move(fromOffsets: source, toOffset: destination)
for (index, quest) in ordered.enumerated() {
quest.sortOrder = index
}
DataChange.commit(context: context)
}
Button {
if !premium.canAddQuest(currentCount: goal.quests.count) {
showQuestLimitAlert = true
} else {
showingAddQuest = true
}
} label: {
Label("다짐 추가", systemImage: "plus.circle")
.foregroundStyle(AppTheme.green)
}
} header: {
Text("다짐")
} footer: {
if goal.quests.isEmpty {
Text("이 목표를 이루기 위한 다짐(행동/꼬리표 + 주기 + 목표량)을 추가하세요.")
} else {
Text("오른쪽 위 ‘순서’를 누르면 다짐 순서를 바꿀 수 있어요.")
}
}
if goal.status == .inProgress && goal.endDate == nil {
Section {
Button("목표 수동 종료") {
showingManualFinish = true
}
.foregroundStyle(AppTheme.yellow)
} footer: {
if goal.achieveThresholdPercent < 100 {
Text("종료 시 다짐 평균 달성률이 \(goal.achieveThresholdPercent)% 이상이면 달성으로 판정돼요.")
} else {
Text("종료 시 하위 다짐들의 달성 여부에 따라 달성/미달성이 결정돼요.")
}
}
}
Section {
Button("목표 삭제", role: .destructive) {
showingDelete = true
}
}
}
.scrollContentBackground(.hidden)
.background(AppTheme.background)
.navigationTitle(goal.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar(.hidden, for: .tabBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if !goal.quests.isEmpty {
Button(editMode == .active ? String(localized: "완료") : String(localized: "순서")) {
withAnimation {
editMode = editMode == .active ? .inactive : .active
}
}
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("수정") { showingEdit = true }
}
}
.environment(\.editMode, $editMode)
.sheet(isPresented: $showingEdit) {
GoalEditorView(goal: goal)
}
.sheet(isPresented: $showingAddQuest) {
QuestEditorView(goal: goal, quest: nil)
}
.sheet(item: $editingQuest) { quest in
QuestEditorView(goal: goal, quest: quest)
}
.alert("무료 사용 한도", isPresented: $showQuestLimitAlert) {
Button("프리미엄 알아보기") { showingPremiumSheet = true }
Button("확인", role: .cancel) {}
} message: {
Text("무료 버전에서는 목표당 다짐을 최대 \(FreeLimits.questsPerGoal)개까지 만들 수 있어요. 프리미엄으로 업그레이드하면 무제한으로 사용할 수 있습니다.")
}
.sheet(isPresented: $showingPremiumSheet) {
PremiumSheetView()
}
.alert("목표 수동 종료", isPresented: $showingManualFinish) {
Button("종료") {
if goal.manualFinish() == nil {
askManualResult = true
} else {
DataChange.commit(context: context)
}
}
Button("취소", role: .cancel) {}
} message: {
Text("목표를 지금 종료할까요?")
}
.alert("목표 확인", isPresented: $askManualResult) {
Button("달성했어요") {
goal.status = .achieved
DataChange.commit(context: context)
}
Button("달성하지 못했어요", role: .destructive) {
goal.status = .notAchieved
DataChange.commit(context: context)
}
Button("취소", role: .cancel) {}
} message: {
Text("다짐이 없는 목표예요. 달성했나요?")
}
.alert("목표 삭제", isPresented: $showingDelete) {
Button("삭제", role: .destructive) {
context.delete(goal)
DataChange.commit(context: context)
dismiss()
}
Button("취소", role: .cancel) {}
} message: {
Text("\(goal.title) 목표를 삭제할까요? 다짐도 함께 삭제됩니다.")
}
}
}
// MARK: - /
struct GoalEditorView: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
/// nil
let goal: Goal?
@State private var title = ""
@State private var symbolName = "flag.fill"
@State private var color: Color = Color(hex: "#2F6B4F")
@State private var startDate: Date = .now
@State private var hasEndDate = false
@State private var endDate: Date = .now
@State private var achieveThreshold = 100
/// (LocalPrefs),
@State private var showsOnWatch = false
@State private var showingSymbolPicker = false
var body: some View {
NavigationStack {
Form {
Section("목표 내용") {
TextField("예: 토익 700점 이상 받기", text: $title, axis: .vertical)
}
Section("아이콘과 색") {
Button {
showingSymbolPicker = true
} label: {
HStack {
Image(systemName: symbolName)
.font(.system(size: 18))
.foregroundStyle(.white)
.frame(width: 34, height: 34)
.background(color, in: RoundedRectangle(cornerRadius: 9))
Text("아이콘 선택")
.foregroundStyle(.primary)
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.tertiary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
ColorPicker("", selection: $color, supportsOpacity: false)
}
Section("기간") {
DatePicker("시작일 (과거 가능)", selection: $startDate, displayedComponents: .date)
Toggle("종료일 지정", isOn: $hasEndDate.animation())
if hasEndDate {
DatePicker(
"종료일",
selection: $endDate,
in: startDate...,
displayedComponents: .date
)
}
}
Section {
HStack {
Text("다짐 평균 달성률")
Spacer()
Text(verbatim: "\(achieveThreshold)%")
.font(.body.weight(.semibold).monospacedDigit())
.foregroundStyle(achieveThreshold == 100 ? Color.secondary : AppTheme.green)
}
Slider(
value: Binding(
get: { Double(achieveThreshold) },
set: { achieveThreshold = Int($0.rounded()) }
),
in: 10...100,
step: 5
)
} header: {
Text("달성 판정 기준")
} footer: {
Text("목표가 끝날 때 다짐들의 평균 달성률이 이 값 이상이면 '달성 완료'로 판정해요. 100%면 모든 다짐을 채워야 달성이에요. 다짐이 없는 목표는 지금처럼 직접 선택해요.")
}
// iPhone iPad
if !DeviceLayout.isPad {
Section {
Toggle(isOn: $showsOnWatch) {
Label {
Text("애플워치에서 보기")
} icon: {
Image(systemName: "applewatch")
.foregroundStyle(AppTheme.green)
}
}
} header: {
Text("애플워치")
} footer: {
Text("켜면 이 목표와 소속 다짐들의 진행률이 애플워치 컴플리케이션 목록에 나타나요. 완료된 목표는 목록에서 자동으로 사라져요. 이 설정은 워치와 연결된 이 아이폰에만 저장돼요.")
}
}
}
.navigationTitle(goal == nil ? String(localized: "목표 추가") : String(localized: "목표 수정"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("취소") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("저장") { save() }
.disabled(title.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
.sheet(isPresented: $showingSymbolPicker) {
SymbolPickerView(selection: $symbolName)
}
.onAppear(perform: load)
}
}
private func load() {
guard let goal else { return }
title = goal.title
symbolName = goal.symbolName
color = goal.color
startDate = goal.startDate
achieveThreshold = goal.achieveThresholdPercent
showsOnWatch = LocalPrefs.showsOnWatch(goal.uuid)
if let end = goal.endDate {
hasEndDate = true
endDate = end
}
}
private func save() {
let trimmed = title.trimmingCharacters(in: .whitespaces)
if let goal {
let previousEndDate = goal.endDate
goal.title = trimmed
goal.symbolName = symbolName
goal.colorHex = color.hexString
goal.startDate = startDate
goal.endDate = hasEndDate ? endDate : nil
goal.achieveThresholdPercent = achieveThreshold
// CloudKit (LocalPrefs )
LocalPrefs.setShowsOnWatch(goal.uuid, enabled: showsOnWatch)
if goal.status == .inProgress {
//
// ( evaluateIfEnded )
goal.evaluateIfEnded()
} else if previousEndDate != goal.endDate {
// :
// / ' ' ,
goal.status = .inProgress
goal.evaluateIfEnded()
}
} else {
let newGoal = Goal(
title: trimmed,
symbolName: symbolName,
colorHex: color.hexString,
startDate: startDate,
endDate: hasEndDate ? endDate : nil
)
newGoal.achieveThresholdPercent = achieveThreshold
//
let existing = (try? context.fetch(FetchDescriptor<Goal>())) ?? []
newGoal.sortOrder = (existing.map(\.sortOrder).max() ?? -1) + 1
context.insert(newGoal)
LocalPrefs.setShowsOnWatch(newGoal.uuid, enabled: showsOnWatch)
}
DataChange.commit(context: context)
dismiss()
}
}