本文来自独立开发者实战记录,使用 ActivityKit + SwiftUI,实现一款可以把简短便签常驻显示在 iPhone 锁屏的工具。 大家平时写备忘录、待办,大多依赖 App 或者桌面小组件。但是桌面 Widget 在锁屏点亮状态默认不会展示,想要看提醒必须解锁手机,很多重要提示就被忽略掉。 iOS 从 iOS16.1 提供 Live‑Activity(实时活动),它不仅用于外卖、球赛,我们也可以拿来做常驻锁屏文字便签。 我基于这套能力开发了「钉字条‑锁屏常驻便签」,本文记录开发踩坑、核心代码、系统限制与产品取舍。 ⚠️注意:Live‑Activity 不是无限常驻,系统有严格生命周期限制,开发前必须先读懂系统约束。 一、基础概念:ActivityKit 核心限制(踩坑重点) 很多新手会误以为 Live‑Activity 可以永久挂在锁屏,实际苹果有硬性规则: 活动活跃最长 8 小时,8 小时后灵动岛自动消失;锁屏界面最多继续保留 4 小时,整体最大 12 小时,之后系统自动销毁。 Live‑Activity 进程是独立沙盒,不能直接发起网络请求;更新数据只能本地 App 调用 update,或者推送通知更新。 不支持动画、Transaction;部分 SwiftUI 修饰符会失效。 需要用户授权实时活动权限,用户可以在设置随时关闭权限。 适合展示简短文本,空间有限,不适合大段长文章。 产品取舍:我的便签 App 不会做 “永久常驻” 的虚假宣传,在 UI 和说明里告诉用户系统时间限制,避免用户预期落差。 二、工程准备 Xcode 新建项目,SwiftUI App。 添加 Target → Widget Extension,勾选 Include Live Activity,会自动生成 LiveActivity 模板代码。 项目开启后台模式不是 Live‑Activity 必须;LiveActivity 更新优先在 App 前台调用。 最低版本设置:iOS 16.1+。 关键结构体说明 ActivityAttributes:静态不变的数据,创建时传入,运行过程不会改变。 ContentState:动态可变内容,用于便签文字、颜色,用来 update 更新。 ActivityContent:包装 state、staleDate 过期时间。 三、定义便签的 Attributes 与 ContentState
import ActivityKit
// MARK: 便签LiveActivity模型
struct NoteActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var noteText: String // 便签正文
var textColorHex: String // 文字颜色
var bgColorHex: String // 背景颜色
}
let noteId: String // 静态:笔记唯一id
}
四、定义锁屏 + 灵动岛 UI(Widget 扩展 Target 内) 这里就是锁屏上展示的 UI,SwiftUI 编写,同时实现锁屏视图与灵动岛各区域视图。
import WidgetKit
import SwiftUI
struct NoteLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: NoteActivityAttributes.self) { context in
// MARK: 锁屏展示UI
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Image(systemName: "note.text")
Text("待机便签")
.font(.caption2)
.fontWeight(.semibold)
.foregroundColor(Color(hex: context.state.textColorHex).opacity(0.5))
}
Text(context.state.noteText)
.font(.system(size: 26, weight: .heavy))
.foregroundColor(Color(hex: context.state.textColorHex))
.lineLimit(nil)
.minimumScaleFactor(0.4)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.padding(12)
.activityBackgroundTint(Color(hex: context.state.bgColorHex))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
// MARK: 灵动岛UI
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Text("钉字条")
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.noteText)
}
} compactLeading: {
Image(systemName: "note.text")
} compactTrailing: {
Text(context.state.noteText.prefix(6))
} minimal: {
Image(systemName: "note.text")
}
}
}
}
五、App 主工程:封装 Activity 管理器
在主 App Target,写管理器,负责检查权限、启动、更新、关闭实时活动。
import ActivityKit
@MainActor
final class NoteActivityManager {
static let shared = NoteActivityManager()
private var currentActivity: Activity<NoteActivityAttributes>?
/// 是否开启权限
var isEnable: Bool {
ActivityAuthorizationInfo().areActivitiesEnabled
}
/// 启动便签活动
func startNoteActivity(noteId: String, text: String, textHex: String, bgHex: String) async throws {
guard isEnable else { throw ActivityError.notAuth }
let attr = NoteActivityAttributes(noteId: noteId)
let state = NoteActivityAttributes.ContentState(
noteText: text,
textColorHex: textHex,
bgColorHex: bgHex
)
let content = ActivityContent(state: state, staleDate: nil)
let activity = try Activity.request(attributes: attr, content: content)
currentActivity = activity
}
/// 更新正在运行的便签内容
func updateNote(text: String, textHex: String, bgHex: String) async {
guard let act = currentActivity else { return }
let newState = NoteActivityAttributes.ContentState(
noteText: text,
textColorHex: textHex,
bgColorHex: bgHex
)
let newContent = ActivityContent(state: newState, staleDate: nil)
await act.update(newContent)
}
/// 结束销毁
func endActivity() async {
guard let act = currentActivity else { return }
await act.end(nil)
currentActivity = nil
}
enum ActivityError: Error {
case notAuth
}
}
六、开发中遇到的坑点总结
- 颜色十六进制解析在 LiveActivity 内崩溃 LiveActivity 渲染进程和主 App 进程隔离,不要写复杂逻辑,颜色解析要简单可靠,不要依赖主 App 全局单例。
- 明明调用 update,锁屏 UI 不刷新
- 检查 ContentState 是否遵守
Codable, Hashable;结构体有成员发生变化才会触发更新。 - Live‑Activity 不能在后台模式频繁更新,尽量在 App 前台执行更新逻辑。
- 测试的时候活动频繁消失 不要忽略系统 12 小时最大生命周期;产品层面做好提示,而不是强行对抗系统规则。
- 提交 App Store 审核
- 必须向用户说明 Live‑Activity 用途,不能滥用实时活动。
- 隐私清单要把 ActivityKit 加入用到的 API 列表。
- iCloud 同步笔记 Live‑Activity 本身不能读 iCloud;笔记同步在主 App 完成,同步完成之后手动调用
update刷新锁屏 UI。
七、产品层面思考:技术的边界在哪里?
很多用户需求 “永久在锁屏显示便签”,但iOS 系统没有开放永久锁屏常驻的 API。Live‑Activity 是目前最接近的方案,但受 12 小时生命周期约束。
所以产品设计要接受现实:
- 把功能定位:短期重要提醒、备考短句、临时清单。
- UI 上提示用户系统的时间限制,避免差评。
- 搭配普通桌面小组件做补充。
基于以上这套技术,我独立开发完成 App「钉字条‑锁屏常驻便签」:一次性买断、无订阅,iCloud 同步,自定义文字与背景颜色。App Store 可搜索:钉字条。
写在最后 Live‑Activity 不只是外卖、赛事工具,普通工具类 App 也可以玩出很多有意思场景。但是开发前一定要吃透系统的沙盒、生命周期、权限约束,不要做超出系统能力的产品预期。 实际效果:
- 线上展示
2.锁屏 即见
3. 灵动岛效果
希望本篇实战记录对你有帮助。