見出し画像

SwiftUI+Combineを完全に理解したい

どうも、新しく取り入れる知識に納得するまで時間がかかるマンです。

先日、「SwitUI + Combineでポケモンのタイプ相性アプリ作るぜ!」
と以下の記事にて意気込んで仕様とか決めようと考察してました。

しかし「そもそもCombineってなぁになぁに?🥹」という状態なので、自分が理解するまでの備忘録を残します。

認識や例えに誤りがあればご指摘いただけますと幸いです🙇‍♂️

Combineとは

まずは「Combineってなに?」
ということをGoogle先生やAIさんに聞く。

◆英単語

英単語としてはEEL英語教育研究所様曰く、以下の通り。

「combine」は、英語において「組み合わせる」「結合する」「一体化する」などの意味を持つ重要な動詞であり、名詞としては「コンバイン(収穫機)」や「組合、連合」を表します。


EEL英語教育研究所:「combine」の語源と意味の進化」

◆画像

サムネに使う画像検索したところ、農業の種の収穫に使用する重機「コンバイン ハーベスター」の画像しかでてこなかった。公式のアイコンとかはないみたいですね(どうでもよい)

◆SwiftUI

SwitUIにおいてのCombineは、Appleが提供するリアクティブプログラミングを行うためのiOS 13以降で利用可能なフレームワーク。
Combineを使うことで、非同期処理をよりシンプルに、可読性の高いコードで記述することができるという。

Combineの主要素

Combineは主に3つの要素から成り立っています。
今回イメージしやすい(?)ように身近なレストランに例えてみました。

◆Publisher(パブリッシャー)

データやイベントをPublish(発行)する役割を担います。
→レストランに例えると、料理🍝というイベントを提供するシェフ👨‍🍳に当たる。

◆Operator(オペレーター)

PublisherがPublish(発行)したイベントに対して、加工や変換などの処理を行う役割を担います。
→発行される料理🍝(イベント)を運んだり、加工する役目を追う調理補助&ウェイター🤵です。

◆Subscriber(サブスクライバー)

 PublisherがPublish(発行)したデータやイベントをSubscribe(購読)する役割を担います。
料理🍝というイベントをシェフ🧑‍🍳に注文し、受け取るお客さん🙍‍♀️

Combineでは、主にこれらの要素を組み合わせてデータ処理の流れ(ストリーム)を構築します。

データ処理の流れ(ストリーム)

データの流れは「基本的に」以下のように一方通行となってます⛔️

Publisher👨‍🍳 → Operator🤵 → Subscriber🙍‍♀️ 

「Operatorは1人につき1つのことしかできない」
ということがポイント!

例えば「料理のトッピングをする(加工)」のと「料理の提供方法を制御(配膳)」という処理は、それぞれ専門のOperator🤵が行う為、以下のような流れになるケースが多いです。

Publisher👨‍🍳 → Operator🤵(加工)  → Operator🤵(配膳) → Subscriber🙍‍♀️ 

厳密に言えば、外部からのイベント等を含めると流れは色々あるのですが、一旦こんなイメージを持てば良いのではないでしょうか👨‍🍳🤵🙍‍♀️ 

イベントの種類

イベントの種類は以下の3種類となります。

◆値の発行

  • Publisher が生成する実際のデータ値を発行

  • 整数、文字列、カスタムオブジェクトなど。

◆完了イベント

  • データストリームの終了を示す

    • 正常完了: すべてのデータが送信された後の正常な終了。

    • 異常終了: エラーが発生した場合の異常終了。

◆購読イベント

  • Publisher と Subscriber が接続されたときに発生

  • Subscription オブジェクトを通じて通知されます。

最後の「購読イベント」は目に見えないところで行われるので「値の発行」「完了イベント(正常終了と異常終了)」があると思っておけば良いと思います。たぶん。

リアクティブプログラミングとは

ん?そもそもリアクティブプログラミングって何だっけ?
と思い、AIに聞いたところ、、

「リアクティブプログラミングとは、データの流れやイベントを監視し、それに応じて処理を行うプログラミングのパラダイムです。」

by AI

パラダイムとか横文字を使わんといて。

パラダイム(paradigm)とは、特定の時代や分野において支配的な規範となる「物の見方や捉え方」のことです。

パラダイムとは?【意味をわかりやすく】パラダイムシフト

プログラミングパラダイムとは、プログラミングの考え方と記述方法の枠組みを規定するものです。

プログラミングパラダイムとは?種類も紹介

つまり パラダイム=記述方法の枠組み ってことですね。
ということでニュアンスは理解した(?)けど具体例がないと理解しにくいな、と。

これを理解するには表計算ソフトをイメージするとKINTO小山様が仰っていたので、参考にさせて頂きました🙇‍♂️⬇️

Combineを使ってMVVMを実現した話

C1のセルに「データの処理の仕方(=A1+B1を宣言しておけば、「実データ(A1に値を代入)」するだけでリアルタイムにC1の値が更新される、というイメージで考えると良い、とのことでした。

とてもわかりやすいですね🤔
単語の意味を理解した上で再度AIの回答を見ると、理解が深まる。

「リアクティブプログラミングとは、データの流れやイベントを監視し、それに応じて処理を行うプログラミングのパラダイムです。」

by AI

Combineの有無でどう変わる?

先ほどの表計算ソフトの例を、Combineの有無(リアクティブがどうか)でどう変わるかコードを使って説明してみます。

まずはCombine無し(非リアクティブ)なコード

// セルA,B,Cの変数
var cellA = 10
var cellB = 20
var cellC = cellA + cellB

print(cellC) // 出力: 30

// aの値を変更
cellA = 15

print(cellC) // 出力: 30 (変更されていない)

↑cellCを更新するには明示的に再計算しないといけない。

そして以下がCombine有り(リアクティブ)なコード

import Combine

class ReactiveValues {
    //  @Published プロパティとして定義し、値の変更を監視可能
    @Published var cellA: Int = 10
    @Published var cellB: Int = 20
    
        // aとbを処理してcに入れる
    lazy var c = Publishers.CombineLatest($cellA, $cellB)
        .map { $0 + $1 } // aとbを足し算する
        .eraseToAnyPublisher() // 複雑なPublisherの型情報を隠して、簡略化している
    
    // 購読(subscription)を保持するためのコレクションで,メモリリークを防ぐために使用
    private var cancellables = Set<AnyCancellable>()
    
    // クラスの初期化
    init() {
        c.sink { value in  // sink は、Publisher からの値を受け取るための関数
            print(value)
        }.store(in: &cancellables)
    }
}

let values = ReactiveValues()
// 出力: 30

// Aの値を変更
values.cellA = 15
// 出力: 35

こちらの場合は、作成したReactiveValues()クラス内で、cellAとcellBを計算後にcellCに代入する処理が仕込まれています。

そのため、ReactiveValues()を呼び出して、「values.cellA = 15」とcellAの値を代入することで、cellCの値が更新されて「35」と出力される。という仕組みです。

今回出てきた要素(@PublishedとかCombineLatestなど)を今理解する必要はないです!(自分もサンプルコード引っ張ってきただけなので完全に理解できていません😇)

あとで見返したら理解できていること目指していきます!

MVVMと相性が良い理由

SwiftUI+Combineの組み合わせがMVVMアーキテクチャと相性が良いと言われるのはなぜか?とAIに聞いたら「相性が良い理由はデータバインディングにあり」とのこと。

SwiftUIは宣言的なUIフレームワークであり、Combineはリアクティブプログラミングをサポートするフレームワークです。

MVVMアーキテクチャにおけるViewModelは、Combineを使用してデータの変更を監視し、SwiftUIがその変更を自動的にUIに反映します。
SwiftUIはデータの状態変化に応じて自動的にUIを更新します。これにより、UIの更新が簡略化され、ユーザーインターフェースとビジネスロジックの分離が促進されます。

具体的には、Combineを用いることで、Modelのデータ変更をViewModelが監視し、その変更をViewに通知することでデータバインディングを実現します。このデータバインディングによって、ViewはModelの変更を意識することなく、常に最新のデータを表示することができます。

MVVM以外のアーキテクチャとの違い
MVC(Model-View-Controller): UIKitのようなフレームワークでよく使われますが、Controllerが肥大化しやすい傾向があります。
SwiftUIの宣言的な性質と相性が悪く、ControllerがUIと密接に結びついているため、SwiftUIのViewの概念と衝突します。

MVP(Model-View-Presenter): Presenterを介してViewとModelが疎結合になりますが、SwiftUIのViewの概念と相性が悪い場合があります。
特に、監視コントローラ型MVPは、ViewがModelを監視するため、SwiftUIの宣言的なUIの構築と衝突する可能性があります。

VIPER(View-Interactor-Presenter-Entity-Router): より複雑で、小規模なアプリケーションには過剰な場合があります。SwiftUIの単純さと相性が悪い可能性があります。

クリーンアーキテクチャ: MVVMと組み合わせて使用することができますが、単純なアプリケーションでは過剰な複雑さをもたらす可能性があります。

上記のように、SwiftUIとCombineを使用する場合、MVVMは自然な選択肢となります。これは、フレームワークの設計思想と最もよく合致し、コードの構造化と保守性を向上させるためです。

by NotebookLM

◆データバインディングって、、?👀

データバインディングは、アプリケーションのユーザーインターフェース(UI)要素とデータソース(通常はモデルまたはビューモデル)を自動的に同期させる技術です。これにより、データの変更が自動的にUIに反映され、逆にUI操作がデータに反映されます。

by AI

つまり、お互いの強みが合わさることで

  • データバインディングによってデータとUI操作が連携できる。

  • 開発者は複雑なUI更新ロジックを書く必要がない(簡略化)。

  • アプリケーションの状態管理に集中できる。

ということですかね🤔

たしかにUIKitで値に代入したけどUI更新ロジックを忘れて画面に反映されてない。みたいなことよくありました、、

では、コード例を交えて各要素の特徴を学んでいきたいと思います📝

Publisher(パブリッシャー):シェフ👨‍🍳

冒頭で、Publisherを「シェフ👨‍🍳と例えさせて頂きました🍳

データやイベントを発行する役割を担います。
→レストランに例えると、料理🍝というイベントを発行するシェフ👨‍🍳に当たる。

では、どのようにPublisher(シェフ)値(料理)を用意するのかコードで見てみます👀

import Combine
import Foundation

// 1.料理を表す構造体
struct Dish {
    let name: String
}

// 2.シェフ(Publisher)を表すクラス
class Chef {
    // メニュー(Publisherを返すメソッド)
    func prepareDishes() -> AnyPublisher<Dish, Never> {
        // 3.この例では、Just publisherを使って単一の料理を提供します
        return Just(Dish(name: "スパゲッティ・カルボナーラ"))
            .eraseToAnyPublisher() //4.パブリッシャーを AnyPublisher でラップする
    }
}

// シェフのインスタンスを作成
let chef = Chef()

// メニューを取得(Publisherを取得)
let menu = chef.prepareDishes()

print("シェフが料理を準備しています。")

上記コードの簡単な解説📝

  1. Dish{ }

    • 料理を表現した構造体。

  2. Chef{ }

    • シェフ(Publisher)を表現したクラス

      • prepareDishes() メソッド:シェフ(Publisher)が作成する「メニュー」

        • AnyPublisher<Dish, Never>:内部実装の詳細(例:Just、Future、カスタムPublisherなど)を隠蔽するAnyPublisher型を返します。
          <Output型, Failure型>:正常の場合はDish型で返し、Neverはエラー型を返さないという意味です。

  3. Just()

    • Publisher🧑‍🍳の仲間で、単一の値を発行します。この例では、カルボナーラのみを返しています。

  4. eraseToAnyPublisher()

    • Publisherの具体的な型を隠蔽し、AnyPublisher型にラップします。これにより、Publisherの実装詳細を隠すことができます。

    • AnyPublisher<Dish, Never>でreturnするPublisherには.eraseToAnyPublisher()」が必須!と言っても過言でない。

このコードでは、Publisher(シェフ🧑‍🍳)が値(料理🍝)を準備していますが、まだSubscriber(お客さん🙍‍♀️ )がSubscribe(注文🛎️)をしていないため、値(料理🍝)が発行されていません。Printもできない。

◆AnyPublisher<Dish, Never>とeraseToAnyPublisher()

上記について理解しきれなかったので先に進む前に少しだけ深掘りさせてください🤏

どちらも「Publisherの実装詳細を隠すことができる」という説明があるのですが、なんで実装詳細を隠すの?という素朴な疑問🤔

まずは上記がない場合のコードを見てみる👀

func prepareDish() -> Just<Dish> {
    return Just(Dish(name: "パスタ"))
}

これだと、返り値がJust<Dish>に固定されているため、prepareDish()メソッドの中身を修正したときに、返り値の変更が必要になります。

しかし、AnyPublisher<Dish, Never>eraseToAnyPublisher()を使ったコードでメソッドの中身を変更してみる👀

func prepareDish() -> AnyPublisher<Dish, Never> {
    return [Dish(name: "スープ"), Dish(name: "メイン"), Dish(name: "デザート")]
        .publisher
        .eraseToAnyPublisher()
}

すると、上記のコードのようにreturnする値がJust型から変更しても、返す型がAnyPublisher型に統一されます。

つまり、内部実装を変更しても外部インターフェースが変わらないため、使用側(呼び出し元)のコードを変更する必要がなく、カプセル化と保守性が高くなることがメリットとなる。

実装内容を隠すというより、抽象的な型で返すことで実装を柔軟にしている。というイメージ?

◆Publisher(パブリッシャー)の種類👨‍🍳👨‍🍳👨‍🍳

よく出勤するシェフたちを紹介します。

  • Just:1 つの値を発行し、完了する Publisher です。失敗しない。
    1つの料理を極め、失敗を許さない職人タイプ👨‍🍳

  • Future:非同期処理を扱う Publisher です。値を 1 つ発行して完了するか、エラーを発行する。
    時間のかかる料理を担当する予約制のシェフ👨‍🍳料理を作ってもらっている時に他のシェフ👨‍🍳の処理ができる。予約のキャンセル可能。

  • Deferred:購読が開始されるまで Publisher の生成を遅延させることができます。Publisher の生成処理にコストがかかる場合に有効です。
    予約制で、注文が入ってから料理の準備をする👨‍🍳

  • Empty:値を発行せずに完了する Publisher です。
    料理しないで退勤するやつ👨‍🍳(どんな場面で使うのか不明)

  • Sequence:配列などのシーケンスから、要素を 1 つずつ発行する Publisher です。
    ビュッフェ担当で数ある料理を1品ずつ提供する👨‍🍳

  • Fail:購読開始と同時にエラーを発行する Publisher です。
    即座に失敗する新人ポンコツシェフ👨‍🍳

  • Record:事前に記録した値を再生する Publisher です。
    事前に覚えてきた料理を提供する👨‍🍳いつものアレで通じる🍝

  • Timer:指定した間隔で値を発行する Publisher です。
    時間厳守のコース料理担当シェフ👨‍🍳事前に指定したタイミングで料理を提供する。

  • NotificationCenter:通知を発行する Publisher です。
    →店内放送係👨‍🍳キッチン、ホール、お客様など、誰でも情報を発信・受信できる。

  • URLSession:ネットワークリクエストを発行する Publisher です。
    料理を外注して、届いたものを提供する👨‍🍳

  • @Published:プロパティに付与することで、プロパティの変更を通知する Publisher を生成できます。
    料理の進捗状況を常に報告するシェフ👨‍🍳 料理の各段階で自動的に状況を更新し、関心のある人全員に知らせます。材料切れたよー!

  • CurrentValueSubject:値を保持し、新しい値が設定されるたびに、その値を購読者に発行します。
    今日のスペシャルメニュー担当のシェフ👨‍🍳 現在のおすすめ料理を常に把握し、変更があればすぐに全員に通知する。

  • PassthroughSubject:外部から値を注入できる Publisher です。
    オーダーメイド料理の受付係👨‍🍳 お客様からの特別なリクエストを受け付け、それをすぐにキッチンに伝えます。

ちゃんと理解しきれてない状態で例えようとして変なことになっている可能性あるので、理解が深まったり、紹介したいシェフがいれば随時情報を追加修正していきます。(※ご指摘コメントもお待ちしております🙇‍♂️)

また、パブリッシャーごとの動きをコードで紹介し始めると長くなるので別記事にて執筆予定です🧑‍🍳🧑‍🍳🧑‍🍳

Subscriber(サブスクライバー):お客さん🙍‍♀️

冒頭で、Subscriberを「お客さん🙍‍♀️」と例えさせて頂きました

Publisherが発行したデータやイベントを購読する役割を担います。
料理🍝というイベントを受け取るお客さん🙍‍♀️

シェフ🧑‍🍳がいくら料理を作っても、注文してくれるお客さん🙍‍♀️がいなければ始まりませんよね。

では、Subscriberが値(料理🍝)をどのように購読(注文🛎️)するのかコードで見てみます👀

import Combine
import Foundation

struct Dish {
    let name: String
}

class Chef {
    func prepareDish() -> AnyPublisher<Dish, Never> {
        return Just(Dish(name: "スパゲッティ・カルボナーラ"))
            .eraseToAnyPublisher()
    }
}

let chef = Chef()

print("シェフが料理を準備しています。")

// ------ ここまで Publisher のコードと一緒 ------

// 1.メニューを取得(Publisherを取得)
let menu = chef.prepareDish()

// 2.シェフの料理を注文(Publisherを.sinkで購読)
let cancellable = menu.sink(receiveCompletion: { completion in // 3.receiveCompletion
    switch completion {
    case .finished:
        print("食事が終わりました")
    case .failure(let error):
        print("エラーが発生しました: \(error)")
    }
}, receiveValue: { dish in // 4.receiveCompletion
    print("料理が届きました: \(dish.name)")
})

// 出力:
// シェフが料理を準備しています。
// メニューが用意されました。しかし、まだ誰も注文していません。
// 料理が届きました: スパゲッティ・カルボナーラ
// 食事が終わりました

上記コードの簡単な解説📝

  1. let menu = chef.prepareDish()

    • Chef クラスの prepareDish() メソッドを呼び出し、その結果を menu 変数に格納している。

    • menu は AnyPublisher<Dish, Never> 型のオブジェクト。

    • この時点では、まだ実際のデータ(料理🍝)は生成されておらず、Publisher が作成されただけ。

  2. let cancellable = menu.sink(...)

    • ここで menu(Publisher)に対して sink メソッドを呼び出し、購読を開始しています。

    • sink メソッドは Publisher からのデータを受け取るための便利なSubscriberです。

    • 戻り値の cancellable は AnyCancellable 型のオブジェクトで、必要に応じて購読をキャンセルするために使用できます。

  3. receiveCompletion: { completion in ... }

    • これは Publisher が完了したときに呼ばれるクロージャです。

      • 完了のタイプは2種類:

        • .finished: 正常に完了した場合

        • .failure(let error): エラーが発生した場合

    • この例では .failure は起こり得ませんが、エラーハンドリングのために記述されています。

  4. receiveValue: { dish in ... }

    • これは Publisher が値(この場合は Dish オブジェクト)を発行するたびに呼ばれるクロージャです。

    • ここで受け取った dish オブジェクトを使って、必要な処理を行います。

    • この例では、受け取った料理の名前を出力しています。

sink メソッドを使用することで、Publisher からのデータの受信と完了の処理を簡潔に記述できます。これは Combine フレームワークを使用する際の一般的なパターンです。

◆Subscriber(サブスクライバー)の種類🙍‍♀️🙍‍♀️🙍‍♀️

よくご来店するお客さんたちを紹介します。

  • sink:値の受信とCompletion処理を行う最も一般的なSubscriber
    メニューから料理を注文し、提供された料理を食べ、食事の終了まで対応する一般的なお客さん🙍‍♀️

  • assign:受信した値を特定のプロパティに直接割り当てるSubscriber
    注文した料理が自動的に指定されたテーブルに運ばれるシステムを利用するお客さん🤖🙍‍♀️

  • Demand:Subscriberが受信したい値の数を指定する機能
    お客様が一度に注文できる料理の量を指定するお客さん🙍‍♀️

  • receive(completion:):完了イベントを受け取る

    • 正常終了(.finished)の場合:食事を終えて帰る準備をする🙍‍♀️

    • エラー(.failure)の場合:問題があった際に、スタッフに報告する🙍‍♀️

  • AnySubscriber:AnyPublisherのSubscriber版
    どんなジャンルの料理でも楽しめる万能な食通のお客様🙍‍♀️

Operator(オペレーター):調理補助ウェイター🤵

冒頭で、Operatorを「調理補助&ウェイター🤵」と例えさせて頂きました

Publisherが発行したイベントに対して、加工や変換などの処理を行う役割を担います。
→発行される料理🍝(イベント)を運んだり、加工する役目を追う調理補助&ウェイター🤵です。

では、Operatorが値(料理)をどのように加工・配膳するのかコードで見てみます👀

import Combine
import Foundation

struct Dish {
    let name: String
    var topping: String?
}

class Chef {
    func prepareDish() -> AnyPublisher<Dish, Never> {
        return Just(Dish(name: "スパゲッティ・カルボナーラ"))
            .map { dish in. // 1.料理を加工する
                var updatedDish = dish
                updatedDish.topping = "パルメザンチーズ"
                return updatedDish
            }
            .delay(for: .seconds(3), scheduler: DispatchQueue.main) // 2.タイミングを遅らせる
            .eraseToAnyPublisher()
    }
}

let chef = Chef()

print("シェフが料理を準備しています。")

let menu = chef.prepareDish()

let cancellable = menu.sink(receiveCompletion: { completion in
    switch completion {
    case .finished:
        print("食事が終わりました")
    case .failure(let error):
        print("エラーが発生しました: \(error)")
    }
}, receiveValue: { dish in
    if let topping = dish.topping {
        print("料理が届きました: \(dish.name) (トッピング: \(topping))")
    } else {
        print("料理が届きました: \(dish.name)")
    }
})

// 出力:
// シェフが料理を準備しています。
// 料理が届きました: スパゲッティ・カルボナーラ (トッピング: パルメザンチーズ)
// 食事が終わりました

上記コードの簡単な解説📝

  1. map { dish in ... }:

    • 料理にトッピング(パルメザンチーズ)を追加します。これは、調理補助が料理を加工する過程を表現しています。

  2. delay(for: .seconds(3), scheduler: DispatchQueue.main):

    • 料理の提供を3秒遅らせます。

シェフ🧑‍🍳お客さん🙍‍♀️の動きは変わらず、ウェイター🤵が料理を加工して、配膳のタイミングを制御しています。

◆Opelator(オペレーター)の種類🤵🤵🤵

よく出勤するウェイターたちを紹介します。

  • map:入力値を変換して新しい値を出力します。
    今回登場した、料理の盛り付け担当🤵

  • filter:条件に合う値のみを通過させる
    一定の温度以上の料理のみを提供。など取捨選択や品質管理を担当🤵

  • reduce: 複数の値を計算して一つの値にまとめる。
    コース料理の全カロリーを計算する🤵 知りたくない。

  • collect:複数の値をまとめて配列として発行します。
    アフタヌーンティーセットみたいな?🤵

  • combineLatest:複数のPublisherの最新の値を組み合わせます。
    メイン、サイド、デザートを同時に提供する🤵

  • merge:複数のPublisherの値を一つのストリームにまとめる
    複数のシェフからの料理を完成した順に一つのテーブルに集める🤵

  • debounce:一定時間内に新しい値が来なければ最後の値を発行する。
    5分に10回料理が流れてきても、最新の料理しか対応しない🤵

  • throttle:指定した時間間隔で最新の値のみを発行する。
    debounceの親戚でほぼ同じ動き🤵(厳密には動きや用途が違いますが、ここでは割愛)

  • retry:エラーが発生した場合に指定回数再試行する。
    料理の失敗時に再調理を試みる、めげないタイプ🤵

MVVMにしてみる

では、登場人物が一通りそろったコードをMVVMパターンに変換してみたいと思います。

◆Model

import Foundation

struct Dish {
    let name: String
    var topping: String?
}

◆View

SwiftUIでテキストとボタンを配置し、ユーザーの操作をViewModelに伝えます。

import SwiftUI

struct RestaurantView: View {
    @StateObject private var viewModel = RestaurantViewModel()
    
    var body: some View {
        VStack {
            Text(viewModel.dishStatus)
                .padding()
            
            Button("料理を注文") {
                viewModel.orderDish()
            }
        }
    }
}

◆ViewMode

ビジネスロジックと状態管理を担当します。@Published プロパティを使用して、View に表示するデータを保持します。

import Combine
import Foundation

class RestaurantViewModel: ObservableObject {
    @Published var dishStatus: String = "シェフが料理を準備しています。"
    @Published var dish: Dish?
    
    private let chef = Chef()
    private var cancellables = Set<AnyCancellable>()
    
    func orderDish() {
        chef.prepareDish()
            .sink(receiveCompletion: { [weak self] completion in
                switch completion {
                case .finished:
                    self?.dishStatus = "食事が終わりました"
                case .failure(let error):
                    self?.dishStatus = "エラーが発生しました: \(error)"
                }
            }, receiveValue: { [weak self] dish in
                self?.dish = dish
                if let topping = dish.topping {
                    self?.dishStatus = "料理が届きました: \(dish.name) (トッピング: \(topping))"
                } else {
                    self?.dishStatus = "料理が届きました: \(dish.name)"
                }
            })
            .store(in: &cancellables)
    }
}

class Chef {
    func prepareDish() -> AnyPublisher<Dish, Never> {
        return Just(Dish(name: "スパゲッティ・カルボナーラ"))
            .map { dish in
                var updatedDish = dish
                updatedDish.topping = "パルメザンチーズ"
                return updatedDish
            }
            .delay(for: .seconds(3), scheduler: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

あとがき

最後まで読んでいただきありがとうございました🙇‍♂️

気づけば1万5千文字弱の長文記事となっていました。
最後のほうはめんどくさくなって解説が雑

アウトプットしたもののまだ完全に理解とまではいかなかったので、また別記事でも深掘りしていけたらと思っています。

わかりにくい点や誤っている認識があれば修正していきたいので、コメント頂けますと幸いです。

引き続きよろしくお願いいたします🙇‍♂️

この記事が気に入ったらサポートをしてみませんか?