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

この記事からわかること
- SwiftのCombineフレームワーク
- flatMapメソッドの使い方
- 非同期処理を直列に実行する方法
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- macOS:Sonoma 14.6.1
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)
エラーが発生しないようにするためにはエラー型を統一させる必要があります。統一させるにはエラー自体を共通のものに置き換えたりするのが綺麗ですがmapError
やcatch
などを使って<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()
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。