iOS の通信状態表示を検証:NWPathMonitor とオフライン判定の境界

iOS の通信状態表示を検証:NWPathMonitor とオフライン判定の境界

iOS の通信状態表示を検証:NWPathMonitor とオフライン判定の境界

ユーザーが更新ボタンを押すと「ネットワークを利用できません」と表示されるのに、少し待って再試行すると成功する。よくある原因はリクエスト処理の不具合ではなく、まだ更新されていないネットワーク経路の状態を、リクエストの送信可否の判定に使っていることです。クラウド Mac で iOS アプリを開発する場合は、まず決定的なイベント列で状態モデルを検証し、シミュレータと実機ではシステムの挙動を確認するとよいでしょう。障害を再現するために、リモート接続を何度も切断する必要はありません。

状態表示とリクエスト結果を切り分ける

NWPathMonitor が監視するのは、監視条件に合うネットワーク経路です。.satisfied はサーバーが稼働していることも、認証が有効であることも保証しません。.requiresConnection は接続の確立が必要な状態を表しており、継続的なオフライン状態と解釈してはいけません。

アプリ側では、少なくとも「未確定・経路利用可能・接続確立が必要・経路利用不可」の四つの状態を持たせます。起動直後でコールバックをまだ受け取っていない間は未確定とし、オフラインを初期値にしないでください。API のタイムアウト、認証失敗、サーバーエラーはそれぞれリクエスト結果のモデルで扱い、経路の状態を逆に上書きしないようにします。

ネットワーク状態はユーザーへの案内に向いていますが、「すべてのリクエストを禁止する」一括スイッチには向きません。ユーザーが明示的に再試行した場合は、実際のリクエストで成否を確かめます。

この方法は、ネットワーク状態のバナーや自動再試行の案内が必要なアプリに適しています。一度リクエストを送るだけの機能なら、監視を追加せずにリクエスト結果を直接処理するほうが、たいていはシンプルです。

単独で実行できる状態モデルを作る

リモート macOS のコマンドラインで NetworkStateCheck.swift を作成し、以下の内容をすべて保存します。このモデルは実際のネットワークに依存せず、世代番号で監視インスタンスを区別します。サンプルは呼び出しを直列に実行する前提です。複数のキューから同時に変更するコードとして、そのまま使うことはできません。

enum PathState: Equatable {
    case unknown, available, needsConnection, unavailable
}

struct NetworkHint {
    private(set) var generation = 0
    private(set) var state: PathState = .unknown
    private(set) var expensive = false
    private(set) var constrained = false

    mutating func restart() -> Int {
        generation += 1
        state = .unknown
        expensive = false
        constrained = false
        return generation
    }

    mutating func accept(
        generation incoming: Int,
        state next: PathState,
        expensive: Bool = false,
        constrained: Bool = false
    ) {
        guard incoming == generation else { return }
        state = next
        self.expensive = expensive
        self.constrained = constrained
    }
}

var hint = NetworkHint()
precondition(hint.state == .unknown)

let old = hint.restart()
hint.accept(generation: old, state: .available, expensive: true)
precondition(hint.state == .available && hint.expensive)

let current = hint.restart()
hint.accept(generation: old, state: .unavailable)
precondition(hint.state == .unknown)

hint.accept(generation: current, state: .needsConnection)
precondition(hint.state == .needsConnection)

hint.accept(generation: current, state: .available, constrained: true)
precondition(hint.constrained && !hint.expensive)

hint.accept(generation: current, state: .unavailable)
precondition(hint.state == .unavailable)

hint.accept(generation: current, state: .available)
precondition(hint.state == .available && !hint.constrained)

xcrun swift NetworkStateCheck.swift を実行します。検証に通れば何も出力されず、失敗するとゼロ以外の終了ステータスで終了します。検証するのはアプリ自身の状態に関する契約であり、システムのネットワークスタックの正しさではありません。

監視を組み込む際は実行順序を固定する

本番用のアダプターでは Network をインポートして NWPathMonitor() を作成し、pathUpdateHandler を設定してから start(queue:) を呼び出します。三種類の NWPath.Status をモデルの列挙型に対応付け、isExpensive と isConstrained も渡します。将来追加される未知の列挙値は、未確定状態に対応付けます。

監視を作成するときに、現在の世代番号をキャプチャします。コールバックが状態モデルに入る直前に、世代番号をもう一度照合してください。監視をやり直す際は、まず世代番号を進めて古いイベントを無効化し、次に古いインスタンスをキャンセルして、最後に新しいインスタンスを作成します。キャンセル済みのインスタンスを再び開始してはいけません。

監視のライフサイクルとモデルの更新は、同じ直列実行コンテキストで扱います。メインの実行コンテキストを使う場合は、コールバックの処理をそこへ移してから世代番号を検証してください。移す前に検証すると、キューで待機していた古いイベントが新しい状態を汚染するおそれがあります。UI に状態を通知するときも、この順序を維持します。複数の非同期通知が逆順で状態を上書きしないようにしてください。

requiredInterfaceType: .wifi だけで、デバイスがネットワークを利用できるかどうかを判断してはいけません。これは特定のインターフェース条件を監視するため、ほかの利用可能な経路を見落とす場合があります。この初期化方法を使うのは、機能の要件としてインターフェースを限定する場合だけにします。

イベントのテストマトリクスで境界条件を補う

スクリプトをプロジェクトのユニットテストターゲットへ移し、テストケースごとに独立したモデルを作成します。少なくとも、次の項目を追加で確認してください。

  • 起動後にコールバックが届かない:状態は未確定のままで、リクエストは送信できる。
  • 経路は利用可能だが API 呼び出しに失敗する:リクエストのエラーだけを更新し、経路が切断されたとは表示しない。
  • 利用不可だった経路が復旧する:バナーが消え、同じ業務処理を重複して実行しない。
  • キャンセル後に古いイベントが届く:状態も通信コストの表示も変わらない。
  • 同じイベントが繰り返し届く:表示は一貫し、再試行タスクが積み重ならない。
  • 高コストまたは制約付きの状態が変わる:業務要件で許容される範囲のダウンロード方針だけを変更する。

isExpensive は特定のインターフェース種別と同義ではなく、isConstrained も完全なオフラインを意味しません。大容量ファイルを扱うタスクでは、URLSessionConfiguration の通信コストや制約付きネットワークに関するポリシーを組み合わせられます。表示用モデルの判断だけで、ユーザーの操作を黙って破棄しないでください。

自動テストと実機での受け入れ確認を分ける

シミュレータはバナーやボタン、状態モデルとの接続を確認するのに適していますが、ネットワークの挙動はホストの影響を受けます。「シミュレータの表示が変わった」だけで、モバイル通信への切り替えを検証できたと考えてはいけません。また、テストのためにクラウド Mac のネットワークインターフェースを無効にして、リモートデスクトップへの接続を失わないようにしてください。

実機では、無線ネットワークの切り替え、制約付きネットワーク、バックグラウンドからの復帰を追加で確認し、経路イベントの順序、UI の変化、リクエスト結果をそれぞれ記録します。テスト用サーバーが接続を拒否する場合と、経路が利用できない場合は別のシナリオです。検証の記録も分けて残してください。

最後に、状態モデル、イベントのテストケース、デバイスでの確認記録を提出します。合格基準は、バナーが常に即座にネットワーク状態を言い当てることではありません。状態表示がリクエストを誤って阻止しないこと、古い監視が新しいセッションの状態を上書きしないこと、そして失敗後のユーザー操作の処理先が明確であることです。

よくある質問

satisfied なら API に必ず接続できますか?

いいえ。監視条件に合う経路が存在することを示すだけで、サーバー、認証、応答内容は確認しません。成功の判断には実際のリクエスト結果が必要です。

キャンセル後の古い通知はどう排除しますか?

同じ直列実行領域で世代番号を先に更新し、その後で古い監視をキャンセルします。通知に元の世代を付け、現在の世代だけを受け付けます。再開時は新しい監視インスタンスを作成します。

自分の作業でリモートMacを試す

Mac mini M4を専有でレンタルし、macOSのGUIとコマンドラインを使えます。接続やワークフローの検証に最適です。長時間のオフライン作業や専門機器の接続が必要な場合は、購入も比較検討しましょう。

Mac miniをレンタル

まず1日レンタルして、購入するか決めましょう。

接続の使い心地と実際の作業を試して、自分に合った利用期間を選びましょう。