以下是一个完整的 Xcode 项目模板,展示了如何使用 SwiftUI、AppIntent 和 Live Activity 来实现一个计时器应用。用户可以通过 Siri 命令“开始长休息”来启动计时器,并在锁屏、动态岛等界面实时查看倒计时。
📁 项目结构概览 #
Main App Target:包含 SwiftUI 主界面和 AppIntent 实现。
Widget Extension:用于展示 Live Activity,包括锁屏和动态岛界面。
共享代码:如计时器类型定义、管理器等,供主应用和扩展使用。
🛠️ 主要功能实现 #
1. 定义计时器类型 #
enum TimerType: String, CaseIterable, Codable {
case focus = "专注"
case shortBreak = "短休息"
case longBreak = "长休息"
var duration: TimeInterval {
switch self {
case .focus: return 25 * 60
case .shortBreak: return 5 * 60
case .longBreak: return 15 * 60
}
}
}
2. 实现 AppIntent #
import AppIntents
struct StartTimerIntent: AppIntent {
static var title: LocalizedStringResource = "开始计时器"
static var description = IntentDescription("开始一个指定类型的计时器")
@Parameter(title: "计时器类型")
var timerType: TimerType
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
TimerManager.shared.start(type: timerType)
return .result(dialog: "已开始\(timerType.rawValue)计时")
}
}
3. 启动计时器并触发 Live Activity #
final class TimerManager {
static let shared = TimerManager()
func start(type: TimerType) {
let endTime = Date().addingTimeInterval(type.duration)
LiveActivityManager.shared.startActivity(for: type, endTime: endTime)
}
}
4. 管理 Live Activity #
import ActivityKit
final class LiveActivityManager {
static let shared = LiveActivityManager()
private var currentActivity: Activity<TimerActivityAttributes>?
func startActivity(for type: TimerType, endTime: Date) {
let attributes = TimerActivityAttributes(title: type.rawValue)
let state = TimerActivityAttributes.ContentState(endTime: endTime)
do {
currentActivity = try Activity<TimerActivityAttributes>.request(
attributes: attributes,
contentState: state,
pushType: nil
)
} catch {
print("启动 Live Activity 失败: \(error)")
}
}
func stopActivity() {
Task {
await currentActivity?.end(dismissalPolicy: .immediate)
}
}
}
5. 定义 Live Activity 的 UI #
import ActivityKit
import WidgetKit
import SwiftUI
struct TimerWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerActivityAttributes.self) { context in
VStack {
Text(context.attributes.title)
.font(.headline)
CountdownView(to: context.state.endTime)
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
VStack {
Text(context.attributes.title)
.font(.headline)
CountdownView(to: context.state.endTime)
}
}
} compactLeading: {
Text("⏱")
} compactTrailing: {
CountdownView(to: context.state.endTime)
.font(.caption)
} minimal: {
Text("⏱")
}
}
}
}
🧪 测试方法 #
使用 Siri:对设备说“嘿 Siri,开始长休息”。
快捷指令:在快捷指令应用中找到“开始计时器”操作。
Spotlight 搜索:在主屏幕下拉搜索“开始长休息”。
🧨 1. 添加通知功能(倒计时结束时提醒) #
📌 功能目标: #
计时结束时,自动推送通知提醒用户(即使 App 被杀掉或未激活)。
✅ 实现步骤: #
① 申请通知权限(App 启动时) #
import UserNotifications
func requestNotificationPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if granted {
print("通知权限已授予")
} else {
print("用户拒绝通知权限")
}
}
}
在
App
的init()
或onAppear
中调用。
② 发送通知 #
func scheduleEndNotification(for type: TimerType, at endTime: Date) {
let content = UNMutableNotificationContent()
content.title = "\(type.rawValue) 结束啦!"
content.body = "你的 \(type.rawValue) 计时已经完成。"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: endTime.timeIntervalSinceNow, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
✅ 在 TimerManager.start(type:)
中添加:
#
let endTime = Date().addingTimeInterval(type.duration)
scheduleEndNotification(for: type, at: endTime)
LiveActivityManager.shared.startActivity(for: type, endTime: endTime)
⚙️ 2. 支持后台运行(确保计时器/通知/Live Activity 在后台也能运行) #
iOS 限制较多,但我们能确保:
✅ 你能依靠以下机制: #
功能 | 是否后台可用 | 说明 |
---|---|---|
通知 | ✅ 可后台发送 | 完全支持 |
Live Activity | ✅ 自动后台更新 | 系统托管,后台更新稳定 |
App 本体 Timer | ❌ 被杀就停 | 建议不要依赖 Timer ,而是使用 Date 比较时间 |
✅ 推荐计时方式(与后台兼容): #
struct TimerRecord {
let start: Date
let end: Date
var remaining: TimeInterval {
max(0, end.timeIntervalSinceNow)
}
}
这样无论 App 是否活跃,你都可以随时重新加载 Date
来确定剩余时间。
🎨 3. 自定义 UI(Live Activity + SwiftUI 页面) #
🔹 自定义锁屏 & 动态岛 UI #
你可以在 TimerWidgetLiveActivity.swift
中完全自定义:
ActivityConfiguration(for: TimerActivityAttributes.self) { context in
VStack(spacing: 12) {
Text("🧘♀️ \(context.attributes.title)")
.font(.title2.bold())
CountdownView(to: context.state.endTime)
.font(.system(size: 36, weight: .semibold, design: .rounded))
ProgressView(value: progress(from: context.state.endTime))
.progressViewStyle(.linear)
.tint(.green)
}
.padding()
}
动态岛也可设计成圆形/小组件形式: #
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
}
DynamicIslandExpandedRegion(.trailing) {
CountdownView(to: context.state.endTime)
}
🧩 进阶建议(可选) #
功能 | 实现方式 |
---|---|
✅ Live Activity 停止后触发自动记录 | 在后台延迟结束后写入 UserDefaults |
✅ 多个计时器并行支持 | 多个 Live Activity(仅 iOS 17 起支持)或内部管理数组 |
✅ 历史记录 UI | 使用 SwiftUI 列表 + Core Data 或 AppStorage |
完整代码 #
明白了!下面是这个进阶版 Siri + Live Activity 计时器项目的完整结构和实现说明,包含:
✅ SwiftUI 主界面(可选)
✅ AppIntent + Siri 启动
✅ Live Activity(支持锁屏、动态岛)
✅ 通知(计时结束提醒)
✅ 后台兼容(通过时间戳比对)
✅ 自定义 UI(倒计时 + 进度条 + 图标)
📁 项目结构(文件分布) #
TimerApp/
├── TimerAppApp.swift // App 启动入口
├── TimerManager.swift // 管理计时器逻辑(Siri 调用核心)
├── NotificationHelper.swift // 通知权限申请 + 推送封装
├── LiveActivityManager.swift // 启动 / 结束 Live Activity
├── Intents/
│ └── StartTimerIntent.swift // AppIntent(Siri 命令)
├── Models/
│ └── TimerType.swift // 计时器类型定义
├── Views/
│ └── CountdownView.swift // 倒计时显示组件
└── TimerWidgetExtension/
├── TimerActivityAttributes.swift
├── TimerWidgetLiveActivity.swift
└── (Info.plist + 启动代码等)
🔧 核心实现 #
✅ 1. TimerType.swift #
enum TimerType: String, CaseIterable, AppEnum {
case focus = "专注"
case shortBreak = "短休息"
case longBreak = "长休息"
var duration: TimeInterval {
switch self {
case .focus: return 25 * 60
case .shortBreak: return 5 * 60
case .longBreak: return 15 * 60
}
}
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "计时器类型")
static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
.focus: "专注",
.shortBreak: "短休息",
.longBreak: "长休息"
]
}
✅ 2. AppIntent(StartTimerIntent.swift) #
struct StartTimerIntent: AppIntent {
static var title: LocalizedStringResource = "开始计时器"
@Parameter(title: "计时器类型")
var timerType: TimerType
static var openAppWhenRun: Bool = false
func perform() async throws -> some IntentResult {
TimerManager.shared.start(type: timerType)
return .result(dialog: "已开始 \(timerType.rawValue)")
}
}
✅ 3. TimerManager.swift #
final class TimerManager {
static let shared = TimerManager()
func start(type: TimerType) {
let endTime = Date().addingTimeInterval(type.duration)
LiveActivityManager.shared.startActivity(for: type, endTime: endTime)
NotificationHelper.scheduleNotification(for: type, at: endTime)
print("⏱ 启动 \(type.rawValue) 倒计时,结束时间: \(endTime)")
}
}
✅ 4. NotificationHelper.swift #
import UserNotifications
enum NotificationHelper {
static func requestPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
if granted {
print("✅ 通知权限已授权")
}
}
}
static func scheduleNotification(for type: TimerType, at date: Date) {
let content = UNMutableNotificationContent()
content.title = "\(type.rawValue) 结束啦!"
content.body = "你的 \(type.rawValue) 计时已完成。"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: date.timeIntervalSinceNow, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
}
✅ 5. LiveActivityManager.swift #
import ActivityKit
final class LiveActivityManager {
static let shared = LiveActivityManager()
private var currentActivity: Activity<TimerActivityAttributes>?
func startActivity(for type: TimerType, endTime: Date) {
let attributes = TimerActivityAttributes(title: type.rawValue)
let state = TimerActivityAttributes.ContentState(endTime: endTime)
do {
currentActivity = try Activity.request(
attributes: attributes,
contentState: state
)
} catch {
print("❌ Live Activity 启动失败: \(error)")
}
}
func stopActivity() {
Task {
await currentActivity?.end(dismissalPolicy: .immediate)
}
}
}
✅ 6. TimerActivityAttributes.swift #
import ActivityKit
struct TimerActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var endTime: Date
}
var title: String
}
✅ 7. TimerWidgetLiveActivity.swift(UI 自定义) #
struct TimerWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerActivityAttributes.self) { context in
VStack(spacing: 10) {
Text("🧘♀️ \(context.attributes.title)")
.font(.headline)
CountdownView(to: context.state.endTime)
.font(.largeTitle.bold())
ProgressView(value: progress(to: context.state.endTime))
.progressViewStyle(.linear)
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
VStack {
Text(context.attributes.title)
CountdownView(to: context.state.endTime)
}
}
} compactLeading: {
Text("⏱")
} compactTrailing: {
CountdownView(to: context.state.endTime)
} minimal: {
Text("⏱")
}
}
}
private func progress(to end: Date) -> Double {
let total = end.timeIntervalSinceNow
let passed = max(0, 1 - total / 900.0) // 假设最长为15分钟
return passed
}
}
✅ 8. CountdownView.swift #
struct CountdownView: View {
let to: Date
@State private var now = Date()
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(remainingTime)
.onReceive(timer) { _ in now = Date() }
}
var remainingTime: String {
let diff = max(0, Int(to.timeIntervalSince(now)))
let minutes = diff / 60
let seconds = diff % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
🧪 测试方式 #
打开 App 或快捷指令,或用 Siri 说:
开始长休息
检查:
动态岛是否弹出
通知是否在计时结束后触发
后台是否能恢复倒计时