我做了个专注 App,把连续打卡阈值从 3/7/14 改成 2/5 之后留存明显好了

起点

我在 App Store 搜「专注计时」,前十名的截图几乎一模一样:一个大圆圈倒计时,白底或深色背景,偶尔配个绿色进度环。点进去功能也差不多,计时、响铃、记录时长,结束。

作为开发者我看这些截图的第一反应是:这赛道已经死了吗?还是说用户根本不在意差异化?

我选择赌后者不成立。声境护照的核心假设是:计时工具留不住人,不是因为功能不够,而是因为「完成一次专注」这件事本身没有叙事------没有积累感,没有值得回头看的东西。所以我把每次专注包装成一段「声音旅行」:选声景、积里程、攒护照印章,结束后拿一张可以发朋友圈的战报卡片。

下面聊几个做这个 App 时真正踩过坑的技术决定。


探险系统:把定义层和状态层拆开

探险章节的数据模型是我返工次数最多的部分。

早期我把任务进度直接存在 definition 结构里,targetValueprogresscompletedAt 全塞一起。有次用户完成任务后触发回写逻辑,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 的时候,怎么处理这两类用户的需求冲突?或者说,你们自己用专注工具,更看重哪一面?

相关推荐
传奇开心果编程5 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne10 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记1 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩1 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年1 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008891 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
HouWan2 天前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios
用户38034165882972 天前
Broadcast Extension 里的 Live Activity:一次跨进程妥协
ios·音视频开发
匠测AI说2 天前
XCUITest 自动化全解:同进程白盒、XCTest 架构、自动同步机制与 iOS 端选型指南
测试工具·ios·自动化
silianpan2 天前
CAD 文档预览 UTS 插件
android·ios·harmonyos