起点
我在 App Store 搜「专注计时」,前十名的截图几乎一模一样:一个大圆圈倒计时,白底或深色背景,偶尔配个绿色进度环。点进去功能也差不多,计时、响铃、记录时长,结束。
作为开发者我看这些截图的第一反应是:这赛道已经死了吗?还是说用户根本不在意差异化?
我选择赌后者不成立。声境护照的核心假设是:计时工具留不住人,不是因为功能不够,而是因为「完成一次专注」这件事本身没有叙事------没有积累感,没有值得回头看的东西。所以我把每次专注包装成一段「声音旅行」:选声景、积里程、攒护照印章,结束后拿一张可以发朋友圈的战报卡片。
下面聊几个做这个 App 时真正踩过坑的技术决定。
探险系统:把定义层和状态层拆开
探险章节的数据模型是我返工次数最多的部分。
早期我把任务进度直接存在 definition 结构里,targetValue、progress、completedAt 全塞一起。有次用户完成任务后触发回写逻辑,targetValue 被意外覆盖成了 0------因为写进度的地方用了同一个赋值路径------任务直接从列表里消失了。用户以为是 bug,其实是数据结构没设计好。
后来拆成了「定义层」和「状态层」两套结构:
swift
// 定义层:静态配置,只读,不随用户行为变化
struct ExpeditionMissionDefinition: Identifiable, Codable {
let id: String
let title: String
let kind: ExpeditionMissionKind // sessionCount / focusMinutes / deepFocusCount
let targetValue: Int
let rewardMiles: Int
}
// 状态层:只存进度和完成时间戳
struct ExpeditionMissionState: Identifiable, Codable {
let id: String
var progress: Int
var completedAt: Date? // nil = 未完成
var completed: Bool { completedAt != nil }
}
两层通过 id 关联,定义层只从远端下发,本地不写。这样之后就算服务端更新了任务内容,也不会碰用户本地的进度状态。deepFocusCount 的判定逻辑是单次时长超过 20 分钟且中途没有中断,这个阈值调了四五次才定下来,最开始设的是 15 分钟,太容易达到,用户没有「深度」的感觉。
连续天数阈值:从 3/7/14 改成 2/5
会话结束后 App 会给一条下一步建议,根据当天计划完成情况和连续打卡天数动态生成:
swift
let streakHint: String
if store.streakDays >= 5 {
streakHint = "你已连续 \(store.streakDays) 天,重点是稳定复用。"
} else if store.streakDays >= 2 {
streakHint = "再坚持 1-2 天可进入稳定习惯区。"
} else {
streakHint = "建议先连续 3 天完成每天的最低目标。"
}
早期版本阈值是 3/7/14,对应「习惯养成」里常见的节点说法。结果我发现第一个门槛「再坚持 4 天」对新用户压力很大------刚用第一天,看到这句话心理上已经开始算成本了。
改成 2/5 之后,第一个提示变成「再坚持 1-2 天」,心理距离近了很多。我没有大样本数据来证明这个改动有多少提升,但从我自己用和几个测试用户的反馈来看,看到「1-2 天」比看到「4 天」更容易当天再开一次计时。
说白了就是:第一个里程碑要近到「今晚就能拿到」。
统计页的 Demo 模式
新用户第一次打开,专注记录为空,统计页一片白------这是工具类 App 最难看的冷启动体验。
处理方式是:focusLogs 为空时用 StatsService.createDemoFocusLogs() 生成假数据填充,同时打 isDemo: true 标记,UI 层显示「这是示例数据」的提示。几乎所有 ViewModel 都是这一行:
swift
private var logs: [FocusLog] {
store.focusLogs.isEmpty
? StatsService.createDemoFocusLogs()
: store.focusLogs
}
这个方案有个明显缺陷:demo 数据是静态写死的,不会根据时区或当前时间调整,周一打开看到的「本周统计」热力图和周五打开是一样的。这是我已知的技术债,下个版本会改成基于当前时间动态生成。但在真实数据出现之前,给用户看一个「满血状态」的统计页,比空白页的跳失率要低------这是我在几个类似工具上观察到的规律,所以先凑合用着。
分享卡片:为什么最终选了 SwiftUI 截图方案
会话结束后可以导出一张数据卡片:时长、效率指数、声景名、里程和等级称号。这个功能我试了三个方案。
Core Graphics 手绘:可控性最高,但每次改卡片样式要同时维护 UI 代码和绘制代码两套,改了一个忘了另一个,有次导出的卡片和 App 里显示的样式差了半个版本,挺尴尬的。
WKWebView 渲染 HTML:样式灵活,服务端可以随时更新模板,但首次渲染有明显延迟,用户点「生成卡片」之后要等将近一秒才出图,这个等待感在分享场景里特别割裂。
最后选了 SwiftUI 视图截图:UI 和卡片共用同一套组件,改一处两边同步,维护成本低。代价是在部分低端设备上截图后文字抗锯齿发虚,看起来不如原生渲染清晰。我接受这个取舍------大多数用户用的是近三年的机型,发虚的问题不常见。
卡在一个问题上,想听听大家的看法
App 现在刚上线,还在冷启动阶段。我目前卡在一个判断上:「护照 + 里程」这套叙事,对重度效率用户来说会不会显得幼稚?
我身边用这类工具的人大概分两种:一种要的是纯粹的效率,恨不得界面越简单越好;另一种喜欢打卡晒图,仪式感对他们来说本身就是动力。声境护照明显是为第二种人做的,但我不确定第一种人会不会因为「太花哨」直接关掉。
你们做工具类 App 的时候,怎么处理这两类用户的需求冲突?或者说,你们自己用专注工具,更看重哪一面?