AsyncSequenceでReactiveSwiftライクなバインディングの仕組みを作ってみた

# 概要
ReactiveSwiftではViewなどへ値をバインドする時、`<~`演算子を使うことで以下のような雰囲気で簡単にバインドできる。
```Swift
disposable += label.reactive.text <~ viewModel.text
```

一方、Combineを使った場合は以下のようになり、記述量が増える。
```Swift
viewModel.text.receive(on: DispatchQueue.main).sink { [weak self] in
    self?.label.text = $0
}
.store(in: &self.cancellables)
```

また、AsyncSequenceを使う場合も一々for文を書く必要があり面倒である。
```Swift
Task { @MainActor in
    for try await text in viewModel.text {
        self.label.text = text
    }
}
```

そこで今回`AsyncSequence`を使ってReactiveSwiftのようなバインドができる仕組みを作成してみた。

最終的には以下のようにバインドできるものを目指す。
```Swift
disposable += label.reactive.bind(\.text) <~ viewModel.text
```

# 方法
ReactiveSwift本家ではKeyPathを使わずに全てのプロパティを自前実装しているが、個人で実装するには困難なため、今回はKeyPathを利用する方法を採用した。

### 1. NSObjectに`asyncBinding`プロパティを作成
`label.reactive`のreactive部分を実現するため、まず`AsyncBindingExtensionsProvider`というプロトコルを定義する。
```Swift
public protocol AsyncBindingExtensionsProvider {}

extension AsyncBindingExtensionsProvider {
    public var asyncBinding: AsyncBinding<Self> {
        AsyncBinding(self)
    }
}
```

次に、`NSObject`をextensionで`AsyncBindingExtensionsProvider`に準拠させる。
```Swift
extension NSObject: AsyncBindingExtensionsProvider {}
```

この仕組みについては「Targeted Extensions」で検索すれば良い解説が多数存在するため、ここでは割愛する。

### 2. `asyncBinding`に`bind(_:ReferenceWritableKeyPath<Base, Value>)`を作成
"1."で`asyncBinding`のプロパティで返している値の`AsyncBinding`で、`ReferenceWritableKeyPath`を使ってバインド先を指定できるようにしている。
```Swift
public struct AsyncBinding<Base> {
    public let base: Base

    public init(_ base: Base) {
        self.base = base
    }

    @MainActor
    public func bind<Value>(_ keyPath: ReferenceWritableKeyPath<Base, Value>)
        -> AsyncBindingTarget<Base, Value>
    {
        AsyncBindingTarget(base: self.base, keyPath: keyPath)
    }
}
```
### 3. `<~`演算子の作成
Swiftには`<~`という演算子はないため、新たに演算子を宣言する。
`==`や`+`のような左右から値が挟むような演算子を作る場合は`infix operator　〇〇`と書くことで宣言できる。
```Swift
infix operator <~
```

### 4. BindingTargetを作成
`<~`演算子を用いて実際にバインドを行う仕組みを実装する。
今回はViewへのバインドを前提としているため、`MainActor`を指定している。
別の`actor`を利用する場合は、`BindingTarget`を`actor`毎に分けると良さそうだ。
```Swift
@MainActor
public struct AsyncBindingTarget<Base: Sendable, Value>: Sendable
where Base: AnyObject {
    public weak var base: Base?
    public let keyPath: ReferenceWritableKeyPath<Base, Value>

    public init(base: Base, keyPath: ReferenceWritableKeyPath<Base, Value>) {
        self.base = base
        self.keyPath = keyPath
    }

    public static func <~ <RHS>(lhs: AsyncBindingTarget<Base, Value>, rhs: RHS)
        -> Disposable where RHS: AsyncSequence, RHS.Element == Value
    {
        Task { @MainActor in
            for try await value in rhs {
                lhs.base?[keyPath: lhs.keyPath] = value
            }
        }
    }
}
```

### 5. disposableの作成
ReactiveSwiftではCombineの`AnyCancellable`のようなものとして`Disposable`が用意されている。
Combineと異なりReactiveSwiftの`Disposable`は使わなくても良い設計になっているが、今回はTaskを破棄する必要がある関係上必要となってくる。

`CompositeDisposable`はCombineの`Set<AnyCancellable>`にあたるものだが、`+=`を使った値の追加が便利なため実装しておく。（実際にはSetのextensionで定義しても良い)
```Swift
public protocol Disposable {
    func dispose()
}

extension Task: Disposable {
    public func dispose() {
        cancel()
    }
}

public final class CompositeDisposable {
    private var disposables: [Disposable] = []

    deinit {
        dispose()
    }

    func add(_ disposable: Disposable) {
        disposables.append(disposable)
    }

    func dispose() {
        disposables.forEach { $0.dispose() }
        disposables.removeAll()
    }

    static func += (lhs: CompositeDisposable, rhs: Disposable) {
        lhs.add(rhs)
    }
}
```

# 実際の利用例
利用例として、MVVMパターンにおけるViewとViewModelのバインド部分を作成した。
雰囲気が掴めれば良いと思うのでMVVMとしてはかなり適当である。
実際に使う時、直接`AsyncSequence`を使うのは煩雑なため、ViewModel側ではCombineを用い、バインド時に`values`を利用して`AsyncSequence`に変換している。
```Swift
class ViewController: UIViewController {
    @IBOutlet private weak var label: UILabel!

    private let viewModel = ViewModel()

    private let disposables = CompositeDisposable()

    override func viewDidLoad() {
        super.viewDidLoad()

        disposables += label.asyncBinding.bind(\.text) <~ viewModel.countPublisher.values
    }

    @IBAction private func incrementDidTap(_ sender: Any) {
        viewModel.increment()
    }

    @IBAction private func decrementDidTap(_ sender: Any) {
        viewModel.decrement()
    }
}

class ViewModel {
    private let count = CurrentValueSubject<Int, Never>(0)

    func increment() {
        count.value += 1
    }

    func decrement() {
        count.value -= 1
    }

    var countPublisher: AnyPublisher<String?, Never> {
        count.map { $0.description }.eraseToAnyPublisher()
    }
}
```

# 完成したコード
以下が完成したコード全体である。
```Swift
public protocol AsyncBindingExtensionsProvider {}

extension AsyncBindingExtensionsProvider {
    public var asyncBinding: AsyncBinding<Self> {
        AsyncBinding(self)
    }
}

extension NSObject: AsyncBindingExtensionsProvider {}

public struct AsyncBinding<Base> {
    public let base: Base

    public init(_ base: Base) {
        self.base = base
    }

    @MainActor
    public func bind<Value>(_ keyPath: ReferenceWritableKeyPath<Base, Value>)
        -> AsyncBindingTarget<Base, Value>
    {
        AsyncBindingTarget(base: self.base, keyPath: keyPath)
    }
}

infix operator <~

@MainActor
public struct AsyncBindingTarget<Base: Sendable, Value>: Sendable
where Base: AnyObject {
    public weak var base: Base?
    public let keyPath: ReferenceWritableKeyPath<Base, Value>

    public init(base: Base, keyPath: ReferenceWritableKeyPath<Base, Value>) {
        self.base = base
        self.keyPath = keyPath
    }

    public static func <~ <RHS>(lhs: AsyncBindingTarget<Base, Value>, rhs: RHS)
        -> Disposable where RHS: AsyncSequence, RHS.Element == Value
    {
        Task { @MainActor in
            for try await value in rhs {
                lhs.base?[keyPath: lhs.keyPath] = value
            }
        }
    }
}

public protocol Disposable {
    func dispose()
}

extension Task: Disposable {
    public func dispose() {
        cancel()
    }
}

public final class CompositeDisposable {
    private var disposables: [Disposable] = []

    deinit {
        dispose()
    }

    func add(_ disposable: Disposable) {
        disposables.append(disposable)
    }

    func dispose() {
        disposables.forEach { $0.dispose() }
        disposables.removeAll()
    }

    static func += (lhs: CompositeDisposable, rhs: Disposable) {
        lhs.add(rhs)
    }
}
```