Skip to content

Commit e3ca774

Browse files
committed
Merge pull request #10 from ashfurrow/alertaction
Added UIAlertAction extension.
2 parents 73a9e82 + 12255d4 commit e3ca774

File tree

8 files changed

+232
-66
lines changed

8 files changed

+232
-66
lines changed

Action.podspec

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ Pod::Spec.new do |s|
1818
s.tvos.deployment_target = '9.0'
1919

2020
s.source = { :git => "https://github.com/ashfurrow/Action.git", :tag => s.version }
21-
s.source_files = "*.swift"
21+
s.source_files = "*.{swift}"
22+
2223
s.frameworks = "Foundation"
2324
s.dependency "RxSwift", '~> 2.0.0-beta'
2425
s.dependency "RxCocoa", '~> 2.0.0-beta'
2526

26-
s.watchos.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift"
27-
s.osx.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift"
28-
s.tvos.exclude_files = "UIBarButtonItem+Action.swift"
27+
s.watchos.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift", "AlertAction.swift"
28+
s.osx.exclude_files = "UIButton+Rx.swift", "UIBarButtonItem+Action.swift", "AlertAction.swift"
29+
s.tvos.exclude_files = "UIBarButtonItem+Action.swift", "AlertAction.swift"
2930
end

AlertAction.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import UIKit
2+
import RxSwift
3+
import RxCocoa
4+
5+
public extension UIAlertAction {
6+
7+
public static func Action(title: String?, style: UIAlertActionStyle) -> UIAlertAction {
8+
return UIAlertAction(title: title, style: style, handler: { action in
9+
action.rx_action?.execute()
10+
})
11+
}
12+
13+
/// Binds enabled state of action to button, and subscribes to rx_tap to execute action.
14+
/// These subscriptions are managed in a private, inaccessible dispose bag. To cancel
15+
/// them, set the rx_action to nil or another action.
16+
public var rx_action: CocoaAction? {
17+
get {
18+
var action: CocoaAction?
19+
doLocked {
20+
action = objc_getAssociatedObject(self, &AssociatedKeys.Action) as? Action
21+
}
22+
return action
23+
}
24+
25+
set {
26+
doLocked {
27+
// Store new value.
28+
objc_setAssociatedObject(self, &AssociatedKeys.Action, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
29+
30+
// This effectively disposes of any existing subscriptions.
31+
self.resetActionDisposeBag()
32+
33+
// Set up new bindings, if applicable.
34+
if let action = newValue {
35+
action
36+
.enabled
37+
.bindTo(self.rx_enabled)
38+
.addDisposableTo(self.actionDisposeBag)
39+
}
40+
}
41+
}
42+
}
43+
}
44+
45+
extension UIAlertAction {
46+
var rx_enabled: AnyObserver<Bool> {
47+
return AnyObserver { [weak self] event in
48+
MainScheduler.ensureExecutingOnScheduler()
49+
50+
switch event {
51+
case .Next(let value):
52+
self?.enabled = value
53+
case .Error(let error):
54+
let error = "Binding error to UI: \(error)"
55+
#if DEBUG
56+
rxFatalError(error)
57+
#else
58+
print(error)
59+
#endif
60+
break
61+
case .Completed:
62+
break
63+
}
64+
}
65+
}
66+
}

Demo/Demo.xcodeproj/project.pbxproj

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

Demo/Demo/Base.lproj/Main.storyboard

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,29 @@
3535
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
3636
<subviews>
3737
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SZL-3G-VZh">
38-
<rect key="frame" x="277" y="31" width="46" height="30"/>
38+
<rect key="frame" x="264" y="31" width="73" height="30"/>
3939
<animations/>
40-
<state key="normal" title="Button"/>
40+
<state key="normal" title="Show alert"/>
4141
</button>
42-
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OrY-7C-i77">
43-
<rect key="frame" x="290" y="258" width="20" height="20"/>
44-
</activityIndicatorView>
4542
<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">
46-
<rect key="frame" x="234" y="229" width="133" height="21"/>
43+
<rect key="frame" x="234" y="467" width="133" height="21"/>
4744
<fontDescription key="fontDescription" type="system" pointSize="17"/>
4845
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
4946
<nil key="highlightedColor"/>
5047
</label>
48+
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OrY-7C-i77">
49+
<rect key="frame" x="290" y="496" width="20" height="20"/>
50+
</activityIndicatorView>
5151
</subviews>
5252
<animations/>
5353
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
5454
<constraints>
5555
<constraint firstItem="OrY-7C-i77" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Dyl-A6-bud"/>
56-
<constraint firstItem="OrY-7C-i77" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="KWX-Rs-PVL"/>
5756
<constraint firstItem="SZL-3G-VZh" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="31" id="Np0-fa-c3f"/>
5857
<constraint firstItem="ulN-2s-mAd" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="a5T-qt-gDR"/>
5958
<constraint firstItem="OrY-7C-i77" firstAttribute="top" secondItem="ulN-2s-mAd" secondAttribute="bottom" constant="8" id="eqf-Tp-OJd"/>
6059
<constraint firstItem="SZL-3G-VZh" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="hb0-j9-0rK"/>
60+
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="OrY-7C-i77" secondAttribute="bottom" constant="20" id="jhU-Jj-aCX"/>
6161
</constraints>
6262
</view>
6363
<extendedEdge key="edgesForExtendedLayout"/>

Demo/Demo/ViewController.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,23 @@ class ViewController: UIViewController {
2222
super.viewDidLoad()
2323

2424
// Demo: add an action to a button in the view
25-
let action = CocoaAction { _ in
26-
return create { observer -> Disposable in
27-
// Do whatever work here.
28-
print("Doing work for button at \(NSDate())")
29-
observer.onCompleted()
30-
return NopDisposable.instance
25+
let action = CocoaAction {
26+
print("Button was pressed, showing an alert and keeping the activity indicator spinning while alert is displayed")
27+
return create {
28+
[weak self] observer -> Disposable in
29+
30+
// Demo: show an alert and complete the view's button action once the alert's OK button is pressed
31+
let alertController = UIAlertController(title: "Hello world", message: "This alert was triggered by a button action", preferredStyle: .Alert)
32+
let ok = UIAlertAction.Action("OK", style: .Default)
33+
ok.rx_action = CocoaAction {
34+
print("Alert's OK button was pressed")
35+
observer.onCompleted()
36+
return empty()
37+
}
38+
alertController.addAction(ok)
39+
self!.presentViewController(alertController, animated: true, completion: nil)
40+
41+
return NopDisposable.instance
3142
}
3243
}
3344
button.rx_action = action
@@ -38,7 +49,7 @@ class ViewController: UIViewController {
3849
return empty().delaySubscription(2, MainScheduler.sharedInstance)
3950
}
4051

41-
// Demo: obseve the output of both actions, spin an activity indicator
52+
// Demo: observe the output of both actions, spin an activity indicator
4253
// while performing the work
4354
combineLatest(
4455
button.rx_action!.executing,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Quick
2+
import Nimble
3+
import RxSwift
4+
import RxBlocking
5+
import Action
6+
7+
class AlertActionTests: QuickSpec {
8+
override func spec() {
9+
it("is nil by default") {
10+
let subject = UIAlertAction.Action("Hi", style: .Default)
11+
expect(subject.rx_action).to( beNil() )
12+
}
13+
14+
it("respects setter") {
15+
let subject = UIAlertAction.Action("Hi", style: .Default)
16+
17+
let action = emptyAction()
18+
19+
subject.rx_action = action
20+
21+
expect(subject.rx_action) === action
22+
}
23+
24+
it("disables the button while executing") {
25+
let subject = UIAlertAction.Action("Hi", style: .Default)
26+
27+
var observer: AnyObserver<Void>!
28+
let action = CocoaAction(workFactory: { _ in
29+
return create { (obsv) -> Disposable in
30+
observer = obsv
31+
return NopDisposable.instance
32+
}
33+
})
34+
35+
subject.rx_action = action
36+
37+
action.execute()
38+
expect(subject.enabled).toEventually( beFalse() )
39+
40+
observer.onCompleted()
41+
expect(subject.enabled).toEventually( beTrue() )
42+
}
43+
44+
it("disables the button if the Action is disabled") {
45+
let subject = UIAlertAction.Action("Hi", style: .Default)
46+
47+
subject.rx_action = emptyAction(just(false))
48+
49+
expect(subject.enabled) == false
50+
}
51+
52+
it("disposes of old action subscriptions when re-set") {
53+
let subject = UIAlertAction.Action("Hi", style: .Default)
54+
55+
var disposed = false
56+
autoreleasepool {
57+
let disposeBag = DisposeBag()
58+
59+
let action = emptyAction()
60+
subject.rx_action = action
61+
62+
action
63+
.elements
64+
.subscribe(onNext: nil, onError: nil, onCompleted: nil, onDisposed: {
65+
disposed = true
66+
})
67+
.addDisposableTo(disposeBag)
68+
}
69+
70+
subject.rx_action = nil
71+
72+
expect(disposed) == true
73+
}
74+
}
75+
}

Demo/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PODS:
2-
- Action (0.1.0):
2+
- Action (0.2.0):
33
- RxCocoa (~> 2.0.0-beta)
44
- RxSwift (~> 2.0.0-beta)
55
- Nimble (3.0.0)
@@ -23,7 +23,7 @@ EXTERNAL SOURCES:
2323
:path: "../"
2424

2525
SPEC CHECKSUMS:
26-
Action: dbcbc4c9255e54f801c14554a58d1ed6b5c1d47e
26+
Action: 836da3ae9615435467a3d1cddd384f4ea6036ba2
2727
Nimble: 4c353d43735b38b545cbb4cb91504588eb5de926
2828
Quick: 563d0f6ec5f72e394645adb377708639b7dd38ab
2929
RxBlocking: 331f8bdedf77198f8ff1a37f09c492f1f4f631e5

Readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ button.rx_action = action
5656

5757
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.
5858

59+
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:
60+
61+
```swift
62+
let action = UIAlertAction.Action("Hi", style: .Default)
63+
```
64+
5965
**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!
6066

6167
Installing

0 commit comments

Comments
 (0)