はじめに
Appleのドキュメント(https://developer.apple.com/documentation/combine/future)に、Futureとは「最終的に単一の値を生成し、その後終了または失敗するPublisher」と記載されていますが、これだけだと使い所がわかりません・・・。使い所に関しては以下のドキュメント(https://developer.apple.com/documentation/combine/using-combine-for-your-app-s-asynchronous-code)に記載があります。ドキュメント内の"Replace Completion-Handler Closures with Futures"を確認しながら、自分でもサンプルを作って挙動を確かめてみました。
開発環境
- macOS Big Sur 11.5.2
- Xcode 12.5.1
- Swift 5.4.2
本題
Futureの使い所
Futureの使い所としては、「Aの処理が完了したらBの処理を実行する」という、これまで通信処理などで利用していたコールバック処理をCombineで実装する(Combineに置き換える)場合に使います。
具体例
「20までカウントアップしたら完了メッセージを表示する」という仕様のサンプルを実装してみます。動きのイメージは以下。
コールバック処理にCombineを使わない場合は以下のようになります。
// カウントアップ処理 func startCounting(completionHandler: @escaping () -> Void) { Timer.publish(every: 0.1, on: .main, in: .common) .autoconnect() .receive(on: RunLoop.main) .sink { [weak self] _ in guard let self = self else { return } if self.count < self.endCount { self.count += 1 } else { // 処理が終了した時点でcompletionHandler()を呼ぶ completionHandler() } } .store(in: &self.cancellables) }
// カウントアップ処理の呼び出し元 startCounting() { [weak self] in // startCounting()の処理内でcompletionHandler()が呼ばれると以下の処理を実行する guard let self = self else { return } withAnimation(.easeOut(duration: 0.8)) { self.isCountingCompleted = true } self.cancellables.removeAll() }
CombineのFutureでコールバック処理を実装すると以下のようになります。
// カウントアップ処理 func startCounting() -> Future<Void, Never> { return Future() { promise in Timer.publish(every: 0.1, on: .main, in: .common) .autoconnect() .receive(on: RunLoop.main) .sink { [weak self] _ in guard let self = self else { return } if self.count < self.endCount { self.count += 1 } else { // カウントアップが完了した時点でpromiseを実行する // promiseを実行するとFutureは値を発行(公開)する promise(Result.success(())) } } .store(in: &self.cancellables) } }
// カウントアップ処理の呼び出し元 // startCounting()の戻り値であるFutureはPublishersの一種であるため、 // Operatorsで値を変換したり、Subscribersで値を受け取ることができる startCounting() // startCounting()の処理内でpromiseが呼ばれると以下の処理を実行する .sink { _ in withAnimation(.easeOut(duration: 0.8)) { self.isCountingCompleted = true } self.cancellables.removeAll() } .store(in: &cancellables)
具体例で使用したサンプルの全体の実装
ContentView.swift
import SwiftUI struct ContentView: View { @ObservedObject private var viewModel: ContentViewModel init(viewModel: ContentViewModel) { self.viewModel = viewModel } var body: some View { GeometryReader { geometry in ZStack { Text("\(viewModel.count)") .font(.title) .fontWeight(.bold) .padding() if viewModel.isCountingCompleted { Text("Completed!") .frame(width: 220, height: 50) .background(Color(red: 0.8, green: 0.8, blue: 0.8)) .cornerRadius(110) .shadow(color: Color(red: 0.85, green: 0.85, blue: 0.85), radius: 20) .position(x: geometry.size.width / 2, y: 30) .transition(.move(edge: .top)) } } .frame(width: geometry.size.width, height: geometry.size.height) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(viewModel: ContentViewModel()) } }
ContentViewModel.swift
import SwiftUI import Combine final class ContentViewModel: ObservableObject { @Published var count = 0 private let endCount: Int = 20 @Published var isCountingCompleted = false private var cancellables = Set<AnyCancellable>() init() { // カウントアップ処理の呼び出し元 // startCounting()の戻り値であるFutureはPublishersの一種であるため、 // Operatorsで値を変換したり、Subscribersで値を受け取ることができる startCounting() // startCounting()の処理内でpromiseが呼ばれると以下の処理を実行する .sink { _ in withAnimation(.easeOut(duration: 0.8)) { self.isCountingCompleted = true } self.cancellables.removeAll() } .store(in: &cancellables) } // カウントアップ処理 func startCounting() -> Future<Void, Never> { return Future() { promise in Timer.publish(every: 0.1, on: .main, in: .common) .autoconnect() .receive(on: RunLoop.main) .sink { [weak self] _ in guard let self = self else { return } if self.count < self.endCount { self.count += 1 } else { // カウントアップが完了した時点でpromiseを実行する // promiseを実行するとFutureは値を発行(公開)する promise(Result.success(())) } } .store(in: &self.cancellables) } } }
おわりに
今回読んだドキュメントに記載のある"Replace Repeatedly Invoked Closures with Subjects"の内容については別途記事にしようと思います。