📱

【RxSwift入門】①Observable、Subject、Disposableを理解する

に公開

はじめに

RxSwiftはクセがあり最初はとっつきにくいのですが、理解すれば非常に強力なツールです。
この記事では、RxSwiftとはなんぞや?というところから、ミニマムなコード例とともに理解を深めていきます。

RxSwiftとは

RxSwiftは、非同期処理とイベント処理を統一的かつ宣言的に扱えるSwiftライブラリです。

そしてMicrosoftで誕生した「Reactive Extensions」という技術を、AppleのSwift言語向けに実装したライブラリです。

Rxの語源と誕生

Rxは「Reactive Extensions(リアクティブ・エクステンションズ)」の略称で、2008年から2009年にかけてマイクロソフトのCloud Programmabilityチームが開発したライブラリ。

プロジェクトを率いたのはC#やLINQの開発にも携わったオランダ人コンピューター科学者Erik Meijer(エリック・マイヤー)で、彼はこの業績により2009年にマイクロソフトの優れた技術リーダーシップ賞を受賞。by Wikipedia

名前の由来を紐解くと、「Reactive」は値の変化に反応するリアクティブプログラミングを指し、「Extensions」は同時期に開発されたParallel Extensions技術に由来する相補的な拡張機能という意味が込められています。プロジェクトのロゴが電気ウナギなのは、元となった大規模プロジェクト「Volta」への言及らしい。

2009年11月のMicrosoft Professional Developer Conference(PDC)で初めて公開され、2011年6月21日に.NET Framework向けの最初の公式リリースが出荷。

2012年11月、マイクロソフトはReactive Extensionsをオープンソース化することを発表し、当初はCodePlex、後にGitHubで公開されました。 Wikipediaその後、JavaScript(RxJS)、Java(RxJava)、C++など多数の言語に実装が広がり、ReactiveXという統一された標準へと進化。

RxSwiftのメリット・デメリット

メリット

  • 非同期処理を簡潔に記述できる - コールバック地獄から解放される
  • 宣言的な記述 - 「何をするか」を明確に書ける
  • データバインディングが容易 - UIとロジックの同期が簡単
  • MVVMアーキテクチャとの相性が良い - データの流れが明確になる
  • コード全体の一貫性 - イベント処理の統一的な扱い

デメリット

  • 学習コストが高い - 習得まで1ヶ月程度かかる
  • デバッグが難しい - ストリームの流れを追いにくい
  • メモリ管理に注意が必要 - DisposeBag、weak selfの理解が必須

Excelから学ぶリアクティブプログラミング

リアクティブプログラミングとは、「データストリーム(値の流れ)と変化の伝播」に焦点を当てたプログラミングパラダイムです。

個人的に最も分かりやすい例は、Excelの計算式です。以下の例を見てください。

A1 B1 C1
2 5 =A1*B1

上記のExcelのセルA1 or B1の値を変更すると、それを参照している計算式の値が自動的に更新されます。
この「関係性を定義すれば自動的に更新される」仕組みこそがリアクティブプログラミングの核心です。

Swiftコードで実現する

このような仕組みをSwiftで実現するのがRxSwiftです。

先ほどのExcelの例をコードで実現すると以下のようになります。

// ExcelのA1セル
let a1 = BehaviorRelay<Int>(value: 2)

// ExcelのB1セル
let b1 = BehaviorRelay<Int>(value: 5)

// ExcelのC1セル(=A1*B1)
let c1 = Observable.combineLatest(a1, b1) { $0 * $1 }

// C1の変化を監視
c1.subscribe(onNext: { result in
    print("C1の値: \(result)")
})

// A1の値を変更すると、C1が自動的に更新される
a1.accept(3) // C1の値: 15 が出力される

この記事では、こうした仕組みを一つずつ理解していきます。

RxSwiftの核となる基本概念

今回の導入編では、RxSwiftの基礎となる以下の概念を学びます:

  • Observable - 時間とともに流れるイベントの川
  • Observer/Subscribe - 流れを受け取り反応する存在
  • Subject - 双方向の橋渡し役
  • Disposable/DisposeBag - メモリ管理の仕組み

これらの概念を理解することで、次回以降で実際にアプリを作る準備が整います。

1. Observable - 時間とともに流れるイベントの川

Observableは「時間の経過とともにイベントやデータを放出する非同期シーケンス」です。
川の流れに例えると理解しやすく、データは上流から下流へと時間とともに流れていきます。

Observableが放出する3種類のイベント

Observableは以下の3種類のイベントを放出します:

  • .next(値) - 通常の値イベント(複数回発生可能)
  • .error(エラー) - エラー通知(1回だけ、これで終了)
  • .completed - 正常完了通知(1回だけ、これで終了)
--●--●--●--|-->  時間の流れ
  ↑  ↑  ↑  ↑
  値 値 値 完了

最もシンプルなコード例

まずは最もシンプルなObservableから始めましょう。

import RxSwift

// 数値を3つ流すObservable
let observable = Observable.of(1, 2, 3)

// 購読して値を受け取る
observable.subscribe { event in
    print(event)
}

// 出力:
// next(1)
// next(2)
// next(3)
// completed

このコードでは:

  1. Observable.of(1, 2, 3) で3つの値を流すObservableを作成
  2. subscribe メソッドで購読開始
  3. 各イベントが順番に流れてくる

Observableの主な生成方法

Observableを生成する方法はいくつかあります:

just - 単一の値を流す

let observable = Observable.just(42)

observable.subscribe { event in
    print(event)
}

// 出力:
// next(42)
// completed

of - 複数の値を流す

let observable = Observable.of(1, 2, 3, 4, 5)

observable.subscribe { event in
    print(event)
}

// 出力:
// next(1)
// next(2)
// next(3)
// next(4)
// next(5)
// completed

from - 配列から生成

let numbers = [10, 20, 30]
let observable = Observable.from(numbers)

observable.subscribe { event in
    print(event)
}

// 出力:
// next(10)
// next(20)
// next(30)
// completed

empty - 何も流さずに完了

let observable = Observable<Int>.empty()

observable.subscribe { event in
    print(event)
}

// 出力:
// completed

never - 何も流さず完了もしない

let observable = Observable<Int>.never()

observable.subscribe { event in
    print(event)
}

// 出力なし(何も起こらない)

create - カスタムObservable

より複雑なObservableを作成したい場合はcreateを使います:

let observable = Observable<Int>.create { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onCompleted()
    
    return Disposables.create()
}

observable.subscribe { event in
    print(event)
}

// 出力:
// next(1)
// next(2)
// next(3)
// completed

2. Observer/Subscribe - 観測する仕組み

ObserverはObservableから放出されるイベントを受け取り、それに反応する側です。
実際のコードでは、subscribe()メソッドを通じてObserverをObservableに接続します。

基本的なsubscribe

先ほどのコードでは簡略化していましたが、実際には各イベントごとに処理を分けることができます:

import RxSwift

let observable = Observable.of(1, 2, 3)

let disposable = observable.subscribe(
    onNext: { value in
        print("値: \(value)")
    },
    onError: { error in
        print("エラー: \(error)")
    },
    onCompleted: {
        print("完了")
    }
)

// 出力:
// 値: 1
// 値: 2
// 値: 3
// 完了

イベント別の処理

それぞれのイベントに対して、異なる処理を書くことができます:

onNext - 通常の値を受け取る

observable.subscribe(onNext: { value in
    print("受け取った値: \(value)")
})

onError - エラーを処理

let errorObservable = Observable<Int>.create { observer in
    observer.onNext(1)
    observer.onError(NSError(domain: "TestError", code: -1))
    return Disposables.create()
}

errorObservable.subscribe(
    onNext: { value in
        print("値: \(value)")
    },
    onError: { error in
        print("エラーが発生: \(error)")
    }
)

// 出力:
// 値: 1
// エラーが発生: Error Domain=TestError Code=-1 "(null)"

onCompleted - 完了を検知

observable.subscribe(
    onNext: { value in
        print("値: \(value)")
    },
    onCompleted: {
        print("すべての値を受け取りました")
    }
)

必要なイベントだけ処理する

すべてのイベントを処理する必要はありません。必要なものだけ指定できます:

// onNextだけ処理
observable.subscribe(onNext: { value in
    print(value)
})

// onErrorとonCompletedだけ処理
observable.subscribe(
    onError: { error in
        print("エラー: \(error)")
    },
    onCompleted: {
        print("完了")
    }
)

3. Subject と Relay - 双方向の橋渡し役

SubjectとRelayは、ObservableでありながらObserverのように値を注入できる特殊な型です。
通常のObservableは読み取り専用ですが、SubjectとRelayは実行時に手動でイベントを送信できます。

RxSwiftには主に以下の種類があります:

Subject(RxSwiftが提供)

  1. PublishSubject - 購読した時点以降のイベントのみを受け取る
  2. BehaviorSubject - 最後の値を保持し、購読時にその値を受け取る
  3. ReplaySubject - 指定した数の過去のイベントを保持し、購読時に受け取る

Relay(RxRelayが提供)

  1. BehaviorRelay - BehaviorSubjectのエラーなし版(実務で推奨

PublishSubject - 購読後のイベントのみ

特徴

  • 購読した時点以降のイベントのみを受信
  • 過去のイベントは受け取れない
  • リアルタイムなイベント通知に適している

コード例

import RxSwift

let subject = PublishSubject<String>()
let disposeBag = DisposeBag()

// イベントを送信(まだ誰も購読していない)
subject.onNext("Hello")

// 購読開始
subject.subscribe(onNext: { value in
    print("購読者1: \(value)")
})
.disposed(by: disposeBag)

// イベントを送信
subject.onNext("World")  // 購読者1: World
subject.onNext("!")      // 購読者1: !

// 新しい購読者を追加
subject.subscribe(onNext: { value in
    print("購読者2: \(value)")
})
.disposed(by: disposeBag)

subject.onNext("RxSwift")
// 購読者1: RxSwift
// 購読者2: RxSwift

出力結果

購読者1: World
購読者1: !
購読者1: RxSwift
購読者2: RxSwift

BehaviorSubject - 最後の値を保持

特徴

  • 初期値が必須
  • 最後に送信された値を保持
  • 新しい購読者は購読時点の最新値を即座に受け取る
  • 状態管理に適している

コード例

import RxSwift

// 初期値 "Initial" で作成
let subject = BehaviorSubject<String>(value: "Initial")
let disposeBag = DisposeBag()

// 購読開始(初期値を受け取る)
subject.subscribe(onNext: { value in
    print("購読者1: \(value)")
})
.disposed(by: disposeBag)

// 出力: 購読者1: Initial

subject.onNext("Hello")   // 購読者1: Hello
subject.onNext("World")   // 購読者1: World

// 新しい購読者を追加(最新値 "World" を受け取る)
subject.subscribe(onNext: { value in
    print("購読者2: \(value)")
})
.disposed(by: disposeBag)

// 出力: 購読者2: World

subject.onNext("!")
// 購読者1: !
// 購読者2: !

出力結果

購読者1: Initial
購読者1: Hello
購読者1: World
購読者2: World
購読者1: !
購読者2: !

ReplaySubject - 複数の過去イベントを保持

特徴

  • 指定した数の過去のイベントを保持
  • 新しい購読者は過去のイベントをまとめて受け取る
  • 履歴管理や分析に適している

コード例

import RxSwift

// 過去2つのイベントを保持
let subject = ReplaySubject<String>.create(bufferSize: 2)
let disposeBag = DisposeBag()

subject.onNext("A")
subject.onNext("B")
subject.onNext("C")

// 購読開始(過去2つ "B", "C" を受け取る)
subject.subscribe(onNext: { value in
    print("購読者1: \(value)")
})
.disposed(by: disposeBag)

// 出力:
// 購読者1: B
// 購読者1: C

subject.onNext("D")  // 購読者1: D

// 新しい購読者(過去2つ "C", "D" を受け取る)
subject.subscribe(onNext: { value in
    print("購読者2: \(value)")
})
.disposed(by: disposeBag)

// 出力:
// 購読者2: C
// 購読者2: D

出力結果

購読者1: B
購読者1: C
購読者1: D
購読者2: C
購読者2: D

BehaviorRelay - エラーなし版(実務推奨)

特徴

  • onErroronCompletedがない(絶対に終了しない
  • BehaviorSubjectと同じく初期値が必須
  • 最新の値を保持
  • .accept()で値を更新
  • UIの状態管理に最適
RxRelayとRxCocoaの関係について

BehaviorRelayは、RxSwift 5.0(2019年4月)以降、RxRelayという独立したフレームワークに分離されました。

フレームワークの構成:

  • RxSwift - コア機能(Observable、Subjectなど)
  • RxRelay - 非終了ストリーム(BehaviorRelay、PublishRelayなど)
  • RxCocoa - UIバインディング機能

import文の選択肢:

  1. import RxRelay(推奨) - 必要最小限
import RxSwift
import RxRelay  // BehaviorRelayのために明示的に
  1. import RxCocoaでも動作する - RxCocoaがRxRelayを再エクスポートしているため
import RxSwift
import RxCocoa  // RxRelayも自動的に含まれる

どちらを使うべきか?

  • ViewModelやビジネスロジック層import RxRelay(UI非依存)
  • ViewやUIコードimport RxCocoa(UIバインディングも使うため)

この記事では、技術的に正確なimport RxRelayを使用していますが、実務では両方のインポート方法が広く使われています。

コード例

import RxSwift
import RxRelay  // ← これが必要!

let relay = BehaviorRelay<Int>(value: 0)
let disposeBag = DisposeBag()

// 購読開始
relay.subscribe(onNext: { value in
    print("現在の値: \(value)")
})
.disposed(by: disposeBag)

// 出力: 現在の値: 0

// 値を更新(onNextではなくacceptを使う)
relay.accept(1)  // 現在の値: 1
relay.accept(2)  // 現在の値: 2
relay.accept(3)  // 現在の値: 3

// 現在の値を取得
print("最新値: \(relay.value)")  // 最新値: 3

なぜBehaviorRelayが実務で推奨されるのか?

  1. エラーで終了しない - UIの状態管理で予期しない終了を防ぐ
  2. シンプルなAPI - .accept()だけで値を更新
  3. 安全性 - エラー処理を気にする必要がない
  4. 型安全 - コンパイル時に終了イベントの送信を防げる

Subject/Relayの選び方

どのSubject/Relayを使うべきか迷ったら、以下のフローチャートを参考にしてください:

ストリームが終了する可能性がある?
├─ Yes(エラーや完了がある)→ Subject
│   └─ 値を保持する必要がある?
│       ├─ No  → PublishSubject
│       ├─ 1個  → BehaviorSubject
│       └─ 複数 → ReplaySubject
│
└─ No(絶対に終了しない)→ Relay
    └─ BehaviorRelay(推奨)

実際の使用例

import RxSwift
import RxRelay

class ViewModel {
    // UI状態(エラーなし、終了なし)→ Relay
    let userName = BehaviorRelay<String>(value: "")
    let isLoading = BehaviorRelay<Bool>(value: false)
    
    // リアルタイムイベント(通知など)→ Subject
    let notification = PublishSubject<String>()
    
    // API結果(エラーあり)→ Subject
    let apiResult = BehaviorSubject<Result<Data, Error>>(value: .success(Data()))
    
    // 履歴(最新3件を保持)→ Subject
    let searchHistory = ReplaySubject<String>.create(bufferSize: 3)
}

メモリ管理

SubjectやRelayを使う際は、必ずdisposed(by: disposeBag)を使ってメモリリークを防ぎましょう:

let subject = PublishSubject<String>()
let disposeBag = DisposeBag()

subject
    .subscribe(onNext: { value in
        print(value)
    })
    .disposed(by: disposeBag)  // ← これ重要!

4. Disposable/DisposeBag - メモリ管理の仕組み

Disposableは購読を管理し、メモリリークを防ぐための仕組みです。
RxSwiftでは、ObservableをsubscribeするとDisposableオブジェクトが返されます。

なぜDisposableが必要なのか?

Observableを購読すると、そのストリームは自動的には解放されません。
購読を解除しないと、以下の問題が発生します:

  1. メモリリーク - 不要なオブジェクトがメモリに残り続ける
  2. 予期しない動作 - 画面を閉じた後もイベントが流れ続ける
  3. 循環参照 - ViewControllerが解放されない

DisposeBag - 自動的な購読解除(基本)

DisposeBagは複数のDisposableをまとめて管理し、自動的に解放するコンテナです。

これがRxSwiftにおける基本的かつ推奨される使い方です。

基本的な使い方

import RxSwift

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.of(1, 2, 3)
            .subscribe(onNext: { value in
                print("値: \(value)")
            })
            .disposed(by: disposeBag)  // DisposeBagに追加
    }
    
    deinit {
        print("ViewControllerが解放されました")
        // この時点でDisposeBagが自動的にすべての購読を解除
    }
}

DisposeBagの仕組み

  1. ViewControllerが作成されると、disposeBagも作成される
  2. 各購読を.disposed(by: disposeBag)でDisposeBagに追加
  3. ViewControllerが解放される(deinit)と、DisposeBagも一緒に解放される
  4. DisposeBagが解放されると、登録されたすべての購読が自動的に解除される

Disposableの手動解除(特殊なケース)

DisposeBagを使わず手動で購読を解除もできますが、これは特殊なケースでのみ使用します。

一般的なiOSアプリ開発では、DisposeBagによる自動解除が推奨されます。

手動で購読解除する例

import RxSwift

let observable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)

// 購読開始
let disposable = observable.subscribe(onNext: { value in
    print("値: \(value)")
})

// 5秒後に購読解除
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    disposable.dispose()
    print("購読解除")
}

手動解除が必要なケース

  • タイマーを途中で停止したい場合
  • 特定の条件で購読を解除したい場合
  • ViewControllerのライフサイクルとは無関係に管理したい場合

循環参照を防ぐ - [weak self]

Observableのクロージャ内でselfを参照すると、循環参照が発生する可能性があります。

循環参照とは?

オブジェクト同士がお互いを強く参照し合うことで、どちらも解放されなくなる問題です。

ViewController → Observable(強参照)
       ↑              ↓
       └──── クロージャ内でselfを参照(強参照)

この状態だと、ViewControllerを閉じてもメモリから解放されません。

悪い例(循環参照が発生)

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private var count = 0
    
    func setupObservable() {
        let button = UIButton()
        
        button.rx.tap
            .subscribe(onNext: {
                self.count += 1  // ← selfを強参照
                print("Count: \(self.count)")
            })
            .disposed(by: disposeBag)
    }
    
    deinit {
        print("解放されました")  // ← これが呼ばれない!
    }
}

良い例([weak self]で解決)

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private var count = 0
    
    func setupObservable() {
        let button = UIButton()
        
        button.rx.tap
            .subscribe(onNext: { [weak self] in
                guard let self = self else { return }
                self.count += 1
                print("Count: \(self.count)")
            })
            .disposed(by: disposeBag)
    }
    
    deinit {
        print("解放されました")  // ← 正しく呼ばれる
    }
}

[weak self]の仕組み

  • [weak self] - selfを弱参照する(循環参照を防ぐ)
  • guard let self = self else { return } - selfがnilでないことを確認

selfが既に解放されている(ViewControllerが閉じられた)場合、クロージャは実行されずにreturnされます。

[weak self] vs [unowned self]

weak self unowned self
安全性 安全(nilチェック必須) 危険(nilの場合クラッシュ)
使用場面 ほとんどの場合 selfが必ず存在する場合のみ
推奨度 ⭐⭐⭐⭐⭐ 推奨 ⭐ 非推奨

DisposeBagの実践的な使い方

ViewControllerでの典型的なパターン

import UIKit
import RxSwift
import RxCocoa

class MyViewController: UIViewController {
    // MARK: - Properties
    private let disposeBag = DisposeBag()
    private let viewModel = MyViewModel()
    
    // MARK: - UI Components
    private let button = UIButton()
    private let label = UILabel()
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
    }
    
    // MARK: - Setup
    private func setupBindings() {
        // ボタンタップを監視
        button.rx.tap
            .subscribe(onNext: { [weak self] in
                self?.handleButtonTap()
            })
            .disposed(by: disposeBag)
        
        // ViewModelの値を監視
        viewModel.text
            .bind(to: label.rx.text)
            .disposed(by: disposeBag)
        
        // 複数のストリームをまとめて管理
        Observable.combineLatest(
            viewModel.isLoading,
            viewModel.isValid
        )
        .subscribe(onNext: { [weak self] isLoading, isValid in
            self?.updateUI(isLoading: isLoading, isValid: isValid)
        })
        .disposed(by: disposeBag)
    }
    
    private func handleButtonTap() {
        print("ボタンがタップされました")
    }
    
    private func updateUI(isLoading: Bool, isValid: Bool) {
        // UI更新処理
    }
    
    deinit {
        print("MyViewControllerが解放されました")
    }
}

DisposeBagのベストプラクティス

  1. ViewControllerごとに1つのDisposeBag - クラスのプロパティとして定義
  2. 必ず.disposed(by: disposeBag)を書く - これを忘れるとメモリリーク
  3. [weak self]を使う - 循環参照を防ぐ
  4. guard let self = self else { return } - selfがnilの場合の安全な処理

まとめ

この記事では、RxSwiftの基礎として以下の概念を学びました。

学んだこと

1. Observable - 時間とともに流れるイベント

  • just, of, from, createなどで生成
  • 3種類のイベント: next, error, completed
  • Observableは時間とともにイベントを流すストリーム

2. Observer/Subscribe - 観測する仕組み

  • subscribe(onNext:onError:onCompleted:)でイベントを受信
  • 必要なイベントだけ処理できる
  • Observableを購読してイベントに反応する

3. Subject と Relay - 双方向の橋渡し

  • Subject(RxSwift提供)
    • PublishSubject:購読後のイベントのみ
    • BehaviorSubject:最後の値を保持
    • ReplaySubject:複数の過去イベントを保持
  • Relay(RxRelay提供)
    • BehaviorRelay:エラーなし版(実務推奨
  • 選択基準:終了する可能性があるか→値の保持が必要か→過去値の数

4. Disposable/DisposeBag - メモリ管理

  • disposed(by: disposeBag)で自動的に購読解除(基本的な使い方
  • [weak self]で循環参照を防ぐ
  • ViewControllerなどのライフサイクルに合わせて自動解放

次のステップ

基礎概念を理解したら、次は実際にアプリを作りながら学んでいきましょう!

今後の学習ロードマップ

第2回:ジェネリクス&エクステンション編 ⭐

  • Observable のジェネリクス - 型安全なストリーム
  • Reactive Extension - UIKitコンポーネントへのRx拡張
  • カスタムExtension - 独自のRx機能を追加

第3回:カウンターアプリ ⭐

  • RxCocoa の基礎 - UIKitとの連携
  • 基本的な Operator - map, bind(to:)
  • MVVM パターン - ViewとViewModelの分離
  • テストの書き方 - RxSwiftアプリのテスト

第4回:ToDoリストアプリ ⭐⭐

  • 配列の管理 - BehaviorRelay<[Todo]>
  • Operator の活用 - filter, map, scan
  • UITableView との連携
  • 複数のイベントを組み合わせる

第5回:リアルタイム検索アプリ ⭐⭐⭐

  • 高度な Operator - debounce, distinctUntilChanged, flatMap
  • API通信 - URLSession + RxSwift
  • Scheduler - バックグラウンド処理とUI更新
  • Hot vs Cold Observable - 詳細解説

第6回以降

  • タイマー/ストップウォッチアプリ - 時間ベースのObservable
  • フォームバリデーションアプリ - 複数条件のリアルタイムチェック
  • 天気アプリ - エラーハンドリングと位置情報連携

参考資料

jinjerテックブログ

Discussion