Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions Alarm/Views/TimerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import SwiftUI
import Combine

struct TimerView: View {
private let presets = [60, 5 * 60, 10 * 60, 25 * 60]

@State private var durationSeconds: Int = 300
@State private var remainingSeconds: Int = 300
@State private var isRunning = false
@State private var didComplete = false
private let ticker = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

var body: some View {
Expand All @@ -18,6 +21,12 @@ struct TimerView: View {
.font(.system(size: 68, weight: .light, design: .rounded))
.monospacedDigit()

Text(statusText)
.font(.system(size: 15, weight: .regular))
.foregroundStyle(Color(white: 0.42))

presetRow

Stepper("时长:\(durationSeconds / 60) 分钟", value: $durationSeconds, in: 60 ... 24 * 3600, step: 60)
.font(.system(size: 16, weight: .regular))
.disabled(isRunning)
Expand All @@ -44,33 +53,102 @@ struct TimerView: View {
.padding()
}
.navigationTitle("计时器")
.alert("计时结束", isPresented: $didComplete) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep completion state separate from alert visibility

Using didComplete as the isPresented binding for .alert causes SwiftUI to reset it to false as soon as the user dismisses the alert, so the timer immediately leaves the completed state and falls through to the paused copy (remainingSeconds == 0 but didComplete == false). This makes the new completion status non-persistent and misleading right after acknowledgement; use a separate showCompletionAlert flag so completion status can remain true until reset/restart.

Useful? React with 👍 / 👎.

Button("确定", role: .cancel) {}
} message: {
Text("本轮 \(durationSummary(durationSeconds)) 已完成。")
}
.onReceive(ticker) { _ in
guard isRunning else { return }
if remainingSeconds > 0 {
if remainingSeconds > 1 {
remainingSeconds -= 1
} else {
stop()
return
}

complete()
}
}
.preferredColorScheme(.light)
}

private func start() {
guard remainingSeconds > 0 else { return }
didComplete = false
isRunning = true
}

private func stop() {
isRunning = false
}

private func complete() {
stop()
remainingSeconds = 0
didComplete = true
}

private func format(_ seconds: Int) -> String {
let h = seconds / 3600
let m = (seconds % 3600) / 60
let s = seconds % 60
return String(format: "%02d:%02d:%02d", h, m, s)
}

private var presetRow: some View {
HStack(spacing: 10) {
ForEach(presets, id: \.self) { preset in
Button {
applyPreset(preset)
} label: {
Text(durationSummary(preset))
.font(.system(size: 14, weight: .medium))
.foregroundStyle(durationSeconds == preset ? .white : Color(white: 0.2))
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
Capsule()
.fill(durationSeconds == preset ? Color.orange : Color.white)
)
}
.buttonStyle(.plain)
.disabled(isRunning)
.opacity(isRunning ? 0.45 : 1)
}
}
}

private var statusText: String {
if didComplete {
return "本轮倒计时已完成"
}
if isRunning {
return "进行中 · 剩余 \(durationSummary(remainingSeconds))"
}
if remainingSeconds == durationSeconds {
return "选择常用时长后即可开始"
}
return "已暂停,可继续或重置"
}

private func applyPreset(_ preset: Int) {
guard !isRunning else { return }
durationSeconds = preset
remainingSeconds = preset
didComplete = false
}

private func durationSummary(_ seconds: Int) -> String {
if seconds < 3600 {
return "\(seconds / 60) 分钟"
}

let hours = seconds / 3600
let minutes = (seconds % 3600) / 60
if minutes == 0 {
return "\(hours) 小时"
}
return "\(hours) 小时 \(minutes) 分钟"
}
}

#Preview {
Expand Down