forked from ReactiveX/RxSwift
-
Notifications
You must be signed in to change notification settings - Fork 0
/
NSObject+Rx.swift
532 lines (412 loc) · 17.2 KB
/
NSObject+Rx.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
//
// NSObject+Rx.swift
// RxCocoa
//
// Created by Krunoslav Zaher on 2/21/15.
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
//
#if !os(Linux)
import Foundation.NSObject
#if !RX_NO_MODULE
import RxSwift
#if SWIFT_PACKAGE && !DISABLE_SWIZZLING && !os(Linux)
import RxCocoaRuntime
#endif
#endif
#if !DISABLE_SWIZZLING && !os(Linux)
fileprivate var deallocatingSubjectTriggerContext: UInt8 = 0
fileprivate var deallocatingSubjectContext: UInt8 = 0
#endif
fileprivate var deallocatedSubjectTriggerContext: UInt8 = 0
fileprivate var deallocatedSubjectContext: UInt8 = 0
#if !os(Linux)
/**
KVO is a tricky mechanism.
When observing child in a ownership hierarchy, usually retaining observing target is wanted behavior.
When observing parent in a ownership hierarchy, usually retaining target isn't wanter behavior.
KVO with weak references is especially tricky. For it to work, some kind of swizzling is required.
That can be done by
* replacing object class dynamically (like KVO does)
* by swizzling `dealloc` method on all instances for a class.
* some third method ...
Both approaches can fail in certain scenarios:
* problems arise when swizzlers return original object class (like KVO does when nobody is observing)
* Problems can arise because replacing dealloc method isn't atomic operation (get implementation,
set implementation).
Second approach is chosen. It can fail in case there are multiple libraries dynamically trying
to replace dealloc method. In case that isn't the case, it should be ok.
*/
extension Reactive where Base: NSObject {
/**
Observes values on `keyPath` starting from `self` with `options` and retains `self` if `retainSelf` is set.
`observe` is just a simple and performant wrapper around KVO mechanism.
* it can be used to observe paths starting from `self` or from ancestors in ownership graph (`retainSelf = false`)
* it can be used to observe paths starting from descendants in ownership graph (`retainSelf = true`)
* the paths have to consist only of `strong` properties, otherwise you are risking crashing the system by not unregistering KVO observer before dealloc.
If support for weak properties is needed or observing arbitrary or unknown relationships in the
ownership tree, `observeWeakly` is the preferred option.
- parameter keyPath: Key path of property names to observe.
- parameter options: KVO mechanism notification options.
- parameter retainSelf: Retains self during observation if set `true`.
- returns: Observable sequence of objects on `keyPath`.
*/
public func observe<E>(_ type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.new, .initial], retainSelf: Bool = true) -> Observable<E?> {
return KVOObservable(object: base, keyPath: keyPath, options: options, retainTarget: retainSelf).asObservable()
}
}
#endif
#if !DISABLE_SWIZZLING && !os(Linux)
// KVO
extension Reactive where Base: NSObject {
/**
Observes values on `keyPath` starting from `self` with `options` and doesn't retain `self`.
It can be used in all cases where `observe` can be used and additionally
* because it won't retain observed target, it can be used to observe arbitrary object graph whose ownership relation is unknown
* it can be used to observe `weak` properties
**Since it needs to intercept object deallocation process it needs to perform swizzling of `dealloc` method on observed object.**
- parameter keyPath: Key path of property names to observe.
- parameter options: KVO mechanism notification options.
- returns: Observable sequence of objects on `keyPath`.
*/
public func observeWeakly<E>(_ type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.new, .initial]) -> Observable<E?> {
return observeWeaklyKeyPathFor(base, keyPath: keyPath, options: options)
.map { n in
return n as? E
}
}
}
#endif
// Dealloc
extension Reactive where Base: AnyObject {
/**
Observable sequence of object deallocated events.
After object is deallocated one `()` element will be produced and sequence will immediately complete.
- returns: Observable sequence of object deallocated events.
*/
public var deallocated: Observable<Void> {
return synchronized {
if let deallocObservable = objc_getAssociatedObject(base, &deallocatedSubjectContext) as? DeallocObservable {
return deallocObservable._subject
}
let deallocObservable = DeallocObservable()
objc_setAssociatedObject(base, &deallocatedSubjectContext, deallocObservable, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return deallocObservable._subject
}
}
#if !DISABLE_SWIZZLING && !os(Linux)
/**
Observable sequence of message arguments that completes when object is deallocated.
Each element is produced before message is invoked on target object. `methodInvoked`
exists in case observing of invoked messages is needed.
In case an error occurs sequence will fail with `RxCocoaObjCRuntimeError`.
In case some argument is `nil`, instance of `NSNull()` will be sent.
- returns: Observable sequence of object deallocating events.
*/
public func sentMessage(_ selector: Selector) -> Observable<[Any]> {
return synchronized {
// in case of dealloc selector replay subject behavior needs to be used
if selector == deallocSelector {
return deallocating.map { _ in [] }
}
do {
let proxy: MessageSentProxy = try registerMessageInterceptor(selector)
return proxy.messageSent.asObservable()
}
catch let e {
return Observable.error(e)
}
}
}
/**
Observable sequence of message arguments that completes when object is deallocated.
Each element is produced after message is invoked on target object. `sentMessage`
exists in case interception of sent messages before they were invoked is needed.
In case an error occurs sequence will fail with `RxCocoaObjCRuntimeError`.
In case some argument is `nil`, instance of `NSNull()` will be sent.
- returns: Observable sequence of object deallocating events.
*/
public func methodInvoked(_ selector: Selector) -> Observable<[Any]> {
return synchronized {
// in case of dealloc selector replay subject behavior needs to be used
if selector == deallocSelector {
return deallocated.map { _ in [] }
}
do {
let proxy: MessageSentProxy = try registerMessageInterceptor(selector)
return proxy.methodInvoked.asObservable()
}
catch let e {
return Observable.error(e)
}
}
}
/**
Observable sequence of object deallocating events.
When `dealloc` message is sent to `self` one `()` element will be produced and after object is deallocated sequence
will immediately complete.
In case an error occurs sequence will fail with `RxCocoaObjCRuntimeError`.
- returns: Observable sequence of object deallocating events.
*/
public var deallocating: Observable<()> {
return synchronized {
do {
let proxy: DeallocatingProxy = try registerMessageInterceptor(deallocSelector)
return proxy.messageSent.asObservable()
}
catch let e {
return Observable.error(e)
}
}
}
fileprivate func registerMessageInterceptor<T: MessageInterceptorSubject>(_ selector: Selector) throws -> T {
let rxSelector = RX_selector(selector)
let selectorReference = RX_reference_from_selector(rxSelector)
let subject: T
if let existingSubject = objc_getAssociatedObject(base, selectorReference) as? T {
subject = existingSubject
}
else {
subject = T()
objc_setAssociatedObject(
base,
selectorReference,
subject,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
if subject.isActive {
return subject
}
var error: NSError?
let targetImplementation = RX_ensure_observing(base, selector, &error)
if targetImplementation == nil {
throw error?.rxCocoaErrorForTarget(base) ?? RxCocoaError.unknown
}
subject.targetImplementation = targetImplementation!
return subject
}
#endif
}
// MARK: Message interceptors
#if !DISABLE_SWIZZLING && !os(Linux)
fileprivate protocol MessageInterceptorSubject: class {
init()
var isActive: Bool {
get
}
var targetImplementation: IMP { get set }
}
fileprivate final class DeallocatingProxy
: MessageInterceptorSubject
, RXDeallocatingObserver {
typealias E = ()
let messageSent = ReplaySubject<()>.create(bufferSize: 1)
@objc var targetImplementation: IMP = RX_default_target_implementation()
var isActive: Bool {
return targetImplementation != RX_default_target_implementation()
}
init() {
}
@objc func deallocating() -> Void {
messageSent.on(.next())
}
deinit {
messageSent.on(.completed)
}
}
fileprivate final class MessageSentProxy
: MessageInterceptorSubject
, RXMessageSentObserver {
typealias E = [AnyObject]
let messageSent = PublishSubject<[Any]>()
let methodInvoked = PublishSubject<[Any]>()
@objc var targetImplementation: IMP = RX_default_target_implementation()
var isActive: Bool {
return targetImplementation != RX_default_target_implementation()
}
init() {
}
@objc func messageSent(withArguments arguments: [Any]) -> Void {
messageSent.on(.next(arguments))
}
@objc func methodInvoked(withArguments arguments: [Any]) -> Void {
methodInvoked.on(.next(arguments))
}
deinit {
messageSent.on(.completed)
methodInvoked.on(.completed)
}
}
#endif
fileprivate final class DeallocObservable {
let _subject = ReplaySubject<Void>.create(bufferSize:1)
init() {
}
deinit {
_subject.on(.next(()))
_subject.on(.completed)
}
}
// MARK: KVO
#if !os(Linux)
fileprivate protocol KVOObservableProtocol {
var target: AnyObject { get }
var keyPath: String { get }
var retainTarget: Bool { get }
var options: NSKeyValueObservingOptions { get }
}
fileprivate final class KVOObserver
: _RXKVOObserver
, Disposable {
typealias Callback = (Any?) -> Void
var retainSelf: KVOObserver? = nil
init(parent: KVOObservableProtocol, callback: @escaping Callback) {
#if TRACE_RESOURCES
_ = Resources.incrementTotal()
#endif
super.init(target: parent.target, retainTarget: parent.retainTarget, keyPath: parent.keyPath, options: parent.options, callback: callback)
self.retainSelf = self
}
override func dispose() {
super.dispose()
self.retainSelf = nil
}
deinit {
#if TRACE_RESOURCES
_ = Resources.decrementTotal()
#endif
}
}
fileprivate final class KVOObservable<Element>
: ObservableType
, KVOObservableProtocol {
typealias E = Element?
unowned var target: AnyObject
var strongTarget: AnyObject?
var keyPath: String
var options: NSKeyValueObservingOptions
var retainTarget: Bool
init(object: AnyObject, keyPath: String, options: NSKeyValueObservingOptions, retainTarget: Bool) {
self.target = object
self.keyPath = keyPath
self.options = options
self.retainTarget = retainTarget
if retainTarget {
self.strongTarget = object
}
}
func subscribe<O : ObserverType>(_ observer: O) -> Disposable where O.E == Element? {
let observer = KVOObserver(parent: self) { (value) in
if value as? NSNull != nil {
observer.on(.next(nil))
return
}
observer.on(.next(value as? Element))
}
return Disposables.create(with: observer.dispose)
}
}
#endif
#if !DISABLE_SWIZZLING && !os(Linux)
fileprivate func observeWeaklyKeyPathFor(_ target: NSObject, keyPath: String, options: NSKeyValueObservingOptions) -> Observable<AnyObject?> {
let components = keyPath.components(separatedBy: ".").filter { $0 != "self" }
let observable = observeWeaklyKeyPathFor(target, keyPathSections: components, options: options)
.finishWithNilWhenDealloc(target)
if !options.intersection(.initial).isEmpty {
return observable
}
else {
return observable
.skip(1)
}
}
// This should work correctly
// Identifiers can't contain `,`, so the only place where `,` can appear
// is as a delimiter.
// This means there is `W` as element in an array of property attributes.
fileprivate func isWeakProperty(_ properyRuntimeInfo: String) -> Bool {
return properyRuntimeInfo.range(of: ",W,") != nil
}
fileprivate extension ObservableType where E == AnyObject? {
func finishWithNilWhenDealloc(_ target: NSObject)
-> Observable<AnyObject?> {
let deallocating = target.rx.deallocating
return deallocating
.map { _ in
return Observable.just(nil)
}
.startWith(self.asObservable())
.switchLatest()
}
}
fileprivate func observeWeaklyKeyPathFor(
_ target: NSObject,
keyPathSections: [String],
options: NSKeyValueObservingOptions
) -> Observable<AnyObject?> {
weak var weakTarget: AnyObject? = target
let propertyName = keyPathSections[0]
let remainingPaths = Array(keyPathSections[1..<keyPathSections.count])
let property = class_getProperty(object_getClass(target), propertyName)
if property == nil {
return Observable.error(RxCocoaError.invalidPropertyName(object: target, propertyName: propertyName))
}
let propertyAttributes = property_getAttributes(property)
// should dealloc hook be in place if week property, or just create strong reference because it doesn't matter
let isWeak = isWeakProperty(propertyAttributes.map(String.init) ?? "")
let propertyObservable = KVOObservable(object: target, keyPath: propertyName, options: options.union(.initial), retainTarget: false) as KVOObservable<AnyObject>
// KVO recursion for value changes
return propertyObservable
.flatMapLatest { (nextTarget: AnyObject?) -> Observable<AnyObject?> in
if nextTarget == nil {
return Observable.just(nil)
}
let nextObject = nextTarget! as? NSObject
let strongTarget: AnyObject? = weakTarget
if nextObject == nil {
return Observable.error(RxCocoaError.invalidObjectOnKeyPath(object: nextTarget!, sourceObject: strongTarget ?? NSNull(), propertyName: propertyName))
}
// if target is alive, then send change
// if it's deallocated, don't send anything
if strongTarget == nil {
return Observable.empty()
}
let nextElementsObservable = keyPathSections.count == 1
? Observable.just(nextTarget)
: observeWeaklyKeyPathFor(nextObject!, keyPathSections: remainingPaths, options: options)
if isWeak {
return nextElementsObservable
.finishWithNilWhenDealloc(nextObject!)
}
else {
return nextElementsObservable
}
}
}
#endif
// MARK Constants
fileprivate let deallocSelector = NSSelectorFromString("dealloc")
// MARK: AnyObject + Reactive
extension Reactive where Base: AnyObject {
func synchronized<T>( _ action: () -> T) -> T {
objc_sync_enter(self.base)
let result = action()
objc_sync_exit(self.base)
return result
}
}
extension Reactive where Base: AnyObject {
/**
Helper to make sure that `Observable` returned from `createCachedObservable` is only created once.
This is important because there is only one `target` and `action` properties on `NSControl` or `UIBarButtonItem`.
*/
func lazyInstanceObservable<T: AnyObject>(_ key: UnsafeRawPointer, createCachedObservable: () -> T) -> T {
if let value = objc_getAssociatedObject(base, key) {
return value as! T
}
let observable = createCachedObservable()
objc_setAssociatedObject(base, key, observable, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return observable
}
}
#endif