ここメモ — 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 プラグインだけでは届かない領域に手が届きます。