Skip to content

Commit 948e685

Browse files
authored
Merge pull request #92 from Econa77/feature/replace-subject
Replace PublishSubject with InputSubject
2 parents 4b9c476 + 7fd2ce5 commit 948e685

File tree

4 files changed

+219
-1
lines changed

4 files changed

+219
-1
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/Action.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public final class Action<Input, Element> {
2727
/// This subject also includes inputs as aguments of execute().
2828
/// All inputs are always appear in this subject even if the action is not enabled.
2929
/// Thus, inputs count equals elements count + errors count.
30-
public let inputs = PublishSubject<Input>()
30+
public let inputs = InputSubject<Input>()
3131

3232
/// Errors aggrevated from invocations of execute().
3333
/// Delivered on whatever scheduler they were sent from.

Sources/Action/InputSubject.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
_lock.lock()
27+
let isDisposed = _isDisposed
28+
_lock.unlock()
29+
return isDisposed
30+
}
31+
32+
/// Creates a subject.
33+
public init() {
34+
#if TRACE_RESOURCES
35+
_ = Resources.incrementTotal()
36+
#endif
37+
}
38+
39+
/// Notifies all subscribed observers abount only `.next` event.
40+
///
41+
/// - parameter event: Event to send to the observers.
42+
public func on(_ event: Event<Element>) {
43+
_lock.lock()
44+
switch event {
45+
case .next(_) where !_isDisposed:
46+
_observers.values.forEach { $0(event) }
47+
default:
48+
break
49+
}
50+
_lock.unlock()
51+
}
52+
53+
/**
54+
Subscribes an observer to the subject.
55+
56+
- parameter observer: Observer to subscribe to the subject.
57+
- returns: Disposable object that can be used to unsubscribe the observer from the subject.
58+
*/
59+
public func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == Element {
60+
_lock.lock()
61+
62+
if _isDisposed {
63+
observer.on(.error(RxError.disposed(object: self)))
64+
return Disposables.create()
65+
}
66+
67+
let key = _nextKey
68+
_nextKey += 1
69+
_observers[key] = observer.on
70+
_lock.unlock()
71+
72+
return Disposables.create { [weak self] in
73+
self?._lock.lock()
74+
self?._observers.removeValue(forKey: key)
75+
self?._lock.unlock()
76+
}
77+
}
78+
79+
/// Unsubscribe all observers and release resources.
80+
public func dispose() {
81+
_lock.lock()
82+
_isDisposed = true
83+
_observers.removeAll()
84+
_lock.unlock()
85+
}
86+
87+
#if TRACE_RESOURCES
88+
deinit {
89+
_ = Resources.decrementTotal()
90+
}
91+
#endif
92+
93+
}

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)