前言:一个搁置了两年的想法
我大学时候喜欢骑车在城市里乱逛,每到一个新地方就在手绘地图上涂一小块,像玩现实版的战争迷雾游戏。后来工作了,偶尔用运动App记轨迹,但看着那些线叠在地图上,总觉得缺了点什么。
直到有一天我盯着跑步App里密密麻麻的轨迹线发了会儿呆,突然想明白一件事:线是线,面积是面积。你沿着同一条路跑了一百遍,轨迹记录得再漂亮,你真正"探索"过的地方其实没增加。
我想要的不是"记录我走过哪条路",而是**“我到底覆盖了多少土地”**。
这两个问题看着像,其实差别很大。于是我做了「雁过留痕」——一个iOS App,核心概念只有一句话:把你走过的地方换算成探索面积,单位是 km²。
25米网格:听起来简单粗暴,调起来真不简单
实现"面积"这个概念比我预期难得多。
最早我试过用轨迹围成的多边形来算面积,结果发现你走个来回根本围不成像样的形状,算出来的数字毫无意义。后来换了思路:把地图切成 25m × 25m 的小格子,人经过一个格子就算"点亮"一个。探索面积 = 点亮的格子数 × 每个格子的面积。
这个方案效果出奇地好——你在街上走,地图上会像素画一样慢慢铺开一片颜色,直觉上就是"这块地方我踩过了"。
25米这个精度是一轮轮试出来的:
- 50米:太粗,你明明没走到对面那条街,格子已经亮了
-
- 10米:数据量爆炸,手机存储扛不住
-
- 25米:刚好是精度和性能的平衡点
GPS 轨迹记录本身用的是 iOS 的CoreLocation后台定位能力,这套 API 比较成熟,关键是在电量和精度之间找平衡。我在内部的指标追踪模块里监控了很多维度——总距离、总时长、记录天数、连续打卡天数、夜间出行次数、周末记录天数等等,这些数据最后都会喂给成就系统。
- 25米:刚好是精度和性能的平衡点
这里分享一下核心的网格计算逻辑,思路不复杂但很关键:
struct GridCell: Hashable {
let x: Int
let y: Int
}
func gridCell(for coordinate: CLLocationCoordinate2D, gridSize: Double = 25.0) -> GridCell {
// 将经纬度投影到平面坐标后,按 gridSize 取整
let metersPerDegreeLat = 111_320.0
let metersPerDegreeLon = 111_320.0 * cos(coordinate.latitude * .pi / 180)
let x = Int(floor(coordinate.longitude * metersPerDegreeLon / gridSize))
let y = Int(floor(coordinate.latitude * metersPerDegreeLat / gridSize))
return GridCell(x: x, y: y)
}
// 探索面积 = 去重后的格子数 × 单格面积
let exploredArea = Double(uniqueGridCells.count) * 0.000625 // 25×25 = 625m² = 0.000625km²
逻辑不难理解:把每个 GPS 坐标点映射到对应的网格,用 Set 去重,最后乘以单格面积就是总探索面积。真正麻烦的是工程层面的问题——后台定位的频率控制、大量格子数据的持久化性能、以及地图渲染时怎么高效绘制数万个已点亮格子。
省份解锁:比想象中上头
做着做着发现,光有面积数字还不够"爽"。你告诉用户"你探索了 12.7km²",他可能没什么概念。但如果你说**“你已经解锁了 3 个省份、15 个城市”**,感觉就完全不一样了。
所以我内置了中国行政区的边界数据,做了省市解锁的玩法。轨迹落在哪个省、哪个市,系统自动识别,地图上对应区域被染上颜色。
这里有个经典的坑:国内地图的坐标偏移问题(GCJ-02)。GPS 拿到的是 WGS-84 坐标,不做转换的话你在上海走的路可能"飘"到黄浦江里去。这个适配花了不少时间,做国内地图相关开发的同学应该都踩过这个。
成就徽章也是自然长出来的功能。我在代码里按不同维度分了好几个 track:
| Track | 含义 | 示例 |
|---|---|---|
exploration | 探索类 | 累计探索面积达到 10km² |
consistency | 坚持类 | 连续记录 30 天 |
china | 中国版图类 | 解锁 10 个省份 |
world | 世界探索类 | 足迹覆盖 3 个国家 |
每个 track 下面又分多个系列,每个系列有多个层级。比如连续记录 7 天解锁一个徽章,30 天又是一个,90 天又是一个。做这套东西的时候自己都上瘾了——有一次周末专门绕远路走了一段没去过的巷子,就为了多亮几个格子。
数据安全:数据是用户的,不是我的
做这种长期积累型的 App,最怕数据丢失。你攒了大半年的探索面积,换个手机没了,用户肯定跟你急。
所以我做了本地备份导出,格式是自定义的 .wingprintBackup(底层是 JSON),同时也支持 GPX 格式导出,方便用户在其他平台使用。
这个决定其实挺纠结的。GPX 导出意味着用户可以把数据带走,对"留存"来说不是好事。但我觉得数据是用户的,不是我的。独立开发者跟大厂不一样,没必要靠锁数据留人。
一些做得还不错的功能,和坦白的问题
做得还不错的:
- 桌面 Widget:显示当日进度和累计面积,这个好评率挺高
-
- 路线回放:选一条历史轨迹,在地图上像看动画一样回放你的路线。去年十一出去玩回来看了一遍自己的行程回放,有点像用上帝视角重走了一遍
坦白说的问题:
- 路线回放:选一条历史轨迹,在地图上像看动画一样回放你的路线。去年十一出去玩回来看了一遍自己的行程回放,有点像用上帝视角重走了一遍
现在下载量不大。一个没有推广预算的新 App,在 App Store 里就是沧海一粟。而且"探索面积"这个概念需要解释成本,不像"记步"或者"打卡"那么直觉。
社交分享卡片一直想做——让用户一键生成好看的探索报告图,比如"2024年我走过了 XX km²,解锁了 XX 个城市"——还在打磨中。
写在最后
有个用户评论说:“人生总要留下一些什么,走过的路,见过的人,看过的风景。”
我看到这条评论的时候愣了一下,觉得这个 App 没白做。
如果你也是那种喜欢在城市里乱逛、走没走过的路、发现犄角旮旯小地方的人,可以在 App Store 搜「雁过留痕」试试。它是一个纯 iOS 原生 App,Swift 开发,目前还是我一个人在做。
另外也想和社区里的开发者交流几个技术方向:大规模网格数据的存储优化(目前用的 Core Data,数据量上去之后查询性能是个挑战)、后台定位的省电策略、以及地图上大量自定义图层的渲染性能。如果你有相关经验,欢迎评论区聊聊。
你所在的城市,你觉得自己探索了百分之几?
5537



被折叠的 条评论
为什么被折叠?



