【Swift/Combine】flatMapメソッドの使い方!publisherを直列処理

【Swift/Combine】flatMapメソッドの使い方!publisherを直列処理

この記事からわかること

  • SwiftCombineフレームワーク
  • flatMapメソッド使い方
  • 非同期処理直列に実行する方法

index

[open]

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

みんなの誕生日

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

posted withアプリーチ

公式リファレンス:Combine Framework

環境

flatMapメソッドとは?

公式リファレンス:flatMapメソッド

func flatMap<T, P>(
    maxPublishers: Subscribers.Demand = .unlimited,
    _ transform: @escaping (Self.Output) -> P
) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure

CombineのflatMapメソッドは上流から流れてきた複数のpublisherの値を整形して新しいpublisherを下流へ流すメソッドです。これによりpublisherを直列に繋げたり、上流からの値を元に新規でpublisherを作成したりすることができるようになります。

引数maxPublishersには並列に処理をするpublisherの数を指定できます。初期値は無制限unlimitedです。

引数transform部分で上流のpublisher(非同期処理)の出力値(Self.Output)を参照し、下流へPublisherを流すことができます。

またflatMapで指定したpublisherは上流のpublisher成功した場合のみ実行されます。エラーが発生した場合はそのエラーを下流に伝播します。

使い方

flatMapの使い方を見てみます。Just生成したパイプラインに直列で非同期処理を挟んでいきます。flatMap内では上流の結果を参照でき、そこから新しいpublisherを生成して下流に流しています。

// Publisherを返す何かしらの非同期処理
func printAddNumber(num: Int) -> AnyPublisher<Int, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            let reuslt = num + 1
            print("Number: \(reuslt)")
            promise(.success(reuslt))
        }
    }.eraseToAnyPublisher()
}

let cancellable = Just(1)
    .flatMap { number in
        print(number) // 1
        // Justのパイプラインにpublisher(非同期処理)を追加
        return printAddNumber(num: number)
    }
    // ここでは返却する型を明示的に指定しないと以下エラーが発生する
    // Generic parameter 'P' could not be inferred
    .flatMap { number -> AnyPublisher<Int, Error> in
        print(number) // 2
        // さらに別のpublisher(非同期処理)を追加
        return printAddNumber(num: number)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("All numbers printed successfully")
        case .failure(let error):
            print("Failed with error: \(error)")
        }
    }, receiveValue: { number in
        print("Received number: \(number)")
    })

引数maxPublishersで並列処理数を制限

大元のpublisherから連続で値が流れてくるような場合並列でパイプラインが処理されていくので、出力(処理が完了)される数値の順番は1〜5とか限らず処理の早いものから完了していきます。

このような場合に直列に処理をしていきたい場合maxPublishers.max(並列処理数)を渡すことで制御することが可能です。例えば以下のように1を指定すれば1〜5までが順番に出力されていくようにすることができます。

func printAddNumber(num: Int) -> AnyPublisher<Int, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            let reuslt = num + 1
            print("Number: \(reuslt)")
            promise(.success(reuslt))
        }
    }.eraseToAnyPublisher()
}

let nums = Array(1...5)
let cancellable = Publishers.Sequence(sequence: nums)
    .flatMap(maxPublishers: .max(1)) { number in
        return printAddNumber(num: number)
    }
    .flatMap { number -> AnyPublisher<Int, Error> i
        return printAddNumber(num: number)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("All numbers printed successfully")
        case .failure(let error):
            print("Failed with error: \(error)")
        }
    }, receiveValue: { number in
        print("Received number: \(number)")
    })

出力結果

Number: 2
Number: 3
Number: 3
Received number: 3
Number: 4
Number: 4
Received number: 4
Number: 5
Number: 5
Received number: 5
Number: 6
Number: 6
Received number: 6
Number: 7
Received number: 7
All numbers printed successfully

Swiftには多次元配列を一次元配列にするためflatMapメソッドもありますが、複数のpublisherをフラット(1つのpublisher)にできる点では似ているかもしれません。

異なるエラー型の場合は連結できない

例えば以下のようにAnyPublisher<String, MyError>AnyPublisher<String, TestError>の2つのpublisherを連結したいとします。そのままflatMapで連結しようとするとコンパイルエラーが発生します。

class MyError: Error { }
class TestError: Error { }

var cancellables = Set<AnyCancellable>()

func publisher() -> AnyPublisher<String, MyError> {
    
    let helloPublisher = Just("Hello")
        .setFailureType(to: TestError.self)
    
    let worldPublisher = Just("World")
        .setFailureType(to: MyError.self)
    // コンパイルエラー発生
    return helloPublisher.flatMap { _ in
        worldPublisher
    }.eraseToAnyPublisher()
}

publisher()
    .sink { error in
        print(error)
    } receiveValue: { value in
        print(value)
    }.store(in: &cancellables)

発生するエラーは以下のとおりです。

No exact matches in call to instance method 'flatMap'

Candidate requires that the types 'TestError' and 'MyError' be equivalent (requirement specified as 'Self.Failure' == 'P.Failure') (Combine.Publisher)

エラーが発生しないようにするためにはエラー型を統一させる必要があります。統一させるにはエラー自体を共通のものに置き換えたりするのが綺麗ですがmapErrorcatchなどを使って<String, MyError>型に合わせてあげればOKです。

let helloPublisher = Just("Hello")
    .setFailureType(to: TestError.self)
    .mapError { _ in MyError() }
    ----- OR ------
let helloPublisher = Just("Hello")
    .setFailureType(to: TestError.self)
    .catch { _ in Fail<String, MyError>(error: MyError()) } 

エラーがないPublisherは連結可能

エラーがNeverのPublisherの場合は特に気にすることなく連結することが可能です。

func publisher() -> AnyPublisher<String, MyError> {
    
    let intPublisher: AnyPublisher<Int, Never> = Just(2).eraseToAnyPublisher()
    
    let strPublisher = Just("Hello")
        .setFailureType(to: MyError.self)
    
    return intPublisher.flatMap { _ in
        strPublisher
    }.eraseToAnyPublisher()
}

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

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

searchbox

スポンサー

ProFile

ame

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

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

New Article

index