Skip to content

Commit 0dc512d

Browse files
committed
Add InputSubject
1 parent 0edd8bf commit 0dc512d

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

Action.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
7FB791E91D7F1BB200789D53 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F612AAB1D7F106900B93BC5 /* RxSwift.framework */; };
5959
7FB791EA1D7F1BB200789D53 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F612AAC1D7F106900B93BC5 /* RxCocoa.framework */; };
6060
7FB791EC1D7F1BB600789D53 /* Action.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE73AD201CDCD101006F8B98 /* Action.framework */; };
61+
CA2861C81ED6979400BB327A /* InputSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2861C71ED6979400BB327A /* InputSubject.swift */; };
62+
CA2861CA1ED6A41700BB327A /* InputSubjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2861C91ED6A41700BB327A /* InputSubjectTests.swift */; };
63+
CA2861CB1ED6B08300BB327A /* InputSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2861C71ED6979400BB327A /* InputSubject.swift */; };
64+
CA2861CC1ED6B08400BB327A /* InputSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2861C71ED6979400BB327A /* InputSubject.swift */; };
6165
/* End PBXBuildFile section */
6266

6367
/* Begin PBXContainerItemProxy section */
@@ -126,6 +130,8 @@
126130
7F612AD81D7F13B800B93BC5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
127131
7F612ADA1D7F13B800B93BC5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
128132
BE73AD201CDCD101006F8B98 /* Action.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Action.framework; sourceTree = BUILT_PRODUCTS_DIR; };
133+
CA2861C71ED6979400BB327A /* InputSubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSubject.swift; sourceTree = "<group>"; };
134+
CA2861C91ED6A41700BB327A /* InputSubjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSubjectTests.swift; sourceTree = "<group>"; };
129135
/* End PBXFileReference section */
130136

131137
/* Begin PBXFrameworksBuildPhase section */
@@ -205,6 +211,7 @@
205211
children = (
206212
7F0569E21DE28587007E1D0D /* Action.swift */,
207213
7F0569E01DE28587007E1D0D /* Action+Internal.swift */,
214+
CA2861C71ED6979400BB327A /* InputSubject.swift */,
208215
7F0569E41DE28587007E1D0D /* UIKitExtensions */,
209216
7F0569EF1DE28598007E1D0D /* Supporting Files */,
210217
);
@@ -258,6 +265,7 @@
258265
7F0569F11DE288EB007E1D0D /* AlertActionTests.swift */,
259266
7F0569F21DE288EB007E1D0D /* BarButtonTests.swift */,
260267
7F0569F31DE288EB007E1D0D /* ButtonTests.swift */,
268+
CA2861C91ED6A41700BB327A /* InputSubjectTests.swift */,
261269
7F0569F41DE288EB007E1D0D /* Info.plist */,
262270
);
263271
path = Tests;
@@ -563,6 +571,7 @@
563571
1FCDDA651EAC31EF006EB95B /* Action+Internal.swift in Sources */,
564572
1FCDDA661EAC31EF006EB95B /* AlertAction.swift in Sources */,
565573
1FCDDA671EAC31EF006EB95B /* UIBarButtonItem+Action.swift in Sources */,
574+
CA2861CB1ED6B08300BB327A /* InputSubject.swift in Sources */,
566575
1FCDDA681EAC31EF006EB95B /* UIButton+Rx.swift in Sources */,
567576
1FCDDA691EAC31EF006EB95B /* UIControl+Rx.swift in Sources */,
568577
);
@@ -572,6 +581,7 @@
572581
isa = PBXSourcesBuildPhase;
573582
buildActionMask = 2147483647;
574583
files = (
584+
CA2861CC1ED6B08400BB327A /* InputSubject.swift in Sources */,
575585
1FCDDA8A1EAC329E006EB95B /* Action.swift in Sources */,
576586
1FCDDA8B1EAC329E006EB95B /* Action+Internal.swift in Sources */,
577587
);
@@ -593,6 +603,7 @@
593603
7F0569F61DE288EB007E1D0D /* AlertActionTests.swift in Sources */,
594604
7F0569F51DE288EB007E1D0D /* ActionTests.swift in Sources */,
595605
7F0569F81DE288EB007E1D0D /* ButtonTests.swift in Sources */,
606+
CA2861CA1ED6A41700BB327A /* InputSubjectTests.swift in Sources */,
596607
);
597608
runOnlyForDeploymentPostprocessing = 0;
598609
};
@@ -613,6 +624,7 @@
613624
7F0569E81DE28587007E1D0D /* Action+Internal.swift in Sources */,
614625
7F0569ED1DE28587007E1D0D /* UIBarButtonItem+Action.swift in Sources */,
615626
7BD1C7551E1D5562000D82DA /* UIControl+Rx.swift in Sources */,
627+
CA2861C81ED6979400BB327A /* InputSubject.swift in Sources */,
616628
7F0569EA1DE28587007E1D0D /* Action.swift in Sources */,
617629
7F0569EE1DE28587007E1D0D /* UIButton+Rx.swift in Sources */,
618630
);

Sources/Action/InputSubject.swift

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
import RxSwift
3+
4+
/// A special subject for Action.inputs. It only emits `.next` event.
5+
public class InputSubject<Element>: ObservableType, Cancelable, SubjectType, ObserverType {
6+
7+
public typealias E = Element
8+
typealias Key = UInt
9+
10+
/// Indicates whether the subject has any observers
11+
public var hasObservers: Bool {
12+
_lock.lock()
13+
let count = _observers.count > 0
14+
_lock.unlock()
15+
return count
16+
}
17+
18+
// state
19+
private let _lock = NSRecursiveLock()
20+
private var _nextKey: Key = 0
21+
private var _observers: [Key: (Event<Element>) -> ()] = [:]
22+
private var _isDisposed = false
23+
24+
/// Indicates whether the subject has been isDisposed.
25+
public var isDisposed: Bool {
26+
return _isDisposed
27+
}
28+
29+
/// Creates a subject.
30+
public init() {
31+
#if TRACE_RESOURCES
32+
_ = Resources.incrementTotal()
33+
#endif
34+
}
35+
36+
/// Notifies all subscribed observers abount only `.next` event.
37+
///
38+
/// - parameter event: Event to send to the observers.
39+
public func on(_ event: Event<Element>) {
40+
_lock.lock()
41+
switch event {
42+
case .next(_) where !_isDisposed:
43+
_observers.values.forEach { $0(event) }
44+
default:
45+
break
46+
}
47+
_lock.unlock()
48+
}
49+
50+
/**
51+
Subscribes an observer to the subject.
52+
53+
- parameter observer: Observer to subscribe to the subject.
54+
- returns: Disposable object that can be used to unsubscribe the observer from the subject.
55+
*/
56+
public func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == Element {
57+
_lock.lock()
58+
59+
if _isDisposed {
60+
observer.on(.error(RxError.disposed(object: self)))
61+
return Disposables.create()
62+
}
63+
64+
let key = _nextKey
65+
_nextKey += 1
66+
_observers[key] = observer.on
67+
_lock.unlock()
68+
69+
return Disposables.create { [weak self] in
70+
self?._lock.lock()
71+
self?._observers.removeValue(forKey: key)
72+
self?._lock.unlock()
73+
}
74+
}
75+
76+
/// Unsubscribe all observers and release resources.
77+
public func dispose() {
78+
_lock.lock()
79+
_isDisposed = true
80+
_observers.removeAll()
81+
_lock.unlock()
82+
}
83+
84+
#if TRACE_RESOURCES
85+
deinit {
86+
_ = Resources.decrementTotal()
87+
}
88+
#endif
89+
90+
}

Tests/InputSubjectTests.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Quick
2+
import Nimble
3+
import RxSwift
4+
import RxTest
5+
import Action
6+
7+
class InputSubjectTests: QuickSpec {
8+
override func spec() {
9+
var scheduler: TestScheduler!
10+
var disposeBag: DisposeBag!
11+
12+
beforeEach {
13+
scheduler = TestScheduler(initialClock: 0)
14+
disposeBag = DisposeBag()
15+
}
16+
17+
describe("Disposable observable") {
18+
it("observables can be dispose") {
19+
let subject = InputSubject<Int>()
20+
let disposable1 = subject.subscribe()
21+
let disposable2 = subject.subscribe()
22+
expect(subject.hasObservers).to(beTrue())
23+
disposable2.dispose()
24+
expect(subject.hasObservers).to(beTrue())
25+
disposable1.dispose()
26+
expect(subject.hasObservers).to(beFalse())
27+
}
28+
29+
it("dispose all observables") {
30+
let subject = InputSubject<Int>()
31+
_ = subject.subscribe()
32+
_ = subject.subscribe()
33+
expect(subject.hasObservers).to(beTrue())
34+
subject.dispose()
35+
expect(subject.hasObservers).to(beFalse())
36+
expect(subject.isDisposed).to(beTrue())
37+
}
38+
}
39+
40+
describe("emit events") {
41+
it("emit .next events") {
42+
let subject = InputSubject<Int>()
43+
let observer = scheduler.createObserver(Int.self)
44+
subject.asObservable()
45+
.bind(to: observer)
46+
.disposed(by: disposeBag)
47+
scheduler.scheduleAt(10) { subject.onNext(1) }
48+
scheduler.scheduleAt(20) { subject.onNext(2) }
49+
scheduler.scheduleAt(30) { subject.onNext(3) }
50+
scheduler.start()
51+
52+
XCTAssertEqual(observer.events, [
53+
next(10, 1),
54+
next(20, 2),
55+
next(30, 3)
56+
])
57+
}
58+
59+
it("ignore .error events") {
60+
let subject = InputSubject<Int>()
61+
let observer = scheduler.createObserver(Int.self)
62+
subject.asObservable()
63+
.bind(to: observer)
64+
.disposed(by: disposeBag)
65+
scheduler.scheduleAt(10) { subject.onNext(1) }
66+
scheduler.scheduleAt(20) { subject.onError(TestError) }
67+
scheduler.scheduleAt(30) { subject.onNext(3) }
68+
scheduler.start()
69+
70+
XCTAssertEqual(observer.events, [
71+
next(10, 1),
72+
next(30, 3)
73+
])
74+
}
75+
76+
it("ignore .completed events") {
77+
let subject = InputSubject<Int>()
78+
let observer = scheduler.createObserver(Int.self)
79+
subject.asObservable()
80+
.bind(to: observer)
81+
.disposed(by: disposeBag)
82+
scheduler.scheduleAt(10) { subject.onNext(1) }
83+
scheduler.scheduleAt(20) { subject.onCompleted() }
84+
scheduler.scheduleAt(30) { subject.onNext(3) }
85+
scheduler.start()
86+
87+
XCTAssertEqual(observer.events, [
88+
next(10, 1),
89+
next(30, 3)
90+
])
91+
}
92+
93+
it("event does not fire on disposed subject") {
94+
let subject = InputSubject<Int>()
95+
let observer = scheduler.createObserver(Int.self)
96+
subject.asObservable()
97+
.bind(to: observer)
98+
.disposed(by: disposeBag)
99+
scheduler.scheduleAt(10) { subject.onNext(1) }
100+
scheduler.scheduleAt(20) { subject.onNext(2) }
101+
scheduler.scheduleAt(30) { subject.dispose() }
102+
scheduler.scheduleAt(40) { subject.onNext(4) }
103+
scheduler.start()
104+
105+
XCTAssertEqual(observer.events, [
106+
next(10, 1),
107+
next(20, 2),
108+
])
109+
}
110+
}
111+
112+
}
113+
}

0 commit comments

Comments
 (0)