【Swift UI】Combineフレームワークの使い方!PublisherとSubscriberの違い

この記事からわかること

  • SwiftCombineフレームワーク使い方
  • PublisherSubscriber違い
  • Subject(被写体)とは?
  • sendメソッドの使い方
  • Observerパターン
  • sinkメソッドの使い方
  • RxSwiftとの違いと使い分け

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

公式リファレンス:Combine Framework

Combineフレームワークとは?

SwiftのCombineフレームワークとは非同期処理やストリーム処理、データのバインディング、イベントハンドリングなどのリアクティブプログラミングに倣った機能を提供しているApple純正のフレームワークです。非同期処理やイベントドリブンな処理を実現できるため、ユーザーインタラクション(ユーザーの操作に対してシステムが反応を返すこと)やネットワーク通信などの処理に適しています。

このフレームワークはSwift5.0から導入され、Xcode11以降からはデフォルトで組み込まれるようになりました。そのため手動で導入する必要がなくimport文を記述するだけですぐに利用可能になります。

import Combine

CombineフレームワークはGoFのデザインパターンの1つ「Observerパターン」に基づいた機能を提供します。Observerパターンとはオブジェクトの状態変化を観測し変化したタイミングで別オブジェクトの状態を更新する設計を施すパターンです。Combineフレームワークでは「Publisher」が変化を監視し、「Subscriber」に対して通知する仕組みが用意されています。

Combineフレームワークのプロトコル名を見る限り正確には「Publish-Subscribeパターン」なのかも知れません。

コールバックとの違い

データベースへのCRUD処理など時間がかかり非同期で実行される処理の結果を取得したい場合にコールバックは活用されます。しかしコールバックはネストが深くなりがちでコールバックが幾重にも重なってしまうコールバック地獄を引き起こす可能性があります。Combineではそれをスッキリとさせるために利用されることが多いです。両者のコードの違いを見てみます。

CallBackの実装

func getCallBack(completion: @escaping (String) -> Void ) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        completion("成功したよ")
    }
}

受け取り側

getCallBack { str in
    print(str)
}

Combineの実装

func getPublisher() -> Future<String, Never> {
    return Future() { promise in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            promise(.success("成功したよ"))
        }
    }
}

受け取り側

cancellable = getPublisher().sink { str in
    print(str)
}

Publisher(発行者)とは?

公式リファレンス:Publisher

protocol Publisher<Output, Failure>

Combineフレームワークにおける「Publisher(パブリッシャー:発行者)」はいわゆるデータストリームを生成するためのオブジェクトです。ストリームとはデータの入出力の流れを保持する抽象的なオブジェクトであり時間の経過と共に変化する一連の値の変化を配信(通知を送信)する特徴があります。

その流れは「データの変更から完了またはエラーまで」を一連の順序(シーケンス)として管理しておりマーブルダイアグラムとして表現されることが多いです。

RxSwiftとは?導入方法と使い方まとめ!ストリームを理解する

Combineフレームワークでは複数のストリームを組み合わせたり、ストリームを加工したりすることも可能になっています。

Subscriber(購読者)とは?

公式リファレンス:Subscriber

protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible

Subscriber(サブスクライバー:購読者)」とはPublisherが送信するデータストリームを受け取るオブジェクトです。Publisherから送信されたデータに対して「データを加工して別の形式に変換する」「データをフィルタリングして特定の条件に一致するものだけを処理する」「複数のストリームを結合して新しいストリームを作成する」と言ったさまざまな処理を行うことが可能になります。またデータ変更が通知された時だけでなくエラーや完了を通知したときにも処理を実行することができます。

そしてCombineフレームワークでは「Publisher」と「Subscriber」がプロトコルとして定義されています。

PublisherとSubscriberの使用例

PublisherとSubscriberを使用したシンプルな例を作成してみました。Justオペレーターは値を一回だけ発行するPublisherです。続いてSubscribers.Sinkメソッドで値を受け取った時や完了した時の処理を定義しています。最後に「値が一回だけ生成されるストリームを持つPublisher」に対して「String型を受け取り出力する処理を持つSubscriber」を設定することで変化を検知して処理が実行されています。

import Combine
// Publisherインスタンスを生成
let publisher = Just("Hello, World!")
// Subscriberインスタンスを生成
let subscriber = Subscribers.Sink<String, Never>(
    receiveCompletion: { _ in },
    receiveValue: { value in
        print(value)
    }
)
// PublisherインスタンスにSubscriberインスタンスを登録
publisher.subscribe(subscriber)
// Hello, World!

この方法ではPublisherとSubscriberが明確に分かれていたので分かりやすかったですが、Subjectを使用すると少しややこしくなります。

Subject(被写体)とは?

そもそもObserverパターンは対象となる2つのオブジェクトとして「観測される側(Subject)」と「観測する側(Observer)」に分けて考えられます。CombineフレームワークでもObserverパターンの説明として挙げられる「観測される側(Subject)」と同名のSubjectプロトコルが定義されています。

公式リファレンス:Subjectプロトコル

protocol Subject<Output, Failure> : AnyObject, Publisher

Combineフレームワークで定義されているSubjectとは「Publisherプロトコルを継承し、値を発行する役割(sendメソッド)」と、「他のPublisherが送信するデータストリームを受け取るSubscriber」の両方の役割を持つオブジェクトです。

Combineフレームワークで実際に使用されるPublisherは後述するSubjectやNotificationCenterクラスのpublisherメソッドなどが継承しています。NotificationCenterクラスでは簡単に「アプリがアクティブになった」などのイベントを検知することが可能です。

sendメソッドの役割

Subjectプロトコルに定義されているsendメソッドはSubjectが生成したデータを購読しているSubscriberに配信するために使用されるメソッドです。以下のような4種類が定義されています。

func send(Self.Output) // サブスクライバーに値を送信
func send() //  サブスクライバーにvoid値を送信
func send(subscription: Subscription) // サブスクライバーにサブスクリプションを送信
func send(completion: Subscribers.Completion<Self.Failure>) // サブスクライバーにコールバック関数を送信

sendメソッドは、Subjectが完了状態になっている場合や、エラーが発生している場合には呼び出すことができません。

Subjectの種類

Subjectには2つの種類があり、役割が異なります。実際の使用方法は後述していきます。

2つの使い分けの使用例を考えてみます。CurrentValueSubjectは常に最新値を保持してくれるのでログイン状態や設定値など参照する可能性のある場合に活用できそうです。

PassthroughSubjectは値自体を保持しないので常に最新の値に更新され続けるようなWeb API取得などが良いのかもしれません。

CurrentValueSubjectの使い方

公式リファレンス:CurrentValueSubjectクラス

final class CurrentValueSubject<Output, Failure> where Failure : Error

CurrentValueSubjectクラスは値が更新されるたびに要素を公開するシンプルなSubjectです。このCurrentValueSubjectオブジェクトは常に最新の値をvalueプロパティに保持しています。インスタンス化する際にCurrentValueSubjectの後に対象となる値のデータ型と初期値を渡します。

// 初期値を「0」でインスタンス化
let subject = CurrentValueSubject<Int, Never>(0)

エラーが発生する可能性がない場合はNeverを渡します。イニシャライザや主なプロパティ、メソッドは以下の通りです。

init(Output) // 指定された初期値で現在の値のサブジェクトを作成します。
var value: Output // このサブジェクトによってラップされた値で、変更されるたびに新しい要素として公開されます。
func send(Output) // サブスクライバーへの要素の配信

単純に値を参照する

まずは単純にsubject.valueで値を参照してみます。ここではsendメソッドが値を更新するためのメソッドだと思うと理解しやすいかもしれません。更新された後にはvalue取得する値も変化していることを確認できます。

import Combine

let subject = CurrentValueSubject<Int, Never>(0)
print(subject.value) // 0 

subject.send(1)
print(subject.value) // 1

subject.send(2)
print(subject.value) // 2

Subjectを購読する

公式リファレンス:Publisher.sink(receiveValue:)メソッド

続いてこのSubjectを購読(サブスクライブ)してみます。今回のsubjectを購読するためのサブスクライバーはPublisherプロトコルの持つsink(receiveValue:)メソッドを使用して定義します。引数のコールバック関数は値を受け取ったときに実行されます。ここからはPassthroughSubjectでも基本的には同じになります。

import Combine

let subject = CurrentValueSubject<Int, Never>(0)

let cancellable = subject
    .sink { value in
        print(value)
    }

subject.send(1)
subject.send(2)

// 0
// 1
// 2

購読を停止させる

sinkメソッドの返り値であるAnyCancellableオブジェクトからcancelメソッドを呼び出すことで購読状態のSubscriberに対して、Publisherからの値を受信することを停止させることができます。

cancellable.cancel()

subject.send(3) // 何も出力されない

値の変化(ストリーム)を完了させる

ストリームで観測していた値の変化を完了させてみます。先程の購読するためのsinkメソッドの引数を少し変更し、完了時と値の受け取り時に実行する処理を追加してみました。エラーは発生しない(Never)のでエラー処理はありません。

import Combine

let subject = CurrentValueSubject<Int, Never>(0)

let cancellable = subject.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Completed")
        }
    },
    receiveValue: { value in
        print("Received value: \(value)")
    }
)

// Received value: 0
subject.send(1) // Received value: 1
subject.send(2) // Received value: 2
subject.send(3) // Received value: 3
subject.send(completion: .finished) // Completed
subject.send(4) // 何も表示されない

データバインディング:assignメソッド

Publisherから流れてくるデータをオブジェクトに紐づける(データバインディングする)にはassignメソッドを使用します。

このメソッドを使うことで変化する値と指定したオブジェクトのプロパティをリンクさせることが可能になります。

class Sample {
    var count:Int = 0
}

let obj = Sample()

subject.assign(to: \.count,on: obj)

print(obj.count)
subject.send(1)  // Received value: 1
print(obj.count) // 1
subject.send(2)  // Received value: 2
print(obj.count) // 2 
subject.send(3)  // Received value: 3
print(obj.count) // 3
subject.send(completion: .finished) // Complete

sinkメソッドの役割

公式リファレンス:sink(receivecompletion:receivevalue:)

何度か登場していたsinkメソッドは受け取った通知に対しての処理を実装するためのメソッドです。引数にはクロージャー単位で処理を定義できるようになっており、引数違いで以下の2つが定義されています。

func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable

1つ目は受け取った値に対してのみ任意の処理を実行できるクロージャーを持たせることができ、2つ目は完了またはエラー時にも処理を実行できるクロージャーを持たせることができます。

func sink(
    receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void),
    receiveValue: @escaping ((Self.Output) -> Void)
) -> AnyCancellable

Swift UIとCombineフレームワーク

Swift UIを使用しているとクラスのプロパティの変化をリアルタイムにビューに反映させるために @Publishedを使用したことがあると思います。このプロパティラッパこそがCombineフレームワークから提供されています。

class Article1:ObservableObject{
    @Published  var title:String = ""
    var body:String = ""
}

RxSwiftとの違いと使い分け

似たようなObserverパターンを構築するために「RxSwift」と呼ばれるライブラリが有名です。RxSwiftはApple純正のライブラリではなく、MicrosoftがリリースしているReactiveXに含まれるSwift版のライブラリです。

CombineフレームワークはSwift独自のシンタックスや基本構文(関数など)を利用することが可能です。一方、RxSwiftはReactiveXの標準ライブラリを基にしているため、Swift言語とは異なるオペレーターやメソッドも持っているため、独自の文法やAPIを理解する必要があります。

Combineフレームワーク

  1. Apple純正
  2. Swift言語の基本構文を流用
  3. 手動でインポートする必要なし

RxSwiftライブラリ

  1. MicrosoftのReactiveX
  2. 独自の構文もあり
  3. 手動でインポートする必要あり

RxSwiftは他の言語のバージョンと共通の文法を持っているため、異なる言語バージョン間の移植性を考慮する必要がある場合に有用です。

使い分け

  1. Combine:iOSやmacOSなどApple限定アプリの開発時
  2. RxSwift:他言語を使用してのアプリ開発も想定している時

実装例

Combineフレームワークを使って練習がてら「Swift UIでボタンタップでイベントを発火して真偽値を入れ替える」コードを書いてみました。これは正直意味のないコードですが何かの参考になるかもしれないので載せておきます。

import Combine
import SwiftUI

class Test: ObservableObject {
    
    static let shared = Test()
    
    @Published  var subject = CurrentValueSubject<Bool, Never>(false)
    
    private var cancellables = Set<AnyCancellable>()
    init() {
        subject
            .sink { [weak self] _ in
                self?.objectWillChange.send()
            }
            .store(in: &cancellables)
    }
    
    func sendTrue() {
        subject.send(true)
    }
    
    func sendFalse() {
        subject.send(false)
    }
}

struct TestComBineView: View {
    @ObservedObject  var test = Test.shared
    
    var body: some View {
        VStack {
            Text(test.subject.value ? "YES" : "NO")
            ChildView()
        }
    }
}

struct ChildView: View {
    @ObservedObject  var test = Test.shared
    
    var body: some View {
        VStack {
            Button("Toggle") {
                if test.subject.value {
                    test.sendFalse()
                } else {
                    test.sendTrue()
                }
            }
        }
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index