Skip to content

Commit 000c0e6

Browse files
committed
Add withUnretained operator and tests for ObservableType
1 parent 787ec31 commit 000c0e6

File tree

4 files changed

+191
-2
lines changed

4 files changed

+191
-2
lines changed

Rx.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@
8989
914FCD671CCDB82E0058B304 /* UIPageControl+RxTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914FCD661CCDB82E0058B304 /* UIPageControl+RxTest.swift */; };
9090
914FCD681CCDB82E0058B304 /* UIPageControl+RxTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914FCD661CCDB82E0058B304 /* UIPageControl+RxTest.swift */; };
9191
9BA1CBD31C0F7D550044B50A /* UIActivityIndicatorView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1CBD11C0F7C0A0044B50A /* UIActivityIndicatorView+Rx.swift */; };
92+
A20CC6C9259F3FE700370AE3 /* WithUnretained.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20CC6C8259F3FE700370AE3 /* WithUnretained.swift */; };
93+
A20CC6EA259F40A100370AE3 /* Observable+WithUnretainedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20CC6D4259F408100370AE3 /* Observable+WithUnretainedTests.swift */; };
94+
A20CC6F5259F40A100370AE3 /* Observable+WithUnretainedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20CC6D4259F408100370AE3 /* Observable+WithUnretainedTests.swift */; };
95+
A20CC6F6259F40A200370AE3 /* Observable+WithUnretainedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20CC6D4259F408100370AE3 /* Observable+WithUnretainedTests.swift */; };
9296
A2690E7D22688CAE0032C00E /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C809396D1B8A71760088E94D /* RxCocoa.framework */; };
9397
A2690E7E22688CAE0032C00E /* RxBlocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8093BC71B8A71F00088E94D /* RxBlocking.framework */; };
9498
A2690E7F22688CAE0032C00E /* RxTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C88FA50C1C25C44800CCFEA4 /* RxTest.framework */; };
@@ -992,6 +996,8 @@
992996
927A78C82117BCB400A45638 /* NSTextView+RxTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextView+RxTests.swift"; sourceTree = "<group>"; };
993997
9BA1CBD11C0F7C0A0044B50A /* UIActivityIndicatorView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIActivityIndicatorView+Rx.swift"; sourceTree = "<group>"; };
994998
A111CE961B91C97C00D0DCEE /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
999+
A20CC6C8259F3FE700370AE3 /* WithUnretained.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithUnretained.swift; sourceTree = "<group>"; };
1000+
A20CC6D4259F408100370AE3 /* Observable+WithUnretainedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+WithUnretainedTests.swift"; sourceTree = "<group>"; };
9951001
A2897D53225CA1E7004EA481 /* RxRelay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxRelay.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9961002
A2897D61225CA3F3004EA481 /* Observable+Bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Bind.swift"; sourceTree = "<group>"; };
9971003
A2897D65225D0182004EA481 /* PublishRelay+Signal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublishRelay+Signal.swift"; sourceTree = "<group>"; };
@@ -1735,6 +1741,7 @@
17351741
C820A82B1EB4DA5900D431BC /* Zip+arity.tt */,
17361742
C820A80B1EB4DA5900D431BC /* Zip+Collection.swift */,
17371743
788DCE5C24CB8249005B8F8C /* Decode.swift */,
1744+
A20CC6C8259F3FE700370AE3 /* WithUnretained.swift */,
17381745
);
17391746
path = Observables;
17401747
sourceTree = "<group>";
@@ -2049,6 +2056,7 @@
20492056
C83508F31C38706D0027C24C /* TestImplementations */,
20502057
C835091C1C38706D0027C24C /* VirtualSchedulerTest.swift */,
20512058
78C385EA256859DC005E39B3 /* Infallible+Tests.swift */,
2059+
A20CC6D4259F408100370AE3 /* Observable+WithUnretainedTests.swift */,
20522060
);
20532061
path = RxSwiftTests;
20542062
sourceTree = "<group>";
@@ -3203,6 +3211,7 @@
32033211
C820A96E1EB4F7AC00D431BC /* Observable+SequenceTests.swift in Sources */,
32043212
C8A9B6F41DAD752200C9B027 /* Observable+BindTests.swift in Sources */,
32053213
271A97441CFC9F7B00D64125 /* UIViewController+RxTests.swift in Sources */,
3214+
A20CC6EA259F40A100370AE3 /* Observable+WithUnretainedTests.swift in Sources */,
32063215
C83509631C38706E0027C24C /* SubjectConcurrencyTest.swift in Sources */,
32073216
C82FF0EF1F93DD2E00BDB34D /* ObservableType+SubscriptionTests.swift in Sources */,
32083217
C820A9721EB4F84000D431BC /* Observable+OptionalTests.swift in Sources */,
@@ -3311,6 +3320,7 @@
33113320
C8C4F1801DE9DF0200003FA7 /* UIProgressView+RxTests.swift in Sources */,
33123321
C85217EE1E33C8E60015DD38 /* PerformanceTools.swift in Sources */,
33133322
C8C4F17C1DE9DF0200003FA7 /* UIDatePicker+RxTests.swift in Sources */,
3323+
A20CC6F5259F40A100370AE3 /* Observable+WithUnretainedTests.swift in Sources */,
33143324
C83509C41C3875220027C24C /* NSLayoutConstraint+RxTests.swift in Sources */,
33153325
C820A9731EB4F84000D431BC /* Observable+OptionalTests.swift in Sources */,
33163326
C820A9931EB4FD1400D431BC /* Observable+SwitchIfEmptyTests.swift in Sources */,
@@ -3441,6 +3451,7 @@
34413451
buildActionMask = 2147483647;
34423452
files = (
34433453
C8350A201C38756B0027C24C /* QueueTests.swift in Sources */,
3454+
A20CC6F6259F40A200370AE3 /* Observable+WithUnretainedTests.swift in Sources */,
34443455
C820A9541EB4ECC000D431BC /* Observable+ToArrayTests.swift in Sources */,
34453456
1AF67DA41CED427D00C310FA /* PublishSubjectTest.swift in Sources */,
34463457
C820A9781EB4F92100D431BC /* Observable+GenerateTests.swift in Sources */,
@@ -3748,6 +3759,7 @@
37483759
C820A8401EB4DA5900D431BC /* Buffer.swift in Sources */,
37493760
C820A82C1EB4DA5900D431BC /* Map.swift in Sources */,
37503761
C8093CF31B8A72BE0088E94D /* Errors.swift in Sources */,
3762+
A20CC6C9259F3FE700370AE3 /* WithUnretained.swift in Sources */,
37513763
C86781781DB8129E00B2029A /* PriorityQueue.swift in Sources */,
37523764
C820A8D01EB4DA5A00D431BC /* Sequence.swift in Sources */,
37533765
C820A8E01EB4DA5A00D431BC /* Deferred.swift in Sources */,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// WithUnretained.swift
3+
// RxSwift
4+
//
5+
// Created by Vincent Pradeilles on 01/01/2021.
6+
// Copyright © 2020 Krunoslav Zaher. All rights reserved.
7+
//
8+
9+
extension ObservableType {
10+
/**
11+
Provides an unretained, safe to use (i.e. not implicitly unwrapped), reference to an object along with the events emitted by the sequence.
12+
13+
In the case the provided object cannot be retained successfully, the seqeunce will complete.
14+
15+
- parameter obj: The object to provide an unretained reference on.
16+
- parameter resultSelector: A function to combine the unretained referenced on `obj` and the value of the observable sequence.
17+
- returns: An observable sequence that contains the result of `resultSelector` being called with an unretained reference on `obj` and the values of the original sequence.
18+
*/
19+
public func withUnretained<Object: AnyObject, Out>(
20+
_ obj: Object,
21+
resultSelector: @escaping ((Object, Element)) -> Out
22+
) -> Observable<Out> {
23+
map { [weak obj] element -> Out in
24+
guard let obj = obj else { throw UnretainedError.failedRetaining }
25+
26+
return resultSelector((obj, element))
27+
}
28+
.catch{ error -> Observable<Out> in
29+
guard let unretainedError = error as? UnretainedError,
30+
unretainedError == .failedRetaining else {
31+
return .error(error)
32+
}
33+
34+
return .empty()
35+
}
36+
}
37+
38+
/**
39+
Provides an unretained, safe to use (i.e. not implicitly unwrapped), reference to an object along with the events emitted by the sequence.
40+
41+
In the case the provided object cannot be retained successfully, the seqeunce will complete.
42+
43+
- parameter obj: The object to provide an unretained reference on.
44+
- returns: An observable sequence of tuples that contains both an unretained reference on `obj` and the values of the original sequence.
45+
*/
46+
public func withUnretained<Object: AnyObject>(_ obj: Object) -> Observable<(Object, Element)> {
47+
return withUnretained(obj) { ($0, $1) }
48+
}
49+
}
50+
51+
private enum UnretainedError: Swift.Error {
52+
case failedRetaining
53+
}

Tests/RxCocoaTests/KVOObservableTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ final class KVOObservableTests : RxTest {
2222
var parentWithChild: ParentWithChild!
2323
var hasStrongProperty: HasStrongProperty!
2424
var hasWeakProperty: HasWeakProperty!
25-
var testClass: TestClass!
25+
fileprivate var testClass: TestClass!
2626
}
2727

28-
final class TestClass : NSObject {
28+
private final class TestClass : NSObject {
2929
@objc dynamic var pr: String? = "0"
3030
}
3131

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//
2+
// Observable+WithUnretainedTests.swift
3+
// RxSwift
4+
//
5+
// Created by Vincent Pradeilles on 01/01/2021.
6+
// Copyright © 2021 Krunoslav Zaher. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import RxSwift
11+
import RxTest
12+
13+
class WithUnretainedTests: XCTestCase {
14+
fileprivate var testClass: TestClass!
15+
var values: TestableObservable<Int>!
16+
var tupleValues: TestableObservable<(Int, String)>!
17+
let scheduler = TestScheduler(initialClock: 0)
18+
19+
override func setUp() {
20+
super.setUp()
21+
22+
testClass = TestClass()
23+
values = scheduler.createColdObservable([
24+
.next(210, 1),
25+
.next(215, 2),
26+
.next(220, 3),
27+
.next(225, 5),
28+
.next(230, 8),
29+
.completed(250)
30+
])
31+
32+
tupleValues = scheduler.createColdObservable([
33+
.next(210, (1, "a")),
34+
.next(215, (2, "b")),
35+
.next(220, (3, "c")),
36+
.next(225, (5, "d")),
37+
.next(230, (8, "e")),
38+
.completed(250)
39+
])
40+
}
41+
42+
func testObjectAttached() {
43+
let testClassId = testClass.id
44+
45+
let correctValues: [Recorded<Event<String>>] = [
46+
.next(410, "\(testClassId), 1"),
47+
.next(415, "\(testClassId), 2"),
48+
.next(420, "\(testClassId), 3"),
49+
.next(425, "\(testClassId), 5"),
50+
.next(430, "\(testClassId), 8"),
51+
.completed(450)
52+
]
53+
54+
let res = scheduler.start {
55+
self.values
56+
.withUnretained(self.testClass)
57+
.map { "\($0.id), \($1)" }
58+
}
59+
60+
XCTAssertEqual(res.events, correctValues)
61+
}
62+
63+
func testObjectDeallocates() {
64+
_ = self.values
65+
.withUnretained(self.testClass)
66+
.subscribe()
67+
68+
// Confirm the object can be deallocated
69+
XCTAssertTrue(testClass != nil)
70+
testClass = nil
71+
XCTAssertTrue(testClass == nil)
72+
}
73+
74+
func testObjectDeallocatesSequenceCompletes() {
75+
let testClassId = testClass.id
76+
77+
let correctValues: [Recorded<Event<String>>] = [
78+
.next(410, "\(testClassId), 1"),
79+
.next(415, "\(testClassId), 2"),
80+
.next(420, "\(testClassId), 3"),
81+
.completed(425)
82+
]
83+
84+
let res = scheduler.start {
85+
self.values
86+
.withUnretained(self.testClass)
87+
.do(onNext: { _, value in
88+
// Release the object in the middle of the sequence
89+
// to confirm it properly terminates the sequence
90+
if value == 3 {
91+
self.testClass = nil
92+
}
93+
})
94+
.map { "\($0.id), \($1)" }
95+
}
96+
97+
XCTAssertEqual(res.events, correctValues)
98+
}
99+
100+
func testResultsSelector() {
101+
let testClassId = testClass.id
102+
103+
let correctValues: [Recorded<Event<String>>] = [
104+
.next(410, "\(testClassId), 1, a"),
105+
.next(415, "\(testClassId), 2, b"),
106+
.next(420, "\(testClassId), 3, c"),
107+
.next(425, "\(testClassId), 5, d"),
108+
.next(430, "\(testClassId), 8, e"),
109+
.completed(450)
110+
]
111+
112+
let res = scheduler.start {
113+
self.tupleValues
114+
.withUnretained(self.testClass) { ($0, $1.0, $1.1) }
115+
.map { "\($0.id), \($1), \($2)" }
116+
}
117+
118+
XCTAssertEqual(res.events, correctValues)
119+
}
120+
}
121+
122+
private class TestClass {
123+
let id: String = UUID().uuidString
124+
}

0 commit comments

Comments
 (0)