開発

Combine(Swift)とは?Publisher・Subscriberの仕組みと2026年の採用判断を解説

CombineはAppleがWWDC 2019で公開したフレームワークで、時間の経過とともに届く値を宣言的に処理するためのSwift APIです。対応OSはiOS 13.0以降、macOS 10.15以降、tvOS 13.0以降、watchOS 6.0以降。登場から7年の間に、Swift Concurrency(asyncawait)とObservationフレームワークが標準の座を取りました。この記事ではPublisher・Subscriber・Operatorの役割分担を押さえたうえで、2026年7月時点でCombineをどこまで使い、既存コードをどこまで移すのかという判断まで整理します。

まとめ:Combineの立ち位置と採用判断

Combineは「値が1回だけ返る処理」ではなく「値が何度も届く処理」のための道具です。ネットワークリクエストを1本叩いて結果を受け取るだけならasync関数のほうが読みやすく、テキストフィールドの入力・タイマー・通知・WebSocketのように値が繰り返し流れる場面ではCombineのOperator連結が短く書けます。両者は置き換え関係ではなく、扱う対象が違います。

2026年7月時点で、AppleはCombineの廃止を予告していません。ただしWWDCのセッションもドキュメント更新もSwift Concurrency側に寄っており、新規API追加は事実上止まった状態。SwiftUIの状態管理も、iOS 17のObservationフレームワークでCombine非依存の経路が用意されました。新規プロジェクトの既定はSwift Concurrencyとし、Combineは連続する値の処理という限定用途で足す。この順序が現実的な落としどころです。

既存のCombineコードを全面書き換えする理由も、ほとんどありません。移行の判断軸は「1回きりの処理を無理にPublisherで包んでいるか」の一点で、そこだけをasync関数へ寄せます。

Combineの定義とPublisher・Subscriberが担う役割

Combineが担うのは時間の経過とともに届く値の宣言的な処理

Apple公式ドキュメントはCombineを「時間の経過とともに変化する値を処理するための宣言的Swift API」と定義しています。肝心なのは「時間の経過とともに」の部分。ボタンのタップ、テキスト入力の1文字ごとの変化、1秒ごとのタイマー、プッシュ通知は、いずれも「いつ・何回来るか分からない値の連なり」です。

従来これらはデリゲート、通知センター、クロージャのコールバックと、バラバラの仕組みで受け取っていました。CombineはそれらをPublisherという単一のプロトコルに揃えます。標準フレームワーク側にもTimer.publish(every:on:in:)URLSession.shared.dataTaskPublisher(for:)のようにPublisherを返すAPIが用意されています。

Publisher・Subscriber・Operatorの3者が果たす役割

登場人物は下の3つに整理できます。PublisherはOutputFailureという2つの型を持ち、Subscriberは対応するInputFailureを持つ。この型が噛み合ったときだけ購読が成立します。

役割 担当すること 代表的な型
Publisher 値と完了・失敗を発行する Just、Future、Timer
Operator 値の変換・絞り込み・合成 map、filter、debounce
Subscriber 受け取って副作用を実行 Sink、Assign

Operatorは戻り値がまたPublisherになるため、mapの後にfilter、その後にremoveDuplicatesと数珠つなぎにできます。この連結が読みやすさの源であり、保守コストの源でもあります。

iOS 13で登場したCombineとRxSwiftの立ち位置の違い

Combineが出るまで、Swiftでリアクティブに書く手段はRxSwiftなどサードパーティ製が主流でした。概念の対応関係はほぼ一対一で、RxSwiftのObservableがPublisher、DisposeBagがSet<AnyCancellable>、subscribeがsinkに相当します。設計思想はRxSwiftのリアクティブプログラミング解説で先に押さえると、Combine側の語彙も入りやすくなります。

実務上の差は3点。CombineはOS標準のためライブラリ追加もバージョン追従も不要で、代わりにiOS 13より前は切り捨てになります。3つめが見落とされがちな点で、CombineはDarwin系OS専用の非公開実装であり、Linux上のサーバーサイドSwiftでは動きません。クライアントとサーバーでコードを共有する構成なら、この制約が採用可否を左右します。

Subject・AnyCancellableと購読ライフサイクルの実装作法

AnyCancellableを保持しないと購読が即座に終わる理由

購読を始めるSubscriberは実質2種類です。sinkは受け取った値に任意の処理を書く汎用型、assign(to:on:)は届いた値をプロパティへ直接代入する専用型。どちらも戻り値としてAnyCancellableを返し、これが購読の寿命を握ります。この値が解放された瞬間にcancel()が呼ばれ、購読は打ち切られる。戻り値を変数に受けずに書き捨てると、その行を抜けた時点で購読が消えます。イベントが1つも届かない典型的な症状は、たいていこれが原因です。

実装の定石は、クラスにvar cancellables = Set<AnyCancellable>()を1つ持ち、各購読の末尾でstore(in:)を呼ぶ形。画面単位のViewModelなら、この集合1本で寿命管理は完結します。

PassthroughSubjectとCurrentValueSubjectの選択基準

Subjectは外部から手動で値を流し込めるPublisherで、デリゲートのコールバックとパイプラインを繋ぐ橋渡しに使います。選択基準は「購読を始めた瞬間に値が要るかどうか」の一点です。

PassthroughSubjectは値を保持せず、購読後に発行された値だけを流します。ボタンのタップやエラー通知など、過去の出来事を後から受け取っても意味がないイベント向き。対するCurrentValueSubjectは最新値を1つ保持し、購読した直後にその値を1回配ります。画面の表示状態やログイン中かどうかはこちら。状態の配布にPassthroughSubjectを使うと、購読タイミング次第で初期表示が空になります。

エラー型Failureの設計とretry・catchでの復旧手順

PublisherはFailureという関連型でエラーの型を持ち、失敗が起きるとその時点でストリームが終了します。一度失敗したPublisherは、以後どれだけ値が発生しても何も流しません。通信エラーが1回起きただけで画面が二度と更新されなくなる不具合は、この性質が原因です。

復旧は次の順で組み立てます。

  1. retry(3)で一時的な失敗を再試行させる
  2. それでも駄目ならcatchで代替Publisherへ差し替える
  3. UIへ流す直前にFailureNeverへ潰し、購読が切れない形にする

UI更新用のPublisherをFailure == Neverにしておくとassign(to:on:)がそのまま使え、エラーは値の側(Result型や表示用の状態)に載せて運べます。

async/awaitとの役割分担とAsyncSequenceへの橋渡し

1回で終わる処理をasyncとawaitへ寄せると決める判断軸

判断軸は「値が何回届くか」の1つで足ります。APIを1回叩いてJSONを1つ受け取る、ファイルを1つ読む、画像を1枚デコードする。返る値が1つならasync関数のほうが行数が減り、エラーもtrycatchで普通に書けます。ここをCombineのFutureで包むと、購読の保持とキャンセル管理が増えるだけ。同期処理との一般的な違いは非同期処理の実装方式まとめが前提として使えます。

valuesプロパティでPublisherをAsyncSequenceに変換

既存のPublisherをSwift Concurrency側から使う経路は用意されています。PublisherのvaluesプロパティはAsyncPublisherを返し、これはAsyncSequenceに適合するためfor awaitループでそのまま回せる。利用可能なのはiOS 15以降、macOS 12以降です。

この変換が効くのは、標準フレームワークがPublisherしか返さない場面。NotificationCenterの通知やTimerの発火をTask内で受け取りたいとき、Operatorを挟まずにasyncコンテキストへ持ち込めます。ループを抜ければ購読も終わるため、AnyCancellableの持ち回りも不要です。

Futureとasync関数の相互変換で混在期を乗り切る手順

逆方向、async関数の結果をCombineのパイプラインへ流す場合はFutureを使います。クロージャ内でTaskを起こし、awaitした結果をpromiseに渡す形。既存のOperator連結へ新しいasync APIを差し込むなら、この包み方が最短です。

注意点が1つ。Futureは生成された時点で処理を開始し、結果を1つ保持する仕様です。購読されるまで待たせたいならDeferredで包む必要があり、忘れると画面を開く前にAPIが叩かれます。変換レイヤーは1ファイルに集約し、どこで橋渡ししているかを追える状態にしておいてください。

SwiftUIの状態管理でCombineが残る箇所とObservationの境界

ObservableObjectと@Publishedが動く仕組みと現在の位置づけ

ObservableObjectobjectWillChangeというPublisherを持ち、@Published付きのプロパティが変更される直前にそれを発火させます。SwiftUIはこの通知を受けてViewを再描画する。@Published$接頭辞でPublisherも露出するため、そのままパイプラインへ繋げられます。

この仕組みには粒度の問題がありました。オブジェクト単位で「変わった」としか伝わらず、Viewが参照していないプロパティの更新でも再描画が走ります。UI層の選択そのものはSwiftUIとUIKitの使い分けの解説を前提にしてください。

Observationの@Observableへ移すときに失うCombineの機能

iOS 17以降で使える@Observableマクロは、プロパティ単位で読み取りを追跡します。Viewが実際に読んだプロパティが変わったときだけ再描画が走るため、無駄な更新が減る。クラス宣言にマクロを付けるだけで済み、各プロパティへ@Publishedを書く手間もなくなります。

ただし、移すと失うものがあります。@Publishedが提供していた$のPublisherが@Observableにはありません。入力値をdebounceしてから検索APIを叩く処理など、変化をOperatorで加工していたコードは動かなくなる。代替のAsyncStreamwithObservationTrackingは記述量が増えます。状態の保持は@Observableへ、値の加工はCombineへと層で分けるのが移行時の現実解です。

onReceiveとdebounceが今も現役で使われる入力処理の実例

SwiftUIのonReceiveモディファイアは、任意のPublisherをViewで直接受け取る口です。Observation導入後もこのAPIは残っており、Publisherを返す標準APIとの組み合わせで引き続き使われています。

典型例が入力補完です。テキストフィールドの1文字ごとの変化にdebounce(for: .milliseconds(300), scheduler: RunLoop.main)を挟むと、入力が止まって300ミリ秒後に一度だけAPIを叩く形にできます。removeDuplicates()を足せば重複リクエストも消える。素のasyncで書き直すとタイマー管理とキャンセル処理を自前で持つことになります。連続する値の間引きは、Combineが今も最短で書ける領域です。

2026年の新規プロジェクトでCombineを採用する条件と見送る条件

新規プロジェクトでCombineの採用が妥当になる4つの場面

Combineを足す価値があるのは4つ。入力の間引き(debouncethrottle)、複数の非同期結果の合成(combineLatestzip)、Publisherを返す既存APIとの接続、UIKit時代のコードを含む混在プロジェクトです。いずれも「値が複数回届く」か「既にPublisherがある」かを満たします。

言語バージョンの前提も確認しておきましょう。Swift 6.3が2026年3月、Swift 6.4とXcode 27が2026年6月のWWDCで提示され、Swift 6系の言語モードでは並行処理の安全性をコンパイラが厳密に検査します。Combineのパイプラインが複数のスレッドをまたいで可変状態を運ぶ書き方だと、この検査で修正が必要な箇所が出ます。Swift 6.2以降の変更点まとめで前提を押さえ、receive(on:)の位置を設計時に決めておいてください。

Combineを採用しないと判断すべき3つの条件と代替の選択肢

次の3条件のいずれかに当たるなら、Combineは入れません。1つめ、サーバーサイドSwiftやLinux環境とコードを共有する場合。CombineはDarwin系OS専用のため、そもそも動きません。この場合はOpenCombineのような互換実装か、Swift Concurrencyへ寄せる選択になります。

2つめ、チームに経験者が1人もおらず、処理が1回きりの非同期呼び出し中心の場合。学習コストに見合いません。3つめ、AndroidとiOSで設計を揃える方針があるならasyncawaitのほうが構造の対応を取りやすくなります。それでもリアクティブ実装を入れるなら、Swift Package Managerでの依存管理でバージョン固定まで決めておいてください。

既存Combineコードを全面移行しない方がよい見極めの基準

稼働中のCombineコードを一斉に書き換える判断は、ほぼ間違いです。リアクティブなコードは処理の順序と副作用がOperatorの並びに埋まっており、書き換えの差分をテストで拾いきれません。デグレのリスクが、得られる可読性の改善を上回ります。

移すのは2種類だけ。1つはFutureJustで1回きりの処理を包んでいる箇所で、async関数に置き換えると行数が減ります。もう1つは新規に書き足す機能で、既存パイプラインに接続しないなら最初からSwift Concurrencyで書く。それ以外のOperator連結は触らずに残します。

Combine実装でよく踏む失敗パターンと受託開発での保守判断

メモリリークを生むretain cycleとweak selfの入れどころ

最頻出の事故がこれです。ViewModelがSet<AnyCancellable>を保持し、その購読のsinkクロージャがselfを強参照すると、互いを掴んだまま解放されません。画面を閉じてもViewModelが生き続け、購読も動き続けます。

対処はsinkのクロージャに[weak self]を付けることの一点。assign(to:on: self)にも同じ問題があり、iOS 14以降ならassign(to: &$property)の形を使うと循環が起きません。レビュー時はstore(in:)を含む行を洗い出し、クロージャの捕捉リストを1つずつ確認してください。

Publisherの多段連結が保守コストになる分岐点と分割の目安

Operatorの連結は3〜4段までなら読めます。それを超えてflatMapの中でさらにPublisherを作り、switchToLatestで切り替え、share()して分岐する構造になると、値がどの経路で画面へ届いたのか追えなくなる。障害調査の初動が遅れる原因はここに集中しています。

目安を1つ置くなら、1つのパイプラインでflatMapが2回以上出たら分割の合図です。中間結果に名前を付けて別のプロパティやメソッドへ切り出し、それぞれにテストを書く。デバッグ時はhandleEventsを差し込んで購読・値・完了のタイミングを出力すると、経路が可視化されます。

受託案件で引き継ぎ工数が膨らむCombine依存の見積もり観点

納品後の保守を別会社が担う案件では、Combineの多用が引き継ぎコストとして跳ね返ります。Operator連結は書いた本人には読みやすく、初見の担当者には追いにくい。この非対称性が障害対応の初動時間に効いてきます。

見積もりで見るべきは3点。パイプラインの最大段数、Subjectを経由して状態が書き換わる箇所の数、CombineとSwift Concurrencyが混在するファイル数です。3つめが多い案件は、どちらの流儀で書くかの判断が担当者ごとに揺れています。引き継ぎ時に方針を1本化する工数を見込んでおいてください。

iOSアプリ開発を内製と外注で分けるときの保守体制の判断基準

Combineの採用可否は、技術的な優劣より体制の問題として現れます。社内にiOS担当が1人しかいない状態でOperator連結を厚く積むと、その1人が抜けた瞬間に改修が止まる。逆に複数人でレビューが回る体制なら、入力処理や合成処理をCombineへ寄せた分だけコードは短くなります。

判断基準は「そのコードを6か月後に別の人が読んで直せるか」の一点。読めないなら、行数が増えてもSwift Concurrencyで素直に書いたほうが総コストは下がります。既存アプリの改修やSwift Concurrencyへの移行を外部に切り出す検討をしているなら、一創のiOSアプリ開発で対応範囲を確認してください。現行コードを見たうえで、移行する範囲と残す範囲を切り分けるところから相談できます。

よくある質問

Combineの導入検討でよく挙がる5つの疑問に答えます。

Combineは非推奨になったのですか?

2026年7月時点で、Appleから廃止や非推奨の告知は出ていません。ドキュメントも公開され続けています。ただし新機能の追加は止まっており、WWDCのセッションや公式サンプルはSwift Concurrencyを前提としたものが中心。実務上は「使い続けられるが、新しい機能を待つ対象ではない」という位置づけで扱うのが妥当です。

CombineとRxSwiftはどちらを選ぶべきですか?

新規のiOSアプリで、iOS 13以降だけを対象にできるならCombineです。OS標準のため依存関係が増えず、バージョン追従も不要になります。RxSwiftを選ぶ理由が残るのは、iOS 12以前を含む場合、Linux上のSwiftとコードを共有する場合、チームにRxSwiftの資産がある場合の3つだけです。

Combineの学習にはどれくらい時間がかかりますか?

Publisher・Subscriber・Operatorの基本とAnyCancellableの寿命管理までなら、Swiftの経験者で数日程度。難所はその先で、switchToLatestによる切り替え、share()による購読の共有、receive(on:)subscribe(on:)の使い分けには相応の期間がかかります。1回きりの非同期処理しか扱わない案件なら、この投資は回収できません。

@ObservableとObservableObjectはどう使い分けますか?

対象OSをiOS 17以降に揃えられるなら、新規のViewModelは@Observableを既定にします。プロパティ単位で変更を追跡するため再描画の範囲が狭くなり、記述量も減る。ObservableObjectを残す判断になるのは、iOS 16以前を対象に含む場合と、@Published$付きPublisherをパイプラインで加工している既存コードがある場合です。

CombineのコードはXcodeでテストできますか?

できます。XCTestExpectationsinkを組み合わせ、期待する値が届いた時点でfulfill()を呼ぶ形が基本。時間に依存するOperatorを含む場合は、スケジューラを外から受け取る設計にしておくと、テスト時に差し替えて待ち時間なしで検証できます。

関連記事

資料請求

RELATED POSTS 関連記事