Skip to content

Commit

Permalink
Additional UKit Bonds. Up to 2.2.0. Update Readme.
Browse files Browse the repository at this point in the history
  • Loading branch information
srdanrasic committed Feb 12, 2015
1 parent 88154f8 commit 3b31a4a
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 15 deletions.
68 changes: 65 additions & 3 deletions Bond/Bond+UIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,42 @@ class ControlDynamic<T, U: ControlDynamicHelper where U.T == T>: Dynamic<T>
}
}

private var backgroundColorBondHandleUIView: UInt8 = 0;
private var alphaBondHandleUIView: UInt8 = 0;
private var hiddenBondHandleUIView: UInt8 = 0;

extension UIView {
public var backgroundColorBond: Bond<UIColor> {
if let b: AnyObject = objc_getAssociatedObject(self, &backgroundColorBondHandleUIView) {
return (b as? Bond<UIColor>)!
} else {
let b = Bond<UIColor>() { [unowned self] v in self.backgroundColor = v }
objc_setAssociatedObject(self, &backgroundColorBondHandleUIView, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return b
}
}

public var alphaBond: Bond<CGFloat> {
if let b: AnyObject = objc_getAssociatedObject(self, &alphaBondHandleUIView) {
return (b as? Bond<CGFloat>)!
} else {
let b = Bond<CGFloat>() { [unowned self] v in self.alpha = v }
objc_setAssociatedObject(self, &alphaBondHandleUIView, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return b
}
}

public var hiddenBond: Bond<Bool> {
if let b: AnyObject = objc_getAssociatedObject(self, &hiddenBondHandleUIView) {
return (b as? Bond<Bool>)!
} else {
let b = Bond<Bool>() { [unowned self] v in self.hidden = v }
objc_setAssociatedObject(self, &hiddenBondHandleUIView, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return b
}
}
}

// MARK: UISlider

@objc class SliderDynamicHelper: NSObject, ControlDynamicHelper {
Expand Down Expand Up @@ -204,19 +240,45 @@ extension UIImageView: Bondable {
}
}

private var designatedBondHandleUIButton: UInt8 = 0;
private var enabledBondHandleUIButton: UInt8 = 0;
private var titleBondHandleUIButton: UInt8 = 0;
private var imageForNormalStateBondHandleUIButton: UInt8 = 0;

extension UIButton: Dynamical, Bondable {
public func eventDynamic() -> Dynamic<UIControlEvents> {
return ControlDynamic<UIControlEvents, ButtonDynamicHelper>(helper: ButtonDynamicHelper(control: self))
}

public var enabledBond: Bond<Bool> {
if let b: AnyObject = objc_getAssociatedObject(self, &designatedBondHandleUIButton) {
if let b: AnyObject = objc_getAssociatedObject(self, &enabledBondHandleUIButton) {
return (b as? Bond<Bool>)!
} else {
let b = Bond<Bool>() { [unowned self] v in self.enabled = v }
objc_setAssociatedObject(self, &designatedBondHandleUIButton, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
objc_setAssociatedObject(self, &enabledBondHandleUIButton, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return b
}
}

public var titleBond: Bond<String> {
if let b: AnyObject = objc_getAssociatedObject(self, &titleBondHandleUIButton) {
return (b as? Bond<String>)!
} else {
let b = Bond<String>() { [unowned self] v in
if let label = self.titleLabel {
label.text = v
}
}
objc_setAssociatedObject(self, &titleBondHandleUIButton, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return b
}
}

public var imageForNormalStateBond: Bond<UIImage?> {
if let b: AnyObject = objc_getAssociatedObject(self, &imageForNormalStateBondHandleUIButton) {
return (b as? Bond<UIImage?>)!
} else {
let b = Bond<UIImage?>() { [unowned self] img in self.setImage(img, forState: .Normal) }
objc_setAssociatedObject(self, &imageForNormalStateBondHandleUIButton, b, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return b
}
}
Expand Down
2 changes: 1 addition & 1 deletion Bond/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.1.1</string>
<string>2.2.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
74 changes: 73 additions & 1 deletion BondTests/UIKitBondTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,48 @@ import Bond

class UIKitTests: XCTestCase {

func testUIViewHiddenBond() {
var dynamicDriver = Dynamic<Bool>(false)
let view = UIView()

view.hidden = true
XCTAssert(view.hidden == true, "Initial value")

dynamicDriver ->> view.hiddenBond
XCTAssert(view.hidden == false, "Value after binding")

dynamicDriver.value = true
XCTAssert(view.hidden == true, "Value after dynamic change")
}

func testUIViewAlphaBond() {
var dynamicDriver = Dynamic<CGFloat>(0.1)
let view = UIView()

view.alpha = 0.0
XCTAssert(abs(view.alpha - 0.0) < 0.0001, "Initial value")

dynamicDriver ->> view.alphaBond
XCTAssert(abs(view.alpha - 0.1) < 0.0001, "Value after binding")

dynamicDriver.value = 0.5
XCTAssert(abs(view.alpha - 0.5) < 0.0001, "Value after dynamic change")
}

func testUIViewBackgroundColorBond() {
var dynamicDriver = Dynamic<UIColor>(UIColor.blackColor())
let view = UIView()

view.backgroundColor = UIColor.redColor()
XCTAssert(view.backgroundColor == UIColor.redColor(), "Initial value")

dynamicDriver ->> view.backgroundColorBond
XCTAssert(view.backgroundColor == UIColor.blackColor(), "Value after binding")

dynamicDriver.value = UIColor.blueColor()
XCTAssert(view.backgroundColor == UIColor.blueColor(), "Value after dynamic change")
}

func testUISliderBond() {
var dynamicDriver = Dynamic<Float>(0)
let slider = UISlider()
Expand Down Expand Up @@ -69,7 +111,7 @@ class UIKitTests: XCTestCase {
XCTAssert(imageView.image == image, "Value after dynamic change")
}

func testUIButtonBond() {
func testUIButtonEnabledBond() {
var dynamicDriver = Dynamic<Bool>(false)
let button = UIButton()

Expand All @@ -83,6 +125,36 @@ class UIKitTests: XCTestCase {
XCTAssert(button.enabled == true, "Value after dynamic change")
}

func testUIButtonTitleBond() {
var dynamicDriver = Dynamic<String>("b")
let button = UIButton()

button.titleLabel?.text = "a"
XCTAssert(button.titleLabel?.text == "a", "Initial value")

dynamicDriver ->> button.titleBond
XCTAssert(button.titleLabel?.text == "b", "Value after binding")

dynamicDriver.value = "c"
XCTAssert(button.titleLabel?.text == "c", "Value after dynamic change")
}

func testUIButtonImageBond() {
let image1 = UIImage()
let image2 = UIImage()
var dynamicDriver = Dynamic<UIImage?>(nil)
let button = UIButton()

button.setImage(image1, forState: .Normal)
XCTAssert(button.imageForState(.Normal) == image1, "Initial value")

dynamicDriver ->> button.imageForNormalStateBond
XCTAssert(button.imageForState(.Normal) == nil, "Value after binding")

dynamicDriver.value = image2
XCTAssert(button.imageForState(.Normal) == image2, "Value after dynamic change")
}

func testUISwitchBond() {
var dynamicDriver = Dynamic<Bool>(false)
let switchControl = UISwitch()
Expand Down
95 changes: 85 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ That one line establishes a _bond_ between text field's text property and label'
More often than not, direct binding is not enough. Usually you need to transform input is some way, like prepending a greeting to a name. Of course, Bond has full confidence in functional paradigm.

```swift
textField.map { "Hi " + $0 } ->> label
textField.textDynamic().map { "Hi " + $0 } ->> label
```

Whenever a change occurs in the text field, new value will be transformed by the closure and propagated to the label.

In addition to `map`, another important functional construct is `filter`. It's useful when we are interested only in some values of a domain. For example, when observing events of a button, we might be interested only in `TouchUpInside` event so we can perform certain action when user taps the button:

```swift
button.filter { $0 == UIControlEvents.TouchUpInside } ->> { event in
button.eventDynamic().filter { $0 == UIControlEvents.TouchUpInside } ->> { event in
login()
}
```
Expand All @@ -44,7 +44,7 @@ Bond can also `reduce` multiple inputs into a single output. Following snippet d
} ->> loginButton
```

Whenever user types something into any of the text fields, expression will be evaluated and button stated updated.
Whenever user types something into any of the text fields, expression will be evaluated and button state updated.

Bond's power is not, however, in coupling various UI components, but in a bonding of Model (or ViewModel) to View and vice-versa. It's great for MVVM paradigm. Here is how one could bond user's number of followers property of a model to a label.

Expand Down Expand Up @@ -141,29 +141,99 @@ The parameter `fire` in `bind` method indicated whether listener should be calle

### What about UIKit

UIKit views and controls are not, of course, Dynamics and Bonds, so how can they act as agents in Bond word? Controls and views for which it makes sense to are extended with Swift extensions to adhere to one or both of these protocols:
UIKit views and controls are not, of course, Dynamics and Bonds, so how can they act as agents in Bond word?

#### Dynamics

Controls and views for which it makes sense to are extended to provide Dynamics for commonly used properties, like UITextField's `text` property, UISlider's `value` property or UISwitch's `on` property.

To get a Dynamic representation of a property of UIKit object, call a method ending in `dynamic()`. For example, to get dynamic representation of UITextField's `text` property, call its `textDynamic()` method. Calling that method creates a Dynamic object that is coupled to the control or the view whose value it observes through mechanism like _action-target_ for controls or delegation for table views.

Each call to `*Dynamic()` method creates a new Dynamic object. **Returned object is not retained by the caller nor does it retains the caller.** In order to keep it alive, you have to either retain it or bind it to some Bond object (as mentioned previously - Bond retains bonded Dynamic).

Following table lists all available Dynamics of UIKit objects:

| Class | Dynamic(s) | Designated Dynamic |
|--------------|----------------|--------------------|
| UISlider | valueDynamic() | valueDynamic() |
| UIButton | eventDynamic() | eventDynamic() |
| UISwitch | onDynamic() | onDynamic() |
| UITextField | textDynamic() | textDynamic() |
| UIDatePicker | dateDynamic() | dateDynamic() |


You might be wondering what _Designated Dynamic_ is. It's way to access most commonly used Dynamic through method `designatedDynamic()`. Currently all UIKit objects have only one Dynamic property that is also a designed one. Having common name enables us to define protocol like

```swift
public protocol Dynamical {
typealias DynamicType
func designatedDynamic() -> Dynamic<DynamicType>
}

```

and use that protocol to make binding easier. Instead of doing binding like

```swift
titleTextField.textDynamic() ->> titleLabel
```

it allows us to do just

```swift
titleTextField ->> titleLabel
```

because operator `->>` is overloaded to work with `Dynamicals`.

#### Bonds

Controls or views that present some data or have user-visible state, like UITextField's `text` property, UILabel's `text` property, UIImageView's `image` property or UIButton's `disabled` state property allow us to bind a Dynamic to them by providing a Bond object.

Provided Bond object is saved in `*Bond` property. Property holds a Bond that has a `Listener` implemented in a way that whenever a change is observed in any of bonded Dynamics, it updates control's or view's property that it represents.

For example, UITextField provides `textBond` property that is coupled with its `text` property in a way that whenever a change is observed in any of bonded Dynamics, `text` is updated.

**Unlike Dynamics created from UIKit object, Bonds are retained by their view or control through Objective-C's associated objects mechanism.** Of course, Bond does not retain its parent.

Following table lists all available Bonds of UIKit objects:

| Class | Bonds | Designated Bond |
|----------------|---------------------------------------------------------|-----------------|
| UIView | alphaBond <br> hiddenBond <br> backgroundColorBond | -- |
| UISlider | valueBond | valueBond |
| UILabel | textBond | textBond |
| UIProgressView | progressBond | progressBond |
| UIImageView | imageBond | imageBond |
| UIButton | enabledBond <br> titleBond <br> imageForNormalStateBond | enabledBond |
| UISwitch | onBond | onBond |
| UITextField | textBond | textBond |
| UIDatePicker | dateBond | dateBond |
| UITableView | dataSourceBond | dataSourceBond |


Like as for Dynamics, we can define protocol `Bondable`

```swift
public protocol Bondable {
typealias BondType
var designatedBond: Bond<BondType> { get }
}
```

Controls or views whose value can be set by the user, like UITextField's `text` property, UISlider's `value` property or UISwitch's `on` property, implement protocol `Dynamical`. Protocol defines one required method - `designatedDynamic()`. Calling that method creates a 'designated' Dynamic object that is coupled to the control or the view whose value it observes through mechanism like _action-target_ for controls or delegation for table views.
and use it to make binding easier. Instead of doing binding like

Each call to `designatedDynamic()` creates a new Dynamic object. Returned object is not retained by the caller nor does it retains the caller. In order to keep it alive, you have to either retain it or bind it to some Bond object (as mentioned previously - Bond retains bonded Dynamic).
```swift
titleTextField ->> titleLabel.textBond
```

Controls or views that present some data or have user-visible state, like UITextField's `text` property, UILabel's `text` property, UIImageView's `image` property or UIButton's `disabled` state property, implement protocol `Bondable`. Protocol defines one property - `designatedBond`. Property holds a Bond that has implemented a `Listener` in a way that whenever a change is observed in any of bonded Dynamics, it updates control's or view's 'designated' property.
it allows us to do just

Unlike designated Dynamic, Bond is retained by it's view or control through Objective-C's associated objects mechanism. Of course, Bond does not retain its parent.
```swift
titleTextField ->> titleLabel
```

because operator `->>` is overloaded to work with `Bondables`.

The attribute designated is chosen because some control or views might have additional Dynamic or Bond properties. For example, UIButton might provide bond both for its `disabled` property and its `title` property. At the moment, only former is implemented as its designated bond.

### Functional concepts explained

Expand Down Expand Up @@ -323,6 +393,11 @@ Bond has yet to be shipped in an app. It was tested with many examples, but if t

## Release Notes

### v2.2.0

* More specific UIKit Bonds and Dynamics
* Added unit tests for UIKit Bonds

### v2.1.1

* Support for Swift 1.2
Expand Down

0 comments on commit 3b31a4a

Please sign in to comment.