Skip to content

Commit

Permalink
Improve KVO bindings. Add two way KVO bindings. Fix #19.
Browse files Browse the repository at this point in the history
  • Loading branch information
Srđan Rašić committed Mar 4, 2015
1 parent 5d7b135 commit aaf5d6e
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 26 deletions.
4 changes: 2 additions & 2 deletions Bond.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|

s.name = "Bond"
s.version = "3.0.0"
s.version = "3.0.1"
s.summary = "A Swift binding framework"

s.description = <<-DESC
Expand All @@ -18,7 +18,7 @@ Pod::Spec.new do |s|
s.social_media_url = "http://twitter.com/srdanrasic"
s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.10"
s.source = { :git => "https://github.com/SwiftBond/Bond.git", :tag => "v3.0.0" }
s.source = { :git => "https://github.com/SwiftBond/Bond.git", :tag => "v3.0.1" }
s.source_files = "Bond"
s.osx.exclude_files = "Bond/Bond+UI*"
s.framework = 'SystemConfiguration'
Expand Down
10 changes: 8 additions & 2 deletions Bond.xcodeproj/project.pbxproj
Expand Up @@ -19,7 +19,6 @@
DCA979831A83C4F800DD4A30 /* BondTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69491BC01A7C217100A13B6B /* BondTests.swift */; };
DCA979841A83C52D00DD4A30 /* Bond.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69491BCA1A7C21B600A13B6B /* Bond.swift */; };
DCA979851A83C52D00DD4A30 /* Bond+Arrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69491BCB1A7C21B600A13B6B /* Bond+Arrays.swift */; };
DCA979861A83C52D00DD4A30 /* Bond+Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69491BCC1A7C21B600A13B6B /* Bond+Foundation.swift */; };
EC048D181AA238AF00E9794C /* Bond+Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC048D171AA238AF00E9794C /* Bond+Operators.swift */; };
EC048D191AA238AF00E9794C /* Bond+Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC048D171AA238AF00E9794C /* Bond+Operators.swift */; };
EC0F22F61AA353100051723D /* Bond+UIActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0F22F51AA353100051723D /* Bond+UIActivityIndicatorView.swift */; };
Expand All @@ -28,6 +27,9 @@
EC7137E41AA25E9300CFC854 /* FunctionalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD653A51A9B6BF30038A9AC /* FunctionalTests.swift */; };
EC7137E51AA26F0800CFC854 /* UIKitDynamicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC71C0AB1A9F839A009FEA4B /* UIKitDynamicTests.swift */; };
EC74CFD21A8BF1A300ED4026 /* UIKitBondTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC74CFD11A8BF1A300ED4026 /* UIKitBondTests.swift */; };
EC847EB01AA7963F002971B8 /* Bond+Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69491BCC1A7C21B600A13B6B /* Bond+Foundation.swift */; };
EC847EB11AA796F5002971B8 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC867F891AA73AD5007B4BC4 /* FoundationTests.swift */; };
EC847EB21AA796F8002971B8 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC867F891AA73AD5007B4BC4 /* FoundationTests.swift */; };
ECD9CC541A9FA36200484323 /* Bond+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD9CC531A9FA36200484323 /* Bond+UIView.swift */; };
ECD9CC561A9FA38C00484323 /* Bond+UISlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD9CC551A9FA38C00484323 /* Bond+UISlider.swift */; };
ECD9CC581A9FA3AD00484323 /* Bond+UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD9CC571A9FA3AD00484323 /* Bond+UILabel.swift */; };
Expand Down Expand Up @@ -77,6 +79,7 @@
EC0F22F51AA353100051723D /* Bond+UIActivityIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bond+UIActivityIndicatorView.swift"; sourceTree = "<group>"; };
EC71C0AB1A9F839A009FEA4B /* UIKitDynamicTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitDynamicTests.swift; sourceTree = "<group>"; };
EC74CFD11A8BF1A300ED4026 /* UIKitBondTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitBondTests.swift; sourceTree = "<group>"; };
EC867F891AA73AD5007B4BC4 /* FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationTests.swift; sourceTree = "<group>"; };
ECD653A21A9B6B6B0038A9AC /* Bond.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Bond.podspec; sourceTree = "<group>"; };
ECD653A41A9B6B770038A9AC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
ECD653A51A9B6BF30038A9AC /* FunctionalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionalTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -181,6 +184,7 @@
EC74CFD11A8BF1A300ED4026 /* UIKitBondTests.swift */,
EC71C0AB1A9F839A009FEA4B /* UIKitDynamicTests.swift */,
ECD653A51A9B6BF30038A9AC /* FunctionalTests.swift */,
EC867F891AA73AD5007B4BC4 /* FoundationTests.swift */,
69491BBE1A7C217100A13B6B /* Supporting Files */,
);
path = BondTests;
Expand Down Expand Up @@ -413,6 +417,7 @@
files = (
69491BC11A7C217100A13B6B /* BondTests.swift in Sources */,
69491BD31A7C242900A13B6B /* DynamicTests.swift in Sources */,
EC847EB11AA796F5002971B8 /* FoundationTests.swift in Sources */,
EC74CFD21A8BF1A300ED4026 /* UIKitBondTests.swift in Sources */,
EC7137E51AA26F0800CFC854 /* UIKitDynamicTests.swift in Sources */,
EC7137E41AA25E9300CFC854 /* FunctionalTests.swift in Sources */,
Expand All @@ -424,10 +429,10 @@
buildActionMask = 2147483647;
files = (
DCA979851A83C52D00DD4A30 /* Bond+Arrays.swift in Sources */,
EC847EB01AA7963F002971B8 /* Bond+Foundation.swift in Sources */,
EC7137E21AA239FC00CFC854 /* Bond+Functional.swift in Sources */,
DCA979841A83C52D00DD4A30 /* Bond.swift in Sources */,
EC048D191AA238AF00E9794C /* Bond+Operators.swift in Sources */,
DCA979861A83C52D00DD4A30 /* Bond+Foundation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -437,6 +442,7 @@
files = (
DCA979821A83C4F500DD4A30 /* DynamicTests.swift in Sources */,
DCA979831A83C4F800DD4A30 /* BondTests.swift in Sources */,
EC847EB21AA796F8002971B8 /* FoundationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
71 changes: 55 additions & 16 deletions Bond/Bond+Foundation.swift
Expand Up @@ -74,29 +74,68 @@ private var XXContext = 0
}
}

public extension Dynamic {
public func dynamicObservableFor<T>(object: NSObject, #keyPath: String, #defaultValue: T) -> Dynamic<T> {
let keyPathValue: AnyObject? = object.valueForKeyPath(keyPath)
let value: T = (keyPathValue != nil) ? (keyPathValue as? T)! : defaultValue
let dynamic = InternalDynamic(value, faulty: false)

public class func asObservableFor(object: NSObject, keyPath: String) -> Dynamic<T> {
let dynamic = InternalDynamic((object.valueForKeyPath(keyPath) as? T)!, faulty: false)
let helper = DynamicKVOHelper(keyPath: keyPath, object: object as NSObject) {
[unowned dynamic] (v: AnyObject) -> Void in

let helper = DynamicKVOHelper(keyPath: keyPath, object: object as NSObject) {
[unowned dynamic] (v: AnyObject) -> Void in
if v is NSNull {
dynamic.value = defaultValue
} else {
dynamic.value = (v as? T)!
}

dynamic.retain(helper)
return dynamic
}

public class func asObservableFor(notificationName: String, object: AnyObject?, parser: NSNotification -> T) -> InternalDynamic<T> {
let dynamic: InternalDynamic<T> = InternalDynamic(parser(NSNotification(name: notificationName, object: nil)), faulty: true)

let helper = DynamicNotificationCenterHelper(notificationName: notificationName, object: object) {
[unowned dynamic] notification in
dynamic.value = parser(notification)
dynamic.retain(helper)
return dynamic
}

public func dynamicObservableFor<T>(object: NSObject, #keyPath: String, #from: AnyObject? -> T, #to: T -> AnyObject?) -> Dynamic<T> {
let keyPathValue: AnyObject? = object.valueForKeyPath(keyPath)
let dynamic = InternalDynamic(from(keyPathValue), faulty: false)

let helper = DynamicKVOHelper(keyPath: keyPath, object: object as NSObject) {
[unowned dynamic] (v: AnyObject?) -> Void in
dynamic.value = from(v)
}

let feedbackBond = Bond<T>() { [weak object] value in
if let object = object {
object.setValue(to(value) ?? NSNull(), forKey: keyPath)
}

dynamic.retain(helper)
}

dynamic.bindTo(feedbackBond, fire: false, strongly: false)
dynamic.retain(feedbackBond)

dynamic.retain(helper)
return dynamic
}

public func dynamicObservableFor<T>(notificationName: String, #object: AnyObject?, #parser: NSNotification -> T) -> InternalDynamic<T> {
let dynamic: InternalDynamic<T> = InternalDynamic(parser(NSNotification(name: notificationName, object: nil)), faulty: true)

let helper = DynamicNotificationCenterHelper(notificationName: notificationName, object: object) {
[unowned dynamic] notification in
dynamic.value = parser(notification)
}

dynamic.retain(helper)
return dynamic
}


public extension Dynamic {
public class func asObservableFor(object: NSObject, keyPath: String, defaultValue: T) -> Dynamic<T> {
let dynamic: Dynamic<T> = dynamicObservableFor(object, keyPath: keyPath, defaultValue: defaultValue)
return dynamic
}

public class func asObservableFor(notificationName: String, object: AnyObject?, parser: NSNotification -> T) -> Dynamic<T> {
let dynamic: InternalDynamic<T> = dynamicObservableFor(notificationName, object: object, parser: parser)
return dynamic
}
}
2 changes: 1 addition & 1 deletion Bond/Bond+UITextView.swift
Expand Up @@ -35,7 +35,7 @@ extension UITextView: Bondable {
if let d: AnyObject = objc_getAssociatedObject(self, &textDynamicHandleUITextView) {
return (d as? Dynamic<String>)!
} else {
let d = Dynamic.asObservableFor(UITextViewTextDidChangeNotification, object: self) {
let d: InternalDynamic<String> = dynamicObservableFor(UITextViewTextDidChangeNotification, object: self) {
notification -> String in
if let textView = notification.object as? UITextView {
return textView.text ?? ""
Expand Down
2 changes: 1 addition & 1 deletion Bond/Info.plist
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
<string>3.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
81 changes: 81 additions & 0 deletions BondTests/FoundationTests.swift
@@ -0,0 +1,81 @@
//
// FoundationTests.swift
// Bond
//
// Created by Srdan Rasic on 04/03/15.
// Copyright (c) 2015 Bond. All rights reserved.
//

import XCTest
import Bond

@objc class User: NSObject {
dynamic var name: NSString?
dynamic var height: NSNumber = NSNumber(float: 0.0)

init(name: NSString?) {
self.name = name
super.init()
}
}

class FoundationTests: XCTestCase {

func testKVO() {
let user = User(name: nil)
let dynamic: Dynamic<String> = dynamicObservableFor(user, keyPath: "name", defaultValue: "")

XCTAssert(dynamic.value == "", "Value after initialization.")

user.name = "Spock"
XCTAssert(dynamic.value == "Spock", "Value after property change.")

user.name = nil
XCTAssert(dynamic.value == "", "Value after property change.")
}

func testKVO2() {
let user = User(name: nil)
let dynamic: Dynamic<String?> = dynamicObservableFor(user, keyPath: "name", from: { $0 as? String }, to: { $0 })

XCTAssert(dynamic.value == nil, "Value after initialization.")

user.name = "Spock"
XCTAssert(dynamic.value == "Spock", "Value after property change.")

user.name = nil
XCTAssert(dynamic.value == nil, "Value after property change.")

dynamic.value = "Jim"
XCTAssert(user.name == "Jim", "Value after dynamic change.")
}

func testKVO3() {
let user = User(name: nil)
let dynamic: Dynamic<String> = dynamicObservableFor(user, keyPath: "name", from: { ($0 as? String) ?? "" }, to: { $0 })

XCTAssert(dynamic.value == "", "Value after initialization.")

user.name = "Spock"
XCTAssert(dynamic.value == "Spock", "Value after property change.")

user.name = nil
XCTAssert(dynamic.value == "", "Value after property change.")

dynamic.value = "Jim"
XCTAssert(user.name == "Jim", "Value after dynamic change.")
}

func testKVO4() {
let user = User(name: nil)
let height: Dynamic<Float> = dynamicObservableFor(user, keyPath: "height", from: { ($0 as NSNumber).floatValue }, to: { NSNumber(float: $0) })

XCTAssert(abs(height.value - 0) < 0.0001, "Value after initialization.")

user.height = 6.9
XCTAssert(abs(height.value - 6.9) < 0.0001, "Value after property change.")

height.value = 7.1
XCTAssert(abs(user.height.floatValue - 7.1) < 0.0001, "Value after dynamic change.")
}
}
2 changes: 1 addition & 1 deletion BondTests/Info.plist
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2.3.0</string>
<string>3.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
39 changes: 36 additions & 3 deletions README.md
Expand Up @@ -536,18 +536,51 @@ If your table view needs to display more than one section, you can feed it with

### Key-Value-Observing

You can create a Dynamic that observers value chnages of some KVO-observable property. For example, you can bond a property of your existing Objective-C model object to a label with this simple one-liner:
You can create a Dynamic that observers value changes of some KVO-observable property. For example, you can bind a property of your existing Objective-C model object to a label with this simple one-liner:

```swift
Dynamic.asObservableFor(self.user, keyPath: "numberOfFollowers") ->> label
dynamicObservableFor(self.user, keyPath: "name", defaultValue: "") ->> nameLabel
```

Default value is used when observed property is set to `nil`. Dynamic returned by this method will only observe changes the property. Setting its `value` will have no effect on bound property. If you need two way binding, keep reading.

#### Two way Key-Value-Observing

To create bi-directional Dynamic representation a KVO property, use the following variant of the method:

```swift
dynamicObservableFor<T>(object: NSObject, #keyPath: String, #from: AnyObject? -> T, #to: T -> AnyObject?) -> Dynamic<T>
```

Difference is that instead of the default value you need to provide transformations *from* and *to* observed type. KVO is not type-safe so you can't see actually type, rather you see `AnyObject?`.

For example, if KVO property is of NSString type and you want its `Dynamic<String>` representation, you can do following:

```swift
let name: Dynamic<String> = dynamicObservableFor(self.user, keyPath: "name", from: { ($0 as? String) ?? "" }, to: { $0 })
```

`from` closure optionally downcasts passed value to String. That will succeed if passed value is of NSString type. It will fail if it is of some other type or if it is `nil`. `to` closure converts value to NSString. Swift can do that implicitly, so you can just pass the object.

After you get a Dynamic, you can easily bind it to, for example, UITextField.

```swift
name <->> nameTextField
```

For some other types, you might need to do something like this:

```swift
let height: Dynamic<Float> = dynamicObservableFor(self.user, keyPath: "height", from: { ($0 as NSNumber).floatValue }, to: { NSNumber(float: $0) })
```


### NSNotificationCenter

You can create a Dynamic that observers notifications posted by NSNotificationCenter. During initialization you need to provide notification name and a closure that'll parse notification into Dynamic's type. If you are interested only in notifications from specific object, pass that object too.

```swift
let orientation: Dynamic<UIDeviceOrientation> = Dynamic.asObservableFor(UIDeviceOrientationDidChangeNotification, object: nil) {
let orientation: Dynamic<UIDeviceOrientation> = dynamicObservableFor(UIDeviceOrientationDidChangeNotification, object: nil) {
notification -> UIDeviceOrientation in
return UIDevice.currentDevice().orientation
}
Expand Down

0 comments on commit aaf5d6e

Please sign in to comment.