前置き
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
もっとこうした方が良いなどあればコメントいただけると!