@_implementationOnlyを利用してRxSwiftのreactive unit を隠蔽しつつConcurrency利用する
Swift Concurrency の話です。まとめると以下です
- なるべく Concurrency でコードを書いていきたい
- けれど、RxSwift(RxCocoa) の UI イベントを
Observable
にできる reactive unit に相当するものがない。あれ便利。-
import RxSwift
するとview.rx
が生えて Global 空間の Extension も汚れてしまう
-
- 特定のパッケージで
@_implementationOnly
を利用することで RxSwift の実装をパッケージ内に完全に閉じつつ、UI イベントをAsyncStream<Element>
で発火できる仕組みを提供する
Concurrency はまだ reactive unit に相当するものがない
swift-async-algorithmsも登場して、ますます非同期処理は Concurrency に寄せていきたい!ときに UI の tap イベントをどうするか悩んだ方も多いのではないでしょうか。RxSwift のような FRP 系列のライブラリでは
final class ViewController: UIViewController {
lazy var collectionView = UICollectionView()
let disposeBag = DisposeBag()
init() {
collectionView.rx
.itemSelected
.subscribe()
.disposed(by: disposeBag)
}
}
こういった形で UI イベントを Observable で購読することができました。いわゆる delegate
メソッドや RxCocoa の sentMessage()
を利用して特定の UI イベントの検知ができたわけです。Concurrency では現状こういった UI 系のイベントを拾うためには自前で実装しなければいけません。
RxSwift 6.5.0 からは values / value で async に
final class ViewController: UIViewController {
lazy var collectionView = UICollectionView()
let disposeBag = DisposeBag()
init() {
for try await indexPath in collectionView.rx.itemSelected.values {
print(indexPath)
}
}
}
RxSwift も 6.5.0 から Swift Concurrency への変換に対応したため、これらを利用すれば資産を再利用することができます!すごく助かります。しかし、import RxSwift
してしまうと、いわゆる Rx に関する Extension が公開されてしまうため、せっかく RxSwift の依存を減らそうとしているけど、、、、と悩んだ方も多いのではないでしょうか。この記事はそういった問題を軽減するアプローチを紹介します。
@_implementationOnly を利用して、 RxSwift の依存を外部に公開しない
@_implementationOnly
という Underscored Attribute が Swift にはあります。
こちらを利用すると、そのモジュールの型が API (public func、public extension) および ABI (@inlinable) で公開されるのを防げるようです。実際に利用すると以下のようなことが実現できました。
// SPMパッケージ ViewEvent
@_implementationOnly import RxSwift
import UIKit
extension UICollectionView {
public var itemSelected: AsyncThrowingStream<IndexPath, Error> {
rx.itemSelected.values
}
}
============================
============================
import ViewEvent
final class ViewController: UIViewController {
lazy var collectionView = UICollectionView()
init() {
Task {
for try await indexPath in collectionView.itemSelected {
// async で使える!
print(indexPath)
}
}
// collectionView.rx.itemSelected
// # => コンパイルエラー!!
}
}
通常の import RxSwift
では ViewEvent パッケージを import すると collectionView.rx
へもアクセスできてしまって Rx への依存が露出してしまっていましたが、 @_implementationOnly import RxSwift
を用いることで、特定のパッケージ内にのみ RxSwift への依存を閉じておくことが実現できました!これで RxSwift(RxCocoa) の UI イベントのみうまいこと Concurrency 向けに提供する橋渡しが実現できました。
まとめ
Concurrency を利用していきたいけど、足りないところは RxSwift をうまいこと使いたい。けど、 RxSwift を利用する場所はなるべく最小限になるように閉じておきたい......!!というニッチな要望を解決する方法を考えてみました。将来的にどういう形がデファクトになるかはまだわかりませんが、ひとまず RxSwift の資産をうまく再利用させてもらう方法としていかがでしょうか?
Discussion