🚀

Combineを誰よりも詳しく

に公開

Combineとは

swiftにて、非同期処理やイベント駆動型の処理を宣言的に記述できる仕組みを持つフレームワーク。
iOS13以降、macOS10.15以降で利用可能。
Combineでは、非同期処理の各イベント(値の発行、エラーの発生、完了)を一つの「ストリーム」として統一的に扱うことができるため、従来のコールバックを用いた記述法よりもシンプルに非同期処理を記述することができる。

目次


Collbackを用いた記述との比較

1. コールバック方式

struct Post: Decodable {
    let id: Int
    let title: String
}

struct Comment: Decodable {
    let id: Int
    let text: String
}

enum FetchError: Error {
    case noData
    case badURL
}

func fetchPost(completion: @escaping (Result<Post, Error>) -> Void) {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
        completion(.failure(FetchError.badURL))
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(FetchError.noData))
            return
        }
        do {
            let post = try JSONDecoder().decode(Post.self, from: data)
            completion(.success(post))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

func fetchComments(forPostId id: Int, completion: @escaping (Result<[Comment], Error>) -> Void) {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)/comments") else {
        completion(.failure(FetchError.badURL))
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(FetchError.noData))
            return
        }
        do {
            let comments = try JSONDecoder().decode([Comment].self, from: data)
            completion(.success(comments))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// 呼び出し例
fetchPost { result in
    switch result {
    case .success(let post):
        print("Post title: \(post.title)")
        fetchComments(forPostId: post.id) { result in
            switch result {
            case .success(let comments):
                print("Comments count: \(comments.count)")
            case .failure(let error):
                print("Error fetching comments: \(error)")
            }
        }
    case .failure(let error):
        print("Error fetching post: \(error)")
    }
}

この方式では、まず投稿を取得し、その結果を元にコメントを取得するため、非同期処理がネストして書いている。エラー処理も各段階で個別に記述する必要があるため、全体の流れが分かりにくくなっている(個人の感想ではあるが)。

2. Combine方式

import Combine
import Foundation

struct Post: Decodable {
    let id: Int
    let title: String
}

struct Comment: Decodable {
    let id: Int
    let text: String
}

enum FetchError: Error {
    case badURL
    case noData
}

func fetchPost() -> AnyPublisher<Post, Error> {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
        return Fail(error: FetchError.badURL).eraseToAnyPublisher()
    }
    
    return URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: Post.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

func fetchComments(for postID: Int) -> AnyPublisher<[Comment], Error> {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(postID)/comments") else {
        return Fail(error: FetchError.badURL).eraseToAnyPublisher()
    }
    
    return URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: [Comment].self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

var cancellable: AnyCancellable? = fetchPost()
    .flatMap { post in
        // 投稿取得後、コメント取得を連結し、投稿とコメントの組み合わせを生成
        fetchComments(for: post.id)
            .map { comments in (post, comments) }
    }
    .eraseToAnyPublisher()
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Finished fetching post and comments.")
            case .failure(let error):
                print("Error: \(error)")
            }
        },
        receiveValue: { post, comments in
            print("Post title: \(post.title)")
            print("Comments count: \(comments.count)")
        }
    )

投稿取得とその後のコメント取得を、1つの直線的な流れとして記述している。
値の変換や連結が宣言的に行われ、全体の流れ、エラー処理、終了処理が1カ所で管理されている。
取得した結果をまとめた後、不要になった場合のキャンセルも容易に行える(具体的な説明は後述する)。


基本的な構成要素

Combineは以下の4要素から成る

  • Publisher
    値やイベントを発行する。非同期処理の結果、ユーザー操作、タイマーイベントなど、さまざまなデータの流れを生成する。

  • Operator
    Publisherから流れるデータに対して中間処理(変換、フィルタリング、結合、平坦化など)を施す。複数のOperatorを連結することで、複雑なデータ処理のパイプラインを宣言的に構築できる。

  • Subscriber
    Publisher(やOperatorを通った後)の出力を受け取り、最終的な処理(UIの更新など)を行う。

  • Subscription
    PublisherとSubscriberがデータのやりとりを始めるための接続を管理する。
    この接続は、後で不要になったときにキャンセルすることでリソースを解放できる。

Subscriptionの補足説明
Publisherは、SubscriberがそのPublisherを購読(=接続の確立)した時点からデータの発信を開始する。つまり、SubscriberがPublisherを購読することで、Publisherが発信するデータがSubscriberに伝達される状態になる。Subscriptionは、その「接続状態」を管理するオブジェクトである。
また、例えばネットワーク通信など長時間の処理では、処理が不要になった場合に接続を解除してリソースを解放する必要がある。Subscriptionは、確立された接続を後からキャンセル(切断)する機能を提供し、不要な処理やリソースの浪費を防ぐ役割を果たす。

[Publisher]
     │
     ▼
[Operator]  → (必要に応じて複数のOperatorを連結可能)
     │
     ▼
[Subscription]  ← PublisherとSubscriberの「接続」を管理する
     │
     ▼
[Subscriber]

先ほどのサンプルコードの各部分がどの構成要素に対応しているのか、またそれぞれの役割を説明する

Publisher

func fetchPost() -> AnyPublisher<Post, Error> {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
        return Fail(error: FetchError.badURL).eraseToAnyPublisher()
    }
    
    return URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: Post.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

この関数全体が、Publisherとして機能する。つまり、ネットワークから取得した「投稿」データを発信する元となっている。同様に、fetchComments(for:) もコメントデータを発信するPublisherとして動作する。

Operator

fetchPost()
    .flatMap { post in
        // 投稿取得後、その投稿のIDを使ってコメント取得を開始し、
        // 結果として「投稿」と「コメント」のタプルを生成する
        fetchComments(for: post.id)
            .map { comments in (post, comments) }
    }
    .eraseToAnyPublisher()

この部分全体は、Operator に該当する。
fetchPost()により投稿データが取得されると、flatMap を使ってその投稿データを受け取り、次に fetchComments(for:) を呼び出している。
つまり、複数の非同期処理を連結している。
補足:flatMapとは、Publisherにより値が発行された後に、新たに非同期処理を開始するための仕組みである(詳しい説明は後述する)。

Subscriber の設定と Subscription(接続の管理)

var cancellable: AnyCancellable? = 
   fetchPost()
   .flatMap { post in
       fetchComments(for: post.id)
           .map { comments in (post, comments) }
   }
   .eraseToAnyPublisher()
   .sink(
       receiveCompletion: { completion in
           switch completion {
           case .finished:
               print("Finished fetching post and comments.")
           case .failure(let error):
               print("Error: \(error)")
           }
       },
       receiveValue: { post, comments in
           print("Post title: \(post.title)")
           print("Comments count: \(comments.count)")
       }
   )

Subscriber の役割
最終的に、sink を使ってPublisherから発信されたデータ(ここでは投稿とコメントのタプル)を受け取り、
receiveValue クロージャでデータを利用して処理(ここでは投稿タイトルやコメント数の表示)を実行する。(詳しい説明は後述する)。
receiveCompletion クロージャで、処理が正常に終了したか、エラーが発生したかを確認する(詳しい説明は後述する)。。
この sink が、データ受信側(Subscriber)の役割を担う。


1. Publisherの書き方

Publisherは、非同期のデータやイベントを発行するための型を定義するプロトコルであり、発行するデータの型(Output)と、エラー型(Failure)をジェネリックに指定できる。以下に主要なPublisherの種類とその書き方、特徴を詳しく解説する。

主なPublisherの種類と文法

  • Just
    単一の値を発行し、すぐに完了する。常に成功となるので、Failure型はNeverとなる.

    let publisher = Just(42)
    
    let cancellable = publisher.sink(
        receiveCompletion: { completion in
            print("Completion: \(completion)") // "Completion: finished"
        },
        receiveValue: { value in
            print("Received value: \(value)") // "Received value: 42"
        }
    )
    
  • Future
    非同期処理の結果を、将来的に一度だけ発行する。クロージャ内で非同期処理を実施し、結果をpromiseを介して送信する。エラーが発生する可能性がある場合に利用する.

    enum MyError: Error {
        case example
    }
    
    // FutureによるPublisherの定義
    let publisher = Future<Int, Error> { promise in
        // 非同期処理のシミュレーション
        let isSuccess = true  // 成功の場合は true、エラーの場合は false にする
    
        if isSuccess {
            // 成功の場合: 42を発行する
            promise(.success(42))
        } else {
            // エラーの場合: MyError.example を発行する
            promise(.failure(MyError.example))
        }
    }
    
    let cancellable = publisher.sink(
        receiveCompletion: { completion in
            print("Completion: \(completion)")
            // 成功の場合は、以下のように出力される:
            // "Completion: finished"
            //
            // エラーの場合は、以下のように出力される:
            // "Completion: failure(MyError.example)"
        },
        receiveValue: { value in
            print("Received value: \(value)")
            // 成功の場合は、以下のように出力される:
            // "Received value: 42"
            //
            // エラーの場合は、このクロージャは呼ばれない
        }
    )
    
  • Empty
    値を一切発行せず、すぐに完了するかエラーを発行する。主に初期化時に空のストリームを提供する際に利用する。
    completeImmediatelyに応じて二種類の使い方がある.
    trueの場合

    let publisher = Empty<Int, Never>()
    let cancellableEmpty = publisher.sink(
        receiveCompletion: { completion in
            print("Empty Completion: \(completion)")
            // 出力: "Empty Completion: finished"
        },
        receiveValue: { value in
            print("Empty Received value: \(value)")
            // 値は発信されないため、ここは実行されない
        }
    )
    

    falseの場合

    let publisher = Empty<Int, Error>(completeImmediately: false)
    let cancellableEmptyError = publisher.sink(
        receiveCompletion: { completion in
            print("EmptyError Completion: \(completion)")
            // 出力: 何も表示されない(自動的には完了イベントが発行されないため)
        },
        receiveValue: { value in
            print("EmptyError Received value: \(value)")
            // 値は発信されないため、ここは実行されない
        }
    )
    
    // ※ emptyError の購読は、キャンセルされるまで値も完了も送信されない。
    // 必要に応じて、以下のように手動でキャンセルすると、購読は終了する:
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        cancellableEmptyError?.cancel()
        print("EmptyError subscription cancelled.")
        // キャンセル後も、完了イベントは自動的には発行されない
    }
    
  • Fail
    初期化時に指定したエラーを直ちに発行する。エラー処理のテストや、ある条件下で即座に処理を中断したい場合に利用する.

    enum MyError: Error {
        case example
    }
    
    let publisher = Fail<Int, Error>(error: MyError.example)
    
    // 購読してエラーを受け取る
    let cancellable = publisher.sink(
        receiveCompletion: { completion in
            print("Completion: \(completion)")
            // 出力例(エラーの場合):
            // "Completion: failure(MyError.example)"
        },
        receiveValue: { value in
            print("Received value: \(value)")
            // 値は発信されないため、このクロージャは実行されない
        }
    )
    
  • Deferred
    購読時に新たなPublisherを生成する。購読されるまで実際のPublisherの生成を遅延させるため、最新の状態を反映させるのに有用.

    var counter = 0
    let deferredPublisher = Deferred { () -> Just<Int> in
        counter += 1
        return Just(counter)
    }
    
    deferredPublisher.sink { print("Subscriber1 received: \($0)") } // 1が出力される。
    deferredPublisher.sink { print("Subscriber2 received: \($0)") } // 2が出力される。
    

    ※ 複数のSubscriberが同じDeferredから購読する場合、それぞれの購読のたびにクロージャが実行され、毎回新しいPublisherが生成される。
    ※ Deferredのクロージャ内に状態を変化させる処理や副作用がある場合、各購読で異なる結果が得られる点に注意が必要.
    ※ そのため、同じ結果をすべてのSubscriberで共有したい場合は、Deferredではなく固定のPublisher(例: Just(42))を使用するか、一度発行された結果をキャッシュする仕組みを導入する必要がある.

  • Publishers.Sequence
    シーケンス(配列やその他のコレクション)からPublisherを生成する。シーケンス内の各要素を順次発行する.

    let publisher = [1, 2, 3].publisher
    let cancellableSequence = publisher.sink(
        receiveCompletion: { completion in
            print("Sequence Completion: \(completion)")
            // 出力例:
            // "Sequence Completion: finished"
        },
        receiveValue: { value in
            print("Sequence Received value: \(value)")
            // 出力例:
            // "Sequence Received value: 1"
            // "Sequence Received value: 2"
            // "Sequence Received value: 3"
        }
    )
    
  • PassthroughSubject
    Subjectの一種で、外部から任意のタイミングで値を発行できる。内部に値を保持せず、購読者にそのまま値を流す.

    let publisher = PassthroughSubject<String, Never>()
    let cancellableSubject = publisher.sink(
        receiveCompletion: { completion in
            print("PassthroughSubject Completion: \(completion)")
            // 出力例(完了時): "PassthroughSubject Completion: finished"
        },
        receiveValue: { value in
            print("PassthroughSubject Received value: \(value)")
            // 出力例:
            // "PassthroughSubject Received value: Hello"
            // "PassthroughSubject Received value: Combine"
        }
    )
    publisher.send("Hello")
    publisher.send("Combine")
    publisher.send(completion: .finished)
    

    ※ UIイベントなど、外部から値が流入する状況で利用する。エラーも手動で送信可能.

  • CurrentValueSubject
    Subjectの一種で、最新の値を内部に保持し、購読開始時にその最新値を直ちに送信する.

    let currentValueSubject = CurrentValueSubject<Int, Never>(0) // 初期値は0
    let cancellableCurrentValue = currentValueSubject.sink(
        receiveCompletion: { completion in
            print("CurrentValueSubject Completion: \(completion)")
            // 出力例(完了時): "CurrentValueSubject Completion: finished"
        },
        receiveValue: { value in
            print("CurrentValueSubject Received value: \(value)")
            // 出力例:
            // 最初の購読時に "CurrentValueSubject Received value: 0"(初期値)
            // その後、値が送信されると更新される値が表示される
        }
    )
    // 購読開始後、現在の状態を更新する
    currentValueSubject.send(42)
    currentValueSubject.send(100)
    currentValueSubject.send(completion: .finished)
    

    ※ 状態管理(例: ViewModelのプロパティ)に利用することで、購読開始直後に現在の状態を反映できる.


2. Subscriber

Subscriberは、Publisherが発行するイベント(値、完了、エラー)を受け取り、適切に処理を行うためのプロトコルである。実装例としては、標準で用意されるsinkやassign、またはカスタムのSubscriberとしてAnySubscriberを利用できる.

主なSubscriberの種類と文法

  • sink
    クロージャを利用して受信した値や完了イベント、エラーイベントを処理する最も一般的なSubscriber.

    let cancellable = publisher.sink(
        receiveCompletion: { completion in
            // 完了イベントやエラーの処理
            // 例: .finishedの場合の後処理、.failureの場合のエラーログ出力
        },
        receiveValue: { value in
            // 受信した値の処理
        }
    )
    

    ※ receiveValue

    Publisherが発信する各値を受け取るクロージャである.
    受信した値は、PublisherのOutput型に対応しており、受け取ったデータに対して必要な処理(例:UIの更新、データの加工、保存など)を実行する。
    Publisherが複数の値を連続して発行する場合、receiveValueクロージャはそれぞれの値に対して呼ばれます.

    let cancellable = publisher.sink(
        receiveCompletion: { completion in },
        receiveValue: { value in
            // ここで受信した値を使って何らかの処理を実施する
            print("Received value: \(value)")
        }
    )
    

    この例では、Publisherが発信するたびに "Received value: ..." と値が出力される.

    ※ receiveCompletion

    Publisherがすべての値を発信し終えた(正常に完了した)場合、またはエラーが発生した場合に、最後に呼ばれるクロージャです.
    このクロージャは、Subscribers.Completion型の引数を受け取ります. 引数には、以下の2種類があります:
    .finished: Publisherが正常にすべての値を発信して完了したことを示す.
    .failure(Error): 発生したエラーが含まれる. Publisherがエラーによって終了したことを示す.
    受け取った完了イベントに基づいて、後処理(例えば、画面のローディング状態の解除、エラーメッセージの表示など)を行うのに用います.

    let cancellable = publisher.sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Finished successfully.")
            case .failure(let error):
                print("An error occurred: \(error)")
            }
        },
        receiveValue: { value in
            // 値の処理
        }
    )
    
  • assign
    Publisherが発行した値を、指定のオブジェクトのプロパティに直接代入するSubscriber.
    SwiftUIのViewModelや状態管理でよく利用される.

    class ViewModel {
        @Published var text: String = ""
    }
    
    let viewModel = ViewModel()
    let cancellable = publisher.assign(to: \.text, on: viewModel)
    

    ※ assignは、プロパティの変更通知が自動的に行われるため、UIの更新と連動しやすい.

  • AnySubscriber
    型消去されたSubscriber.
    独自のSubscriberロジックを定義したい場合に、受信処理をクロージャとして実装できる.
    複雑な購読ロジックが必要な場合に利用する.

    let anySubscriber = AnySubscriber<Int, Error>(
        receiveSubscription: { subscription in
            // 購読開始時の設定
            subscription.request(.unlimited)
        },
        receiveValue: { value in
            // 値の処理
            return .none  // Demandを調整可能(.noneは追加需要なし)
        },
        receiveCompletion: { completion in
            // 完了またはエラー時の処理
        }
    )
    publisher.subscribe(anySubscriber)
    

    ※ AnySubscriberを用いることで、Subscriberの詳細な実装を隠蔽し、柔軟な購読が可能となる.


3. Subscription

Subscriptionは、PublisherとSubscriberの間の接続を管理するオブジェクト。購読のキャンセルやリソース管理に利用する.
通常、sinkやassignなどのメソッドから返されるAnyCancellableを保持することで、接続状態を管理する.

主なSubscriptionの種類と文法

  • Cancellable
    Cancellable は、cancel() メソッドを定義するプロトコルである. sink や assign などの購読メソッドは、このプロトコルに準拠したオブジェクトを返す

  • AnyCancellable
    AnyCancellable は、そのCancellableプロトコルに準拠した具体的な実装で、sinkやassignなどの購読メソッドが返す型として用いられる.

    // Cancellableプロトコルに準拠。オブジェクトの型としてはanyCancellable
    var subscription: Cancellable = publisher.sink { _ in }
    // 必要なくなったらキャンセル
    subscription.cancel()
    

4. Operator

Operatorは、Publisherから流れるデータに対して中間処理を施すためのメソッド群。チェーン可能なため、複数のOperatorを連結してデータ変換、フィルタ、結合、平坦化などの処理パイプラインを構築できる。以下に主要なOperatorとその文法、用途について詳しく解説する.

主なOperatorの種類と文法

  • map
    各要素に対して指定の変換処理を実行し、新たな値に変換する. 単純なデータ変換に最適.

    publisher
        .map { value in
            return value * 2
        }
    

    ※ 入力値を2倍する処理. 入力の型と出力の型を任意に変換可能.

  • filter
    発行される値の中から、指定した条件を満たすものだけを通過させる. 条件に合致しない値は破棄される.

    publisher
        .filter { value in
            return value % 2 == 0
        }
    

    ※ 偶数のみを受信する例. 条件式により受信する値を選別できる.

  • flatMap
    各要素を別のPublisherに変換し、すべてのPublisherの出力を1つのストリームに統合する. 非同期処理のネストや複数の非同期タスクの合流に利用される.

    publisher
        .flatMap { value in
            return Just(value * 2)
        }
    

    ※ ここでは、各値を2倍した結果を持つJustを生成し、すべての出力を一つにまとめている.

  • removeDuplicates
    連続して同一の値が発行された場合、最初の1回のみを送信し、それ以降の重複を除外する. 連続する同一データの処理を避けたい場合に有用.

    publisher
        .removeDuplicates()
    
  • debounce
    指定された期間内に連続して発行されるイベントをまとめ、最後のイベントのみを発行する. 例えば、ユーザーの連続入力の中から一定時間入力が止まったタイミングで処理を実行する場合に利用する.

    publisher
        .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
    

    ※ ユーザー入力や検索ワードの自動補完など、短期間の連続イベントのノイズを除去する.

  • throttle
    指定期間内に最初または最後の値のみを発行する. debounceとは異なり、定期的な値の送信を保証しながら過剰なイベントを制限する.

    publisher
        .throttle(for: .milliseconds(300), scheduler: RunLoop.main, latest: true)
    

    ※ 最新の値を定期的に受信し、頻度を制限する際に用いる.

  • scan
    初期値から各要素を累積して計算し、その都度結果を発行する. 累積計算や状態の更新に利用できる.

    publisher
        .scan(0) { accumulator, value in
            return accumulator + value
        }
    

    ※ 例えば、数値の合計や積算値の逐次計算を実現する.

  • reduce
    ストリーム全体を1つの値にまとめる. ストリームが完了した後に、最終的な集計結果を発行する.

    publisher
        .reduce(0) { accumulator, value in
            return accumulator + value
        }
    

    ※ 全ての値を受け取った後に最終的な合計値などを計算する際に利用する.

  • combineLatest
    複数のPublisherの最新の値を組み合わせ、タプルとして発行する. いずれかのPublisherが新しい値を発行するたびに、最新の組み合わせが更新される.

    publisher1
        .combineLatest(publisher2)
    

    ※ UIの複数の入力項目など、複数データの最新状態を同時に利用するケースに適している.

  • merge
    複数のPublisherの発行する値を1つのストリームに統合する. 各Publisherの順序に関わらず、値が発行され次第流れる.

    publisher1
        .merge(with: publisher2)
    

    ※ 同じ型の複数のストリームを一つにまとめたい場合に利用する.

  • zip
    複数のPublisherの値を順番にペアにして発行する. 全てのPublisherからそれぞれ1つずつ値が揃った場合にのみペアが生成される.

    publisher1
        .zip(publisher2)
    

    ※ 例えば、同時に取得した2つのデータセットを対応付ける際に用いる.

  • catch
    エラー発生時に、代替のPublisherに切り替えてエラー処理を行う. エラー復旧や代替値の供給に利用される.

    publisher
        .catch { error in
            return Just(defaultValue)
        }
    

    ※ エラー発生時に、フォールバックとしてデフォルト値や別の処理に切り替える.

  • retry
    エラーが発生した場合に、指定回数分再試行する. ネットワークリクエストなど、一時的なエラーに対するリトライ処理に有用.

    publisher
        .retry(3)
    

    ※ 最大3回の再試行後も失敗する場合は、最終的にエラーが発行される.

  • delay
    発行タイミングを指定した期間だけ遅延させる. 処理のタイミング調整やアニメーションとの連動などに利用できる.

    publisher
        .delay(for: .seconds(1), scheduler: RunLoop.main)
    

    ※ 値の発行自体は変わらず、指定の時間だけ遅れて発行される.

  • prepend / append
    発行前または発行後に、指定した値を追加で送信する. ストリームの先頭や末尾に固定の値を差し込む場合に利用する.

    publisher
        .prepend(initialValue)
        .append(finalValue)
    

    ※ 初期値や最終値を強制的に流したいときに使用する.

  • subscribe(on:) / receive(on:)
    subscribe(on:)は上流の処理(データの発行やOperatorの実行)のスケジューラを指定し、receive(on:)は下流の処理(Subscriberが値を受け取る処理)のスケジューラを指定する. これにより、バックグラウンド処理とUI更新のスレッドを明確に分離できる.

    publisher
        .subscribe(on: DispatchQueue.global())
        .receive(on: RunLoop.main)
    

    ※ 主にネットワークリクエストなどのバックグラウンド処理後、UI更新をメインスレッドで行うケースに用いる.

  • eraseToAnyPublisher
    複数のOperatorを連結した結果として得られるPublisherの型は、内部で多数のジェネリック型が入れ子になった非常に複雑なものになる. たとえば、URLSessionのdataTaskPublisherに対してmap、decode、flatMapなどを連結すると、その型は次のような長いネストになった型になることがある.

    Publishers.Map<
        Publishers.Decode<
            Publishers.Map<URLSession.DataTaskPublisher, Data>,
            Post,
            JSONDecoder
        >,
        Post
    >
    

    AnyPublisher は、これら複雑な型情報を隠蔽するための型消去(type erasure)を行うためのジェネリックなPublisherである.

    つまり、eraseToAnyPublisher() を使用することで、内部の詳細な型を隠し、たとえば AnyPublisher<Post, Error> のようなシンプルな型に変換できる. これにより、APIの戻り値がシンプルになり、内部でどのようなOperatorがチェーンされているかに依存せず、実装の変更が利用側に影響しにくくなる.

    サンプルコード(eraseToAnyPublisherを使わない場合)

    import Combine
    import Foundation
    
    struct Post: Decodable {
        let id: Int
        let title: String
    }
    
    func fetchPostWithoutErasure() -> Publishers.Decode<
        Publishers.Map<URLSession.DataTaskPublisher, Data>,
        Post,
        JSONDecoder
    > {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: Post.self, decoder: JSONDecoder())
    }
    
    let publisher1 = fetchPostWithoutErasure()
    // publisher1の型は非常に複雑なままで、APIとして公開すると利用側に不都合が生じる可能性がある
    publisher1.sink(
        receiveCompletion: { completion in print("Completion: \(completion)") },
        receiveValue: { post in print("Received post: \(post)") }
    )
    

    サンプルコード(eraseToAnyPublisherを使用する場合)

    import Combine
    import Foundation
    
    struct Post: Decodable {
        let id: Int
        let title: String
    }
    
    func fetchPost() -> AnyPublisher<Post, Error> {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: Post.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()  // 型情報を隠蔽してシンプルなAnyPublisherに変換
    }
    
    let publisher2 = fetchPost()
    // publisher2の型はAnyPublisher<Post, Error>となり、外部に公開するAPIとしては非常に扱いやすい
    publisher2.sink(
        receiveCompletion: { completion in print("Completion: \(completion)") },
        receiveValue: { post in print("Received post: \(post)") }
    )
    

まとめ

Combineはなかなか良い!
自分が後から見返すために、記事にしましたが。他の方の役にも立てば幸いです。

Discussion