axiom-network-framework-ref について
Appleプラットフォーム開発者向けのNetwork.framework APIリファレンススキルです。NWConnectionからiOS 26のNetworkConnectionへの移行に加え、接続、メッセージフレーミング、デバイス検出の例を扱います。
主な機能
- 01NWConnectionとNetworkConnectionの比較
- 02TCP、UDP、TLS、QUIC接続の設定
- 03async/awaitによるデータ送受信
- 04TLVとCoderによるメッセージ処理
- 05リスナーとBonjour、Wi-Fi Aware検出の設定
活用シーン
- 01BSDソケットや旧APIからNetwork.frameworkへの移行
- 02カスタムプロトコルの通信方式選定
- 03ピアツーピア通信やデバイス検出の実装
- 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 バージョン | 主な機能 |
|---|---|---|
| 2018 | iOS 12 | NWConnection、NWListener、NWBrowser を導入 |
| 2019 | iOS 13 | ユーザー空間ネットワーキング(CPU 使用率を 30% 削減)、TLS 1.3 をデフォルトに |
| 2021 | iOS 15 | URLSession で WebSocket をサポート |
| 2025 | iOS 26 | NetworkConnection(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:) による一時停止 |
| フレーミング | 手動またはカスタム NWFramer | TLV を組み込み(TLV { TLS() }) |
| Codable | JSON の手動エンコード/デコード | 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 プロトコル
移行マッピング
| NWConnection | NetworkConnection |
|---|---|
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:) |
| 手動のJSON | Coder(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件のコード例