WWDC Quick Look 💓 By SwiftGGTeam
Use structured concurrency with Network framework

Use structured concurrency with Network framework

元の動画を見る

Highlight

本セッションでは、iOS/macOS 26 における Network framework の 3 つの新しい Swift 型である NetworkConnection、NetworkListener、NetworkBrowser を紹介します。いずれも Swift の structured concurrency をベースに構築されており、設計思想は SwiftUI に近いものになっています。宣言的な記述、自動的なライフサイクル管理、そして task のキャンセル時に自動でクリーンアップされる、というのが特徴です。


主要内容

低レベルなネットワーク処理は、多くの app 開発者にとって頭の痛い領域です。BSD socket 系の API は記述が煩雑で、sockaddr や ioctl、ブロッキング呼び出しを自分で扱う必要があり、TLS ライブラリも手動で組み込まなければなりません。Wi-Fi とセルラーの切り替えに対応するためのステートマシンを書き、メッセージ境界を保つためのバッファリング処理も自前で実装する必要があります。さらに、ある日 TCP から QUIC に切り替えたいと思ったときには、ほとんどゼロから書き直しに近い作業になりがちです。こうした細部にとらわれていては、本来集中したいビジネスロジックに手が回りません。

iOS/macOS 26 の Network framework では、こうした問題を解決するために 3 つの新しい Swift 型が導入されました。NetworkConnection は単一の接続を扱い、NetworkListener はインバウンド接続を待ち受け、NetworkBrowser はネットワーク上のサービスを発見します。これら 3 つの型はいずれも Swift の structured concurrency と深く統合されており、書き味は SwiftUI に近い感覚です。プロトコルスタックを宣言的に組み立て、async/await でデータを送受信し、task がキャンセルされればリソースが自動的に解放されます。

NetworkConnection は 3 つの要素から構成されます。endpoint(接続先)、protocol stack(接続方法、たとえば TLS over TCP over IP)、そして parameters(制限ネットワークの利用禁止などの動作制約)です。接続の状態は framework が管理し、preparing → ready → failed/canceled と遷移します。開発者は UI を更新するために stateUpdateHandler を取り付けることもできますし、状態に一切関与しないことも可能です。sendreceive は ready になるまで自動的に待機してから実行されます。

データの送受信に関して、今年は 2 つの新機能が追加されました。1 つは TLV(Type-Length-Value)framer の組み込みです。これによりメッセージが自動的にフレーム化され、TCP のようなストリーム型プロトコルではメッセージ境界が保持されないという問題を解消できます。もう 1 つは Coder プロトコルで、Codable 型を直接送受信できるようになり、JSON や property list に対応しています。手動でのシリアライズ処理という定型コードを省くことができます。


詳細

最もシンプルな接続: TLS over TCP over IP

スピーカーの Scott は動画の冒頭で、最小限の例を示しています(04:04)。

// Make a connection

import Network

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

ポイントは次のとおりです。

  • NetworkConnection(to:) の第 1 引数は endpoint です。.hostPort はホスト名とポートを表し、framework が DNS 解決を行い、Happy Eyeballs によって最適なアドレスを選択してくれます。
  • 末尾のクロージャでプロトコルスタックを記述します。ここでは TLS() だけを記述しており、TCP と IP は下位層として自動的に推論されます。
  • プロトコルスタックは宣言的に記述され、SwiftUI の view builder に近いスタイルです。

プロトコルスタックのオプションをカスタマイズする

IP の fragmentation を無効化したい場合は、下位層のプロトコルを明示的に書き出す必要があります(04:41)。

let connection = NetworkConnection(to: .hostPort(host: "www.example.com", port: 1029) {
  TLS {
    TCP {
      IP()
        .fragmentationEnabled(false)
    }
  }
}

ポイントは次のとおりです。

  • プロトコルスタックは上から下へとネストする形で記述します。TLS が最も外側にあり、その内側に TCP、さらにその内側に IP が来ます。
  • .fragmentationEnabled(false) は IP 層に対する modifier で、カスタマイズが必要なときだけ書けば十分です。
  • デフォルト値はほとんどのケースで適切に動作するので、必要な部分だけ上書きすれば問題ありません。

バイトストリームの送受信: ストリーム系プロトコルではバイト数指定が必須

TLS/TCP はバイトストリーム型のプロトコルなので、receive のときに何バイト読むかを framework に明示する必要があります(07:30)。

// Send and receive on a connection

import Network

public func sendAndReceiveWithTLS() async throws {
  let connection = NetworkConnection(to: .hostPort(host: "www.example.com", port: 1029)) {
    TLS()
  }

  let outgoingData = Data("Hello, world!".utf8)
  try await connection.send(outgoingData)

  let incomingData = try await connection.receive(exactly: 98).content
  print("Received data: \(incomingData)")
}

ポイントは次のとおりです。

  • send は async で、framework がデータを処理し終えるまでサスペンドします。
  • 最初の send または receive 呼び出しで自動的に接続が開始されるので、手動で start() を呼ぶ必要はありません。
  • receive(exactly: 98) は正確に 98 バイトを読み込む指定で、(content, metadata) のタプルを返します。ここでは .content だけを取り出しています。
  • 機内モードによる切断などのエラーは throws でスローされるため、do/catch で扱えます。

TLV framer: メッセージ境界を保持する

バイトストリームの厄介な点は、送信側で send(3 bytes) を 1 回呼び出しても、受信側では 2 回に分かれて届くことも、別の送信と一緒に 1 回にまとまって届くこともある、というところです。今年新たに導入された TLV framer はメッセージを自動的にフレーム化します(11:24)。

// Send TicTacToe game messages with TLV

import Network

public func sendWithTLV() async throws {
  let connection = NetworkConnection(to: .hostPort(host: "www.example.com", port: 1029)) {
    TLV {
      TLS()
    }
  }

  let characterData = try JSONEncoder().encode(GameCharacter(character: "🐨"))
  try await connection.send(characterData, type: GameMessage.selectedCharacter.rawValue)
}

ポイントは次のとおりです。

  • TLV { TLS() } のように TLV framer を TLS の上に積み重ねます。
  • send には type: 引数が追加されており、メッセージのヘッダにタイプフィールドが書き込まれます。受信側はこの値を使って異なるメッセージハンドラに振り分けることができます。
  • シリアライズは引き続き開発者の責任です(ここでは JSONEncoder を使用)。TLV はあくまで「このメッセージの長さは何バイトで、タイプは何か」を扱うだけです。

Coder プロトコル: Codable 型を直接送受信する

シリアライズ処理も省きたい場合は、Coder を使います(13:13)。

// Send TicTacToe game messages with Coder

import Network

public func sendWithCoder() async throws {
  let connection = NetworkConnection(to: .hostPort(host: "www.example.com", port: 1029)) {
    Coder(GameMessage.self, using: .json) {
      TLS()
    }
  }

  let selectedCharacter: GameMessage = .selectedCharacter("🐨")
  try await connection.send(selectedCharacter)
}

ポイントは次のとおりです。

  • Coder(GameMessage.self, using: .json) は、メッセージ型とエンコード形式(.json または property list)をプロトコルスタックの中で宣言します。
  • connection.send(selectedCharacter) のように Swift の値を直接送信でき、framework 内部で encode と frame 化が完結します。
  • 受信側で connection.receive().content を呼び出すと GameMessage の enum 値が直接得られるので、そのまま switch で分岐処理できます。
  • クライアントとサーバの両方を自分で書き、どちらも Swift で実装するシナリオに向いています。既存のプロトコルを話すサーバと通信する場合は、引き続き TLV やバイトストリームを使う必要があります。

インバウンド接続を待ち受ける

サーバ側では NetworkListener を使います。新しい接続ごとに自動的に child task が起動されるので、複数の接続を並行して受け付けてもブロックされません(15:16)。

// Listen for incoming connections with NetworkListener

import Network

public func listenForIncomingConnections() async throws {
  try await NetworkListener {
    Coder(GameMessage.self, using: .json) {
      TLS()
    }
  }.run { connection in
    for try await (gameMessage, _) in connection.messages {
      // Handle the GameMessage
    }
  }
}

ポイントは次のとおりです。

  • NetworkListener { ... } も同じプロトコルスタック builder を再利用できます。
  • .run { connection in ... } は structured concurrency のエントリポイントで、新しい接続ごとに 1 回ずつクロージャが呼ばれます。
  • connection.messages は AsyncSequence なので、for try await でそのままメッセージをイテレートできます。
  • クロージャから抜けると child task は自動的に終了し、listener がキャンセルされればすべての接続が連鎖的にキャンセルされます。

Wi-Fi Aware: iOS 26 のクロスプラットフォームな P2P ディスカバリ

NetworkBrowser はネットワーク上のサービスを探索する型で、iOS 26 では新たに Wi-Fi Aware への対応が追加されました(17:39)。

// Browse for nearby paired Wi-Fi Aware devices

import Network
import WiFiAware

public func findNearbyDevice() async throws {
  let endpoint = try await NetworkBrowser(for: .wifiAware(.connecting(to: .allPairedDevices, from: .ticTacToeService))).run { endpoints in
    .finish(endpoints.first!)
  }

  // Make a connection to the endpoint
}

ポイントは次のとおりです。

  • .wifiAware(.connecting(to: .allPairedDevices, from: .ticTacToeService)) で探索対象のサービスとデバイスの範囲を指定します。
  • .run { endpoints in ... } クロージャが .finish(endpoint) を返すと browse が停止して結果が返ります。.continue を返せばそのまま新しいデバイスを待ち続けます。
  • 取得した endpoint は NetworkConnection(to:) に渡してそのまま接続を確立できます。

重要ポイント

1. NetworkConnection で BSD socket ベースの自作プロトコルを置き換える

  • なぜ取り組む価値があるのか: 自前のバイナリプロトコルを socket + 手書きのフレームヘッダパースで実装している場合、NetworkConnection { TLV { TLS() } } に移行するだけで、TLS、Happy Eyeballs、Wi-Fi Assist、ネットワーク切り替えの処理を一気に手に入れられます。
  • どこから始めるか: まずは単一接続の送信パスを connection.send(data, type:) に置き換え、受信側のバイトバッファ処理を connection.receive() に切り替えます。対向のプロトコル形式を変えなくても結合テストが進められます。

2. App 間通信は Coder over TLS を第一候補にする

  • なぜ取り組む価値があるのか: 双方とも自分で書く Swift app で、過去の互換性に縛られないなら、Coder を使うことで Codable な enum をそのまま転送でき、JSON encode/decode の glue code を省けます。型の食い違いはコンパイル時に検出されます。
  • どこから始めるか: メッセージを Codable な enum として定義し(各 case に associated value を持たせる)、Coder(MessageType.self, using: .json) { TLS() } を組むだけで動き始められます。後でバイナリ形式に切り替えたくなれば、.json.propertyList に変えるだけで済みます。

3. NetworkListener でサーバ側の accept ループをリファクタリングする

  • なぜ取り組む価値があるのか: 従来の accept ループでは、接続ごとに GCD/Thread にディスパッチする処理を自前で書く必要がありました。NetworkListener.run であれば接続ごとに child task が自動で起動され、task がキャンセルされれば接続も連鎖的にキャンセルされます。structured concurrency に沿った形になり、サービス停止時に孤児となる接続が残ることもありません。
  • どこから始めるか: 既存の dispatch_async 内の処理関数を .run { connection in ... } クロージャの中に移し、手動の receive ループを connection.messages に置き換えます。

4. stateUpdateHandler を UI レイヤに接続する

  • なぜ取り組む価値があるのか: 接続の preparing/waiting/ready/failed という状態は、そのまま「接続中」「ネットワーク制限あり」「切断済み」といった connectionStatus バナーに反映できます。何も表示せずに黙って待たせるよりも、ユーザ体験は格段に良くなります。
  • どこから始めるか: NetworkConnection に state handler を登録し、状態を SwiftUI の @Observable プロパティにマッピングします。あとは View 側で購読するだけです。

5. iOS 限定なら Wi-Fi Aware、クロスプラットフォームなら引き続き Bonjour

  • なぜ取り組む価値があるのか: Wi-Fi Aware は共通の Wi-Fi ネットワークを必要としないため、屋外や複数人でのローカルコラボに向いています。ただし現状は iOS 26 の iPhone 限定なので、クロスプラットフォーム対応には引き続き Bonjour が必要です。
  • どこから始めるか: まずは NetworkBrowser(for: .wifiAware(...)) で iOS 端末同士のデモを動かし、その上で抽象化レイヤを 1 枚かぶせて、デバイスのケイパビリティに応じて .bonjour(...) にフォールバックする形にしていきます。

関連セッション

コメント

GitHub Issues · utterances