LoginSignup
7
2

More than 1 year has passed since last update.

[Swift] CombineでAnyCancellableを省略する

Last updated at Posted at 2021-04-18

前置き

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

前提

RxSwiftでの実装を見る

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

上記、中身はたくさんありますが、実質的には このコード だけで機能しています。

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時代からある黒魔術を使って追加しています。(もっとシンプルな例が見たい方はこちら

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

実装

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

1. NSObject を拡張する

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

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

Xcode12以前のコード
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)
        }
    }
}

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

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

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を拡張して、その部分を隠蔽化することで対応できます。

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

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 で制限する

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

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

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

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

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

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 を適応することで使用することができます。

final class Object: NSObject, Storable {

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

ライブラリ

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

  • Cocoapods
  • Carthage
  • SPM

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

終わりに

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

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

7
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2