🐥

自分的、RxSwiftで実装された非同期処理をSwiftConcurrencyへいい感じに移行する方法

に公開

こんにちは。株式会社PREVENTでiOSエンジニアをしている佐藤です。
弊社で開発しているiOS版のMystarアプリは、非同期処理の一部がRxSwiftで実装されている箇所があるのですが、Swift6対応としてSwiftConcurrencyへ移行しようとしています。
いい感じに移行するためにどのように実装すれば良いか自分なりに考え、良さそうな方法に辿り着いたので、その方法の内容を記事にしようと思います!

RxSwiftとSwiftConcurrencyの相互変換は公式のドキュメントでわかりやすく説明されています。
このドキュメントに記載されている内容を使って、いい感じにRxSwiftの非同期処理をSwiftConcurrencyへ段階的に移行する方法を検討していきます。
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/SwiftConcurrency.md

前提

RxSwift ≧ 6.5.0

移行方法

以下のような、RxSwiftで実装された非同期処理に依存するViewModelがあったときに、どうやってSwiftConcurrencyに移行していくでしょうか?

ViewModel
@MainActor
public final class SampleViewModel {
    public var number: Driver<Int>
    private var numberRelay: BehaviorRelay<Int> = .init(value: 0)
    private let repository: AsyncSampleRepository = AsyncSampleRepositoryImpl()
    private var disposeBag = DisposeBag()
    
    public init() {
        number = numberRelay.asDriver()
    }
    
    public func onAppear() {
        repository.execute()
            .subscribe(on: MainScheduler.asyncInstance)
            .subscribe { [weak self] result in
                guard let self else { return }
                if case .success(let value) = result {
                    self.numberRelay.accept(value)
                }
            }
            .disposed(by: disposeBag)
    }
}
Repository
protocol AsyncSampleRepository: Sendable {
    func execute() -> Single<Int>
}

struct AsyncSampleRepositoryImpl: AsyncSampleRepository {
    func execute() -> RxSwift.Single<Int> {
        Single.create { observer in
            observer(.success(100))
            return Disposables.create()
        }
    }
}

非同期処理をSwiftConcurrenyに置き換えるとなると、修正が必要なのはざっくり以下二つの要素でしょう。

  • 非同期処理を呼ぶ側(ViewModel)
  • 非同期処理を実装する側(Repository)

セオリーとしては、誰にも依存されていない依存関係の上位に位置する要素から修正していくのが良いと思っています。
なぜなら、誰にも依存されていない要素を修正しても、どの要素にも影響が及ばないからです。
逆に依存されている側を修正すると、依存している側に何らかの影響が出る可能性があります。
今回で言うと、ViewModelはRepositoryに依存しているので、もしRepository側の関数のインターフェースを変えるとViewModelでRepositoryを呼び出している箇所も修正しないとビルドエラーになったりします。

と言うことで、ViewModelから修正する方法を考えてみます。

まずは、onAppearメソッドで行っている非同期処理からSwiftConcurrencyに置き換えていきましょう。

public func onAppear() {
        Task {
            let result = try await repository.execute().value
            numberRelay.accept(result)
        }
}

Singleのように、一つの値だけを返すことがわかっている場合は、valueプロパティで非同期に値を取り出せます。
同じ理屈で、Completableでも同様にvalueawaitを使うことも可能です。
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/SwiftConcurrency.md#awaiting-a-single-value

非同期処理をRxSwiftで実装する場合、SingleもしくはCompletableで実装されることが多いように思いますので、上記方法でほとんどの非同期処理はSwiftConcurrencyに移行できるかと思います。

あとは、Repository側をSwiftConcurrencyに移行して、ViweModel側でRxSwift→SwiftConcurrency変換の実装を削除することで、非同期処理を完全にSwiftConcurrencyへ移行できます。

Repository側のSwiftConcurrencyへ移行する修正の例
protocol AsyncSampleRepository: Sendable {
    func execute() async -> Int
}

struct AsyncSampleRepositoryImpl: AsyncSampleRepository {
    func execute() -> async -> Int {
        await Task { 100 }.value
    }
}
Repositoryの処理を呼び出す方法の修正
public func onAppear() {
        Task {
            let result = await repository.execute()
            numberRelay.accept(result)
        }
}

おわり

以上が、私が思うRxSwiftで実装された非同期処理をSwiftConcurrencyへいい感じに移行する方法でした。
RxSwiftが提供しているSwiftConcurrency相互変換用のAPIを積極的に使用することで、既存コードに大きな影響を与えることなく、少ないコード量でSwiftConcurrencyに移行できるようになっています。
他にも、もっと良い方法がある場合は、コメントから共有してもらえると嬉しいです!

Discussion