SiriKit — 完整示例
This article is extracted from the chat log with AI. Please identify it with caution.

以下是一个完整的 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("⏱")
            }
        }
    }
}

🧪 测试方法 #

  1. 使用 Siri:对设备说“嘿 Siri,开始长休息”。

  2. 快捷指令:在快捷指令应用中找到“开始计时器”操作。

  3. Spotlight 搜索:在主屏幕下拉搜索“开始长休息”。


🧨 1. 添加通知功能(倒计时结束时提醒) #

📌 功能目标: #

计时结束时,自动推送通知提醒用户(即使 App 被杀掉或未激活)。

✅ 实现步骤: #

① 申请通知权限(App 启动时) #

import UserNotifications

func requestNotificationPermission() {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
        if granted {
            print("通知权限已授予")
        } else {
            print("用户拒绝通知权限")
        }
    }
}

Appinit()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)
    }
}

🧪 测试方式 #

  1. 打开 App 或快捷指令,或用 Siri 说:

    开始长休息
  2. 检查:

    • 动态岛是否弹出

    • 通知是否在计时结束后触发

    • 后台是否能恢复倒计时

本文共 2519 字,创建于 May 22, 2025
相关标签: Xcode, ByAI, SwiftUI