forked from cx-org/CombineX
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Publishers+KeyValueObserving.swift
216 lines (188 loc) · 7.94 KB
/
Publishers+KeyValueObserving.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
// Adapted from the original file:
// https://github.com/apple/swift/blob/102bc6a2cd70f81d13c519718764bdcd7b8e3a6d/stdlib/public/Darwin/Foundation/Publishers%2BKeyValueObserving.swift
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
// Only support 64bit
//#if !(os(iOS) && (arch(i386) || arch(arm)))
#if canImport(ObjectiveC)
import Foundation
import CombineX
import CXUtility
// The following protocol is so that we can reference `Self` in the Publisher
// below. This is based on a trick used in the the standard library's
// implementation of `NSObject.observe(key path)`
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
//public protocol _KeyValueCodingAndObservingPublishing {}
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
//extension NSObject: _KeyValueCodingAndObservingPublishing {}
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension CXWrappers.NSObject {
/// Publish values when the value identified by a KVO-compliant keypath changes.
///
/// - Parameters:
/// - keyPath: The keypath of the property to publish.
/// - options: Key-value observing options.
/// - Returns: A publisher that emits elements each time the property’s value changes.
public func publisher<Value>(for keyPath: KeyPath<Base, Value>,
options: NSKeyValueObservingOptions = [.initial, .new])
-> CXWrappers.NSObject<Base>.KeyValueObservingPublisher<Base, Value> {
return CXWrappers.NSObject<Base>.KeyValueObservingPublisher(object: base, keyPath: keyPath, options: options)
}
}
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension CXWrappers.NSObject.KeyValueObservingPublisher {
/// Returns a publisher that emits values when a KVO-compliant property changes.
///
/// - Returns: A key-value observing publisher.
public func didChange()
-> Publishers.Map<CXWrappers.NSObject<Base>.KeyValueObservingPublisher<Subject, Value>, Void> {
return map { _ in () }
}
}
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension CXWrappers.NSObject {
/// A publisher that emits events when the value of a KVO-compliant property changes.
public struct KeyValueObservingPublisher<Subject: Foundation.NSObject, Value> : Equatable {
public let object: Subject
public let keyPath: KeyPath<Subject, Value>
public let options: NSKeyValueObservingOptions
public init(
object: Subject,
keyPath: KeyPath<Subject, Value>,
options: NSKeyValueObservingOptions
) {
self.object = object
self.keyPath = keyPath
self.options = options
}
public static func == (
lhs: KeyValueObservingPublisher,
rhs: KeyValueObservingPublisher
) -> Bool {
return lhs.object === rhs.object
&& lhs.keyPath == rhs.keyPath
&& lhs.options == rhs.options
}
}
}
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension CXWrappers.NSObject.KeyValueObservingPublisher: Publisher {
public typealias Output = Value
public typealias Failure = Never
public func receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {
let s = CXWrappers.NSObject.KVOSubscription(object, keyPath, options, subscriber)
subscriber.receive(subscription: s)
}
}
//@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension CXWrappers.NSObject {
private final class KVOSubscription<Subject: Foundation.NSObject, Value>: Subscription, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible {
private var observation: NSKeyValueObservation? // GuardedBy(lock)
private var demand: Subscribers.Demand // GuardedBy(lock)
// for configurations that care about '.initial' we need to 'cache' the value to account for backpressure, along with whom to send it to
//
// TODO: in the future we might want to consider interjecting a temporary publisher that does this, so that all KVO subscriptions don't incur the cost.
private var receivedInitial: Bool // GuardedBy(lock)
private var last: Value? // GuardedBy(lock)
private var subscriber: AnySubscriber<Value, Never>? // GuardedBy(lock)
private let lock = Lock()
// This lock can only be held for the duration of downstream callouts
private let downstreamLock = RecursiveLock()
var description: String { return "KVOSubscription" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
return Mirror(self, children: [
"observation": observation as Any,
"demand": demand
])
}
var playgroundDescription: Any { return description }
init<S: Subscriber>(
_ object: Subject,
_ keyPath: KeyPath<Subject, Value>,
_ options: NSKeyValueObservingOptions,
_ subscriber: S)
where
S.Input == Value,
S.Failure == Never
{
demand = .max(0)
receivedInitial = false
self.subscriber = AnySubscriber(subscriber)
observation = object.observe(
keyPath,
options: options
) { [weak self] obj, _ in
guard let self = self else {
return
}
let value = obj[keyPath: keyPath]
self.lock.lock()
if self.demand > 0, let sub = self.subscriber {
self.demand -= 1
self.lock.unlock()
self.downstreamLock.lock()
let additional = sub.receive(value)
self.downstreamLock.unlock()
self.lock.lock()
self.demand += additional
self.lock.unlock()
} else {
// Drop the value, unless we've asked for .initial, and this
// is the first value.
if self.receivedInitial == false && options.contains(.initial) {
self.last = value
self.receivedInitial = true
}
self.lock.unlock()
}
}
}
deinit {
lock.cleanupLock()
downstreamLock.cleanupLock()
}
func request(_ d: Subscribers.Demand) {
lock.lock()
demand += d
if demand > 0, let v = last, let sub = subscriber {
demand -= 1
last = nil
lock.unlock()
downstreamLock.lock()
let additional = sub.receive(v)
downstreamLock.unlock()
lock.lock()
demand += additional
} else {
demand -= 1
last = nil
}
lock.unlock()
}
func cancel() {
lock.lock()
guard let o = observation else {
lock.unlock()
return
}
lock.unlock()
observation = nil
subscriber = nil
last = nil
o.invalidate()
}
}
}
#endif /* canImport(ObjectiveC) */
//#endif /* !(os(iOS) && (arch(i386) || arch(arm))) */