SwiftでCombineをマスター!たった10ステップで理解する方法

SwiftのロゴとCombineのロゴが並んでいるイメージSwift
この記事は約21分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

SwiftはAppleが開発したプログラミング言語として、iOS、macOS、watchOS、tvOSなどのアプリケーション開発に広く利用されています。

特に近年では、非同期処理の取り扱いに関して多くの進展があり、Combineフレームワークがその中心となっています。

本記事では、Swiftの非同期処理におけるCombineフレームワークの役割と、その理解を深めるための10のステップを詳しく解説していきます。

●Swiftとは

SwiftはAppleが2014年に発表したプログラミング言語で、Objective-Cに代わる言語として導入されました。

安全性、高速性、そしてモダンな構文を持つことで注目されています。

○Swiftの基本と特徴

Swiftの最大の特徴はその安全性にあります。

変数はデフォルトで不変(immutable)であり、変更可能にしたい場合は明示的に指定する必要があります。

また、オプショナル型を導入することで、nilの参照を安全に取り扱うことができます。

また、Swiftは高速性を持つことで知られています。

最適化されたコンパイラのおかげで、ランタイム時のパフォーマンスが向上しています。

さらに、Swiftは関数型プログラミングの概念を取り入れることで、よりモダンなプログラムの設計が可能となっています。

例えば、mapやfilterといった高階関数が使用でき、コードの読みやすさや再利用性が高まります。

○Swiftでの非同期処理の難しさ

非同期処理は現代のアプリケーション開発において、避けて通れないテーマとなっています。

ユーザーの操作に応答しながら、バックグラウンドでデータを取得したり、複雑な計算を行ったりするためには、非同期処理が欠かせません。

しかし、Swiftでの非同期処理は、従来の方法だと複雑であり、コールバック地獄という問題も発生しやすいものでした。

コールバック地獄とは、非同期処理の中でさらに非同期処理を行う場合、コールバック関数の中にコールバック関数を記述し続けることになり、コードの可読性が低下する問題を指します。

これにより、エラーハンドリングやデータの取り扱いが難しくなります。

このような背景から、Swiftでは非同期処理をより簡単かつ強力にサポートするためのフレームワーク、Combineが登場しました。

●Combineフレームワークの紹介

Swiftの非同期処理の手段として多くの方法が存在しますが、最も最新かつ強力なのが「Combine」というフレームワークです。

この部分では、Combineが何であるか、その役割と特徴、そしてCombineを利用する大きなメリットについて解説していきます。

○Combineの役割と特徴

Combineは、Swiftでの非同期処理やデータストリームを扱うためのフレームワークとして、2019年にAppleによって発表されました。

これにより、非同期のコードをより簡潔かつ効果的に記述することができるようになりました。

具体的な特徴としては、次のような点が挙げられます。

  1. データフローの宣言的記述:Combineではデータの流れを宣言的に表現できます。これにより、データの変化を直感的に追跡しやすくなります。
  2. 複数のデータソースの統合:複数の非同期データソースからのデータを統合し、一貫した方法で取り扱うことができます。
  3. 強力なデータ変換機能:Combineには、データを変換、フィルタリング、合成するための多くのオペレータが用意されています。

○Combineを利用するメリット

Combineを利用すると、Swiftでの非同期処理やデータストリームの管理が大幅に簡単になります。

その主なメリットを紹介します。

  1. より簡潔なコード:伝統的なコールバックベースの非同期処理と比較して、Combineを使うとコードが大幅に簡潔になります。これにより、バグのリスクを減少させると同時にメンテナンスも容易になります。
  2. 強力なエラーハンドリング:Combineでは、エラーをデータストリームとして扱うことができるため、エラーハンドリングが一元化され、よりシンプルになります。
  3. より高いパフォーマンス:適切に使われた場合、Combineは高速で効率的なデータ処理を実現します。
  4. 一貫性のあるデータ管理:Combineは、データの一貫性を保ちつつ、複数のソースからのデータを統一的に管理する能力を持っています。

これらの特徴やメリットを踏まえると、Swiftの非同期処理を行う際にはCombineの使用が非常におすすめと言えるでしょう。

●Combineの基本的なコンセプト

Combineフレームワークは、Swiftの非同期処理とデータの流れを効果的に扱うためのフレームワークです。

ここでは、Combineの基本的なコンセプトを順を追って詳細に解説します。

○PublisherとSubscriberの関係

Combineの中心的なコンセプトは、データの発行元である「Publisher」とデータの受信者である「Subscriber」の関係です。

このコンセプトは、Publisherがデータやイベントを発行し、Subscriberがそのデータやイベントを受け取って処理するというものです。

Publisherはデータの流れを提供し、Subscriberはその流れを購読して反応します。

例えば、外部APIからのデータフェッチやユーザーインタラクションなど、さまざまな非同期イベントがPublisherとして扱われ、そのイベントの結果をUIの更新やデータベースの保存などのタスクで処理するSubscriberがそれを購読します。

○Operatorsの役割

Combineでは、Publisherから送信されるデータの流れを変更、加工、フィルタリングするための多くのOperatorが用意されています。

このコードでは、CombineのOperatorを使ってデータの流れを加工するコードを表しています。

この例では、数値データを2倍にしてフィルタリングしています。

import Combine

let numbers = [1, 2, 3, 4, 5].publisher

numbers
    .map { $0 * 2 }  // 各数値を2倍にする
    .filter { $0 > 5 }  // 5より大きい数値のみを取得する
    .sink { print($0) }  // 結果を表示する

上記のコードでは、mapというOperatorを使用して配列内の各数値を2倍にし、その後filterというOperatorで5より大きい数値のみを取得しています。

最終的に、sinkというメソッドを用いて結果を表示します。

このようなOperatorの使用により、非常に柔軟にデータの流れを制御することができます。

上記のコードを実行すると、6, 8, 10という数値が表示されます。

これは、初めの配列の数値が2倍になった後、5より大きい数値だけがフィルタリングされた結果です。

●Combineの使い方

Combineフレームワークは、Swiftの非同期プログラミングとデータストリーム処理の両方を効果的に行うためのツールセットです。

特に非同期の処理が多い現代のアプリ開発では、Combineをうまく活用することで、コードの可読性やメンテナンス性を高めることができます。

○サンプルコード1:Combineを使った基本的なデータストリームの作成

Combineを使用してデータストリームを作成するための基本的なコードを紹介します。

import Combine

// 1. Publisherの作成
let numbersPublisher = [1, 2, 3, 4, 5].publisher

// 2. Subscriberの作成と接続
numbersPublisher.sink { value in
    print(value)
}

このコードでは、整数の配列を使ってデータストリームを表すPublisherを作成しています。

この例では、配列内の各要素が順番にストリームに流れ、Subscriberであるsinkメソッドがそれを受け取り、その値をコンソールに表示します。

このコードを実行すると、コンソールには1, 2, 3, 4, 5という数字が順番に表示されます。

○サンプルコード2:Operatorsを活用したデータ加工

Combineには、データストリームを加工するための多くのOperatorsが用意されています。

下記のコードは、データストリーム内の各要素を2倍にする操作を表しています。

import Combine

let numbersPublisher = [1, 2, 3, 4, 5].publisher

numbersPublisher
    .map { $0 * 2 }
    .sink { value in
        print(value)
    }

このコードでは、mapというOperatorを使ってデータストリーム内の各要素を2倍にしています。

この例では、配列内の数字が順番に2倍にされ、その結果がコンソールに表示されます。

このコードを実行すると、コンソールには2, 4, 6, 8, 10という数字が順番に表示されます。

●Combineの応用例

Combineフレームワークは、非同期やリアクティブなプログラミングをSwiftで行うための強力なツールです。

基本的な使い方やコンセプトを学んだ後、さまざまな応用例を知ることで、その真価をさらに理解することができます。

○サンプルコード3:非同期APIリクエストの取り扱い

このコードではCombineを使って非同期のAPIリクエストを取り扱う方法を表しています。

この例ではURLSessionを利用してデータを取得し、結果をSubscriberに通知しています。

import Combine

let url = URL(string: "https://api.example.com/data")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: [String].self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print("Error occurred: \(error)")
        }
    }, receiveValue: { data in
        print(data)
    })

このコードで、URLSession.shared.dataTaskPublisher(for: url)を使用することで、APIのリクエストが行われます。

取得したデータは、.decodeでデコードし、メインスレッド上で受け取るように.receive(on: DispatchQueue.main)で指定しています。

最後にsinkで取得した結果を処理します。

このとき、エラーが発生した場合はエラーメッセージを表示し、データが正しく取得できた場合はそのデータを表示します。

○サンプルコード4:Combineを用いたUIの更新

CombineはUIの更新も効果的に行える特徴があります。

このコードでは、非同期に取得したデータを使ってUIを更新する方法を表しています。

import Combine
import SwiftUI

class DataViewModel: ObservableObject {
    @Published var data: [String] = []
    var cancellables = Set<AnyCancellable>()

    init() {
        fetchData()
    }

    func fetchData() {
        let url = URL(string: "https://api.example.com/data")!
        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [String].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] newData in
                self?.data = newData
            })
            .store(in: &cancellables)
    }
}

struct DataView: View {
    @ObservedObject var viewModel = DataViewModel()

    var body: some View {
        List(viewModel.data, id: \.self) { item in
            Text(item)
        }
    }
}

このコードでは、非同期にデータを取得し、そのデータをDataViewModel内の@Publishedプロパティであるdataに保存します。

そして、SwiftUIのDataViewでこのデータを監視し、更新があればUIを再描画します。

○サンプルコード5:複数のデータストリームを合成

Combineを使うと、複数のデータストリームを一つにまとめることができます。

このコードでは、二つのデータストリームを合成して、新たなデータストリームを生成する方法を表しています。

import Combine

let numbersPublisher = PassthroughSubject<Int, Never>()
let stringsPublisher = PassthroughSubject<String, Never>()

let combinedPublisher = Publishers.CombineLatest(numbersPublisher, stringsPublisher)
combinedPublisher
    .sink(receiveValue: { number, string in
        print("Number: \(number), String: \(string)")
    })

numbersPublisher.send(1)
stringsPublisher.send("A")
numbersPublisher.send(2)
stringsPublisher.send("B")

この例では、PassthroughSubjectを使って二つのデータストリーム、numbersPublisherstringsPublisherを作成しています。

これらのストリームをPublishers.CombineLatestで合成し、新たなデータストリームcombinedPublisherを生成しています。

このストリームは、どちらかのストリームに新しいデータが流れ込むたびに、その時点での最新のデータを組み合わせて出力します。

データを送信すると、”Number: 1, String: A”、”Number: 2, String: A”、”Number: 2, String: B”のように出力されます。

●注意点と対処法

SwiftとCombineを使用して非同期プログラミングを行う際には、様々な注意点が存在します。

正しく理解し、適切な対処法を採用することで、効率的なアプリケーションの開発が可能となります。

ここでは、特に重要と思われる2つの注意点を取り上げ、それぞれの対処法について詳しく解説していきます。

○Memory Leakのリスクとその回避方法

Combineを使用する際、最も注意するべき点は「Memory Leak」、すなわちメモリリークのリスクです。

Combineにおける非同期処理は、PublisherとSubscriberの間でデータのやり取りが行われることによって成り立っています。

しかし、この2つのコンポーネント間の接続が不適切に管理されると、意図しないオブジェクトの生存期間が延びてしまい、メモリリークを引き起こす可能性があります。

この問題を回避するためには、Cancellableオブジェクトを適切に管理することが重要です。

Cancellableは、PublisherとSubscriberの接続を表すオブジェクトで、この接続を終了するためには、Cancellableを破棄する必要があります。

このコードでは、CancellableオブジェクトをSet<AnyCancellable>に保存して、メモリリークを防ぐ方法を表しています。

この例では、データストリームを作成し、その結果を受け取るサブスクライバーを作成しています。

import Combine

var cancellables: Set<AnyCancellable> = []

let publisher = Just("Hello Combine!")
publisher
    .sink(receiveValue: { print($0) })
    .store(in: &cancellables)

この方法を採用することで、接続が不要になった際にcancellablesからCancellableを破棄することが可能となり、メモリリークを効果的に回避できます。

○Error Handling in Combine

Combineを使用して非同期処理を行う際、エラーは必ずと言っていいほど発生します。

特にAPIのリクエストなど、外部のサービスとの通信を行う場面では、ネットワークの不安定さやAPIの仕様変更など、さまざまな理由でエラーが生じる可能性が高まります。

そのため、適切なエラーハンドリングは必須となります。

Combineには、エラーハンドリングのための様々なオペレーターが提供されています。

例えば、catchオペレーターを使用することで、エラーが発生した際のフォールバック処理を実装することができます。

このコードでは、エラーが発生した場合にデフォルトのデータを返す方法を表しています。

この例では、APIからデータを取得する処理を行い、エラーが発生した場合にはデフォルトのメッセージを返すサブスクライバーを作成しています。

import Combine

let failPublisher = Fail<String, CustomError>(error: .unknown)
failPublisher
    .catch { _ in Just("エラーが発生しました。") }
    .sink(receiveValue: { print($0) })
    .store(in: &cancellables)

実行すると、エラーが発生したため、”エラーが発生しました。”というメッセージが出力されます。

●Combineのカスタマイズ方法

SwiftのCombineフレームワークは柔軟な非同期処理を可能にします。

しかし、既存のOperatorだけで要件を満たせない場合もあります。

そこで、Combineをカスタマイズする方法を解説していきます。

○独自のOperatorの作成

Combineフレームワークには様々なOperatorが用意されていますが、場合によっては独自のOperatorを作成する必要が出てきます。

独自のOperatorを作成することで、繰り返し使用するロジックを再利用しやすくしたり、コードの可読性を向上させることができます。

独自のOperatorを作成する方法として、次のステップを考えます。

  1. Publisherを拡張して新しいメソッドを追加
  2. サブスクライバーの処理をカスタマイズ

具体的なサンプルコードを見ていきましょう。

import Combine

extension Publisher {
    func customOperator<T>() -> AnyPublisher<T, Self.Failure> where Self.Output == T {
        return self.map { value in
            // ここにカスタマイズしたい処理を記述
            return value
        }
        .eraseToAnyPublisher()
    }
}

このコードでは、Publisherの拡張としてcustomOperatorを定義しています。

mapメソッドを用いて、データの加工を行い、その結果を返しています。

ここでのカスタマイズはシンプルなものとなっていますが、実際にはもっと複雑な処理を組み込むこともできます。

○サンプルコード6:Custom Operatorの使用例

次に、先ほど作成した独自のOperatorを使用する例を見ていきましょう。

let numbers = [1, 2, 3, 4, 5].publisher

numbers
    .customOperator()
    .sink(receiveValue: { print($0) })

この例では、1から5までの数値を順に発行するPublisherから、先ほど作成したcustomOperatorを使用してデータを受け取り、その結果をprintしています。

実際にこのコードを実行すると、1から5までの数値がそのまま出力されることが確認できます。

もちろん、customOperator内での処理を変更すれば、出力結果も変わります。

●Combineと他のフレームワークとの比較

CombineはSwiftの非同期プログラミングのフレームワークとして、近年注目を集めています。

その特性や利点を理解するためには、他の類似フレームワークとの比較が不可欠です。

○RxSwiftとの違い

RxSwiftはSwift向けのReactiveプログラミングライブラリであり、非同期やイベントベースのコードを扱うためのものです。

CombineとRxSwiftは共にリアクティブプログラミングをサポートしていますが、次の点で違いがあります。

  1. 元のプラットフォーム:CombineはAppleが提供するフレームワークで、iOS 13以降やmacOS Catalina以降で利用可能です。一方、RxSwiftはコミュニティ主導のオープンソースプロジェクトです。
  2. 対応OSバージョン:RxSwiftは古いiOSバージョンでも使用することができますが、Combineは新しいOSでしか利用できません。
  3. 関連ライブラリ:RxSwiftはRxCocoaというUIバインディングライブラリとセットで使用されることが多いです。一方、CombineはUIKitやSwiftUIとの統合が強化されています。

この情報を元に、プロジェクトの要件やサポートするOSバージョンに応じて、最適なフレームワークを選ぶことができます。

○SwiftUIとの連携

CombineはSwiftUIとの相性が非常に良いです。

SwiftUIはデクララティブなUIフレームワークであり、Combineを利用することで、UIの状態を非同期に管理・更新することが簡単になります。

ここでは、CombineとSwiftUIを組み合わせた例を紹介します。

このコードでは@Publishedプロパティラッパーを使って値の変更を監視し、SwiftUIのViewが自動的に更新される機能を表しています。

この例では、テキスト入力の変更を監視して、ラベルのテキストを更新しています。

import SwiftUI
import Combine

class TextViewModel: ObservableObject {
    // テキスト入力の内容を監視するプロパティ
    @Published var inputText: String = ""
    // 入力されたテキストを表示するプロパティ
    var displayText: String {
        return "入力されたテキスト: \(inputText)"
    }
}

struct MyView: View {
    // ViewModelのインスタンスを作成
    @ObservedObject var viewModel = TextViewModel()

    var body: some View {
        VStack {
            TextField("テキストを入力してください", text: $viewModel.inputText)
                .padding(10)
                .border(Color.gray, width: 1)
            Text(viewModel.displayText)
        }.padding()
    }
}

上記のコードを実行すると、テキストフィールドに文字を入力するたびに、下のラベルが自動的に更新されます。

これは、Combineの@PublishedとSwiftUIの@ObservedObjectが連携して動作している結果です。

CombineとSwiftUIの連携は非常に強力で、状態の管理やUIの更新を効率的に行うことができます。

特に複雑なUIや多くの非同期処理を持つアプリケーションを開発する際に、この連携の恩恵を受けることができます。

まとめ

SwiftのCombineフレームワークは、非同期プログラミングを効率的かつ簡単に実装するための強力なツールです。

この記事では、Combineの基本から応用まで、多岐にわたる内容を詳しく解説しました。

初めに、Swift自体の基本と特徴、そして非同期処理の難しさを確認しました。

その上で、Combineフレームワークの役割、特徴、そして利用のメリットに焦点を当てました。

基本的なコンセプトとして、Publisher, Subscriber, そしてOperatorsの役割と使い方について解説しました。

さらに、実際のサンプルコードを交えながら、Combineの使い方や応用例を徹底的に解説しました。

特に、非同期APIリクエストの取り扱いやUIの更新、データストリームの合成など、日常の開発で頻繁に遭遇する課題をCombineを使用してどのように解決できるかを見てきました。

また、Combineの使用における注意点、特にメモリリークのリスクやエラーハンドリングについても触れました。

これに続いて、Combineをカスタマイズする方法や、他のフレームワークとの比較を行いました。

この記事を通じて、SwiftのCombineフレームワークの力強さと、その使用方法を理解していただけたことを願っています。

Swift開発における非同期処理の実装が、Combineを使用することでよりシンプルかつ効果的になることを確信しています。

今後の開発での成功をお祈りしています。