ここメモ — iOS バックグラウンドジオフェンスを Native Region Monitoring 化
| 開発記録 | ここメモ
タグ: #iOS #ジオフェンス #Flutter
「スーパーの近くで牛乳を思い出したい」。ここメモはそれだけのアプリです。でも Flutter の位置情報プラグインは、バックグラウンドでぜんぜん粘ってくれませんでした。iOS ネイティブの Region Monitoring に乗り換えて、権限フローまで組み直した記録です。
アプリの狙い
「ここメモ」は、ToDo を「場所」と紐付けるアプリです。その場所に近づくと通知が飛びます。スーパーの近くで「牛乳を買う」。図書館の近くで「本を返す」。そういう使い方を想定しています。
Flutter プラグインの限界
位置情報系の Flutter プラグイン(geofence_service 等)は、フォアグラウンドではしっかり動きます。つまずいたのはその先でした。バックグラウンドでの精度、電池の消費、iOS の制約。どれも課題として残りました。
特に iOS がしんどかったです。アプリが完全終了した状態からの復帰。長時間バックグラウンドでの監視。この二つで、プラグインが期待どおりに動かないケースが何度も出ました(プラグインに任せられると思い込んでいました)。
Native Region Monitoring への移行
iOS には CLLocationManager.startMonitoring(for:) があります。OS が責任を持って地域監視してくれる API です。これならアプリが終了していても、境界を超えたタイミングで OS がアプリを起こして通知してくれます。
この API を Swift で呼ぶラッパーを書きました。Flutter 側からは Method Channel 経由で制御します。
ネイティブ側の最小実装はこの程度に収まります。always 権限が前提なので、起動時に権限状態を確認するロジックも合わせて入れます。
final class GeofenceController: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
}
func register(id: String, lat: Double, lng: Double, radius: Double) {
let center = CLLocationCoordinate2D(latitude: lat, longitude: lng)
let region = CLCircularRegion(
center: center,
radius: min(radius, manager.maximumRegionMonitoringDistance),
identifier: id
)
region.notifyOnEntry = true
region.notifyOnExit = false
manager.startMonitoring(for: region)
}
func locationManager(_ m: CLLocationManager, didEnterRegion r: CLRegion) {
NotificationBridge.fireEnter(id: r.identifier)
}
}
Dart 側は MethodChannel をラップした薄い API として公開しました。画面側は永続化された ToDo から登録するだけです。
class GeofenceBridge {
static const _channel = MethodChannel('kokomemo/geofence');
Future<void> register(GeoTodo todo) async {
await _channel.invokeMethod('register', {
'id': todo.id,
'lat': todo.lat,
'lng': todo.lng,
'radius': todo.radiusMeters,
});
}
Future<void> unregister(String id) async {
await _channel.invokeMethod('unregister', {'id': id});
}
}
バックグラウンドの安定化
移行と合わせて、以下の整備をしました。
UNUserNotificationCenter.delegateを AppDelegate で明示設定- iOS フォアグラウンドでも通知バナーを表示
- 通知許可詳細とアクティブ通知のダンプを取得(デバッグ用)
- 起動 60 秒後に自動テスト通知を発火(実機検証用)
下の二つはどちらも実機検証のためです。ジオフェンスは机の上では試せません。ここがいちばんしんどいところでした。
権限フローの再設計
位置情報と通知の権限フローは、ユーザーから見て複雑になりがちです。必要なものが並びます。
- 位置情報「常に許可」が必要
- 通知の許可も必要
- iOS の Motion 使用許可(NSMotionUsageDescription)も必要
これらを段階的に取得していくフローに設計し直しました。「常に許可」が取れなかったときのフォールバックも入れています。設定アプリへの誘導も組み込みました。
App Check と Cloud Functions
ここメモには ToDo の共有機能があります。家族や友人と ToDo を共有できます。Firestore へのアクセスは App Check で守りました。招待・復元・FCM 通知などのサーバー処理は Cloud Functions に集約しています。
リージョンは asia-northeast1 に固定しました。レイテンシを下げるためです。
バナー広告のアダプティブ化
マネタイズはバナー広告です。メモ一覧の上部に置いています。当初は固定サイズでした。今は画面サイズに応じてアダプティブに変わります。Android の画面比率がとにかく多様だからです。
まとめ
- iOS のバックグラウンド位置情報は Native Region Monitoring 一択
- 権限フローは段階化して、ユーザーが理解できる順番で
- バックエンド連携は App Check と Cloud Functions の組み合わせ
プラットフォームの API を本気で使い倒すと、Flutter プラグインだけでは届かない領域に手が届きます。ネイティブを書くのは遠回りに見えて、結局いちばん近い道でした。