Skip to content

Commit ea39494

Browse files
authored
Merge pull request #37 from ishkawa/feature/inputs-subject
Add inputs subject to bind action trigger with arbitrary observables
2 parents cad7a85 + cc7845b commit ea39494

File tree

8 files changed

+278
-163
lines changed

8 files changed

+278
-163
lines changed

Action.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Pod::Spec.new do |s|
2121
s.source_files = "*.{swift}"
2222

2323
s.frameworks = "Foundation"
24-
s.dependency "RxSwift", '~> 2.0'
25-
s.dependency "RxCocoa", '~> 2.0'
24+
s.dependency "RxSwift", '~> 2.6'
25+
s.dependency "RxCocoa", '~> 2.6'
2626

2727
s.watchos.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift", "AlertAction.swift"
2828
s.osx.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift", "AlertAction.swift"

Action.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ public final class Action<Input, Element> {
1818
public let _enabledIf: Observable<Bool>
1919
public let workFactory: WorkFactory
2020

21+
/// Inputs that triggers execution of action.
22+
/// This subject also includes inputs as aguments of execute().
23+
/// All inputs are always appear in this subject even if the action is not enabled.
24+
/// Thus, inputs count equals elements count + errors count.
25+
public let inputs = PublishSubject<Input>()
26+
private let _completed = PublishSubject<Void>()
27+
2128
/// Errors aggrevated from invocations of execute().
2229
/// Delivered on whatever scheduler they were sent from.
2330
public var errors: Observable<ActionError> {
@@ -67,6 +74,12 @@ public final class Action<Input, Element> {
6774
Observable.combineLatest(self._enabledIf, self.executing) { (enabled, executing) -> Bool in
6875
return enabled && !executing
6976
}.bindTo(_enabled).addDisposableTo(disposeBag)
77+
78+
self.inputs
79+
.subscribeNext { [weak self] input in
80+
self?._execute(input)
81+
}
82+
.addDisposableTo(disposeBag)
7083
}
7184
}
7285

@@ -83,6 +96,29 @@ public extension Action {
8396
public extension Action {
8497

8598
public func execute(input: Input) -> Observable<Element> {
99+
let buffer = ReplaySubject<Element>.createUnbounded()
100+
let error = errors
101+
.flatMap { error -> Observable<Element> in
102+
if case .UnderlyingError(let error) = error {
103+
throw error
104+
} else {
105+
return Observable.empty()
106+
}
107+
}
108+
109+
Observable
110+
.of(elements, error)
111+
.merge()
112+
.takeUntil(_completed)
113+
.bindTo(buffer)
114+
.addDisposableTo(disposeBag)
115+
116+
inputs.onNext(input)
117+
118+
return buffer.asObservable()
119+
}
120+
121+
private func _execute(input: Input) -> Observable<Element> {
86122

87123
// Buffer from the work to a replay subject.
88124
let buffer = ReplaySubject<Element>.createUnbounded()
@@ -119,7 +155,9 @@ public extension Action {
119155
onError: {[weak self] error in
120156
self?._errors.onNext(ActionError.UnderlyingError(error))
121157
},
122-
onCompleted: nil,
158+
onCompleted: {[weak self] in
159+
self?._completed.onNext()
160+
},
123161
onDisposed: {[weak self] in
124162
self?.doLocked { self?._executing.value = false }
125163
})

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changelog
44
Current master
55
--------------
66

7+
- Added inputs subject to trigger actins by observables. See [#37](https://github.com/RxSwiftCommunity/Action/pull/37).
78
- Fixes a problem with observable deallocation related to `rx_action` button property. See [#33](https://github.com/RxSwiftCommunity/Action/pull/33).
89
- Improved Carthage compatibility. See [#34](https://github.com/RxSwiftCommunity/Action/pull/34).
910

Demo/Demo.xcodeproj/project.pbxproj

Lines changed: 52 additions & 52 deletions
Large diffs are not rendered by default.

Demo/DemoTests/ActionTests.swift

Lines changed: 120 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,35 @@ class ActionTests: QuickSpec {
160160
testItems = context()["items"] as! [String]
161161
}
162162

163-
it("sends next elements on elements observable") {
163+
it("sends next elements on inputs and elements observables when execute() is called") {
164164
let subject = testSubject(testItems)
165-
var receivedElements: [String] = []
166165

166+
var receivedInputs: [Void] = []
167+
subject.inputs.subscribeNext { (input) -> Void in
168+
receivedInputs += [input]
169+
}.addDisposableTo(disposeBag)
170+
171+
var receivedElements: [String] = []
167172
subject.elements.subscribeNext { (element) -> Void in
168173
receivedElements += [element]
169174
}.addDisposableTo(disposeBag)
170175

171176
subject.execute()
172177

178+
expect(receivedInputs.count) == 1
179+
expect(receivedElements) == testItems
180+
}
181+
182+
it("sends next elements on elements observable when inputs receives next elements") {
183+
let subject = testSubject(testItems)
184+
var receivedElements: [String] = []
185+
186+
subject.elements.subscribeNext { (element) -> Void in
187+
receivedElements += [element]
188+
}.addDisposableTo(disposeBag)
189+
190+
subject.inputs.onNext()
191+
173192
expect(receivedElements) == testItems
174193
}
175194

@@ -235,102 +254,122 @@ class ActionTests: QuickSpec {
235254
expect(invocations) == 1
236255
}
237256

238-
describe("enabled") {
239-
it("sends true on the enabled observable") {
240-
let subject = emptySubject()
257+
sharedExamples("triggering execution") { (context: QCKDSLSharedExampleContext!) -> Void in
258+
var executer: TestActionExecuter!
241259

242-
let enabled = try! subject.enabled.toBlocking().first()
243-
expect(enabled) == true
260+
beforeEach {
261+
executer = context()["executer"] as! TestActionExecuter
244262
}
245263

246-
it("is externally disabled while executing") {
247-
var observer: AnyObserver<Void>!
248-
let subject = Action<Void, Void>(workFactory: { _ in
249-
return Observable.create { (obsv) -> Disposable in
250-
observer = obsv
251-
return NopDisposable.instance
252-
}
253-
})
264+
describe("enabled") {
265+
it("sends true on the enabled observable") {
266+
let subject = emptySubject()
254267

255-
subject.execute()
268+
let enabled = try! subject.enabled.toBlocking().first()
269+
expect(enabled) == true
270+
}
256271

257-
var enabled = try! subject.enabled.toBlocking().first()
258-
expect(enabled) == false
272+
it("is externally disabled while executing") {
273+
var observer: AnyObserver<Void>!
274+
let subject = Action<Void, Void>(workFactory: { _ in
275+
return Observable.create { (obsv) -> Disposable in
276+
observer = obsv
277+
return NopDisposable.instance
278+
}
279+
})
259280

260-
observer.onCompleted()
281+
executer.execute(subject)
261282

262-
enabled = try! subject.enabled.toBlocking().first()
263-
expect(enabled) == true
264-
}
265-
}
283+
var enabled = try! subject.enabled.toBlocking().first()
284+
expect(enabled) == false
266285

267-
describe("disabled") {
268-
it("sends false on enabled observable") {
269-
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
270-
return .empty()
271-
})
286+
observer.onCompleted()
272287

273-
let enabled = try! subject.enabled.toBlocking().first()
274-
expect(enabled) == false
288+
enabled = try! subject.enabled.toBlocking().first()
289+
expect(enabled) == true
290+
}
275291
}
276-
277-
it("errors observable sends error as next event when execute() is called") {
278-
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
279-
return .empty()
280-
})
281292

282-
var receivedError: ActionError?
293+
describe("disabled") {
294+
it("sends false on enabled observable") {
295+
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
296+
return .empty()
297+
})
283298

284-
subject
285-
.errors
286-
.subscribeNext { error in
287-
receivedError = error
288-
}
289-
.addDisposableTo(disposeBag)
299+
let enabled = try! subject.enabled.toBlocking().first()
300+
expect(enabled) == false
301+
}
302+
303+
it("errors observable sends error as next event when execute() is called") {
304+
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
305+
return .empty()
306+
})
290307

291-
subject.execute()
308+
var receivedError: ActionError?
292309

293-
expect(receivedError).toNot( beNil() )
294-
}
310+
subject
311+
.errors
312+
.subscribeNext { error in
313+
receivedError = error
314+
}
315+
.addDisposableTo(disposeBag)
295316

296-
it("errors observable sends correct error types when execute() is called") {
297-
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
298-
return .empty()
299-
})
317+
executer.execute(subject)
300318

301-
var receivedError: ActionError?
319+
expect(receivedError).toNot( beNil() )
320+
}
302321

303-
subject
304-
.errors
305-
.subscribeNext { error in
306-
receivedError = error
322+
it("errors observable sends correct error types when execute() is called") {
323+
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
324+
return .empty()
325+
})
326+
327+
var receivedError: ActionError?
328+
329+
subject
330+
.errors
331+
.subscribeNext { error in
332+
receivedError = error
333+
}
334+
.addDisposableTo(disposeBag)
335+
336+
executer.execute(subject)
337+
338+
guard let error = receivedError else {
339+
fail("Error is nil"); return
307340
}
308-
.addDisposableTo(disposeBag)
309-
310-
subject.execute()
311341

312-
guard let error = receivedError else {
313-
fail("Error is nil"); return
342+
if case ActionError.NotEnabled = error {
343+
// Nop
344+
} else {
345+
fail("Incorrect error returned.")
346+
}
314347
}
315348

316-
if case ActionError.NotEnabled = error {
317-
// Nop
318-
} else {
319-
fail("Incorrect error returned.")
320-
}
321-
}
349+
it("doesn't invoke the work factory") {
350+
var invoked = false
322351

323-
it("doesn't invoke the work factory") {
324-
var invoked = false
352+
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
353+
invoked = true
354+
return .empty()
355+
})
325356

326-
let subject = Action<Void, Void>(enabledIf: .just(false), workFactory: { _ in
327-
invoked = true
328-
return .empty()
329-
})
357+
executer.execute(subject)
330358

331-
subject.execute()
359+
expect(invoked) == false
360+
}
361+
}
362+
}
332363

333-
expect(invoked) == false
364+
describe("execute via execute()") {
365+
itBehavesLike("triggering execution") { () -> (NSDictionary) in
366+
return ["executer": TestActionExecuter { subject in subject.execute() }]
367+
}
368+
}
369+
370+
describe("execute via inputs subject") {
371+
itBehavesLike("triggering execution") { () -> (NSDictionary) in
372+
return ["executer": TestActionExecuter { subject in subject.inputs.onNext() }]
334373
}
335374
}
336375
}
@@ -378,3 +417,11 @@ func testSubject(elements: [String]) -> Action<Void, String> {
378417
return elements.toObservable()
379418
})
380419
}
420+
421+
class TestActionExecuter {
422+
let execute: Action<Void, Void> -> Void
423+
424+
init(execute: Action<Void, Void> -> Void) {
425+
self.execute = execute
426+
}
427+
}

Demo/Podfile

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
# Uncomment this line to define a global platform for your project
2-
# platform :ios, '8.0'
3-
# Uncomment this line if you're using Swift
4-
5-
6-
use_frameworks!
7-
8-
inhibit_all_warnings!
2+
# platform :ios, '9.0'
93

104
target 'Demo' do
5+
# Comment this line if you're not using Swift and don't want to use dynamic frameworks
6+
use_frameworks!
117

12-
# Action library
13-
pod 'Action', :path => '../'
8+
# Action library
9+
pod 'Action', :path => '../'
1410

15-
end
16-
17-
target 'DemoTests' do
11+
target 'DemoTests' do
12+
inherit! :search_paths
1813

19-
# Dependencies
20-
pod 'RxSwift', '~> 2.1'
21-
pod 'RxCocoa', '~> 2.1'
22-
pod 'RxBlocking', '~> 2.1'
14+
# Dependencies
15+
pod 'Action', :path => '../'
16+
pod 'RxSwift', '~> 2.6'
17+
pod 'RxCocoa', '~> 2.6'
18+
pod 'RxBlocking', '~> 2.6'
2319

24-
# Testing libraries
25-
pod 'Quick'
26-
pod 'Nimble'
20+
# Testing libraries
21+
pod 'Quick'
22+
pod 'Nimble'
23+
end
2724

2825
end
29-

0 commit comments

Comments
 (0)