From b08c1a5969962453dc2a67db1d743a33a2f0a7b2 Mon Sep 17 00:00:00 2001 From: Krunoslav Zaher Date: Sun, 6 Dec 2015 20:33:03 +0100 Subject: [PATCH] Extends `UIView` with `rx_alpha`, `rx_hidden`. `NSLayoutConstraint` with `rx_constant`. --- RxCocoa/Common/NSLayoutConstraint+Rx.swift | 43 ++++++++++ RxCocoa/OSX/NSView+Rx.swift | 53 ++++++++++++ RxCocoa/iOS/UIControl+Rx.swift | 2 +- RxCocoa/iOS/UIView+Rx.swift | 57 +++++++++++++ .../GithubValidationService.swift | 72 +++++++++++++++++ .../Numbers/NumbersViewController.swift | 9 +++ .../SimpleValidationViewController.swift | 59 ++++++++++++++ RxExample/RxExample/Operators.swift | 9 +++ .../NSLayoutConstraint+RxTests.swift | 9 +++ RxTests/RxCocoaTests/NSView+RxTests.swift | 9 +++ RxTests/RxCocoaTests/RxCocoaTests.swift | 15 ---- RxTests/RxCocoaTests/UI+RxTests.swift | 80 ------------------- RxTests/RxCocoaTests/UIView+RxTests.swift | 9 +++ 13 files changed, 330 insertions(+), 96 deletions(-) create mode 100644 RxCocoa/Common/NSLayoutConstraint+Rx.swift create mode 100644 RxCocoa/OSX/NSView+Rx.swift create mode 100644 RxCocoa/iOS/UIView+Rx.swift create mode 100644 RxExample/RxExample/Examples/GitHubSignup/GithubValidationService.swift create mode 100644 RxExample/RxExample/Examples/Numbers/NumbersViewController.swift create mode 100644 RxExample/RxExample/Examples/SimpleValidation/SimpleValidationViewController.swift create mode 100644 RxExample/RxExample/Operators.swift create mode 100644 RxTests/RxCocoaTests/NSLayoutConstraint+RxTests.swift create mode 100644 RxTests/RxCocoaTests/NSView+RxTests.swift delete mode 100644 RxTests/RxCocoaTests/RxCocoaTests.swift delete mode 100644 RxTests/RxCocoaTests/UI+RxTests.swift create mode 100644 RxTests/RxCocoaTests/UIView+RxTests.swift diff --git a/RxCocoa/Common/NSLayoutConstraint+Rx.swift b/RxCocoa/Common/NSLayoutConstraint+Rx.swift new file mode 100644 index 00000000..fc1f56f5 --- /dev/null +++ b/RxCocoa/Common/NSLayoutConstraint+Rx.swift @@ -0,0 +1,43 @@ +// +// NSLayoutConstraint+Rx.swift +// Rx +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +import Foundation + +#if os(OSX) +import Cocoa +#else +import UIKit +#endif + +#if !RX_NO_MODULE +import RxSwift +#endif + +#if os(iOS) || os(OSX) || os(tvOS) +extension NSLayoutConstraint { + /** + Bindable sink for `constant` property. + */ + public var rx_constant: AnyObserver { + return AnyObserver { [weak self] event in + MainScheduler.ensureExecutingOnScheduler() + + switch event { + case .Next(let value): + self?.constant = value + case .Error(let error): + bindingErrorToInterface(error) + break + case .Completed: + break + } + } + } +} + +#endif diff --git a/RxCocoa/OSX/NSView+Rx.swift b/RxCocoa/OSX/NSView+Rx.swift new file mode 100644 index 00000000..04bcc221 --- /dev/null +++ b/RxCocoa/OSX/NSView+Rx.swift @@ -0,0 +1,53 @@ +// +// NSView+Rx.swift +// Rx +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +import Foundation +import Cocoa +#if !RX_NO_MODULE +import RxSwift +#endif + +extension NSView { + /** + Bindable sink for `hidden` property. + */ + public var rx_hidden: AnyObserver { + return AnyObserver { [weak self] event in + MainScheduler.ensureExecutingOnScheduler() + + switch event { + case .Next(let value): + self?.hidden = value + case .Error(let error): + bindingErrorToInterface(error) + break + case .Completed: + break + } + } + } + + /** + Bindable sink for `alphaValue` property. + */ + public var rx_alpha: AnyObserver { + return AnyObserver { [weak self] event in + MainScheduler.ensureExecutingOnScheduler() + + switch event { + case .Next(let value): + self?.alphaValue = value + case .Error(let error): + bindingErrorToInterface(error) + break + case .Completed: + break + } + } + } +} \ No newline at end of file diff --git a/RxCocoa/iOS/UIControl+Rx.swift b/RxCocoa/iOS/UIControl+Rx.swift index 3fdb06a4..86955d22 100644 --- a/RxCocoa/iOS/UIControl+Rx.swift +++ b/RxCocoa/iOS/UIControl+Rx.swift @@ -34,7 +34,7 @@ extension UIControl { } } } - + /** Reactive wrapper for target action pattern. diff --git a/RxCocoa/iOS/UIView+Rx.swift b/RxCocoa/iOS/UIView+Rx.swift new file mode 100644 index 00000000..105496f4 --- /dev/null +++ b/RxCocoa/iOS/UIView+Rx.swift @@ -0,0 +1,57 @@ +// +// UIView+Rx.swift +// Rx +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +#if os(iOS) || os(tvOS) + +import Foundation +import UIKit +#if !RX_NO_MODULE +import RxSwift +#endif + +extension UIView { + /** + Bindable sink for `hidden` property. + */ + public var rx_hidden: AnyObserver { + return AnyObserver { [weak self] event in + MainScheduler.ensureExecutingOnScheduler() + + switch event { + case .Next(let value): + self?.hidden = value + case .Error(let error): + bindingErrorToInterface(error) + break + case .Completed: + break + } + } + } + + /** + Bindable sink for `alpha` property. + */ + public var rx_alpha: AnyObserver { + return AnyObserver { [weak self] event in + MainScheduler.ensureExecutingOnScheduler() + + switch event { + case .Next(let value): + self?.alpha = value + case .Error(let error): + bindingErrorToInterface(error) + break + case .Completed: + break + } + } + } +} + +#endif diff --git a/RxExample/RxExample/Examples/GitHubSignup/GithubValidationService.swift b/RxExample/RxExample/Examples/GitHubSignup/GithubValidationService.swift new file mode 100644 index 00000000..367cda98 --- /dev/null +++ b/RxExample/RxExample/Examples/GitHubSignup/GithubValidationService.swift @@ -0,0 +1,72 @@ +// +// ValidationService.swift +// RxExample +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +import Foundation +import RxSwift + +class ValidationService { + let API: GitHubAPI + + init (API: GitHubAPI) { + self.API = API + } + + // validation + + let minPasswordCount = 5 + + func validateUsername(username: String) -> ValidationObservable { + if username.characters.count == 0 { + return just((false, nil)) + } + + // this obviously won't be + if username.rangeOfCharacterFromSet(NSCharacterSet.alphanumericCharacterSet().invertedSet) != nil { + return just((false, "Username can only contain numbers or digits")) + } + + let loadingValue = (valid: nil as Bool?, message: "Checking availabilty ..." as String?) + + return API.usernameAvailable(username) + .map { available in + if available { + return (true, "Username available") + } + else { + return (false, "Username already taken") + } + } + .startWith(loadingValue) + } + + func validatePassword(password: String) -> ValidationResult { + let numberOfCharacters = password.characters.count + if numberOfCharacters == 0 { + return (false, nil) + } + + if numberOfCharacters < minPasswordCount { + return (false, "Password must be at least \(minPasswordCount) characters") + } + + return (true, "Password acceptable") + } + + func validateRepeatedPassword(password: String, repeatedPassword: String) -> ValidationResult { + if repeatedPassword.characters.count == 0 { + return (false, nil) + } + + if repeatedPassword == password { + return (true, "Password repeated") + } + else { + return (false, "Password different") + } + } +} diff --git a/RxExample/RxExample/Examples/Numbers/NumbersViewController.swift b/RxExample/RxExample/Examples/Numbers/NumbersViewController.swift new file mode 100644 index 00000000..8285cc34 --- /dev/null +++ b/RxExample/RxExample/Examples/Numbers/NumbersViewController.swift @@ -0,0 +1,9 @@ +// +// NumbersViewController.swift +// RxExample +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +import Foundation diff --git a/RxExample/RxExample/Examples/SimpleValidation/SimpleValidationViewController.swift b/RxExample/RxExample/Examples/SimpleValidation/SimpleValidationViewController.swift new file mode 100644 index 00000000..a6316f57 --- /dev/null +++ b/RxExample/RxExample/Examples/SimpleValidation/SimpleValidationViewController.swift @@ -0,0 +1,59 @@ +// +// SimpleValidation.swift +// RxExample +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +import Foundation +import UIKit +import RxSwift +import RxCocoa + +let mininalUsernameLength = 5 +let mininalPasswordLength = 5 + +class SimpleValidationViewController : ViewController { + + @IBOutlet weak var username: UITextField! + @IBOutlet weak var usernameValid: UILabel! + + @IBOutlet weak var password: UITextField! + @IBOutlet weak var passwordValid: UILabel! + + @IBOutlet weak var doSomething: UIButton! + + func showAlert() { + DefaultWireframe.sharedInstance.promptFor("Something wonderful has been done", cancelAction: "OK", actions: []) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let usernameValid = username.rx_text + .map { $0.characters.count >= mininalUsernameLength } + .shareReplay(1) // without this map would be executed once for each binding, rx is stateless by default + + let passwordValid = password.rx_text + .map { $0.characters.count >= mininalPasswordLength } + .shareReplay(1) + + let everythingValid = combineLatest(usernameValid, passwordValid) { $0 && $1 } + .shareReplay(1) + + + + usernameValid + .bindTo(password.rx_enabled) + .addDisposableTo(disposeBag) + + everythingValid + .bindTo(doSomething.rx_enabled) + .addDisposableTo(disposeBag) + + doSomething.rx_tap + .subscribeNext(showAlert) + .addDisposableTo(disposeBag) + } +} \ No newline at end of file diff --git a/RxExample/RxExample/Operators.swift b/RxExample/RxExample/Operators.swift new file mode 100644 index 00000000..fc2afe32 --- /dev/null +++ b/RxExample/RxExample/Operators.swift @@ -0,0 +1,9 @@ +// +// Operators.swift +// RxExample +// +// Created by Krunoslav Zaher on 12/6/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// + +import Foundation diff --git a/RxTests/RxCocoaTests/NSLayoutConstraint+RxTests.swift b/RxTests/RxCocoaTests/NSLayoutConstraint+RxTests.swift new file mode 100644 index 00000000..5256be16 --- /dev/null +++ b/RxTests/RxCocoaTests/NSLayoutConstraint+RxTests.swift @@ -0,0 +1,9 @@ +// +// NSLayoutConstraint+RxTests.swift +// RxTests +// +// Created by Krunoslav Zaher on 12/6/15. +// +// + +import Foundation diff --git a/RxTests/RxCocoaTests/NSView+RxTests.swift b/RxTests/RxCocoaTests/NSView+RxTests.swift new file mode 100644 index 00000000..21fce938 --- /dev/null +++ b/RxTests/RxCocoaTests/NSView+RxTests.swift @@ -0,0 +1,9 @@ +// +// NSView+RxTests.swift +// RxTests +// +// Created by Krunoslav Zaher on 12/6/15. +// +// + +import Foundation diff --git a/RxTests/RxCocoaTests/RxCocoaTests.swift b/RxTests/RxCocoaTests/RxCocoaTests.swift deleted file mode 100644 index 80a2a1f5..00000000 --- a/RxTests/RxCocoaTests/RxCocoaTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// RxCocoaTests.swift -// RxTests -// -// Created by Krunoslav Zaher on 7/6/15. -// -// - -import Foundation -import XCTest -import RxSwift -import RxCocoa - -class RxCocoaTest : RxTest { -} \ No newline at end of file diff --git a/RxTests/RxCocoaTests/UI+RxTests.swift b/RxTests/RxCocoaTests/UI+RxTests.swift deleted file mode 100644 index 367e1506..00000000 --- a/RxTests/RxCocoaTests/UI+RxTests.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// UI+RxTests.swift -// RxTests -// -// Created by Krunoslav Zaher on 5/3/15. -// -// - -import Foundation -import RxSwift -import RxCocoa -import XCTest - -class UITextFieldMock { - - let observableText = Variable("") - - var text: String! = "" { - didSet { - observableText.value = self.text - } - } - - func rx_text() -> Observable { - return observableText.asObservable() - } -} - -extension ObservableType where E == String { - func subscribeTextOf(label: UILabelMock) -> Disposable { - return self.subscribeNext { t in - label.text = t - } - } -} - -class UILabelMock { - var text: String! = "" - -} - -class UIRxTests : RxTest { - - func testArea() { - } - - func testReadmeExample() { - - // We have some async Wolfram Alpha API that calculates is number prime. - let WolframAlphaIsPrime: (Int) -> Observable = { just(PrimeNumber($0, isPrime($0))) } - - let primeTextField = UITextFieldMock() - - let resultLabel = UILabelMock() - - let _ = primeTextField.rx_text() - .map { WolframAlphaIsPrime(Int($0) ?? 0) } - .concat() - .map { "number \($0.n) is prime? \($0.isPrime)" } - .subscribeTextOf(resultLabel) - .scopedDispose() - - // this will set resultLabel.text! == "number 43 is prime? true" - primeTextField.text = "43" - } -} - -struct PrimeNumber : Equatable { - let n: Int - let isPrime: Bool - - init(_ n: Int, _ isPrime: Bool) { - self.n = n - self.isPrime = isPrime - } -} - -func == (lhs: PrimeNumber, rhs: PrimeNumber) -> Bool { - return lhs.n == rhs.n && lhs.isPrime == rhs.isPrime -} \ No newline at end of file diff --git a/RxTests/RxCocoaTests/UIView+RxTests.swift b/RxTests/RxCocoaTests/UIView+RxTests.swift new file mode 100644 index 00000000..653238f8 --- /dev/null +++ b/RxTests/RxCocoaTests/UIView+RxTests.swift @@ -0,0 +1,9 @@ +// +// UIView+RxTests.swift +// RxTests +// +// Created by Krunoslav Zaher on 12/6/15. +// +// + +import Foundation