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