我做了一个把专注计时包装成「护照盖章」的 iOS App,几个让我纠结很久的设计决策

9 阅读6分钟

专注计时器太无聊了。这是我做声境护照的起点。

市面上的番茄钟要么极简到没有存在感,要么堆满金币和小树,热闹但跟专注本身没什么关系。我想要一个有仪式感但不打扰专注过程的东西。最后的方案是:把每次专注包装成一次「旅行」,护照盖章、飞行里程、等级称号,结束后收到战报。听起来有点中二,但我觉得这个隐喻本身就是产品最核心的赌注。

雷达界面:最有画面感的交互,也是最费时间的一个

鸿蒙版里有个我自己挺喜欢的设计:打开声景之后,屏幕上有几个浮动的「音效球(Orb)」,你可以用手指拖拽它们,调整不同环境音的音量和声场混合比例。

两个 Orb 靠近,两种声音融合;拉远,分离。你在「移动」声音,而不是「调参数」。

这个思路来自 DJ 混音台,但简化了很多。实现上用 ArkTS 做手势识别和位置映射,主要的麻烦是边界情况的坐标系换算——Orb 不能飞出雷达范围,但又要让拖拽手感足够流畅,这两个要求在边界处会打架,调了挺多次才顺手。

说实话这个功能开发时间远超预期,但我觉得值。滑动条调音量是「设置」,拖球是「玩」,用户的心态不一样。

📸 (这里建议插入雷达界面的录屏 GIF,掘金支持图片,这个交互用文字描述损失了太多画面感——发布前记得补上)

旅行护照这个隐喻,我想了很久

护照本身就是一本「去过哪里」的记录,有历史感、有积累感。飞行里程对应专注时长,这个比喻不用解释读者就能懂。把连续打卡天数映射成旅行段数,把等级称号设计成城市印章,整套叙事框架不需要额外说明。

更重要的是,这个框架改变了用户的心理预期。不是「我要撑过 25 分钟」,而是「我要完成这次旅程」。开始专注的时候用户是在「前往」某个地方,结束收到战报是「从那里回来了」。

声音场景是这套叙事的载体。每个场景对应一个城市或环境——东京雨夜、京都晨光——配合专门混音的环境声景。这是我看来和其他专注 App 最实质性的差异点,不是皮肤,是整个使用体验的基底。

探险任务系统:定义和状态分开存

产品里有一套探险章节系统,每个城市场景对应一个章节,章节下面挂着几个任务,完成任务解锁里程奖励。任务类型目前只有三种:完成 N 次专注(sessionCount)、累计专注 N 分钟(focusMinutes)、完成 N 次深度专注(deepFocusCount)。

数据结构上,静态配置和用户状态完全分开:

struct ExpeditionMissionDefinition: Identifiable, Codable, Equatable {
    let id: String
    let title: String
    let kind: ExpeditionMissionKind
    let targetValue: Int
    let rewardMiles: Int
}

struct ExpeditionMissionState: Identifiable, Codable, Equatable {
    let id: String
    var progress: Int
    var completedAt: Date?
    var completed: Bool { completedAt != nil }
}

Definition 是随版本更新的配置,State 跟着用户走。这样后续改任务内容不会碰用户进度数据。代价是计算进度时要做一次 join,不过量级完全可以接受。

会话结束后的「下一步建议」,文案分层是我拍的

先说结论:连续打卡天数对应的提示文案,分了三档,这个分层是我自己估的,没有任何数据依据。5 天以上说「稳住」,2-4 天说「快到习惯区了」,1 天说「先连续 3 天」。

做这个功能的动机是我自己用的时候发现:专注结束、看完战报,然后就不知道该干嘛了。所以加了 nextActionAdvice,根据当天计划完成情况、连续天数、历史高峰时段,生成一段文字告诉用户「下一次建议什么时间、多少分钟、哪个场景」。

func nextActionAdvice(taskCompleted: Bool) -> SessionNextActionAdvice {
    let remaining = max(0, store.weeklyPlanTodayTargetSegments
                           - store.weeklyPlanTodayActualSegments)
    let streakHint: String
    if store.streakDays >= 5 {
        streakHint = "你已连续 \(store.streakDays) 天,重点是稳定复用。"
    } else if store.streakDays >= 2 {
        streakHint = "再坚持 1-2 天可进入稳定习惯区。"
    } else {
        streakHint = "建议先连续 3 天完成每日最小闭环。"
    }
    // ...
}

这种「我拍的」的设计决策在独立开发里其实挺常见,没有 A/B 测试,没有用研,就是凭感觉给个分层然后发版。等有足够用户数据再回来调。

分享卡片:上线两周,下载量两位数,暂时没法验证

做分享功能之前我犹豫了很久。很多 App 的分享卡片是自嗨功能,根本没人用。

我的判断是:分享发生需要两个条件——内容本身有展示价值,且分享动作足够低摩擦。专注战报满足第一条,「我今天专注了 3 小时」是值得说出去的事,尤其对有自律社群需求的用户。所以做了几种卡片样式:单次会话战报、周回顾、成就徽章,都有一定的视觉设计,不是截图凑合。

但说实话,App 上线两周,下载量两位数,目前完全没有足够的真实数据来验证这个判断对不对。这是现在最大的未知数。

有一个文件夹叫 _disabled_features

源码里有个文件夹名字就是这个。里面放着还没上线的功能:完整的 Profile 页、详细统计报表、任务管理界面。ViewModel 基本写完了,UI 还差一截。

StatsSheetViewModel 里已经有 CSV 导出能力,用户可以把专注记录导出来自己分析,等界面打磨好了会开放。

独立开发最难受的地方大概就是这个——手里有一堆「差一点」的功能,但每次发版都要狠心禁用一批,不然什么都是半成品。

最后一个没想清楚的问题

游戏化奖励到底是促进习惯还是替代了内在动机?

我现在倾向于「短期促进、长期有风险」。奖励机制能帮用户撑过前两周的冷启动期,但如果用户每次专注的动力是「升级」而不是「把这件事做完」,那习惯本身是脆弱的——一旦奖励停止或者用户把等级升满了,行为就会断。

所以我在设计里刻意弱化了「升级数值」的感知,强化「旅程记录」的感知。但这两者能不能真的解耦,我还没有答案。

如果你做过习惯养成或游戏化相关的产品,你怎么处理这个问题的?