📱

【RxSwift入門】②ジェネリクス&エクステンションを理解する

に公開

はじめに

第1回では、RxSwiftの基本概念(Observable、Subject、Disposable)を学びました。
今回は、これまで何気なく使っていた Observable<T><T>.rx.tap といった記法の仕組みを深く理解していきます。

この記事を読むことで、RxSwiftのコードがより読みやすくなり、自分で拡張機能を作れるようになります。

この記事で学ぶこと

  1. ジェネリクス - Observable<T><T> とは何か

    • 型安全性の理解
    • 複数の型でObservableを使う
    • カスタム型での利用
  2. Reactive Extension - .rx の仕組み

    • UIButton.rx.tap はどこから来たのか
    • RxCocoaが提供する機能
    • 自分で拡張を作る方法

前提知識

この記事は 第1回「基礎知識編」 を読んだ前提で進めます。
以下の概念を理解していることが前提です:

用語 日本語訳 RxSwiftでの概念
Observable 観察可能な イベントを発行するストリームを表現する型。時間とともに値を発行できる
subscribe 購読する Observableが発行するイベントを受け取るための操作。イベントストリームの終点として機能する
Subject 主体 ObservableとObserverの両方の性質を持つ型。値を手動で流せるObservable
Relay 中継器 エラー・完了イベントを流さない安全なSubject。UIバインディングに最適
Disposable 破棄可能な 購読のライフサイクルを管理するオブジェクト
DisposeBag 破棄袋 複数のDisposableをまとめて管理し、解放時に一括で購読を解除する
weak self 弱参照 クロージャ内でのメモリリーク防止のための参照方法

まだ読んでいない方は、第1回の記事から始めることをおすすめします。


1. Observable<T>のジェネリクスを理解する

1-1. なぜObservable<T>なのか

第1回では、以下のようなコードを書きました:

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

実は、このコードは以下のように型が推論されています:

let observable: Observable<Int> = Observable.of(1, 2, 3)

この <Int> の部分が「ジェネリクス」と呼ばれる仕組みです。

ジェネリクスとは?

ジェネリクスは、異なる型に対して同じ処理を適用できる仕組みです。

例えば、Swiftの配列 Array<T> もジェネリクスです:

let numbers: Array<Int> = [1, 2, 3]        // Int型の配列
let names: Array<String> = ["太郎", "花子"]  // String型の配列

同じように、Observableも 流すデータの型<T> で指定します:

let numbers: Observable<Int> = Observable.of(1, 2, 3)
let names: Observable<String> = Observable.of("太郎", "花子")

なぜジェネリクスが必要なのか?

ジェネリクスがないと、型ごとに別のObservableを作る必要があります:

// もしジェネリクスがなかったら...
class ObservableInt { }      // Int専用
class ObservableString { }   // String専用
class ObservableUser { }     // User専用
// ... 型の数だけクラスが必要!

ジェネリクスのおかげで、1つのObservableクラスですべての型に対応できます。

1-2. 型安全性とは

ジェネリクスの最大のメリットは 型安全性 です。

型安全性の例

import RxSwift

// Int型のObservable
let numbers = Observable.of(1, 2, 3)

numbers.subscribe(onNext: { value in
    // valueは必ずInt型
    let doubled = value * 2  // 安全に計算できる
    print("2倍: \(doubled)")
})

// 出力:
// 2倍: 2
// 2倍: 4
// 2倍: 6

もし型の情報が失われると、このような安全な計算はできません:

// Any型を使うと型安全性が失われる
let mixed: [Any] = [1, "Hello", 3]  // Int と String が混在

mixed.forEach { value in
    // value は Any型 → コンパイラが型を判断できない
    // let doubled = value * 2          // ❌ コンパイルエラー(Anyに * は使えない)
    // let doubled = (value as! Int) * 2  // ⚠️ "Hello"で実行時クラッシュ
}

コンパイル時に型エラーを検出

ジェネリクスのおかげで、実行前にエラーを検出できます:

let numbers: Observable<Int> = Observable.of(1, 2, 3)

numbers.subscribe(onNext: { value in
    print(value.uppercased())  // ❌ コンパイルエラー!
    // Int型にuppercased()メソッドは存在しない
})

これにより、実行時のクラッシュを防げます。

1-3. 複数の型でObservableを使う

それでは、実際に複数の型でObservableを作ってみましょう。

Int型のObservable

import RxSwift

let disposeBag = DisposeBag()

// Int型のObservable
let numbers: Observable<Int> = Observable.of(1, 2, 3, 4, 5)

numbers.subscribe(onNext: { value in
    print("値: \(value), 2倍: \(value * 2)")
})
.disposed(by: disposeBag)

// 出力:
// 値: 1, 2倍: 2
// 値: 2, 2倍: 4
// 値: 3, 2倍: 6
// 値: 4, 2倍: 8
// 値: 5, 2倍: 10

String型のObservable

// String型のObservable
let names: Observable<String> = Observable.of("太郎", "花子", "次郎")

names.subscribe(onNext: { name in
    print("こんにちは、\(name)さん")
})
.disposed(by: disposeBag)

// 出力:
// こんにちは、太郎さん
// こんにちは、花子さん
// こんにちは、次郎さん

Bool型のObservable

// Bool型のObservable
let flags: Observable<Bool> = Observable.of(true, false, true, true)

flags.subscribe(onNext: { flag in
    print(flag ? "✅ 有効" : "❌ 無効")
})
.disposed(by: disposeBag)

// 出力:
// ✅ 有効
// ❌ 無効
// ✅ 有効
// ✅ 有効

1-4. カスタム型でObservableを使う

独自の構造体やクラスでもObservableを使えます。

カスタム型の定義

// ユーザー情報を表す構造体
struct User {
    let id: Int
    let name: String
    let age: Int
}

カスタム型のObservable

// User型のObservable
let users: Observable<User> = Observable.of(
    User(id: 1, name: "太郎", age: 25),
    User(id: 2, name: "花子", age: 30),
    User(id: 3, name: "次郎", age: 28)
)

users.subscribe(onNext: { user in
    print("ID: \(user.id), 名前: \(user.name), 年齢: \(user.age)")
})
.disposed(by: disposeBag)

// 出力:
// ID: 1, 名前: 太郎, 年齢: 25
// ID: 2, 名前: 花子, 年齢: 30
// ID: 3, 名前: 次郎, 年齢: 28

より実践的な例:APIレスポンス

実務では、APIから取得したデータをObservableで流すことがよくあります:

// APIレスポンスを表す構造体
struct APIResponse {
    let statusCode: Int
    let data: Data
    let message: String
}

// APIレスポンスのObservable
let apiResponse: Observable<APIResponse> = Observable.create { observer in
    // APIコールを模擬
    let response = APIResponse(
        statusCode: 200,
        data: Data(),
        message: "成功"
    )

    observer.onNext(response)
    observer.onCompleted()

    return Disposables.create()
}

apiResponse.subscribe(onNext: { response in
    print("ステータス: \(response.statusCode)")
    print("メッセージ: \(response.message)")
})
.disposed(by: disposeBag)

// 出力:
// ステータス: 200
// メッセージ: 成功

1-5. 型推論の活用

Swiftの型推論により、多くの場合は型を明示的に書く必要がありません。

型推論の例

// 型を明示的に書く場合
let numbers1: Observable<Int> = Observable.of(1, 2, 3)

// 型推論を使う場合(推奨)
let numbers2 = Observable.of(1, 2, 3)  // Observable<Int>と推論される

// どちらも同じ

型を明示すべき場合

ただし、以下の場合は型を明示する必要があります:

ケース1: 空のObservable

// ❌ これはエラー(型が推論できない)
let empty = Observable.empty()

// ✅ 型を明示する
let empty: Observable<String> = Observable.empty()

ケース2: Subjectの初期化

// ❌ これはエラー(型が推論できない)
let subject = PublishSubject()

// ✅ 型を明示する
let subject = PublishSubject<String>()

ケース3: 複数の型の可能性がある場合

// 型が曖昧な場合は明示する
let value: Observable<Double> = Observable.just(5)  // IntではなくDouble

1-6. Subject/Relayでもジェネリクスは同じ

第1回で学んだSubjectやRelayも、同じようにジェネリクスを使います。

PublishSubject<T>

import RxSwift

let disposeBag = DisposeBag()

// String型のPublishSubject
let subject = PublishSubject<String>()

subject.subscribe(onNext: { value in
    print("受信: \(value)")
})
.disposed(by: disposeBag)

subject.onNext("Hello")   // 受信: Hello
subject.onNext("World")   // 受信: World
subject.onNext("RxSwift") // 受信: RxSwift

BehaviorRelay<T>

import RxRelay

// Int型のBehaviorRelay
let relay = BehaviorRelay<Int>(value: 0)

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

// 出力: 現在の値: 0

relay.accept(10)  // 現在の値: 10
relay.accept(20)  // 現在の値: 20

カスタム型のRelay

struct Todo {
    let id: Int
    let title: String
    let isCompleted: Bool
}

// Todo型のBehaviorRelay
let todoRelay = BehaviorRelay<Todo>(
    value: Todo(id: 1, title: "RxSwiftを学ぶ", isCompleted: false)
)

todoRelay.subscribe(onNext: { todo in
    print("TODO: \(todo.title), 完了: \(todo.isCompleted ? "✅" : "❌")")
})
.disposed(by: disposeBag)

// 出力: TODO: RxSwiftを学ぶ, 完了: ❌

// TODOを更新
todoRelay.accept(
    Todo(id: 1, title: "RxSwiftを学ぶ", isCompleted: true)
)
// 出力: TODO: RxSwiftを学ぶ, 完了: ✅

配列型のRelay(実務でよく使う)

配列型のRelayは、リスト表示などでよく使います:

// Todo配列のBehaviorRelay
let todosRelay = BehaviorRelay<[Todo]>(value: [])

todosRelay.subscribe(onNext: { todos in
    print("TODO数: \(todos.count)")
    todos.forEach { todo in
        print("- \(todo.title)")
    }
})
.disposed(by: disposeBag)

// 出力: TODO数: 0

// TODOを追加
todosRelay.accept([
    Todo(id: 1, title: "RxSwiftを学ぶ", isCompleted: false),
    Todo(id: 2, title: "アプリを作る", isCompleted: false)
])

// 出力:
// TODO数: 2
// - RxSwiftを学ぶ
// - アプリを作る

2. Reactive Extensionの仕組みを理解する

2-1. .rxプロパティの正体

第1回のカウンターアプリでは、以下のようなコードを書きました:

button.rx.tap
    .subscribe(onNext: {
        print("ボタンがタップされました")
    })

この .rx.tap は一体どこから来たのでしょうか?

実は、これは RxCocoaが提供するReactive Extension です。

UIButtonには.rxプロパティがない

通常のUIButtonには .rx というプロパティは存在しません:

let button = UIButton()
// button.rx  ← これはどこから来たのか?

Reactive Extensionの仕組み

RxCocoaは、Extensionを使ってUIKitコンポーネントに .rx プロパティを追加しています。

簡略化したコードで仕組みを見てみましょう:

// RxCocoaが内部で行っていること(簡略版)

// 1. Reactive<Base>という汎用的なラッパー構造体を定義
struct Reactive<Base> {
    let base: Base

    init(_ base: Base) {
        self.base = base
    }
}

// 2. すべてのNSObjectに.rxプロパティを追加
extension NSObject {
    var rx: Reactive<Self> {
        return Reactive(self)
    }
}

// 3. UIButtonに対してReactiveの機能を追加
extension Reactive where Base: UIButton {
    var tap: Observable<Void> {
        // タップイベントをObservableに変換
        // (実際の実装は複雑ですが、概念的にはこのような仕組み)
        return Observable.create { observer in
            // タップイベントのハンドリング
            return Disposables.create()
        }
    }
}

このように、Extensionの連鎖.rx.tap が実現されています:

UIButton
  └─ .rx プロパティ(NSObjectのExtensionで追加)
       └─ .tap プロパティ(Reactive<UIButton>のExtensionで追加)

2-2. RxCocoaが提供するReactive Extension

RxCocoaは、UIKitのほとんどのコンポーネントにReactive Extensionを提供しています。

UIButton - タップイベント

import RxSwift
import RxCocoa

let button = UIButton()
let disposeBag = DisposeBag()

// タップイベントを購読
button.rx.tap
    .subscribe(onNext: {
        print("ボタンがタップされました")
    })
    .disposed(by: disposeBag)

UITextField - テキスト入力

let textField = UITextField()

// テキスト変更を購読(リアルタイム)
textField.rx.text
    .subscribe(onNext: { text in
        print("入力されたテキスト: \(text ?? "")")
    })
    .disposed(by: disposeBag)

UILabel - テキスト設定

let label = UILabel()
let textRelay = BehaviorRelay<String>(value: "初期値")

// Relayの値をLabelに自動的にバインド
textRelay
    .bind(to: label.rx.text)
    .disposed(by: disposeBag)

textRelay.accept("新しいテキスト")  // Labelのテキストが自動更新される

UISwitch - ON/OFFの状態

let toggle = UISwitch()

// ON/OFFの状態変化を購読
toggle.rx.isOn
    .subscribe(onNext: { isOn in
        print(isOn ? "ON" : "OFF")
    })
    .disposed(by: disposeBag)

UISlider - 値の変化

let slider = UISlider()

// スライダーの値変化を購読
slider.rx.value
    .subscribe(onNext: { value in
        print("スライダーの値: \(value)")
    })
    .disposed(by: disposeBag)

2-3. Reactive Extensionの実装パターン

RxCocoaのReactive Extensionは、主に2つのパターンで実装されています。

パターン1: イベントをObservableに変換

ユーザーの操作(タップ、入力など)をObservableに変換:

extension Reactive where Base: UIButton {
    var tap: Observable<Void> {
        // イベントをObservableとして公開
    }
}

使用例:

button.rx.tap  // Observable<Void>
    .subscribe(onNext: {
        print("タップされた")
    })

パターン2: プロパティをObservableに変換

UIコンポーネントのプロパティをObservableに変換:

extension Reactive where Base: UITextField {
    var text: Observable<String?> {
        // テキストの変化をObservableとして公開
    }
}

使用例:

textField.rx.text  // Observable<String?>
    .subscribe(onNext: { text in
        print("テキスト: \(text ?? "")")
    })

パターン3: Binderを使った単方向バインディング

Observableの値をUIに反映する:

extension Reactive where Base: UILabel {
    var text: Binder<String?> {
        // Observableの値をLabelに反映
    }
}

使用例:

let textObservable = Observable.just("こんにちは")

textObservable
    .bind(to: label.rx.text)  // Labelに自動反映
    .disposed(by: disposeBag)

3. 自分でReactive Extensionを作ってみる

それでは、実際に自分でReactive Extensionを作ってみましょう。

3-1. UILabelにアニメーション機能を追加

通常、UILabelのテキストを変更しても、アニメーションなしで即座に変わります。
ここでは、フェードアニメーション付きでテキストを変更する Reactive Extensionを作ります。

実装コード

import UIKit
import RxSwift
import RxCocoa

// UILabelにカスタムReactive Extensionを追加
extension Reactive where Base: UILabel {

    /// アニメーション付きでテキストを設定するBinder
    var animatedText: Binder<String?> {
        return Binder(self.base) { label, text in
            UIView.transition(
                with: label,
                duration: 0.3,
                options: .transitionCrossDissolve,
                animations: {
                    label.text = text
                },
                completion: nil
            )
        }
    }
}

使い方

let label = UILabel()
let textRelay = BehaviorRelay<String>(value: "初期テキスト")
let disposeBag = DisposeBag()

// アニメーション付きでバインド
textRelay
    .bind(to: label.rx.animatedText)  // カスタムExtensionを使用
    .disposed(by: disposeBag)

// テキストを変更すると、フェードアニメーションで切り替わる
textRelay.accept("新しいテキスト")

仕組みの解説

extension Reactive where Base: UILabel {
    //         ↑
    //   「BaseがUILabelの場合のみ」という制約

    var animatedText: Binder<String?> {
        //            ↑
        //      Binderは「値を受け取るだけ」の型

        return Binder(self.base) { label, text in
            //          ↑
            //     self.baseはUILabel自身

            // アニメーション処理
            UIView.transition(with: label, ...)
        }
    }
}
  • extension Reactive where Base: UILabel - UILabelのみに機能を追加
  • Binder<String?> - Observableの値を受け取ってUIに反映する型
  • self.base - 実際のUILabelインスタンス

3-2. デバッグ用のカスタムオペレーターを作る

開発中、Observableの流れをデバッグしたいことがよくあります。
ここでは、ログを出力するカスタムオペレーター を作ります。

実装コード

import RxSwift

// ObservableTypeを拡張(すべてのObservableで使える)
extension ObservableType {

    /// デバッグログを出力するカスタムオペレーター
    func debug(_ identifier: String) -> Observable<Element> {
        return self.do(
            onNext: { value in
                print("[\(identifier)] Next: \(value)")
            },
            onError: { error in
                print("[\(identifier)] Error: \(error)")
            },
            onCompleted: {
                print("[\(identifier)] Completed")
            },
            onSubscribe: {
                print("[\(identifier)] Subscribe開始")
            },
            onDispose: {
                print("[\(identifier)] Dispose")
            }
        )
    }
}

使い方

let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5)
    .debug("数値ストリーム")  // カスタムオペレーターを使用
    .subscribe()
    .disposed(by: disposeBag)

// 出力:
// [数値ストリーム] Subscribe開始
// [数値ストリーム] Next: 1
// [数値ストリーム] Next: 2
// [数値ストリーム] Next: 3
// [数値ストリーム] Next: 4
// [数値ストリーム] Next: 5
// [数値ストリーム] Completed
// [数値ストリーム] Dispose

実践的な使用例

複雑なストリームのデバッグに便利です:

textField.rx.text
    .debug("入力テキスト")
    .orEmpty                          // String? → String に変換
    .filter { $0.count >= 3 }
    .debug("フィルタ後")
    .subscribe(onNext: { text in
        print("検索: \(text)")
    })
    .disposed(by: disposeBag)

// 出力例("abc"と入力した場合):
// [入力テキスト] Subscribe開始
// [入力テキスト] Next: Optional("")
// [入力テキスト] Next: Optional("a")
// [入力テキスト] Next: Optional("ab")
// [入力テキスト] Next: Optional("abc")
// [フィルタ後] Subscribe開始
// [フィルタ後] Next: abc
// 検索: abc

3-3. タップの二重送信を防ぐカスタムExtension

実務でよくある問題:ボタンを連続でタップすると、処理が重複実行されてしまう。
ここでは、一定時間内の連続タップを防ぐ Extensionを作ります。

実装コード

import RxSwift
import RxCocoa

extension Reactive where Base: UIButton {

    /// 連続タップを防ぐObservable(デフォルト0.5秒)
    func throttleTap(seconds: Double = 0.5) -> Observable<Void> {
        return self.tap
            .throttle(.milliseconds(Int(seconds * 1000)), scheduler: MainScheduler.instance)
    }
}

使い方

let button = UIButton()
let disposeBag = DisposeBag()

// 通常のtap(連続タップで何度も実行される)
button.rx.tap
    .subscribe(onNext: {
        print("通常タップ")
    })
    .disposed(by: disposeBag)

// throttleTapを使う(0.5秒以内の連続タップは無視される)
button.rx.throttleTap()
    .subscribe(onNext: {
        print("API送信処理を実行")
    })
    .disposed(by: disposeBag)

3-4. ローディング状態を管理するExtension

API通信中はローディング表示をしたい、というのもよくある要件です。

実装コード

import RxSwift

extension ObservableType {

    /// ローディング状態を自動管理するオペレーター
    func trackActivity(_ activityIndicator: BehaviorRelay<Bool>) -> Observable<Element> {
        return self.do(
            onNext: { _ in
                activityIndicator.accept(false)  // 完了したらfalse
            },
            onError: { _ in
                activityIndicator.accept(false)  // エラーでもfalse
            },
            onCompleted: {
                activityIndicator.accept(false)  // 完了したらfalse
            },
            onSubscribe: {
                activityIndicator.accept(true)   // 開始時はtrue
            }
        )
    }
}

使い方

let isLoading = BehaviorRelay<Bool>(value: false)
let disposeBag = DisposeBag()

// ローディング状態をUIに反映
isLoading
    .bind(to: activityIndicator.rx.isAnimating)
    .disposed(by: disposeBag)

// API通信(模擬)
Observable.just("データ")
    .delay(.seconds(2), scheduler: MainScheduler.instance)  // 2秒待つ
    .trackActivity(isLoading)  // 自動でローディング管理
    .subscribe(onNext: { data in
        print("データ取得: \(data)")
    })
    .disposed(by: disposeBag)

// isLoadingは自動的に:
// 1. Subscribe時に true になる
// 2. 完了時に false になる

まとめ

この記事では、RxSwiftのジェネリクスとReactive Extensionの仕組みを学びました。

学んだこと

1. ジェネリクス - Observable<T>

  • 型安全性 - コンパイル時にエラーを検出
  • 複数の型 - Int, String, カスタム型など、どんな型でも使える
  • 型推論 - 多くの場合、型を明示する必要はない
  • Subject/Relay - これらもジェネリクスを使う
let numbers: Observable<Int> = Observable.of(1, 2, 3)
let names: Observable<String> = Observable.of("太郎", "花子")
let users: Observable<User> = Observable.of(user1, user2)

2. Reactive Extension - .rxの仕組み

  • Extensionの連鎖 - NSObject → Reactive<Base> → 各機能
  • RxCocoaの機能 - button.rx.tap, textField.rx.text など
  • Binder - Observableの値をUIに一方向バインド
button.rx.tap           // Observable<Void>
textField.rx.text       // Observable<String?>
relay.bind(to: label.rx.text)  // Binder<String?>

3. カスタムExtensionの作り方

  • UIコンポーネントの拡張 - extension Reactive where Base: UILabel
  • オペレーターの拡張 - extension ObservableType
  • 実務で使える例 - アニメーション、デバッグ、二重送信防止
// カスタムExtension
extension Reactive where Base: UILabel {
    var animatedText: Binder<String?> { ... }
}

// カスタムオペレーター
extension ObservableType {
    func debug(_ identifier: String) -> Observable<Element> { ... }
}

ここまでで理解したこと

第1回と第2回で、以下のRxSwiftの基礎知識が身につきました:

✅ Observable, Subject, Relay - イベントの流れを作る
✅ Disposable/DisposeBag - メモリ管理
✅ ジェネリクス - 型安全なストリーム
✅ Reactive Extension - UIKitとの連携

次回からは、これらの知識を使って 実際にアプリを作りながら さらに理解を深めていきます!

次回予告

第3回では、MVVMアーキテクチャのカウンターアプリ を作ります。

学ぶ内容:

  • MVVM - View、ViewModel、Modelの分離
  • Input/Outputパターン - ViewModelの設計パターン
  • RxCocoa - UIとの本格的なバインディング
  • テスト - ViewModelの単体テスト

いよいよ実践編です。お楽しみに!


参考資料

jinjerテックブログ

Discussion