Skip to content

Commit 4a3bd79

Browse files
dmcrodriguesRuiAAPeres
authored andcommitted
Add bindable property to wrap a control's value (#119)
* Provide a `setUp` to properly configure the associated property The introduced `setUp` is extremely helpful to setup any signals that may affect the property once. * Add bindable property to wrap a control's value This property is common to every `UIControl` and can be used by providing a getter and a setter for the value wrapped. This property uses `UIControlEvents.ValueChanged` and `UIControlEvents.EditingChanged` events to detect changes and keep the value up-to-date. This kind of logic can be reused instead of being defined and used for each control. * Use `setUp` to install the dependent signal of `rex_dismissAnimated` This prevents repetition of events in cases where the property is accessed more than once (each access adds a new `rac_signalForSelector` for the dismiss of the view controller). * Fixes #129 - UITextField's text bindable property should be of optional type
1 parent 2c38832 commit 4a3bd79

File tree

11 files changed

+169
-81
lines changed

11 files changed

+169
-81
lines changed

Rex.xcodeproj/project.pbxproj

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

Source/Foundation/Association.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ public func associatedProperty<T: AnyObject>(host: AnyObject, keyPath: StaticStr
5050
/// This can be used as an alternative to `DynamicProperty` for creating strongly typed
5151
/// bindings on Cocoa objects.
5252
@warn_unused_result(message="Did you forget to use the property?")
53-
public func associatedProperty<Host: AnyObject, T>(host: Host, key: UnsafePointer<()>, @noescape initial: Host -> T, setter: (Host, T) -> ()) -> MutableProperty<T> {
53+
public func associatedProperty<Host: AnyObject, T>(host: Host, key: UnsafePointer<()>, @noescape initial: Host -> T, setter: (Host, T) -> (), @noescape setUp: MutableProperty<T> -> () = { _ in }) -> MutableProperty<T> {
5454
return associatedObject(host, key: key) { host in
5555
let property = MutableProperty(initial(host))
5656

57+
setUp(property)
58+
5759
property.producer.startWithNext { [weak host] next in
5860
if let host = host {
5961
setter(host, next)

Source/UIKit/UIControl.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import UIKit
1111
import enum Result.NoError
1212

1313
extension UIControl {
14+
1415
#if os(iOS)
1516
/// Creates a producer for the sender whenever a specified control event is triggered.
1617
@warn_unused_result(message="Did you forget to use the property?")
@@ -20,6 +21,21 @@ extension UIControl {
2021
.map { $0 as? UIControl }
2122
.flatMapError { _ in SignalProducer(value: nil) }
2223
}
24+
25+
/// Creates a bindable property to wrap a control's value.
26+
///
27+
/// This property uses `UIControlEvents.ValueChanged` and `UIControlEvents.EditingChanged`
28+
/// events to detect changes and keep the value up-to-date.
29+
//
30+
@warn_unused_result(message="Did you forget to use the property?")
31+
class func rex_value<Host: UIControl, T>(host: Host, getter: Host -> T, setter: (Host, T) -> ()) -> MutableProperty<T> {
32+
return associatedProperty(host, key: &valueChangedKey, initial: getter, setter: setter) { property in
33+
property <~
34+
host.rex_controlEvents([.ValueChanged, .EditingChanged])
35+
.filterMap { $0 as? Host }
36+
.filterMap(getter)
37+
}
38+
}
2339
#endif
2440

2541
/// Wraps a control's `enabled` state in a bindable property.
@@ -41,3 +57,4 @@ extension UIControl {
4157
private var enabledKey: UInt8 = 0
4258
private var selectedKey: UInt8 = 0
4359
private var highlightedKey: UInt8 = 0
60+
private var valueChangedKey: UInt8 = 0

Source/UIKit/UIDatePicker.swift

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,14 @@
55
// Created by Guido Marucci Blas on 3/25/16.
66
// Copyright © 2016 Neil Pankey. All rights reserved.
77
//
8-
import UIKit
8+
99
import ReactiveCocoa
10+
import UIKit
1011

1112
extension UIDatePicker {
12-
13+
14+
// Wraps a datePicker's `date` value in a bindable property.
1315
public var rex_date: MutableProperty<NSDate> {
14-
let initial = { (picker: UIDatePicker) -> NSDate in
15-
picker.addTarget(self, action: #selector(UIDatePicker.rex_changedDate), forControlEvents: .ValueChanged)
16-
return picker.date
17-
}
18-
return associatedProperty(self, key: &dateKey, initial: initial) { $0.date = $1 }
19-
}
20-
21-
@objc
22-
private func rex_changedDate() {
23-
rex_date.value = date
16+
return UIControl.rex_value(self, getter: { $0.date }, setter: { $0.date = $1 })
2417
}
25-
2618
}
27-
28-
private var dateKey: UInt8 = 0

Source/UIKit/UISwitch.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,8 @@ import UIKit
1111

1212
extension UISwitch {
1313

14-
/// Wraps a switch's `on` state in a bindable property.
14+
/// Wraps a switch's `on` value in a bindable property.
1515
public var rex_on: MutableProperty<Bool> {
16-
17-
let property = associatedProperty(self, key: &onKey, initial: { $0.on }, setter: { $0.on = $1 })
18-
19-
property <~ rex_controlEvents(.ValueChanged)
20-
.filterMap { ($0 as? UISwitch)?.on }
21-
22-
return property
16+
return UIControl.rex_value(self, getter: { $0.on }, setter: { $0.on = $1 })
2317
}
24-
2518
}
26-
27-
private var onKey: UInt8 = 0

Source/UIKit/UITextField.swift

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

99
import ReactiveCocoa
1010
import UIKit
11-
import enum Result.NoError
1211

1312
extension UITextField {
1413

15-
/// Sends the field's string value whenever it changes.
16-
public var rex_text: SignalProducer<String, NoError> {
17-
return NSNotificationCenter.defaultCenter()
18-
.rac_notifications(UITextFieldTextDidChangeNotification, object: self)
19-
.filterMap { ($0.object as? UITextField)?.text }
14+
/// Wraps a textField's `text` value in a bindable property.
15+
public var rex_text: MutableProperty<String?> {
16+
let getter: UITextField -> String? = { $0.text }
17+
let setter: (UITextField, String?) -> () = { $0.text = $1 }
18+
#if os(iOS)
19+
return UIControl.rex_value(self, getter: getter, setter: setter)
20+
#else
21+
return associatedProperty(self, key: &textKey, initial: getter, setter: setter) { property in
22+
property <~
23+
NSNotificationCenter.defaultCenter()
24+
.rac_notifications(UITextFieldTextDidChangeNotification, object: self)
25+
.filterMap { ($0.object as? UITextField)?.text }
26+
}
27+
#endif
2028
}
29+
2130
}
31+
32+
private var textKey: UInt8 = 0

Source/UIKit/UIViewController.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,12 @@ extension UIViewController {
6363
host.dismissViewControllerAnimated(unwrapped.animated, completion: unwrapped.completion)
6464
}
6565

66-
let property = associatedProperty(self, key: &dismissModally, initial: initial, setter: setter)
67-
68-
property <~ rac_signalForSelector(#selector(UIViewController.dismissViewControllerAnimated(_:completion:)))
69-
.takeUntilBlock { _ in property.value != nil }
70-
.rex_toTriggerSignal()
71-
.map { _ in return nil }
72-
66+
let property = associatedProperty(self, key: &dismissModally, initial: initial, setter: setter) { property in
67+
property <~ self.rac_signalForSelector(#selector(UIViewController.dismissViewControllerAnimated(_:completion:)))
68+
.takeUntilBlock { _ in property.value != nil }
69+
.rex_toTriggerSignal()
70+
.map { _ in return nil }
71+
}
7372

7473
return property
7574
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// UIControl+EnableSendActionsForControlEvents.swift
3+
// Rex
4+
//
5+
// Created by David Rodrigues on 24/04/16.
6+
// Copyright © 2016 Neil Pankey. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
/// Unfortunately, there's an apparent limitation in using `sendActionsForControlEvents`
12+
/// on unit-tests for any control besides `UIButton` which is very unfortunate since we
13+
/// want test our bindings for `UIDatePicker`, `UISwitch`, `UITextField` and others
14+
/// in the future. To be able to test them, we're now using swizzling to manually invoke
15+
/// the pair target+action.
16+
extension UIControl {
17+
18+
public override class func initialize() {
19+
20+
struct Static {
21+
static var token: dispatch_once_t = 0
22+
}
23+
24+
if self !== UIControl.self {
25+
return
26+
}
27+
28+
dispatch_once(&Static.token) {
29+
30+
let originalSelector = #selector(UIControl.sendAction(_:to:forEvent:))
31+
let swizzledSelector = #selector(UIControl.rex_sendAction(_:to:forEvent:))
32+
33+
let originalMethod = class_getInstanceMethod(self, originalSelector)
34+
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
35+
36+
let didAddMethod = class_addMethod(self,
37+
originalSelector,
38+
method_getImplementation(swizzledMethod),
39+
method_getTypeEncoding(swizzledMethod))
40+
41+
if didAddMethod {
42+
class_replaceMethod(self,
43+
swizzledSelector,
44+
method_getImplementation(originalMethod),
45+
method_getTypeEncoding(originalMethod))
46+
} else {
47+
method_exchangeImplementations(originalMethod, swizzledMethod)
48+
}
49+
}
50+
}
51+
52+
// MARK: - Method Swizzling
53+
54+
func rex_sendAction(action: Selector, to target: AnyObject?, forEvent event: UIEvent?) {
55+
target?.performSelector(action, withObject: self)
56+
}
57+
}

Tests/UIKit/UIDatePickerTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ class UIDatePickerTests: XCTestCase {
3030
XCTAssertEqual(picker.date, date)
3131
}
3232

33-
// FIXME Can this actually be made to work inside XCTest?
34-
func _testUpdatePropertyFromPicker() {
33+
func testUpdatePropertyFromPicker() {
3534
let expectation = self.expectationWithDescription("Expected rex_date to send an event when picker's date value is changed by a UI event")
3635
defer { self.waitForExpectationsWithTimeout(2, handler: nil) }
3736

Tests/UIKit/UISwitchTests.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@ import Result
1313
class UISwitchTests: XCTestCase {
1414

1515
func testOnProperty() {
16-
let s = UISwitch(frame: CGRectZero)
17-
s.on = false
16+
let `switch` = UISwitch(frame: CGRectZero)
17+
`switch`.on = false
1818

1919
let (pipeSignal, observer) = Signal<Bool, NoError>.pipe()
20-
s.rex_on <~ SignalProducer(signal: pipeSignal)
20+
`switch`.rex_on <~ SignalProducer(signal: pipeSignal)
2121

2222
observer.sendNext(true)
23-
XCTAssertTrue(s.on)
23+
XCTAssertTrue(`switch`.on)
2424
observer.sendNext(false)
25-
XCTAssertFalse(s.on)
25+
XCTAssertFalse(`switch`.on)
26+
27+
`switch`.on = true
28+
`switch`.sendActionsForControlEvents(.ValueChanged)
29+
XCTAssertTrue(`switch`.rex_on.value)
2630
}
2731
}

0 commit comments

Comments
 (0)