Skip to content

Commit 0e89e6a

Browse files
committed
Implement the argument bridging logic.
1 parent c514f76 commit 0e89e6a

File tree

4 files changed

+250
-4
lines changed

4 files changed

+250
-4
lines changed

ReactiveCocoa/NSObject+Intercepting.swift

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,134 @@ extension Reactive where Base: NSObject {
4343
return signal
4444
}
4545
}
46+
47+
/// Create a signal which sends a `next` event, containing an array of bridged
48+
/// arguments, at the end of every invocation of `selector` on the object.
49+
///
50+
/// `trigger(for:from:)` can be used to intercept optional protocol
51+
/// requirements by supplying the protocol as `protocol`. The instance need
52+
/// not have a concrete implementation of the requirement.
53+
///
54+
/// However, as Cocoa classes usually cache information about delegate
55+
/// conformances, trigger signals for optional, unbacked protocol requirements
56+
/// should be set up before the instance is assigned as the corresponding
57+
/// delegate.
58+
///
59+
/// - parameters:
60+
/// - selector: The selector to observe.
61+
/// - protocol: The protocol of the selector, or `nil` if the selector does
62+
/// not belong to any protocol.
63+
///
64+
/// - returns:
65+
/// A signal that sends an array of bridged arguments.
66+
public func signal(for selector: Selector, from protocol: Protocol? = nil) -> Signal<[Any?], NoError> {
67+
return base.synchronized {
68+
let map = associatedValue { _ in NSMutableDictionary() }
69+
70+
let selectorName = String(describing: selector) as NSString
71+
if let signal = map.object(forKey: selectorName) as! Signal<[Any?], NoError>? {
72+
return signal
73+
}
74+
75+
let (signal, observer) = Signal<[Any?], NoError>.pipe()
76+
let isSuccessful = base._rac_setupInvocationObservation(for: selector,
77+
protocol: `protocol`,
78+
receiver: bridge(observer))
79+
precondition(isSuccessful)
80+
81+
lifetime.ended.observeCompleted(observer.sendCompleted)
82+
map.setObject(signal, forKey: selectorName)
83+
84+
return signal
85+
}
86+
}
87+
}
88+
89+
private func bridge(_ observer: Observer<[Any?], NoError>) -> (RACSwiftInvocationArguments) -> Void {
90+
return { arguments in
91+
let count = arguments.count
92+
93+
var bridged = [Any?]()
94+
bridged.reserveCapacity(count - 2)
95+
96+
// Ignore `self` and `_cmd`.
97+
for position in 2 ..< count {
98+
let encoding = TypeEncoding(rawValue: arguments.argumentType(at: position).pointee) ?? .undefined
99+
100+
func extract<U>(_ type: U.Type) -> U {
101+
let pointer = UnsafeMutableRawPointer.allocate(bytes: MemoryLayout<U>.size,
102+
alignedTo: MemoryLayout<U>.alignment)
103+
defer {
104+
pointer.deallocate(bytes: MemoryLayout<U>.size,
105+
alignedTo: MemoryLayout<U>.alignment)
106+
}
107+
108+
arguments.copyArgument(at: position, to: pointer)
109+
return pointer.assumingMemoryBound(to: type).pointee
110+
}
111+
112+
switch encoding {
113+
case .char:
114+
bridged.append(extract(CChar.self))
115+
case .int:
116+
bridged.append(extract(CInt.self))
117+
case .short:
118+
bridged.append(extract(CShort.self))
119+
case .long:
120+
bridged.append(extract(CLong.self))
121+
case .longLong:
122+
bridged.append(extract(CLongLong.self))
123+
124+
case .unsignedChar:
125+
bridged.append(extract(CUnsignedChar.self))
126+
case .unsignedInt:
127+
bridged.append(extract(CUnsignedInt.self))
128+
case .unsignedShort:
129+
bridged.append(extract(CUnsignedShort.self))
130+
case .unsignedLong:
131+
bridged.append(extract(CUnsignedLong.self))
132+
133+
case .bitfield:
134+
fallthrough
135+
case .unsignedLongLong:
136+
bridged.append(extract(CUnsignedLongLong.self))
137+
138+
case .float:
139+
bridged.append(extract(CFloat.self))
140+
case .double:
141+
bridged.append(extract(CDouble.self))
142+
143+
case .bool:
144+
bridged.append(extract(CBool.self))
145+
case .void:
146+
bridged.append(())
147+
148+
case .cString:
149+
var pointer: UnsafePointer<Int8>?
150+
arguments.copyArgument(at: position, to: &pointer)
151+
bridged.append(pointer.map(String.init(cString:)))
152+
153+
case .object:
154+
bridged.append(extract((AnyObject?).self))
155+
case .type:
156+
bridged.append(extract((AnyClass?).self))
157+
158+
case .selector:
159+
bridged.append(arguments.selectorString(at: position))
160+
161+
case .array:
162+
bridged.append(extract(OpaquePointer.self))
163+
164+
case .undefined:
165+
bridged.append(nil)
166+
}
167+
}
168+
169+
observer.send(value: bridged)
170+
}
46171
}
47172

173+
48174
private enum TypeEncoding: Int8 {
49175
// Integer
50176
case char = 99
@@ -72,6 +198,7 @@ private enum TypeEncoding: Int8 {
72198
case selector = 58
73199
case array = 91
74200
case bitfield = 98
75-
case pointer = 94
76201
// Note: Structure `{` and union `(` are not supported.
202+
203+
case undefined = -1
77204
}

ReactiveCocoa/RACObjCRuntimeUtilities.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
77

88
-(const char *)argumentTypeAt:(NSInteger)position;
99
-(void)copyArgumentAt:(NSInteger)position to:(void *)buffer;
10+
-(NSString*)selectorStringAt:(NSInteger)position;
1011

1112
@end
1213

ReactiveCocoa/RACObjCRuntimeUtilities.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ -(instancetype) initWithInvocation:(NSInvocation *)inv {
321321
return self;
322322
}
323323

324-
-(NSInteger)getCount {
324+
-(NSInteger)count {
325325
return [[invocation methodSignature] numberOfArguments];
326326
}
327327

@@ -333,4 +333,10 @@ -(void)copyArgumentAt:(NSInteger)position to:(void *)buffer {
333333
[invocation getArgument:buffer atIndex:position];
334334
}
335335

336+
-(NSString*)selectorStringAt:(NSInteger)position {
337+
SEL selector;
338+
[invocation getArgument:&selector atIndex:position];
339+
return NSStringFromSelector(selector);
340+
}
341+
336342
@end

ReactiveCocoaTests/InterceptingSpec.swift

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ class InterceptingSpec: QuickSpec {
5656

5757
(object as TestProtocol).optionalMethod!()
5858
expect(counter) == 2
59-
6059
}
6160

6261
it("should complete when the object deinitializes") {
@@ -110,6 +109,118 @@ class InterceptingSpec: QuickSpec {
110109
}
111110
}
112111
}
112+
113+
describe("signal(for:)") {
114+
var object: InterceptedObject!
115+
weak var _object: InterceptedObject?
116+
117+
beforeEach {
118+
object = InterceptedObject()
119+
_object = object
120+
}
121+
122+
afterEach {
123+
object = nil
124+
expect(_object).to(beNil())
125+
}
126+
127+
it("should send a value with bridged arguments") {
128+
let signal = object.reactive.signal(for: #selector(object.test))
129+
130+
var arguments = [[Any?]]()
131+
signal.observeValues { arguments.append($0) }
132+
133+
expect(arguments.count) == 0
134+
135+
let firstObject = NSObject()
136+
object.test(a: 1, b: 10.0, c: firstObject, d: nil)
137+
expect(arguments.count) == 1
138+
139+
expect(arguments[0][0] as? Int64) == 1
140+
expect(arguments[0][1] as? Double) == 10.0
141+
expect(arguments[0][2] as? NSObject) == firstObject
142+
expect(arguments[0][3] as! NSObject?).to(beNil())
143+
144+
let secondObject = NSObject()
145+
object.test(a: 2, b: 20.0, c: secondObject, d: nil)
146+
expect(arguments.count) == 2
147+
148+
expect(arguments[1][0] as? Int64) == 2
149+
expect(arguments[1][1] as? Double) == 20.0
150+
expect(arguments[1][2] as? NSObject) == secondObject
151+
expect(arguments[1][3] as! NSObject?).to(beNil())
152+
}
153+
154+
it("should send a value when the selector is invoked without implementation") {
155+
let selector = #selector(TestProtocol.optionalMethod)
156+
157+
let signal = object.reactive.signal(for: selector,
158+
from: TestProtocol.self)
159+
expect(object.responds(to: selector)) == true
160+
161+
var counter = 0
162+
signal.observeValues { _ in counter += 1 }
163+
164+
expect(counter) == 0
165+
166+
(object as TestProtocol).optionalMethod!()
167+
expect(counter) == 1
168+
169+
(object as TestProtocol).optionalMethod!()
170+
expect(counter) == 2
171+
}
172+
173+
it("should complete when the object deinitializes") {
174+
let signal = object.reactive.signal(for: #selector(object.increment))
175+
176+
var isCompleted = false
177+
signal.observeCompleted { isCompleted = true }
178+
expect(isCompleted) == false
179+
180+
object = nil
181+
expect(_object).to(beNil())
182+
expect(isCompleted) == true
183+
}
184+
185+
it("should multicast") {
186+
let signal1 = object.reactive.signal(for: #selector(object.increment))
187+
let signal2 = object.reactive.signal(for: #selector(object.increment))
188+
189+
var counter1 = 0
190+
var counter2 = 0
191+
signal1.observeValues { _ in counter1 += 1 }
192+
signal2.observeValues { _ in counter2 += 1 }
193+
194+
expect(counter1) == 0
195+
expect(counter2) == 0
196+
197+
object.increment()
198+
expect(counter1) == 1
199+
expect(counter2) == 1
200+
201+
object.increment()
202+
expect(counter1) == 2
203+
expect(counter2) == 2
204+
}
205+
206+
it("should not deadlock") {
207+
for _ in 1 ... 10 {
208+
var isDeadlocked = true
209+
210+
DispatchQueue.global(priority: .high).async {
211+
_ = object.reactive.signal(for: #selector(object.increment))
212+
213+
DispatchQueue.global(priority: .high).async {
214+
_ = object.reactive.signal(for: #selector(object.increment))
215+
216+
isDeadlocked = false
217+
}
218+
}
219+
220+
expect(isDeadlocked).toEventually(beFalsy())
221+
}
222+
}
223+
}
113224
}
114225
}
115226

@@ -119,9 +230,10 @@ class InterceptingSpec: QuickSpec {
119230

120231
class InterceptedObject: NSObject, TestProtocol {
121232
var counter = 0
122-
var testProtocolCounter = 0
123233

124234
dynamic func increment() {
125235
counter += 1
126236
}
237+
238+
dynamic func test(a: Int, b: Double, c: NSObject, d: NSObject?) {}
127239
}

0 commit comments

Comments
 (0)