Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Action.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ Pod::Spec.new do |s|
s.tvos.deployment_target = '9.0'

s.source = { :git => "https://github.com/ashfurrow/Action.git", :tag => s.version }
s.source_files = "*.swift"
s.source_files = "*.{swift}"

s.frameworks = "Foundation"
s.dependency "RxSwift", '~> 2.0.0-beta'
s.dependency "RxCocoa", '~> 2.0.0-beta'

s.watchos.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift"
s.osx.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift"
s.tvos.exclude_files = "UIBarButtonItem+Action.swift"
s.watchos.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift", "AlertAction.swift"
s.osx.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift", "AlertAction.swift"
s.tvos.exclude_files = "UIBarButtonItem+Action.swift", "AlertAction.swift"
end
66 changes: 66 additions & 0 deletions AlertAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import UIKit
import RxSwift
import RxCocoa

public extension UIAlertAction {

public static func Action(title: String?, style: UIAlertActionStyle) -> UIAlertAction {
return UIAlertAction(title: title, style: style, handler: { action in
action.rx_action?.execute()
})
}

/// Binds enabled state of action to button, and subscribes to rx_tap to execute action.
/// These subscriptions are managed in a private, inaccessible dispose bag. To cancel
/// them, set the rx_action to nil or another action.
public var rx_action: CocoaAction? {
get {
var action: CocoaAction?
doLocked {
action = objc_getAssociatedObject(self, &AssociatedKeys.Action) as? Action
}
return action
}

set {
doLocked {
// Store new value.
objc_setAssociatedObject(self, &AssociatedKeys.Action, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

// This effectively disposes of any existing subscriptions.
self.resetActionDisposeBag()

// Set up new bindings, if applicable.
if let action = newValue {
action
.enabled
.bindTo(self.rx_enabled)
.addDisposableTo(self.actionDisposeBag)
}
}
}
}
}

extension UIAlertAction {
var rx_enabled: AnyObserver<Bool> {
return AnyObserver { [weak self] event in
MainScheduler.ensureExecutingOnScheduler()

switch event {
case .Next(let value):
self?.enabled = value
case .Error(let error):
let error = "Binding error to UI: \(error)"
#if DEBUG
rxFatalError(error)
#else
print(error)
#endif
break
case .Completed:
break
}
}
}
}
99 changes: 53 additions & 46 deletions Demo/Demo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Demo/Demo/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,29 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SZL-3G-VZh">
<rect key="frame" x="277" y="31" width="46" height="30"/>
<rect key="frame" x="264" y="31" width="73" height="30"/>
<animations/>
<state key="normal" title="Button"/>
<state key="normal" title="Show alert"/>
</button>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OrY-7C-i77">
<rect key="frame" x="290" y="258" width="20" height="20"/>
</activityIndicatorView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Doing some work" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ulN-2s-mAd">
<rect key="frame" x="234" y="229" width="133" height="21"/>
<rect key="frame" x="234" y="467" width="133" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OrY-7C-i77">
<rect key="frame" x="290" y="496" width="20" height="20"/>
</activityIndicatorView>
</subviews>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="OrY-7C-i77" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Dyl-A6-bud"/>
<constraint firstItem="OrY-7C-i77" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="KWX-Rs-PVL"/>
<constraint firstItem="SZL-3G-VZh" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="31" id="Np0-fa-c3f"/>
<constraint firstItem="ulN-2s-mAd" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="a5T-qt-gDR"/>
<constraint firstItem="OrY-7C-i77" firstAttribute="top" secondItem="ulN-2s-mAd" secondAttribute="bottom" constant="8" id="eqf-Tp-OJd"/>
<constraint firstItem="SZL-3G-VZh" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="hb0-j9-0rK"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="OrY-7C-i77" secondAttribute="bottom" constant="20" id="jhU-Jj-aCX"/>
</constraints>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
Expand Down
25 changes: 18 additions & 7 deletions Demo/Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,23 @@ class ViewController: UIViewController {
super.viewDidLoad()

// Demo: add an action to a button in the view
let action = CocoaAction { _ in
return create { observer -> Disposable in
// Do whatever work here.
print("Doing work for button at \(NSDate())")
observer.onCompleted()
return NopDisposable.instance
let action = CocoaAction {
print("Button was pressed, showing an alert and keeping the activity indicator spinning while alert is displayed")
return create {
[weak self] observer -> Disposable in

// Demo: show an alert and complete the view's button action once the alert's OK button is pressed
let alertController = UIAlertController(title: "Hello world", message: "This alert was triggered by a button action", preferredStyle: .Alert)
let ok = UIAlertAction.Action("OK", style: .Default)
ok.rx_action = CocoaAction {
print("Alert's OK button was pressed")
observer.onCompleted()
return empty()
}
alertController.addAction(ok)
self!.presentViewController(alertController, animated: true, completion: nil)

return NopDisposable.instance
}
}
button.rx_action = action
Expand All @@ -38,7 +49,7 @@ class ViewController: UIViewController {
return empty().delaySubscription(2, MainScheduler.sharedInstance)
}

// Demo: obseve the output of both actions, spin an activity indicator
// Demo: observe the output of both actions, spin an activity indicator
// while performing the work
combineLatest(
button.rx_action!.executing,
Expand Down
75 changes: 75 additions & 0 deletions Demo/DemoTests/AlertActionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Quick
import Nimble
import RxSwift
import RxBlocking
import Action

class AlertActionTests: QuickSpec {
override func spec() {
it("is nil by default") {
let subject = UIAlertAction.Action("Hi", style: .Default)
expect(subject.rx_action).to( beNil() )
}

it("respects setter") {
let subject = UIAlertAction.Action("Hi", style: .Default)

let action = emptyAction()

subject.rx_action = action

expect(subject.rx_action) === action
}

it("disables the button while executing") {
let subject = UIAlertAction.Action("Hi", style: .Default)

var observer: AnyObserver<Void>!
let action = CocoaAction(workFactory: { _ in
return create { (obsv) -> Disposable in
observer = obsv
return NopDisposable.instance
}
})

subject.rx_action = action

action.execute()
expect(subject.enabled).toEventually( beFalse() )

observer.onCompleted()
expect(subject.enabled).toEventually( beTrue() )
}

it("disables the button if the Action is disabled") {
let subject = UIAlertAction.Action("Hi", style: .Default)

subject.rx_action = emptyAction(just(false))

expect(subject.enabled) == false
}

it("disposes of old action subscriptions when re-set") {
let subject = UIAlertAction.Action("Hi", style: .Default)

var disposed = false
autoreleasepool {
let disposeBag = DisposeBag()

let action = emptyAction()
subject.rx_action = action

action
.elements
.subscribe(onNext: nil, onError: nil, onCompleted: nil, onDisposed: {
disposed = true
})
.addDisposableTo(disposeBag)
}

subject.rx_action = nil

expect(disposed) == true
}
}
}
4 changes: 2 additions & 2 deletions Demo/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- Action (0.1.0):
- Action (0.2.0):
- RxCocoa (~> 2.0.0-beta)
- RxSwift (~> 2.0.0-beta)
- Nimble (3.0.0)
Expand All @@ -23,7 +23,7 @@ EXTERNAL SOURCES:
:path: "../"

SPEC CHECKSUMS:
Action: dbcbc4c9255e54f801c14554a58d1ed6b5c1d47e
Action: 836da3ae9615435467a3d1cddd384f4ea6036ba2
Nimble: 4c353d43735b38b545cbb4cb91504588eb5de926
Quick: 563d0f6ec5f72e394645adb377708639b7dd38ab
RxBlocking: 331f8bdedf77198f8ff1a37f09c492f1f4f631e5
Expand Down
6 changes: 6 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ 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.

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:

```swift
let action = UIAlertAction.Action("Hi", style: .Default)
```

**NOTE**: Due to a temporary issue with RxSwift, there's a [slight issue](https://github.com/ashfurrow/Action/issues/3) that shouldn't affect you, but might. Who knows!

Installing
Expand Down