Skip to content

Commit

Permalink
Merge pull request #126 from mosamer/master
Browse files Browse the repository at this point in the history
Improve naming of public API properties
  • Loading branch information
mosamer committed Apr 22, 2019
2 parents 2a757a8 + ba20f14 commit e5181a9
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 39 deletions.
12 changes: 12 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ Current master
- Added full support for RxSwift 4.2.0
- UIRefreshControl support: binding to an action (or CocoaAction) starts the action itself and updates the control's refreshing status

4.0 (In development)
--------------------

- Cleanup public API [#120](https://github.com/RxSwiftCommunity/Action/issues/120)
- Remove unneccessary properties
- `workFactory`
- `_enabledIf`
- Rename properties to match Swift API design [guidelines](https://swift.org/documentation/api-design-guidelines/#strive-for-fluent-usage)
- `enabled` ~> `isEnabled`
- `executing` ~> `isExecuting`
- Deprecate renamed properties

3.5.0
-----

Expand Down
6 changes: 3 additions & 3 deletions Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ class ViewController: UIViewController {
// Demo: observe the output of both actions, spin an activity indicator
// while performing the work
Observable.combineLatest(
button.rx.action!.executing,
self.navigationItem.rightBarButtonItem!.rx.action!.executing) { a,b in
button.rx.action!.isExecuting,
self.navigationItem.rightBarButtonItem!.rx.action!.isExecuting) { a,b in
// we combine two boolean observable and output one boolean
return a || b
}
Expand All @@ -92,7 +92,7 @@ class ViewController: UIViewController {
}
self.navigationItem.leftBarButtonItem?.rx.bind(to: sharedAction, input: .barButton)

sharedAction.executing.debounce(0, scheduler: MainScheduler.instance).subscribe(onNext: { [weak self] executing in
sharedAction.isExecuting.debounce(0, scheduler: MainScheduler.instance).subscribe(onNext: { [weak self] executing in
if (executing) {
self?.activityIndicator.startAnimating()
}
Expand Down
14 changes: 7 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Usage

You have to pass a `workFactory` that takes input and returns an `Observable`. This represents some work that needs to be accomplished. Whenever you call `execute()`, you pass in input that's fed to the work factory. The `Action` will subscribe to the observable and emit the `Next` events on its `elements` property. If the observable errors, the error is sent as a `Next` even on the `errors` property. Neat.

Actions can only execute one thing at a time. If you try to execute an action that's currently executing, you'll get an error. The `executing` property sends `true` and `false` values as `Next` events.
Actions can only execute one thing at a time. If you try to execute an action that's currently executing, you'll get an error. The `isExecuting` property sends `true` and `false` values as `Next` events.

```swift
action: Action<String, Bool> = Action(workFactory: { input in
Expand All @@ -46,19 +46,19 @@ action: Action<String, Bool> = Action(enabledIf: validEmailAddress, workFactory:

Now `execute()` only does the work if the email address is valid. Super cool!

Note that `enabledIf` isn't the same as the `enabled` property. You pass in `enabledIf` and the action uses that, and its current executing state, to determine if it's currently enabled.
Note that `enabledIf` isn't the same as the `isEnabled` property. You pass in `enabledIf` and the action uses that, and its current executing state, to determine if it's currently enabled.

What's _really_ cool is the `UIButton` extension. It accepts a `CocoaAction`, which is just `Action<Void, Void>`.

```swift
button.rx.action = action
```

Now when the button is pressed, the action is executed. The button's `enabled` state is bound to the action's `enabled` property. That means you can feed your form-validation logic into the action as a signal, and your button's enabled state is handled for you. Also, the user can't press the button again before the action is done executing, since it only handles one thing at a time. Cool. Check out [this code example of CocoaAction _in_ action](https://github.com/artsy/eidolon/blob/cb31168fa29dcc7815fd4a2e30e7c000bd1820ce/Kiosk/Bid%20Fulfillment/GenericFormValidationViewModel.swift).
Now when the button is pressed, the action is executed. The button's `isEnabled` state is bound to the action's `isEnabled` property. That means you can feed your form-validation logic into the action as a signal, and your button's enabled state is handled for you. Also, the user can't press the button again before the action is done executing, since it only handles one thing at a time. Cool. Check out [this code example of CocoaAction _in_ action](https://github.com/artsy/eidolon/blob/cb31168fa29dcc7815fd4a2e30e7c000bd1820ce/Kiosk/Bid%20Fulfillment/GenericFormValidationViewModel.swift).

If you'd like to use `Action` to do a complex operation such as file download with download progress report (to update progress bar in the UI for example) you'd use `Action<Void, Int>` instead of `CocoaAction`. Out of the box `CocoaAction` can't emit progress values, your own `Action<Void, Int>` will do that. For details refer to [this article](http://www.sm-cloud.com/rxswift-action/).

If your scenario involves many buttons that needs to trigger the same `Action` providing different input, you can use `bindTo` on each `UIButton` with a closure that returns correct input.
If your scenario involves many buttons that needs to trigger the same `Action` providing different input, you can use `bind(to:)` on each `UIButton` with a closure that returns correct input.

```swift
let button1 = UIButton()
Expand All @@ -68,13 +68,13 @@ let action = Action<String, String> { input in
print(input)
return .just(input)
}
button1.rx.bindTo(action) { _ in return "Hello"}
button2.rx.bindTo(action) { _ in return "Goodbye"}
button1.rx.bind(to: action) {_ in return "Hello"}
button2.rx.bind(to: action) {_ in return "Goodbye"}
```

`button1` and `button2` are sharing the same `Action`, but they are feeding it with different input (`Hello` and `Goodbye` that will be printed for corresponding tap).

A more complex use case can be a single action related to a `UIViewController` that manages your navigation, error handling and loading state. With this approach, you can have as many `UIButton`s (or `UIBarButtonItem`s) as you want and subscribe to `executing`, `errors` and `elements` once and in a single common place.
A more complex use case can be a single action related to a `UIViewController` that manages your navigation, error handling and loading state. With this approach, you can have as many `UIButton`s (or `UIBarButtonItem`s) as you want and subscribe to `isExecuting`, `errors` and `elements` once and in a single common place.

There's also a really cool extension on `UIAlertAction`, used by [`UIAlertController`](http://ashfurrow.com/blog/uialertviewcontroller-example/). One catch: because of the limitations of that class, you can't instantiate it with the normal initializer. Instead, call this class method:

Expand Down
31 changes: 19 additions & 12 deletions Sources/Action/Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ When this excuted via execute() or inputs subject, it passes its parameter to th
public final class Action<Input, Element> {
public typealias WorkFactory = (Input) -> Observable<Element>

public let _enabledIf: Observable<Bool>
public let workFactory: WorkFactory

/// Inputs that triggers execution of action.
/// This subject also includes inputs as aguments of execute().
/// All inputs are always appear in this subject even if the action is not enabled.
Expand All @@ -40,7 +37,7 @@ public final class Action<Input, Element> {
public let elements: Observable<Element>

/// Whether or not we're currently executing.
public let executing: Observable<Bool>
public let isExecuting: Observable<Bool>

/// Observables returned by the workFactory.
/// Useful for sending results back from work being completed
Expand All @@ -50,7 +47,7 @@ public final class Action<Input, Element> {
/// Whether or not we're enabled. Note that this is a *computed* sequence
/// property based on enabledIf initializer and if we're currently executing.
/// Always observed on MainScheduler.
public let enabled: Observable<Bool>
public let isEnabled: Observable<Bool>

private let disposeBag = DisposeBag()

Expand All @@ -67,17 +64,14 @@ public final class Action<Input, Element> {
enabledIf: Observable<Bool> = Observable.just(true),
workFactory: @escaping WorkFactory) {

self._enabledIf = enabledIf
self.workFactory = workFactory

let enabledSubject = BehaviorSubject<Bool>(value: false)
enabled = enabledSubject.asObservable()
isEnabled = enabledSubject.asObservable()

let errorsSubject = PublishSubject<ActionError>()
errors = errorsSubject.asObservable()

executionObservables = inputs
.withLatestFrom(enabled) { input, enabled in (input, enabled) }
.withLatestFrom(isEnabled) { input, enabled in (input, enabled) }
.flatMap { input, enabled -> Observable<Observable<Element>> in
if enabled {
return Observable.of(workFactory(input)
Expand All @@ -93,7 +87,7 @@ public final class Action<Input, Element> {
elements = executionObservables
.flatMap { $0.catchError { _ in Observable.empty() } }

executing = executionObservables.flatMap {
isExecuting = executionObservables.flatMap {
execution -> Observable<Bool> in
let execution = execution
.flatMap { _ in Observable<Bool>.empty() }
Expand All @@ -107,7 +101,7 @@ public final class Action<Input, Element> {
.share(replay: 1, scope: .forever)

Observable
.combineLatest(executing, enabledIf) { !$0 && $1 }
.combineLatest(isExecuting, enabledIf) { !$0 && $1 }
.bind(to: enabledSubject)
.disposed(by: disposeBag)
}
Expand Down Expand Up @@ -135,3 +129,16 @@ public final class Action<Input, Element> {
return subject.asObservable()
}
}

// MARK: Deprecated
extension Action {
@available(*, deprecated, renamed: "isExecuting")
public var executing: Observable<Bool> {
return isExecuting
}

@available(*, deprecated, renamed: "isEnabled")
public var enabled: Observable<Bool> {
return isEnabled
}
}
2 changes: 1 addition & 1 deletion Sources/Action/CommonUI/Button+Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public extension Reactive where Base: Button {
// Set up new bindings, if applicable.
if let action = newValue {
action
.enabled
.isEnabled
.bind(to: self.isEnabled)
.disposed(by: self.base.actionDisposeBag)

Expand Down
2 changes: 1 addition & 1 deletion Sources/Action/CommonUI/Control+Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public extension Reactive where Base: Control {

// Bind the enabled state of the control to the enabled state of the action
action
.enabled
.isEnabled
.bind(to: self.isEnabled)
.disposed(by: self.base.actionDisposeBag)
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Action/UIKitExtensions/UIAlertAction+Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ public extension Reactive where Base: UIAlertAction {
// Set up new bindings, if applicable.
if let action = newValue {
action
.enabled
.bind(to: self.enabled)
.isEnabled
.bind(to: self.isEnabled)
.disposed(by: self.base.actionDisposeBag)
}
}
}

var enabled: AnyObserver<Bool> {
public var isEnabled: AnyObserver<Bool> {
return AnyObserver { [weak base] event in
MainScheduler.ensureExecutingOnScheduler()

Expand Down
4 changes: 2 additions & 2 deletions Sources/Action/UIKitExtensions/UIBarButtonItem+Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public extension Reactive where Base: UIBarButtonItem {
// Set up new bindings, if applicable.
if let action = newValue {
action
.enabled
.isEnabled
.bind(to: self.isEnabled)
.disposed(by: self.base.actionDisposeBag)

Expand All @@ -47,7 +47,7 @@ public extension Reactive where Base: UIBarButtonItem {
.disposed(by: self.base.actionDisposeBag)

action
.enabled
.isEnabled
.bind(to: self.isEnabled)
.disposed(by: self.base.actionDisposeBag)
}
Expand Down
19 changes: 9 additions & 10 deletions Tests/ActionTests/ActionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ class ActionTests: QuickSpec {
.bind(to: errors)
.disposed(by: disposeBag)

action.enabled
action.isEnabled
.bind(to: enabled)
.disposed(by: disposeBag)

action.executing
action.isExecuting
.bind(to: executing)
.disposed(by: disposeBag)

Expand All @@ -91,14 +91,13 @@ class ActionTests: QuickSpec {
.bind(to: underlyingError)
.disposed(by: disposeBag)

// Dummy subscription for multiple subcription tests
action.inputs.subscribe().disposed(by: disposeBag)
action.elements.subscribe().disposed(by: disposeBag)
action.errors.subscribe().disposed(by: disposeBag)
action.enabled.subscribe().disposed(by: disposeBag)
action.executing.subscribe().disposed(by: disposeBag)
action.executionObservables.subscribe().disposed(by: disposeBag)
action.underlyingError.subscribe().disposed(by: disposeBag)
// Dummy subscription for multiple subcription tests
action.inputs.subscribe().disposed(by: disposeBag)
action.elements.subscribe().disposed(by: disposeBag)
action.errors.subscribe().disposed(by: disposeBag)
action.isEnabled.subscribe().disposed(by: disposeBag)
action.isExecuting.subscribe().disposed(by: disposeBag)
action.executionObservables.subscribe().disposed(by: disposeBag)
}

describe("single element action") {
Expand Down

0 comments on commit e5181a9

Please sign in to comment.