[Swift] CombineでAnyCancellableを省略する

# 前置き

``Combine`` を使うと必ず書くことになるのが ``AnyCancellable`` です。
これを毎回書くのが億劫だなと感じたので、書かずに済む方法を考えました。



# 前提


## RxSwiftでの実装を見る

``RxSwift`` を使った際にも ``DisposeBag`` を書き続けなければならないという問題がありました。
これにはライブラリがあり、それを導入して解決することができました。

https://github.com/RxSwiftCommunity/NSObject-Rx


上記、中身はたくさんありますが、実質的には **[このコード](https://github.com/RxSwiftCommunity/NSObject-Rx/blob/master/NSObject%2BRx.swift)** だけで機能しています。

```.swift
fileprivate var disposeBagContext: UInt8 = 0

extension Reactive where Base: AnyObject {

    func synchronizedBag<T>( _ action: () -> T) -> T {
        objc_sync_enter(self.base)
        let result = action()
        objc_sync_exit(self.base)
        return result
    }
}

public extension Reactive where Base: AnyObject {

    var disposeBag: DisposeBag {
        get {
            return synchronizedBag {
                if let disposeObject = objc_getAssociatedObject(base, &disposeBagContext) as? DisposeBag {
                    return disposeObject
                }
                let disposeObject = DisposeBag()
                objc_setAssociatedObject(base, &disposeBagContext, disposeObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                return disposeObject
            }
        }
        set {
            synchronizedBag {
                objc_setAssociatedObject(base, &disposeBagContext, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
}
```

馴染みがないとわかりづらいですが、行っていることはとてもシンプルです。

通常は``extension``で生やせないメンバ変数を、``Objectice-C``時代からある**黒魔術**を使って追加しています。（もっとシンプルな例が見たい方は**[こちら](https://qiita.com/fmtonakai/items/e9036dec4af2609b5715)**）

上記のコードを **``AnyCancellable`` に適応する**ことで、省略することが可能です。


# 実装

今回は３つの段階を踏んで紹介しますが、プロジェクトに導入する場合は段階の好み選んでいただければと思います。

## 1. NSObject を拡張する

多くのクラスで基底部分にある``NSObject``を拡張する形をとります。

[update 2021/5/22]
Xcode12よりクラッシュするようになったのでコードをアップデートしました

<details><summary>Xcode12以前のコード</summary><div>

```.swift
private var cancellableContext: UInt8 = 0

extension NSObject {

    var cancellables: Set<AnyCancellable> {
        get {
            if let cancellables = objc_getAssociatedObject(self, &cancellableContext) as? Set<AnyCancellable> {
                return cancellables
            }
            let cancellables = Set<AnyCancellable>()
            objc_setAssociatedObject(self, &cancellableContext, cancellables, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return cancellables
        }
        set {
            objc_setAssociatedObject(self, &cancellableContext, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
```

</div></details>


~~前提で記載した``RxSwift``の場合は、``rx.disposeBag``という**「rx」**プレフィックスをワンクッション挟む実装の都合上、若干長くなっていましたが、それがない分だけシンプルなコードになりました。~~

```.swift
private var cancellableContext: UInt8 = 0

private class CancellableWrapper {
    let cancellables: Set<AnyCancellable>
    
    init(cancellables: Set<AnyCancellable>) {
        self.cancellables = cancellables
    }
}

public extension NSObject {

    var cancellables: Set<AnyCancellable> {
        get {
            if let wrapper = objc_getAssociatedObject(self, &cancellableContext) as? CancellableWrapper {
                return wrapper.cancellables
            }
            let cancellables = Set<AnyCancellable>()
            self.cancellables = cancellables
            return cancellables
        }
        set {
            let wrapper = CancellableWrapper(cancellables: newValue)
            objc_setAssociatedObject(self, &cancellableContext, wrapper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
```

``objc_getAssociatedObject``で取り出したものを``Set<AnyCancellable>``にキャストすると、**クラッシュ**するためラッパークラスで包むことで、それを回避しています。

### example

```.swift
final class Object: NSObject {

    // これが不要になる
    // private var cancellables = Set<AnyCancellable>()

    func exec() {
        Just<Int>(0)
            .sink(receiveCompletion: { _ in
                // do something
            }, receiveValue: { _ in 
                // do something
            })
            .store(in: &self.cancellables) // 参照箇所
    }
}
```

このように、**クラス内に ``AnyCancellable`` を定義しなくても呼び出せる**ようになります。


## 2. Publisher を拡張する

先の「1」で行った実装のままでも十分に機能します。

ただ、コードを書いていくうちに ``AnyCancellable`` を登録する部分である
「 ``.store(in: &self.cancellables)`` 」も書くのが億劫になります。


その場合は、さらに``Publisher``を拡張して、その部分を隠蔽化することで対応できます。

```.swift
extension Publisher {
    // case 1
    func sink<T: NSObject>(_ target: T, receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) {
        sink(receiveCompletion: receiveCompletion, receiveValue: receiveValue).store(in: &target.cancellables)
    }
}

extension Publisher where Self.Failure == Never {
    // case 2
    func sink<T: NSObject>(_ target: T, receiveValue: @escaping ((Self.Output) -> Void)) {
        sink(receiveValue: receiveValue).store(in: &target.cancellables)
    }

    // case 3
    func assign<O: NSObject>(to keyPath: ReferenceWritableKeyPath<O, Self.Output>, on object: O) {
        assign(to: keyPath, on: object).store(in: &object.cancellables)
    }
}
```

### example

```.swift
final class Object: NSObject {

    // case 1
    func exec1() {
        Just<Int>(0)
            .sink(self, receiveCompletion: { _ in
                // do something
            }, receiveValue: { _ in 
                // do something
            })
    }

    // case 2
    func exec2() {        
        Just<Int>(0)
            .sink(self, receiveValue: { _ in 
                // do something
            })
    }

    // case 3
    private var number: Int = 0

    func exec3() {
        Just<Int>(10)
            .assign(to: \.number, on: self)
    }  
}
```

``AnyCancellable`` を登録するコードがない分、だいぶ簡潔になりました。

## 3. Protocol で制限する

「１」「２」で行った実装では、``extension`` でグローバルに拡張しているため、どこからでも呼び出せてしまいます。そこに一定の制約を設けたい場合は ``Protocol`` で縛っていきます。

```.swift
protocol Storable: NSObject {
    var cancellables: Set<AnyCancellable> { get set }
}
```

この作成したプロトコルを、拡張したメソッドにも適応して縛ります。

```
旧: where T: NSObject
        ↓↓↓
新: where T: Storable
```

つまり、以下のようになります。

```.swift
extension Publisher {

    func sink<T: Storable>(_ target: T, receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) {
        sink(receiveCompletion: receiveCompletion, receiveValue: receiveValue).store(in: &target.cancellables)
    }
}

extension Publisher where Self.Failure == Never {

    func sink<T: Storable>(_ target: T, receiveValue: @escaping ((Self.Output) -> Void)) {
        sink(receiveValue: receiveValue).store(in: &target.cancellables)
    }

    func assign<O: Storable>(to keyPath: ReferenceWritableKeyPath<O, Self.Output>, on object: O) {
        assign(to: keyPath, on: object).store(in: &object.cancellables)
    }
}
```

### example

使用したい ``NSObject`` にのみ ``Storable`` を適応することで使用することができます。

```.swift
final class Object: NSObject, Storable {

    func exec() {
        Just<Int>(0)
            .sink(receiveCompletion: { _ in
                // do something
            }, receiveValue: { _ in 
                // do something
            })
    }
}
```


# ライブラリ

せっかくなのでライブラリ化しました。

https://github.com/crane-hiromu/CombineStorable

- Cocoapods
- Carthage
- SPM

３つとも対応しています。
何かあればPRお願いいたします🙏


# 終わりに

前置きから長くなってしまいましたが、コードを少しでも**簡単に書くためのテクニック**くらいに思っていただけると幸いですmm

もっとこうした方が良いなどあればコメントいただけると！