mcpField
A
axiom-network-framework-ref

axiom-network-framework-ref

github.com/charleswiltgen開発1.2k170 回インストール今日更新
リポジトリ

axiom-network-framework-ref について

Appleプラットフォーム開発者向けのNetwork.framework APIリファレンススキルです。NWConnectionからiOS 26のNetworkConnectionへの移行に加え、接続、メッセージフレーミング、デバイス検出の例を扱います。

主な機能

  1. 01NWConnectionとNetworkConnectionの比較
  2. 02TCP、UDP、TLS、QUIC接続の設定
  3. 03async/awaitによるデータ送受信
  4. 04TLVとCoderによるメッセージ処理
  5. 05リスナーとBonjour、Wi-Fi Aware検出の設定

活用シーン

  1. 01BSDソケットや旧APIからNetwork.frameworkへの移行
  2. 02カスタムプロトコルの通信方式選定
  3. 03ピアツーピア通信やデバイス検出の実装
  4. 04コールバック式接続コードのasync/await移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-network-framework-ref

インストール後、Claude Code で/axiom-network-framework-refを入力して読み込まれたか確認します。

詳細

Network.framework API リファレンス

概要

Network.framework は、Berkeley ソケットに代わる Apple の最新ネットワーキング API です。スマートな接続確立、ユーザー空間ネットワーキング、組み込み TLS サポート、シームレスなモビリティを提供します。2018 年の iOS 12 で NWConnection とともに導入され、2025 年の iOS 26 では NetworkConnection により構造化並行処理へと進化しました。

進化のタイムライン
  • 2018(iOS 12) 完了ハンドラーを使用する NWConnection。CFSocket/NSStream/SCNetworkReachability を非推奨化
  • 2019(iOS 13) ユーザー空間ネットワーキング(CPU 使用率を 30% 削減)、TLS 1.3 をデフォルトに
  • 2025(iOS 26) async/await に対応した NetworkConnection、組み込み TLV フレーミング、Coder プロトコル、Wi-Fi Aware 検出
主な機能
  • スマートな接続確立 Happy Eyeballs(IPv4/IPv6 の競合接続)、プロキシ評価(PAC)、VPN 検出、WiFi Assist によるフォールバック
  • ユーザー空間ネットワーキング ソケットと比較して CPU 使用率を約 30% 削減、メモリマップ領域、コンテキストスイッチの削減
  • 組み込みセキュリティ デフォルトで TLS 1.3、UDP 用 DTLS、証明書ピンニングのサポート
  • モビリティ ネットワークの自動切り替え処理(WiFi ↔ cellular)、接続可否の通知、Multipath TCP
  • パフォーマンス ECN(明示的輻輳通知)、サービスクラスのマーキング、TCP Fast Open、UDP バッチ処理
URLSession と使い分ける場合
  • URLSession HTTP、HTTPS、WebSocket、単純な TCP/TLS ストリーム → これらに最適化された URLSession を使用します
  • Network.framework UDP、カスタムプロトコル、低レベル制御、ピアツーピア、ゲーム、ストリーミング → Network.framework を使用します
関連スキル
  • アンチパターン、一般的なパターン、負荷シナリオについては axiom-networking を使用します
  • 接続失敗の体系的なトラブルシューティングには axiom-networking-diag を使用します

このスキルを使う場合

以下の場合にこのスキルを使用します。

  • 移行を計画する BSD ソケット、CFSocket、NSStream、または SCNetworkReachability からの移行
  • API の違いを理解する NWConnection(iOS 12~25)と NetworkConnection(iOS 26 以降)の違い
  • WWDC 2025 の 12 個の例をすべて実装する(TLS 接続、TLV フレーミング、Coder プロトコル、NetworkListener、Wi-Fi Aware)
  • プロトコルを選択する ユースケースに応じたプロトコル(TCP、UDP、TLS、QUIC)の選択
  • ピアツーピア検出を設定する NetworkBrowser と Wi-Fi Aware を使用した設定
  • パフォーマンスを最適化する ユーザー空間ネットワーキング、バッチ処理、ペーシングの活用
  • 移行する 完了ハンドラーから async/await への移行(NWConnection → NetworkConnection)

API の進化

タイムライン
年iOS バージョン主な機能
2018iOS 12NWConnection、NWListener、NWBrowser を導入
2019iOS 13ユーザー空間ネットワーキング(CPU 使用率を 30% 削減)、TLS 1.3 をデフォルトに
2021iOS 15URLSession で WebSocket をサポート
2025iOS 26NetworkConnection(async/await)、TLV フレーミング、Coder プロトコル、Wi-Fi Aware
NWConnection(iOS 12~25)と NetworkConnection(iOS 26 以降)の比較
機能NWConnection(iOS 12~25)NetworkConnection(iOS 26 以降)
非同期モデル完了ハンドラーasync/await による構造化並行処理
状態の更新stateUpdateHandler コールバックstates AsyncSequence
送信send(content:completion:) コールバックtry await send(content) による一時停止
受信receive(minimumIncompleteLength:maximumLength:completion:)try await receive(exactly:) による一時停止
フレーミング手動またはカスタム NWFramerTLV を組み込み(TLV { TLS() })
CodableJSON の手動エンコード/デコードCoder プロトコル(Coder(MyType.self, using: .json))
メモリすべてのクロージャーで [weak self] が必要[weak self] は不要(Task のキャンセルは自動)
エラー処理完了時にエラーを確認throws による自然な伝播
状態マシン状態変更時のコールバックfor await state in connection.states
検出NWBrowser(Bonjour のみ)NetworkBrowser(Bonjour + Wi-Fi Aware)
推奨事項
  • iOS 26以降を対象とする新しいアプリ:NetworkConnectionを使用します(よりシンプルで安全です)
  • iOS 12〜25をサポートするアプリ:NWConnectionを使用します(後方互換性があります)
  • 移行:両方のAPIは共存できるため、段階的に移行できます

NetworkConnection(iOS 26以降)完全リファレンス

4.1 接続の作成

NetworkConnectionでは、宣言的なプロトコルスタックの構成を使用します。

例1:基本的なTLS接続(WWDC 4:04)
import Network

// Basic connection with TLS (TCP and IP inferred)
let connection = NetworkConnection(
    to: .hostPort(host: "www.example.com", port: 1029)
) {
    TLS()
}

// Send and receive with async/await
public func sendAndReceiveWithTLS() async throws {
    let outgoingData = Data("Hello, world!".utf8)
    try await connection.send(outgoingData)

    let incomingData = try await connection.receive(exactly: 98).content
    print("Received data: \(incomingData)")
}
主なポイント
  • TLS()はTCP()とIP()を自動的に推論します
  • 明示的にconnection.start()を呼び出す必要はありません(最初の送信または受信時に開始されます)
  • async/awaitにより、コールバックのネストがなくなります
例2:IPオプションのカスタマイズ(WWDC 4:41)
// Customize IP fragmentation
let connection = NetworkConnection(
    to: .hostPort(host: "www.example.com", port: 1029)
) {
    TLS {
        TCP {
            IP()
                .fragmentationEnabled(false) // Disable IP fragmentation
        }
    }
}
IPをカスタマイズする場合
  • .fragmentationEnabled(false) — フラグメンテーションを独自に処理するプロトコル(QUICなど)向けです
  • .ipVersion(.v6) — IPv6のみを強制します(テスト用)
例3:パラメータのカスタマイズ(WWDC 5:07)
// Constrained paths (low data mode) + custom IP
let connection = NetworkConnection(
    to: .hostPort(host: "www.example.com", port: 1029),
    using: .parameters {
        TLS {
            TCP {
                IP()
                    .fragmentationEnabled(false)
            }
        }
    }
    .constrainedPathsProhibited(true) // Don't use cellular in low data mode
)
よく使用するパラメータ
  • .constrainedPathsProhibited(true) — 低データモードを尊重します
  • .expensivePathsProhibited(true) — モバイル通信やホットスポットを使用しません
  • .multipathServiceType(.handover) — Multipath TCPを有効にします
エンドポイントの種類
// Host + Port
.hostPort(host: "example.com", port: 443)

// Service (Bonjour)
.service(name: "MyPrinter", type: "_ipp._tcp", domain: "local.", interface: nil)

// Unix domain socket
.unix(path: "/tmp/my.sock")
プロトコルスタックの構成
// TLS over TCP (most common)
TLS()

// QUIC (TLS + UDP, multiplexed streams)
QUIC()

// UDP (datagrams)
UDP()

// TCP (stream, no encryption)
TCP()

// WebSocket over TLS
WebSocket {
    TLS()
}

// Custom framing
TLV {
    TLS()
}

4.2 状態マシン

NetworkConnectionは、以下の状態を遷移します。

setup
  ↓
preparing (DNS, TCP handshake, TLS handshake)
  ↓
┌─ waiting (no network, retrying)
│    ↓
└→ ready (can send/receive)
     ↓
  failed (error) or cancelled
状態の監視
// Option 1: Async sequence (monitor in background)
Task {
    for await state in connection.states {
        switch state {
        case .preparing:
            print("Connecting...")
        case .waiting(let error):
            print("Waiting for network: \(error)")
        case .ready:
            print("Connected!")
        case .failed(let error):
            print("Failed: \(error)")
        case .cancelled:
            print("Cancelled")
        @unknown default:
            break
        }
    }
}
主な状態
  • .preparing DNSルックアップ、TCP SYN、TLSハンドシェイク
  • .waiting ネットワークが利用できない場合、フレームワークが自動的に再試行します
  • .ready 接続が確立され、送受信できます
  • .failed 回復不能なエラー(サーバーによる拒否、TLSの失敗、タイムアウト)
  • .cancelled タスクがキャンセルされたか、connection.cancel()が呼び出されました

4.3 送受信のパターン
送信:基本
let data = Data("Hello".utf8)
try await connection.send(data)
受信:正確なバイト数(WWDC 7:30)
// Receive exactly 98 bytes
let incomingData = try await connection.receive(exactly: 98).content
print("Received \(incomingData.count) bytes")
受信:可変長(WWDC 8:29)
// Read UInt32 length prefix, then read that many bytes
let remaining32 = try await connection.receive(as: UInt32.self).content
guard var remaining = Int(exactly: remaining32) else { throw MyError.invalidLength }

while remaining > 0 {
    let chunk = try await connection.receive(atLeast: 1, atMost: remaining).content
    remaining -= chunk.count
    // Process chunk...
}
receive()のバリエーション
  • receive(exactly: n) — nバイトちょうどを待ちます
  • receive(atLeast: min, atMost: max) — minバイト以上、maxバイト以下を取得します
  • receive(as: UInt32.self) — 固定サイズの型を読み込みます(ネットワークバイトオーダー)

4.4 TLVフレーミング(iOS 26以降)

**TLV(Type-Length-Value)**は、ストリームプロトコル(TCP/TLS)におけるメッセージ境界の問題を解決します。

フォーマット
  • Type:UInt32(メッセージ識別子)
  • Length:UInt32(メッセージサイズ、自動)
  • Value:メッセージのバイト列
例:TLVを使用したGameMessage(WWDC 11:06、11:24、11:53)
import Network

// Define message types
enum GameMessage: Int {
    case selectedCharacter = 0
    case move = 1
}

struct GameCharacter: Codable {
    let character: String
}

struct GameMove: Codable {
    let row: Int
    let column: Int
}

// Connection with TLV framing
let connection = NetworkConnection(
    to: .hostPort(host: "www.example.com", port: 1029)
) {
    TLV {
        TLS()
    }
}

// Send typed message
public func sendWithTLV() async throws {
    let characterData = try JSONEncoder().encode(GameCharacter(character: "🐨"))
    try await connection.send(characterData, type: GameMessage.selectedCharacter.rawValue)
}

// Receive typed message
public func receiveWithTLV() async throws {
    let (incomingData, metadata) = try await connection.receive()

    switch GameMessage(rawValue: metadata.type) {
    case .selectedCharacter:
        let character = try JSONDecoder().decode(GameCharacter.self, from: incomingData)
        print("Character selected: \(character)")
    case .move:
        let move = try JSONDecoder().decode(GameMove.self, from: incomingData)
        print("Move: row=\(move.row), column=\(move.column)")
    case .none:
        print("Unknown message type: \(metadata.type)")
    }
}
メリット
  • メッセージ境界が維持されます(3つ送信すると、ちょうど3つ受信できます)
  • 型安全なメッセージ処理(enumベースのルーティング)
  • オーバーヘッドが最小限です(メッセージごとに8バイト:type + length)
使用する場合
  • 複数のメッセージタイプを扱う場合(チャット + プレゼンス + 入力中)
  • TLVを使用する既存のプロトコルを扱う場合
  • 複雑なフレーミングを使わずにメッセージ境界が必要な場合

4.5 Coderプロトコル(iOS 26以降)

Coderを使用すると、JSONのエンコード/デコードに必要な定型コードを手動で記述する必要がなくなります。

例:Coderを使用したGameMessage(WWDC 12:50、13:13、13:53)
import Network

// Define message types as Codable enum
enum GameMessage: Codable {
    case selectedCharacter(String)
    case move(row: Int, column: Int)
}

// Connection with Coder
let connection = NetworkConnection(
    to: .hostPort(host: "www.example.com", port: 1029)
) {
    Coder(GameMessage.self, using: .json) {
        TLS()
    }
}

// Send Codable directly (no encoding needed!)
public func sendWithCoder() async throws {
    let selectedCharacter: GameMessage = .selectedCharacter("🐨")
    try await connection.send(selectedCharacter)
}

// Receive Codable directly (no decoding needed!)
public func receiveWithCoder() async throws {
    let gameMessage = try await connection.receive().content // Returns GameMessage!

    switch gameMessage {
    case .selectedCharacter(let character):
        print("Character selected: \(character)")
    case .move(let row, let column):
        print("Move: (\(row), \(column))")
    }
}
対応フォーマット
  • .json — JSONエンコーディング(人間が読みやすく、幅広く互換性があります)
  • .propertyList — プロパティリスト(より高速で、サイズが小さい)
メリット
  • JSONの定型コードが不要です(約50行から約10行に削減)
  • 型安全です(メッセージ構造の変更をコンパイラーが検出します)
  • フレーミングが自動化されます(メッセージ境界を処理します)
使用する場合
  • アプリ間通信(両方の側を管理している場合)
  • プロトタイピング(動作するコードを最短で作成できます)
  • 型安全なプロトコル
使用しない場合
  • Swift以外のサーバーと相互運用する場合
  • 独自のワイヤーフォーマットが必要な場合
  • パフォーマンスが重要な場合(制御性を優先して手動エンコーディングを使用します)

4.6 NetworkListener(iOS 26以降)

サブタスクを自動管理しながら、着信接続をリッスンします。

例:接続のリッスン(WWDC 15:16)
import Network

// Listener with Coder protocol
public func listenForIncomingConnections() async throws {
    try await NetworkListener {
        Coder(GameMessage.self, using: .json) {
            TLS()
        }
    }.run { connection in
        // Each connection gets its own subtask
        for try await (gameMessage, _) in connection.messages {
            switch gameMessage {
            case .selectedCharacter(let character):
                print("Player chose: \(character)")
            case .move(let row, let column):
                print("Player moved: (\(row), \(column))")
            }
        }
    }
}
主な機能
  • 接続ごとのサブタスクを自動作成します(Taskを手動で管理する必要はありません)
  • 構造化された並行処理(リスナーの終了時に、すべてのサブタスクがキャンセルされます)
  • 受信にはconnection.messagesの非同期シーケンスを使用します
リスナーの設定
// Specify port
NetworkListener(port: 1029) { TLS() }

// Let system choose port
NetworkListener { TLS() }

// Bonjour advertising
NetworkListener(service: .init(name: "MyApp", type: "_myapp._tcp")) { TLS() }

4.7 NetworkBrowserとWi-Fi Aware(iOS 26以降)

ローカルネットワーク上または近くのデバイスにあるエンドポイントを検出します。

例:Wi-Fi Aware の検出(WWDC 17:39)
import Network
import WiFiAware

// Browse for nearby paired Wi-Fi Aware devices
public func findNearbyDevice() async throws {
    let endpoint = try await NetworkBrowser(
        for: .wifiAware(.connecting(to: .allPairedDevices, from: .ticTacToeService))
    ).run { endpoints in
        .finish(endpoints.first!) // Use first discovered device
    }

    // Make connection to the discovered endpoint
    let connection = NetworkConnection(to: endpoint) {
        Coder(GameMessage.self, using: .json) {
            TLS()
        }
    }
}
Wi-Fi Aware の機能
  • インフラストラクチャを介さないピアツーピア通信(WiFi ルーターは不要です)
  • ペアリング済みデバイスの自動検出
  • 低レイテンシー、非常に高いスループット
  • iOS 26 以降のみ
ブラウズ記述子
// Bonjour
.bonjour(type: "_http._tcp", domain: "local")

// Wi-Fi Aware (all paired devices)
.wifiAware(.connecting(to: .allPairedDevices, from: .myService))

// Wi-Fi Aware (specific device)
.wifiAware(.connecting(to: .pairedDevice(identifier: deviceID), from: .myService))

NWConnection(iOS 12-25)完全リファレンス

5.1 接続の作成

NWConnection は完了ハンドラーを使用します(async/await より前の方式です)。

基本的な TLS 接続(WWDC 2018 の 133〜166 行)
import Network

// Create connection
let connection = NWConnection(
    host: NWEndpoint.Host("mail.example.com"),
    port: NWEndpoint.Port(integerLiteral: 993),
    using: .tls // TCP inferred
)

// Handle connection state changes
connection.stateUpdateHandler = { [weak self] state in
    switch state {
    case .ready:
        print("Connection established")
        self?.sendData()

    case .waiting(let error):
        print("Waiting for network: \(error)")
        // Show "Waiting..." UI, don't fail immediately

    case .failed(let error):
        print("Connection failed: \(error)")

    case .cancelled:
        print("Connection cancelled")

    default:
        break
    }
}

// Start connection
connection.start(queue: .main)

重要: retain サイクルを防ぐため、stateUpdateHandler では常に [weak self] を使用してください。

カスタムパラメーター
// Create custom parameters
let parameters = NWParameters.tls

// Prohibit expensive networks
parameters.prohibitExpensivePaths = true // Don't use cellular/hotspot

// Prohibit constrained networks
parameters.prohibitConstrainedPaths = true // Respect low data mode

// Require IPv6
parameters.requiredInterfaceType = .wifi
parameters.ipOptions.version = .v6

let connection = NWConnection(host: "example.com", port: 443, using: parameters)

5.2 状態の処理

NWConnection の状態マシン(NetworkConnection と同じです):

setup → preparing → waiting/ready → failed/cancelled
状態処理のベストプラクティス
connection.stateUpdateHandler = { [weak self] state in
    guard let self = self else { return }

    switch state {
    case .preparing:
        // DNS lookup, TCP SYN, TLS handshake in progress
        self.updateUI(.connecting)

    case .waiting(let error):
        // Network unavailable or blocked
        // DON'T fail immediately, framework retries automatically
        print("Waiting: \(error.localizedDescription)")
        self.updateUI(.waiting)

    case .ready:
        // Connection established, can send/receive
        self.updateUI(.connected)
        self.startSending()

    case .failed(let error):
        // Unrecoverable error after all retry attempts
        print("Failed: \(error.localizedDescription)")
        self.updateUI(.failed)

    case .cancelled:
        // connection.cancel() called
        self.updateUI(.disconnected)

    default:
        break
    }
}

5.3 コールバックによる送受信
ペーシングを伴う送信(WWDC 2018 の 320〜341 行)
// Send with contentProcessed callback for pacing
func sendData() {
    let data = Data("Hello, world!".utf8)

    connection.send(content: data, completion: .contentProcessed { [weak self] error in
        if let error = error {
            print("Send error: \(error)")
            return
        }

        // contentProcessed = network stack consumed data
        // NOW send next chunk (pacing)
        self?.sendNextData()
    })
}

contentProcessed コールバック: ネットワークスタックがデータを処理したときに呼び出されます(ブロッキングソケット呼び出しが返るタイミングに相当します)。過剰なデータのバッファリングを避けるためのペーシングに使用してください。

正確なバイト数の受信
// Receive exactly 10 bytes
connection.receive(minimumIncompleteLength: 10, maximumLength: 10) { [weak self] (data, context, isComplete, error) in
    if let error = error {
        print("Receive error: \(error)")
        return
    }

    if let data = data {
        print("Received \(data.count) bytes")
        // Process data...

        // Continue receiving
        self?.receiveMore()
    }
}
受信パラメーター
  • minimumIncompleteLength:コールバックが呼び出されるまでの最小バイト数(1 = データを受信し次第返します)
  • maximumLength:コールバックあたりの最大バイト数
  • 「正確に n バイト」の場合:両方に n を設定します

5.4 UDP バッチ処理(WWDC 2018 の 343〜347 行)
バッチ送信による CPU 使用率の 30%削減
// UDP connection
let connection = NWConnection(
    host: NWEndpoint.Host("game-server.example.com"),
    port: NWEndpoint.Port(integerLiteral: 9000),
    using: .udp
)

connection.start(queue: .main)

// Batch multiple datagrams
func sendVideoFrames(_ frames: [Data]) {
    connection.batch {
        for frame in frames {
            connection.send(content: frame, completion: .contentProcessed { error in
                if let error = error {
                    print("Send error: \(error)")
                }
            })
        }
    }
    // All sends batched into ~1 syscall
    // Result: 30% lower CPU usage vs individual sends
}

バッチ処理なし: データグラム 100 個 = システムコール 100 回 = CPU 使用率が高い バッチ処理あり: データグラム 100 個 = 約 1 回のシステムコール = CPU 使用率を 30%削減(Instruments で測定)


5.5 NWListener(WWDC 2018 の 233〜293 行)

着信接続を受け入れます。

import Network

// Create listener on port 1029
let listener = try NWListener(using: .tcp, on: 1029)

// Advertise Bonjour service
listener.service = NWListener.Service(name: "MyApp", type: "_myapp._tcp")

// Handle service registration
listener.serviceRegistrationUpdateHandler = { update in
    switch update {
    case .add(let endpoint):
        if case .service(let name, let type, let domain, _) = endpoint {
            print("Advertising: \(name).\(type)\(domain)")
        }
    default:
        break
    }
}

// Handle new connections
listener.newConnectionHandler = { [weak self] newConnection in
    print("New connection from: \(newConnection.endpoint)")

    newConnection.stateUpdateHandler = { state in
        if case .ready = state {
            print("Client connected")
            self?.handleClient(newConnection)
        }
    }

    newConnection.start(queue: .main)
}

// Handle listener state
listener.stateUpdateHandler = { state in
    switch state {
    case .ready:
        print("Listener ready on port \(listener.port ?? 0)")
    case .failed(let error):
        print("Listener failed: \(error)")
    default:
        break
    }
}

// Start listening
listener.start(queue: .main)

5.6 NWBrowser(Bonjour 検出)

ローカルネットワーク上のサービスを検出します。

import Network

// Browse for Bonjour services
let browser = NWBrowser(
    for: .bonjour(type: "_http._tcp", domain: nil),
    using: .tcp
)

// Handle discovered services
browser.browseResultsChangedHandler = { results, changes in
    for result in results {
        switch result.endpoint {
        case .service(let name, let type, let domain, _):
            print("Found service: \(name).\(type)\(domain)")

            // Connect to this service
            let connection = NWConnection(to: result.endpoint, using: .tcp)
            connection.start(queue: .main)

        default:
            break
        }
    }
}

// Handle browser state
browser.stateUpdateHandler = { state in
    switch state {
    case .ready:
        print("Browser ready")
    case .failed(let error):
        print("Browser failed: \(error)")
    default:
        break
    }
}

// Start browsing
browser.start(queue: .main)

モビリティとネットワークの切り替え

接続の実行可能性(WWDC 2018 の 453〜463 行)

実行可能性 = 接続がデータを送受信できること(有効な経路があること)。

connection.viabilityUpdateHandler = { isViable in
    if isViable {
        print("✅ Connection viable (can send/receive)")
    } else {
        print("⚠️ Connection not viable (no route)")
        // Don't tear down immediately, may recover
        // Show UI: "Connection interrupted"
    }
}
実行可能性が変化するとき
  • エレベーターに入る(WiFi 信号が失われる)→ 実行不可
  • エレベーターから出る(WiFi が復帰する)→ 再び実行可能
  • WiFi からセルラーに切り替える → 一時的に実行不可 → セルラーで実行可能

ベストプラクティス: 実行可能性が失われても接続を切断しないでください。ネットワークが復帰すると、フレームワークが接続を回復します。

より適切な経路が利用可能(WWDC 2018 の 464〜477 行)

より適切な経路 = 特性の優れた代替ネットワークです。

connection.betterPathUpdateHandler = { betterPathAvailable in
    if betterPathAvailable {
        print("📶 Better path available (e.g., WiFi while on cellular)")
        // Consider migrating to new connection
        self.migrateToNewConnection()
    }
}
シナリオ
  • セルラーで接続中に WiFi のある建物に入る → より適切な経路が利用可能
  • WiFi で接続中に WiFi の品質が低下し、セルラーが利用可能 → より適切な経路が利用可能
移行パターン
func migrateToNewConnection() {
    // Create new connection
    let newConnection = NWConnection(host: host, port: port, using: parameters)

    newConnection.stateUpdateHandler = { [weak self] state in
        if case .ready = state {
            // New connection ready, switch over
            self?.currentConnection?.cancel()
            self?.currentConnection = newConnection
        }
    }

    newConnection.start(queue: .main)

    // Keep old connection until new one ready
}
Multipath TCP(WWDC 2018 の 480〜487 行)

アプリケーションの介入なしで、ネットワーク間を自動的に移行します。

let parameters = NWParameters.tcp
parameters.multipathServiceType = .handover // Seamless network transition

let connection = NWConnection(host: "example.com", port: 443, using: parameters)
Multipath TCP のモード
  • .handover — ネットワーク間(WiFi ↔ セルラー)をシームレスに切り替えます
  • .interactive — 複数の経路を同時に使用します(レイテンシーを最小化)
  • .aggregate — 複数の経路を同時に使用します(スループットを最大化)
利点
  • ネットワークの切り替えを自動化します(実行可能性ハンドラーは不要です)
  • ネットワークの切り替え時に接続が中断されません
  • MPTCP を利用できない場合は単一経路にフォールバックします
NWPathMonitor(WWDC 2018 の 489〜496 行)

ネットワーク状態の変化を監視します(SCNetworkReachability に代わるものです)。

import Network

let monitor = NWPathMonitor()

monitor.pathUpdateHandler = { path in
    if path.status == .satisfied {
        print("✅ Network available")

        // Check interface types
        if path.usesInterfaceType(.wifi) {
            print("Using WiFi")
        } else if path.usesInterfaceType(.cellular) {
            print("Using cellular")
        }

        // Check if expensive
        if path.isExpensive {
            print("⚠️ Expensive path (cellular/hotspot)")
        }

    } else {
        print("❌ No network")
    }
}

monitor.start(queue: .main)
ユースケース
  • path.status == .unsatisfied のときに「ネットワークなし」の UI を表示します
  • path.isExpensive のときに帯域幅を多く使用する機能を無効にします
  • インターフェースの種類に基づいて品質を調整します
使用する場面
  • ネットワーク状態をグローバルに監視する場合
  • 「接続を待機中」だけでは不十分な場合
  • 接続前に利用可能なインターフェースを確認する必要がある場合
使用しない場面
  • 接続前に状態を確認する場合(代わりに待機状態を使用します)
  • 接続ごとに監視する場合(代わりに実行可能性ハンドラーを使用します)

セキュリティ設定

TLS バージョン
// iOS 13+ requires TLS 1.2+ by default
let tlsOptions = NWProtocolTLS.Options()

// Allow TLS 1.2 and 1.3
tlsOptions.minimumTLSProtocolVersion = .TLSv12

// Require TLS 1.3 only
tlsOptions.minimumTLSProtocolVersion = .TLSv13

let parameters = NWParameters(tls: tlsOptions)
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
証明書のピン留め
// Production-grade certificate pinning
let tlsOptions = NWProtocolTLS.Options()

sec_protocol_options_set_verify_block(
    tlsOptions.securityProtocolOptions,
    { (metadata, trust, complete) in
        // Get server certificate
        let serverCert = sec_protocol_metadata_copy_peer_public_key(metadata)

        // Compare with pinned certificate
        let pinnedCertData = Data(/* your pinned cert */)
        let serverCertData = SecCertificateCopyData(serverCert) as Data

        if serverCertData == pinnedCertData {
            complete(true) // Accept
        } else {
            complete(false) // Reject (prevents MITM attacks)
        }
    },
    .main
)

let parameters = NWParameters(tls: tlsOptions)
証明書ピンニングと企業プロキシ

企業ネットワークでは、独自の証明書を提示する TLS インスペクションプロキシがよく使われます。厳格なピンニングは、このような環境で問題になります。

方針:証明書全体ではなく公開鍵(SPKI)に対してピンニングし、設定で無効にできるようにします。

sec_protocol_options_set_verify_block(
    tlsOptions.securityProtocolOptions,
    { (metadata, trust, complete) in
        // 1. Check if system trusts the certificate chain (handles corporate CAs)
        let secTrust = sec_trust_copy_ref(trust).takeRetainedValue()
        SecTrustEvaluateAsyncWithError(secTrust, .main) { _, result, _ in
            guard result else { complete(false); return }

            // 2. If pinning enabled, also verify public key
            if PinningConfig.isEnabled {
                let serverKey = SecTrustCopyKey(secTrust)
                let matches = pinnedKeys.contains { $0 == serverKey }
                complete(matches)
            } else {
                complete(true) // System trust only (enterprise mode)
            }
        }
    },
    .main
)

ルール:

  • 必ず最初にシステムの信頼性を検証します(SecTrustEvaluateAsyncWithError)。これにより、企業がインストールしたルート CA が尊重されます。
  • 証明書ピンニングではなく、公開鍵ピンニングを使用します(証明書のローテーションに対応できます)。
  • 企業環境でピンニングを無効にできる、管理対象の設定(MDM プロファイルまたはアプリ設定)を用意します。
  • ローテーションに備えて、少なくとも 2 つの鍵(現在の鍵とバックアップ)をピンニングします。
暗号スイート
let tlsOptions = NWProtocolTLS.Options()

// Specify allowed cipher suites
tlsOptions.tlsCipherSuites = [
    tls_ciphersuite_t(rawValue: 0x1301), // TLS_AES_128_GCM_SHA256
    tls_ciphersuite_t(rawValue: 0x1302), // TLS_AES_256_GCM_SHA384
]

// iOS defaults to secure modern ciphers, only customize if required

パフォーマンスの最適化

ユーザー空間ネットワーキング(WWDC 2018 の 409〜441 行)

iOS/tvOS では自動的に有効になります。 Network.framework により、TCP/UDP スタックがアプリのプロセス内に移動します。

メリット
  • CPU 使用率が約 30%低下(Instruments で測定)
  • カーネルからユーザー空間へのコピーが不要(メモリーマップ領域)
  • コンテキストスイッチの削減
従来の方式とユーザー空間方式
従来のソケットユーザー空間ネットワーキング
パケット → ドライバー → カーネル → コピー → ユーザー空間パケット → ドライバー → メモリーマップ領域 → ユーザー空間(コピーなし)
100 データグラム = 100 システムコール100 データグラム = 約 1 システムコール(バッチ処理の場合)
CPU 使用率が約 30%高い基準となる CPU 使用率

WWDC のデモでは、UDP によるライブビデオストリーミングで、ソケットと Network.framework の間に 30%の CPU 使用率の差が示されました。

UDP の ECN(WWDC 2018 の 365〜378 行)

スムーズな UDP 送信のための明示的輻輳通知です。

// Create IP metadata with ECN
let ipMetadata = NWProtocolIP.Metadata()
ipMetadata.ecnFlag = .congestionEncountered // Or .ect0, .ect1

// Attach to send context
let context = NWConnection.ContentContext(
    identifier: "video_frame",
    metadata: [ipMetadata]
)

connection.send(content: data, contentContext: context, completion: .contentProcessed { _ in })
ECN フラグ
  • .ect0 / .ect1 — ECN 対応トランスポート
  • .congestionEncountered — 輻輳通知を受信

メリット:ネットワークはパケットを破棄せずに輻輳を通知できます。

サービスクラス(WWDC 2018 の 379〜388 行)

トラフィックの優先度を設定します。

// Connection-wide service class
let parameters = NWParameters.tcp
parameters.serviceClass = .background // Low priority

let connection = NWConnection(host: "example.com", port: 443, using: parameters)

// Per-packet service class (UDP)
let ipMetadata = NWProtocolIP.Metadata()
ipMetadata.serviceClass = .realTimeInteractive // High priority (voice)

let context = NWConnection.ContentContext(identifier: "voip", metadata: [ipMetadata])
connection.send(content: audioData, contentContext: context, completion: .contentProcessed { _ in })
サービスクラス
  • .background — 低優先度(大容量ダウンロード、同期)
  • .default — 通常の優先度
  • .responsiveData — 対話型データ(API 呼び出し)
  • .realTimeInteractive — 時間制約のあるデータ(音声、ゲーム)
TCP Fast Open(WWDC 2018 の 389〜406 行)

TCP SYN パケットで初期データを送信します(ラウンドトリップを節約)。

let parameters = NWParameters.tcp
parameters.allowFastOpen = true

let connection = NWConnection(host: "example.com", port: 443, using: parameters)

// Send initial data BEFORE calling start()
let initialData = Data("GET / HTTP/1.1\r\n".utf8)
connection.send(
    content: initialData,
    contentContext: .defaultMessage,
    isComplete: false,
    completion: .idempotent // Data is safe to replay
)

// Now start connection (initial data sent in SYN)
connection.start(queue: .main)

メリット:接続確立時間を 1 RTT 短縮します。 要件:データは冪等である必要があります(SYN が再送信された場合に再実行しても安全であること)。


移行戦略

BSD ソケットから NWConnection への移行
BSD ソケットNWConnection備考
socket() + connect()NWConnection(host:port:using:) + start()デフォルトでノンブロッキング
send() / sendto()connection.send(content:completion:)非同期コールバック
recv() / recvfrom()connection.receive(min:max:completion:)非同期コールバック
bind() + listen()NWListener(using:on:)ポートの自動バインド
accept()listener.newConnectionHandler接続ごとにコールバック
getaddrinfo()NWEndpoint.Host(hostname) を使用DNS は自動
SCNetworkReachability待機状態を示す connection.stateUpdateHandlerレースコンディションなし
setsockopt()NWParameters型安全なオプション
移行例
移行前(ブロッキングソケット)
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, &addr, addrlen); // BLOCKS
send(sock, data, len, 0);
移行後(NWConnection)
let connection = NWConnection(host: "example.com", port: 443, using: .tls)
connection.stateUpdateHandler = { state in
    if case .ready = state {
        connection.send(content: data, completion: .contentProcessed { _ in })
    }
}
connection.start(queue: .main)
URLSession StreamTask から NetworkConnection への移行
移行する場合
  • UDP が必要(StreamTask は TCP のみをサポート)
  • カスタムプロトコルが必要
  • 低レベルの制御が必要
URLSession を使い続ける場合
  • HTTP/HTTPS(URLSession はこの用途に最適化されています)
  • WebSocket のサポートが必要
  • キャッシュや Cookie が組み込まれていることが必要
移行例
移行前(URLSession StreamTask)
let task = URLSession.shared.streamTask(withHostName: "example.com", port: 443)
task.resume()
task.write(Data("Hello".utf8), timeout: 10) { _ in }
移行後(NetworkConnection iOS 26+)
let connection = NetworkConnection(to: .hostPort(host: "example.com", port: 443)) { TLS() }
try await connection.send(Data("Hello".utf8))
NWConnection から NetworkConnection への移行
移行のメリット
  • async/await(コールバックの入れ子が不要)
  • [weak self] が不要
  • TLV フレーミングが組み込み済み
  • Codable 型向けの Coder プロトコル
移行マッピング
NWConnectionNetworkConnection
connection.stateUpdateHandler = { }for await state in connection.states { }
connection.send(content:completion:)try await connection.send(content)
connection.receive(min:max:completion:)try await connection.receive(exactly:)
手動のJSONCoder(MyType.self, using: .json)
カスタムフレーマーTLV { TLS() }
すべての箇所で [weak self][weak self] は不要
移行例
変更前(NWConnection)
connection.stateUpdateHandler = { [weak self] state in
    if case .ready = state {
        self?.sendData()
    }
}

func sendData() {
    connection.send(content: data, completion: .contentProcessed { [weak self] error in
        self?.receiveData()
    })
}
変更後(NetworkConnection)
Task {
    for await state in connection.states {
        if case .ready = state {
            try await connection.send(data)
            let received = try await connection.receive(exactly: 10).content
        }
    }
}

テストチェックリスト

ネットワークコードをリリースする前に確認してください。

デバイスでのテスト
  • 実機でテスト済み(シミュレーターだけではない)
  • 複数のiOSバージョンでテスト済み(12、15、26)
  • iPhoneとiPadでテスト済み(ネットワーク特性の違いを確認)
ネットワーク環境
  • Wi-Fi(家庭内ネットワーク)
  • モバイル通信(Wi-Fiを無効化)
  • 機内モード → Wi-Fi(待機状態をテスト)
  • Wi-Fi → モバイル通信への切り替え(建物の外へ移動)
  • モバイル通信 → Wi-Fiへの切り替え(建物の中へ移動)
  • 電波が弱い環境(地下室、エレベーター)
  • Network Link Conditioner(遅延100ms、パケットロス3%)
ネットワークの種類
  • IPv4のみのネットワーク
  • IPv6のみのネットワーク(一部の携帯電話事業者)
  • デュアルスタック(IPv4 + IPv6)
  • 企業VPNが有効
  • パーソナルホットスポット(高コストの経路)
パフォーマンス
  • 接続の確立が500ms未満(ログを確認)
  • UDPでバッチ処理を使用(Instrumentsで確認)
  • ペーシングにcontentProcessedを使用(送信タイミングを確認)
  • InstrumentsのNetworkテンプレートでプロファイルを取得済み
  • CPU使用率が許容範囲内(ネットワーク処理で10%未満)
  • メモリが安定(リークなし、[weak self]を確認)
エラー処理
  • .waiting状態を処理(「待機中...」のUIを表示)
  • .failed状態を処理(具体的なエラーメッセージを表示)
  • TLSハンドシェイクエラーを記録
  • タイムアウトを処理(いつまでも待たない)
  • ユーザー向けのエラーが対処可能(「POSIX 61」ではなく「ネットワークを確認してください」)
iOS 26以降の機能(NetworkConnectionを使用する場合)
  • メッセージ境界が必要な場合はTLVフレーミングを使用
  • Codable型を送信する場合はCoderプロトコルを使用
  • NWListenerの代わりにNetworkListenerを使用
  • ピアツーピア通信でWi-Fi Awareを使用する場合はNetworkBrowserを使用

APIクイックリファレンス

NetworkConnection(iOS 26以降)
// Create connection
NetworkConnection(to: .hostPort(host: "example.com", port: 443)) { TLS() }

// Send
try await connection.send(data)

// Receive
try await connection.receive(exactly: n).content

// States
for await state in connection.states { }

// TLV framing
NetworkConnection(to: endpoint) { TLV { TLS() } }

// Coder protocol
NetworkConnection(to: endpoint) { Coder(MyType.self, using: .json) { TLS() } }

// Listener
NetworkListener { TLS() }.run { connection in }

// Browser
NetworkBrowser(for: .wifiAware(...)).run { endpoints in }
NWConnection(iOS 12〜25)
// Create connection
let connection = NWConnection(host: "example.com", port: 443, using: .tls)

// State handler
connection.stateUpdateHandler = { [weak self] state in }

// Start
connection.start(queue: .main)

// Send
connection.send(content: data, completion: .contentProcessed { [weak self] error in })

// Receive
connection.receive(minimumIncompleteLength: min, maximumLength: max) { [weak self] data, context, isComplete, error in }

// Viability
connection.viabilityUpdateHandler = { isViable in }

// Better path
connection.betterPathUpdateHandler = { betterPathAvailable in }

// Cancel
connection.cancel()
NWListener(iOS 12〜25)
let listener = try NWListener(using: .tcp, on: 1029)
listener.newConnectionHandler = { newConnection in }
listener.start(queue: .main)
NWBrowser(iOS 12〜25)
let browser = NWBrowser(for: .bonjour(type: "_http._tcp", domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { results, changes in }
browser.start(queue: .main)
NWPathMonitor
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in }
monitor.start(queue: .main)

リソース

WWDC:2018-715、2025-250

ドキュメント:/network、/network/nwconnection、/network/networkconnection

スキル:axiom-networking、axiom-networking-diag


最終更新日 2025-12-02 ステータス WWDC 2018およびWWDC 2025に基づく、本番環境対応のリファレンス 対象範囲 NWConnection(iOS 12〜25)、NetworkConnection(iOS 26以降)、WWDC 2025の全12件のコード例

コメント