iOS 独立开发实践:零服务器架构下的 GPS 轨迹记录 App 如何做到 Local-First

5 阅读4分钟

前言

作为独立开发者,我做了一个 GPS 轨迹记录应用「雁过留痕」,上线 100 天,服务器费用为零——因为压根没有服务器。

这不是偷懒,而是一个从第一天就确定的架构决策:所有数据只存在用户本地设备上,没有账号体系、没有云端同步、没有任何第三方数据采集。

本文聊聊这个 Local-First 架构在实际 iOS 项目中的落地思路、遇到的坑,以及作为独立开发者在「零遥测」条件下如何迭代产品。

为什么选择纯本地架构

GPS 轨迹数据的敏感程度常常被低估。一条连续的位置记录可以还原一个人每天几点出门、去哪上班、周末在哪消磨时间。这比聊天记录更能勾勒出一个人的真实生活。

市面上绝大多数轨迹类应用默认把数据上传到云端,理由通常是「方便多设备同步」或「防止数据丢失」。但对于我的目标场景——Citywalk 记录、城市探索——用户更在意的是「我的行踪不要被别人知道」。

所以架构上做了一个极端选择:

┌─────────────────────────────┐
│        用户 iPhone           │
│                             │
│  ┌───────────┐  ┌────────┐  │
│  │ CoreData  │  │ GPX/   │  │
│  │ (轨迹点)  │  │ GeoJSON│  │
│  └───────────┘  └────────┘  │
│         ▲                   │
│         │                   │
│  ┌──────┴──────┐            │
│  │ CLLocation  │            │
│  │  Manager    │            │
│  └─────────────┘            │
└─────────────────────────────┘
        ↕ 网络请求 = 0

没有后端,没有 Firebase,没有 Analytics SDK。整个 App 的网络权限都没申请。

技术实现要点

1. 智能省电:运动状态检测

纯 GPS 后台持续定位是电量杀手。我的方案是利用 CMMotionActivityManager 来判断用户当前是否在移动:

let motionManager = CMMotionActivityManager()

motionManager.startActivityUpdates(to: .main) { activity in
    guard let activity = activity else { return }
    if activity.stationary {
        // 用户静止 → 降低定位频率或暂停 GPS
        LocationService.shared.enterLowPowerMode()
    } else if activity.walking || activity.cycling {
        // 用户在移动 → 恢复精确定位
        LocationService.shared.enterTrackingMode()
    }
}

静止时自动关闭 GPS,移动时恢复记录。这样全天后台运行,电量消耗也能控制在可接受范围内。

2. 本地数据持久化

轨迹点使用 CoreData 存储,每条轨迹记录包含:

struct TrackPoint {
    let latitude: Double
    let longitude: Double
    let altitude: Double
    let timestamp: Date
    let horizontalAccuracy: Double
    let speed: Double
}

为了在地图上实现「像素风格」的覆盖效果,会对轨迹点做网格化处理,将经纬度映射到固定大小的格子里:

func gridIndex(for coordinate: CLLocationCoordinate2D, zoomLevel: Int) -> GridCell {
    let gridSize = 360.0 / pow(2.0, Double(zoomLevel))
    let x = Int(floor((coordinate.longitude + 180) / gridSize))
    let y = Int(floor((coordinate.latitude + 90) / gridSize))
    return GridCell(x: x, y: y)
}

这也是「像素地图」视觉效果的底层逻辑——用户走过的区域会以像素格子的形式被点亮。

3. 城市成就系统

没有服务器不代表不能做趣味功能。城市边界数据以 GeoJSON 格式内置在 App Bundle 中,当用户的轨迹点首次落入某个城市的多边形范围内时,触发「解锁」:

func checkCityUnlock(point: CLLocationCoordinate2D) {
    for city in preloadedCities where !city.isUnlocked {
        if city.polygon.contains(point) {
            city.unlock(at: Date())
            NotificationCenter.default.post(name: .cityUnlocked, object: city)
        }
    }
}

像一个只属于自己的探索游戏,没有排行榜、没有社交比较,纯粹记录你去过的地方。

零遥测下的产品迭代困境

说实话,这个架构对开发者非常不友好。

没有 Crashlytics,崩溃日志只能通过 Xcode Organizer 看到用户主动同意上传的系统级报告(量极少)。没有 Mixpanel/Amplitude,完全不知道用户在哪个页面停留最久、哪个功能没人用。没有漏斗分析,不知道新用户引导流程在哪一步流失。

等于闭着眼睛做产品。

我的应对策略:

  1. App Store 评价是唯一反馈渠道 —— 每条评论都认真看
  2. TestFlight 内测群 —— 愿意聊的用户比数据埋点更有价值
  3. 自己重度使用 —— 每天用自己的 App 记录 Citywalk,dogfooding 发现问题
  4. 小步发版 —— 既然不能 A/B test,就快速发版然后看评分变化

100 天的数据

指标数值
服务器成本¥0
用户数据泄露风险0
后端代码行数0
我掌握的用户行为数据0
需要维护的基础设施0

最后一行看起来像缺点,但换个角度:我不需要值班、不需要处理数据合规(GDPR/个保法)、不需要担心数据库被拖。作为一个人的团队,这省下的精力是实打实的。

总结

Local-First 不是银弹,它有明确的代价:放弃社交功能、放弃云同步、放弃数据驱动。但对于位置信息这种高敏感数据,以及「独立开发者精力有限」这个现实约束,它是一个值得认真考虑的架构方向。

如果你也在做类似的工具型应用,不妨想想:你的服务器上真的需要存那些数据吗?


「雁过留痕」是一款 iOS 轨迹记录应用,主打像素地图、纯本地存储、Citywalk 探索,目前在 App Store 可下载。