📱

@_implementationOnlyを利用してRxSwiftのreactive unit を隠蔽しつつConcurrency利用する

2023/02/05に公開

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 に

SwiftConcurrency.md

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