Skip to content

Commit 4daa9d3

Browse files
Merge pull request #72 from RxSwiftCommunity/stefanomondino-bindToAction
Adds tests + formatting for bindTo()
2 parents e606d7d + a06f740 commit 4daa9d3

File tree

8 files changed

+265
-28
lines changed

8 files changed

+265
-28
lines changed

Action.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
3D37A3961DB4A97C0028BC0E /* RxTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D9C98331DB4A87B004A9F7C /* RxTest.framework */; };
11+
5ED520241E1EA199007621B9 /* BindToTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED520231E1EA199007621B9 /* BindToTests.swift */; };
12+
7BD1C7551E1D5562000D82DA /* UIControl+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1C7541E1D5562000D82DA /* UIControl+Rx.swift */; };
1113
7F0569E81DE28587007E1D0D /* Action+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0569E01DE28587007E1D0D /* Action+Internal.swift */; };
1214
7F0569E91DE28587007E1D0D /* Action.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F0569E11DE28587007E1D0D /* Action.h */; settings = {ATTRIBUTES = (Public, ); }; };
1315
7F0569EA1DE28587007E1D0D /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0569E21DE28587007E1D0D /* Action.swift */; };
@@ -72,6 +74,8 @@
7274

7375
/* Begin PBXFileReference section */
7476
3D9C98331DB4A87B004A9F7C /* RxTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxTest.framework; path = Carthage/Checkouts/RxSwift/build/Debug/RxTest.framework; sourceTree = "<group>"; };
77+
5ED520231E1EA199007621B9 /* BindToTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BindToTests.swift; path = ActionTests/BindToTests.swift; sourceTree = "<group>"; };
78+
7BD1C7541E1D5562000D82DA /* UIControl+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIControl+Rx.swift"; sourceTree = "<group>"; };
7579
7F0569E01DE28587007E1D0D /* Action+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Action+Internal.swift"; sourceTree = "<group>"; };
7680
7F0569E11DE28587007E1D0D /* Action.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Action.h; sourceTree = "<group>"; };
7781
7F0569E21DE28587007E1D0D /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
@@ -164,6 +168,7 @@
164168
7F0569E51DE28587007E1D0D /* AlertAction.swift */,
165169
7F0569E61DE28587007E1D0D /* UIBarButtonItem+Action.swift */,
166170
7F0569E71DE28587007E1D0D /* UIButton+Rx.swift */,
171+
7BD1C7541E1D5562000D82DA /* UIControl+Rx.swift */,
167172
);
168173
path = UIKitExtensions;
169174
sourceTree = "<group>";
@@ -194,6 +199,7 @@
194199
isa = PBXGroup;
195200
children = (
196201
7F0569F01DE288EB007E1D0D /* ActionTests.swift */,
202+
5ED520231E1EA199007621B9 /* BindToTests.swift */,
197203
7F0569F11DE288EB007E1D0D /* AlertActionTests.swift */,
198204
7F0569F21DE288EB007E1D0D /* BarButtonTests.swift */,
199205
7F0569F31DE288EB007E1D0D /* ButtonTests.swift */,
@@ -382,6 +388,7 @@
382388
buildActionMask = 2147483647;
383389
files = (
384390
7F0569F71DE288EB007E1D0D /* BarButtonTests.swift in Sources */,
391+
5ED520241E1EA199007621B9 /* BindToTests.swift in Sources */,
385392
7F0569F61DE288EB007E1D0D /* AlertActionTests.swift in Sources */,
386393
7F0569F51DE288EB007E1D0D /* ActionTests.swift in Sources */,
387394
7F0569F81DE288EB007E1D0D /* ButtonTests.swift in Sources */,
@@ -404,6 +411,7 @@
404411
7F0569EC1DE28587007E1D0D /* AlertAction.swift in Sources */,
405412
7F0569E81DE28587007E1D0D /* Action+Internal.swift in Sources */,
406413
7F0569ED1DE28587007E1D0D /* UIBarButtonItem+Action.swift in Sources */,
414+
7BD1C7551E1D5562000D82DA /* UIControl+Rx.swift in Sources */,
407415
7F0569EA1DE28587007E1D0D /* Action.swift in Sources */,
408416
7F0569EE1DE28587007E1D0D /* UIButton+Rx.swift in Sources */,
409417
);

Demo/Base.lproj/Main.storyboard

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9060" systemVersion="14F1021" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="gBp-2p-Zj2">
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="gBp-2p-Zj2">
3+
<device id="retina4_7" orientation="portrait">
4+
<adaptation id="fullscreen"/>
5+
</device>
36
<dependencies>
47
<deployment identifier="iOS"/>
5-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9051"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
9+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
610
</dependencies>
711
<scenes>
812
<!--Navigation Controller-->
@@ -31,28 +35,38 @@
3135
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
3236
</layoutGuides>
3337
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
34-
<rect key="frame" x="0.0" y="64" width="600" height="536"/>
38+
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
3539
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
3640
<subviews>
3741
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SZL-3G-VZh">
38-
<rect key="frame" x="264" y="31" width="73" height="30"/>
39-
<animations/>
42+
<rect key="frame" x="151" y="31" width="73" height="30"/>
4043
<state key="normal" title="Show alert"/>
4144
</button>
42-
<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">
45+
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" text="Doing some work" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ulN-2s-mAd">
4346
<rect key="frame" x="234" y="467" width="133" height="21"/>
4447
<fontDescription key="fontDescription" type="system" pointSize="17"/>
4548
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
4649
<nil key="highlightedColor"/>
4750
</label>
48-
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OrY-7C-i77">
51+
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" misplaced="YES" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OrY-7C-i77">
4952
<rect key="frame" x="290" y="496" width="20" height="20"/>
5053
</activityIndicatorView>
54+
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Rn7-U4-1tI">
55+
<rect key="frame" x="131" y="107" width="113" height="30"/>
56+
<state key="normal" title="Custom button 1"/>
57+
</button>
58+
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KoY-Sn-0Iw">
59+
<rect key="frame" x="130" y="145" width="115" height="30"/>
60+
<state key="normal" title="Custom button 2"/>
61+
</button>
5162
</subviews>
52-
<animations/>
53-
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
63+
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
5464
<constraints>
65+
<constraint firstItem="Rn7-U4-1tI" firstAttribute="centerX" secondItem="SZL-3G-VZh" secondAttribute="centerX" id="2z3-Ib-Bzb"/>
5566
<constraint firstItem="OrY-7C-i77" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Dyl-A6-bud"/>
67+
<constraint firstItem="KoY-Sn-0Iw" firstAttribute="centerX" secondItem="Rn7-U4-1tI" secondAttribute="centerX" id="Esh-u5-4Vv"/>
68+
<constraint firstItem="KoY-Sn-0Iw" firstAttribute="top" secondItem="Rn7-U4-1tI" secondAttribute="bottom" constant="8" id="Gph-05-VIV"/>
69+
<constraint firstItem="Rn7-U4-1tI" firstAttribute="top" secondItem="SZL-3G-VZh" secondAttribute="bottom" constant="46" id="Hhn-4d-Hi7"/>
5670
<constraint firstItem="SZL-3G-VZh" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="31" id="Np0-fa-c3f"/>
5771
<constraint firstItem="ulN-2s-mAd" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="a5T-qt-gDR"/>
5872
<constraint firstItem="OrY-7C-i77" firstAttribute="top" secondItem="ulN-2s-mAd" secondAttribute="bottom" constant="8" id="eqf-Tp-OJd"/>
@@ -62,11 +76,14 @@
6276
</view>
6377
<extendedEdge key="edgesForExtendedLayout"/>
6478
<navigationItem key="navigationItem" title="Action demo" id="6DL-Fh-JcF">
79+
<barButtonItem key="leftBarButtonItem" title="Custom3" id="piR-Gf-7t2"/>
6580
<barButtonItem key="rightBarButtonItem" title="Bar Item" id="7hK-lr-Izl"/>
6681
</navigationItem>
6782
<connections>
6883
<outlet property="activityIndicator" destination="OrY-7C-i77" id="VCS-wT-Lb8"/>
6984
<outlet property="button" destination="SZL-3G-VZh" id="IT1-GJ-Qic"/>
85+
<outlet property="button1" destination="Rn7-U4-1tI" id="zLN-qN-lSH"/>
86+
<outlet property="button2" destination="KoY-Sn-0Iw" id="AUC-hM-Glv"/>
7087
<outlet property="workingLabel" destination="ulN-2s-mAd" id="17Y-xo-0lM"/>
7188
</connections>
7289
</viewController>

Demo/ViewController.swift

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,34 @@ import RxSwift
1111
import RxCocoa
1212
import Action
1313

14+
enum SharedInput {
15+
case button(String)
16+
case barButton
17+
}
18+
1419
class ViewController: UIViewController {
1520
@IBOutlet weak var button: UIButton!
1621
@IBOutlet weak var workingLabel: UILabel!
1722
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
18-
23+
24+
@IBOutlet weak var button1: UIButton!
25+
@IBOutlet weak var button2: UIButton!
26+
1927
var disposableBag = DisposeBag()
28+
let sharedAction = Action<SharedInput, String> { input in
29+
switch input {
30+
case .barButton: return Observable.just("UIBarButtonItem with 3 seconds delay").delaySubscription(3, scheduler: MainScheduler.instance)
31+
case .button(let title): return .just("UIButton " + title)
32+
}
33+
}
2034

2135
override func viewDidLoad() {
2236
super.viewDidLoad()
2337

2438
// Demo: add an action to a button in the view
2539
let action = CocoaAction {
2640
print("Button was pressed, showing an alert and keeping the activity indicator spinning while alert is displayed")
27-
return Observable.create {
28-
[weak self] observer -> Disposable in
29-
41+
return Observable.create { [weak self] observer -> Disposable in
3042
// Demo: show an alert and complete the view's button action once the alert's OK button is pressed
3143
let alertController = UIAlertController(title: "Hello world", message: "This alert was triggered by a button action", preferredStyle: .alert)
3244
var ok = UIAlertAction.Action("OK", style: .default)
@@ -36,15 +48,16 @@ class ViewController: UIViewController {
3648
return .empty()
3749
}
3850
alertController.addAction(ok)
39-
self!.present(alertController, animated: true, completion: nil)
51+
self?.present(alertController, animated: true, completion: nil)
4052

4153
return Disposables.create()
4254
}
4355
}
56+
4457
button.rx.action = action
4558

4659
// Demo: add an action to a UIBarButtonItem in the navigation item
47-
self.navigationItem.rightBarButtonItem!.rx.action = CocoaAction {
60+
self.navigationItem.rightBarButtonItem?.rx.action = CocoaAction {
4861
print("Bar button item was pressed, simulating a 2 second action")
4962
return Observable.empty().delaySubscription(2, scheduler: MainScheduler.instance)
5063
}
@@ -53,15 +66,13 @@ class ViewController: UIViewController {
5366
// while performing the work
5467
Observable.combineLatest(
5568
button.rx.action!.executing,
56-
self.navigationItem.rightBarButtonItem!.rx.action!.executing) {
69+
self.navigationItem.rightBarButtonItem!.rx.action!.executing) { a,b in
5770
// we combine two boolean observable and output one boolean
58-
a,b in
5971
return a || b
6072
}
6173
.distinctUntilChanged()
62-
.subscribe(onNext: {
74+
.subscribe(onNext: { [weak self] executing in
6375
// every time the execution status changes, spin an activity indicator
64-
[weak self] executing in
6576
self?.workingLabel.isHidden = !executing
6677
if (executing) {
6778
self?.activityIndicator.startAnimating()
@@ -70,7 +81,26 @@ class ViewController: UIViewController {
7081
self?.activityIndicator.stopAnimating()
7182
}
7283
})
73-
7484
.addDisposableTo(self.disposableBag)
85+
86+
button1.rx.bindTo(action: sharedAction, input: .button("Button 1"))
87+
88+
button2.rx.bindTo(action: sharedAction) { _ in
89+
return .button("Button 2")
90+
}
91+
self.navigationItem.leftBarButtonItem?.rx.bindTo(action: sharedAction, input: .barButton)
92+
93+
sharedAction.executing.debounce(0, scheduler: MainScheduler.instance).subscribe(onNext: { [weak self] executing in
94+
if (executing) {
95+
self?.activityIndicator.startAnimating()
96+
}
97+
else {
98+
self?.activityIndicator.stopAnimating()
99+
}
100+
}).addDisposableTo(self.disposableBag)
101+
102+
sharedAction.elements.subscribe(onNext: { string in
103+
print(string + " pressed")
104+
}).addDisposableTo(self.disposableBag)
75105
}
76106
}

Sources/Action/UIKitExtensions/UIBarButtonItem+Action.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,32 @@ public extension Reactive where Base: UIBarButtonItem {
3333
self.tap.subscribe(onNext: { (_) in
3434
action.execute()
3535
})
36-
.addDisposableTo(self.base.actionDisposeBag)
36+
.addDisposableTo(self.base.actionDisposeBag)
3737
}
3838
}
3939
}
40+
41+
public func bindTo<Input, Output>(action: Action<Input, Output>, inputTransform: @escaping (Base) -> (Input)) {
42+
unbindAction()
43+
44+
self.tap
45+
.map { inputTransform(self.base) }
46+
.bindTo(action.inputs)
47+
.addDisposableTo(self.base.actionDisposeBag)
48+
49+
action
50+
.enabled
51+
.bindTo(self.isEnabled)
52+
.addDisposableTo(self.base.actionDisposeBag)
53+
}
54+
55+
public func bindTo<Input, Output>(action: Action<Input, Output>, input: Input) {
56+
self.bindTo(action: action) { _ in input}
57+
}
58+
59+
/// Unbinds any existing action, disposing of all subscriptions.
60+
public func unbindAction() {
61+
self.base.resetActionDisposeBag()
62+
}
4063
}
4164
#endif

Sources/Action/UIKitExtensions/UIButton+Rx.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,35 @@ public extension Reactive where Base: UIButton {
5151
}
5252
}
5353
}
54+
55+
/// Binds enabled state of action to button, and subscribes to rx_tap to execute action with given input transform.
56+
/// These subscriptions are managed in a private, inaccessible dispose bag. To cancel
57+
/// them, call bindToAction with another action or call unbindAction().
58+
public func bindTo<Input, Output>(action:Action<Input, Output>, inputTransform: @escaping (Base) -> (Input)) {
59+
// This effectively disposes of any existing subscriptions.
60+
unbindAction()
61+
62+
// Technically, this file is only included on tv/iOS platforms,
63+
// so this optional will never be nil. But let's be safe 😉
64+
let lookupControlEvent: ControlEvent<Void>?
65+
66+
#if os(tvOS)
67+
lookupControlEvent = self.primaryAction
68+
#elseif os(iOS)
69+
lookupControlEvent = self.tap
70+
#endif
71+
72+
guard let controlEvent = lookupControlEvent else {
73+
return
74+
}
75+
self.bindTo(action: action, controlEvent: controlEvent, inputTransform: inputTransform)
76+
}
77+
78+
/// Binds enabled state of action to button, and subscribes to rx_tap to execute action with given input value.
79+
/// These subscriptions are managed in a private, inaccessible dispose bag. To cancel
80+
/// them, call bindToAction with another action or call unbindAction().
81+
public func bindTo<Input, Output>(action: Action<Input, Output>, input: Input) {
82+
self.bindTo(action: action) { _ in input }
83+
}
5484
}
5585
#endif
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#if os(iOS) || os(tvOS)
2+
import UIKit
3+
import RxSwift
4+
import RxCocoa
5+
6+
public extension Reactive where Base: UIControl {
7+
/// Binds enabled state of action to control, and subscribes action's execution to provided controlEvents.
8+
/// These subscriptions are managed in a private, inaccessible dispose bag. To cancel
9+
/// them, set the rx.action to nil or another action, or call unbindAction().
10+
public func bindTo<Input, Output>(action: Action<Input, Output>, controlEvent: ControlEvent<Void>, inputTransform: @escaping (Base) -> (Input)) {
11+
// This effectively disposes of any existing subscriptions.
12+
unbindAction()
13+
14+
// For each tap event, use the inputTransform closure to provide an Input value to the action
15+
controlEvent
16+
.map { inputTransform(self.base) }
17+
.bindTo(action.inputs)
18+
.addDisposableTo(self.base.actionDisposeBag)
19+
20+
// Bind the enabled state of the control to the enabled state of the action
21+
action
22+
.enabled
23+
.bindTo(self.isEnabled)
24+
.addDisposableTo(self.base.actionDisposeBag)
25+
}
26+
27+
/// Unbinds any existing action, disposing of all subscriptions.
28+
public func unbindAction() {
29+
self.base.resetActionDisposeBag()
30+
}
31+
}
32+
33+
#endif

0 commit comments

Comments
 (0)