├── .swift-version ├── Cartfile ├── .github └── PULL_REQUEST_TEMPLATE.md ├── Logo ├── header.png ├── PNG │ ├── Docs.png │ ├── logo.png │ ├── JoinSlack.png │ ├── flat-logo.png │ ├── mono-dark-bg.png │ ├── logo-unpadded.png │ └── mono-light-bg.png ├── Palette.png ├── Icons │ ├── flat-icon.png │ ├── gradient-icon.png │ ├── mono-dark-icon.png │ ├── mono-light-icon.png │ ├── mono-dark-icon-alt.png │ └── mono-light-icon-alt.png └── README.md ├── ReactiveCocoaTests ├── ReactiveCocoaTests-Bridging-Header.h ├── test-data.json ├── ReactiveCocoaTestsConfiguration.swift ├── AppKit │ ├── Swift4TestInteroperability.swift │ ├── NSViewSpec.swift │ ├── AppKitReusableComponentsSpec.swift │ ├── NSImageViewSpec.swift │ ├── NSTableViewSpec.swift │ ├── NSCollectionViewSpec.swift │ ├── NSPopUpButtonSpec.swift │ └── NSButtonSpec.swift ├── UIKit │ ├── UIResponderSpec.swift │ ├── UISegmentedControlSpec.swift │ ├── UIActivityIndicatorViewSpec.swift │ ├── UIProgressViewSpec.swift │ ├── UIDatePickerSpec.swift │ ├── UIImageViewSpec.swift │ ├── UIKitReusableComponentsSpec.swift │ ├── UITableViewSpec.swift │ ├── UICollectionViewSpec.swift │ ├── UIControl+EnableSendActionsForControlEvents.swift │ ├── UISliderSpec.swift │ ├── UIStepperSpec.swift │ ├── UITabBarItemSpec.swift │ ├── UISwitchSpec.swift │ ├── UILabelSpec.swift │ ├── UIButtonSpec.swift │ ├── UIControlSpec.swift │ ├── UIRefreshControlSpec.swift │ ├── UIViewSpec.swift │ ├── UIViewControllerSpec.swift │ ├── UIGestureRecognizerSpec.swift │ ├── UITextViewSpec.swift │ └── UIBarButtonItemSpec.swift ├── Info.plist ├── DeprecationsSpec.swift ├── TestError.swift ├── Shared │ └── NSLayoutConstraintSpec.swift ├── SwizzlingSpec.swift ├── AssociationSpec.swift ├── SignalProducerNimbleMatchers.swift ├── CocoaActionSpec.swift └── BindingTargetSpec.swift ├── Cartfile.private ├── ReactiveCocoa ├── NSObject+ReactiveExtensionsProvider.swift ├── include │ └── module.modulemap ├── Synchronizing.swift ├── UIKit │ ├── UITableView.swift │ ├── UICollectionView.swift │ ├── UINavigationBar.swift │ ├── UIProgressView.swift │ ├── UIApplication.swift │ ├── UIActivityIndicatorView.swift │ ├── iOS │ │ ├── UIFeedbackGenerator.swift │ │ ├── UIImpact​Feedback​Generator.swift │ │ ├── UISelection​Feedback​Generator.swift │ │ ├── UINotification​Feedback​Generator.swift │ │ ├── UIDatePicker.swift │ │ ├── UIStepper.swift │ │ ├── UISwitch.swift │ │ ├── UISlider.swift │ │ ├── UIRefreshControl.swift │ │ ├── UIPickerView.swift │ │ └── UIKeyboard.swift │ ├── UIImageView.swift │ ├── UIKitReusableComponents.swift │ ├── UITabBarItem.swift │ ├── UISegmentedControl.swift │ ├── UIBarItem.swift │ ├── UIResponder.swift │ ├── UILabel.swift │ ├── UIGestureRecognizer.swift │ ├── UIView.swift │ ├── UIScrollView.swift │ ├── UIButton.swift │ ├── UIViewController.swift │ ├── UIBarButtonItem.swift │ ├── UITextField.swift │ ├── UINavigationItem.swift │ └── UITextView.swift ├── AppKit │ ├── NSTableView.swift │ ├── NSImageView.swift │ ├── NSCollectionView.swift │ ├── NSSlider.swift │ ├── NSView.swift │ ├── AppKitReusableComponents.swift │ ├── NSSegmentedControl.swift │ ├── NSTextView.swift │ ├── NSPopUpButton.swift │ ├── NSTextField.swift │ ├── NSButton.swift │ ├── NSControl.swift │ └── ActionProxy.swift ├── WatchKit │ ├── WKInterfaceSeparator.swift │ ├── WKInterfaceController.swift │ ├── WKInterfaceDate.swift │ ├── WKInterfaceVolumeControl.swift │ ├── WKInterfaceObject.swift │ ├── WKInterfaceTimer.swift │ ├── WKInterfaceActivityRing.swift │ ├── WKInterfaceLabel.swift │ ├── WKInterfacePicker.swift │ ├── WKInterfaceImage.swift │ ├── WKInterfaceSlider.swift │ ├── WKInterfaceMovie.swift │ ├── WKInterfaceSwitch.swift │ ├── WKInterfaceInlineMovie.swift │ ├── WKInterfaceGroup.swift │ └── WKInterfaceButton.swift ├── NSObject+ObjCRuntime.swift ├── Shared │ └── NSLayoutConstraint.swift ├── Deprecations+Removals.swift ├── ObjC+Runtime.swift ├── Info.plist ├── ReactiveSwift+Lifetime.swift ├── ObjC+Constants.swift ├── ObjC+Selector.swift ├── CocoaTarget.swift ├── ObjC+Messages.swift ├── NSObject+BindingTarget.swift ├── CocoaAction.swift └── DynamicProperty.swift ├── ReactiveCocoa-iOS.playground ├── contents.xcplayground ├── Sources │ └── PlaygroundUtility.swift └── Pages │ └── Sandbox.xcplaygroundpage │ └── Contents.swift ├── ReactiveCocoa-macOS.playground ├── contents.xcplayground ├── Sources │ └── PlaygroundUtility.swift └── Pages │ └── Sandbox.xcplaygroundpage │ └── Contents.swift ├── ReactiveCocoa-tvOS.playground ├── contents.xcplayground ├── Sources │ └── PlaygroundUtility.swift └── Pages │ └── Sandbox.xcplaygroundpage │ └── Contents.swift ├── ReactiveCocoaObjCTestSupport ├── include │ └── MessageForwardingEntity.h └── MessageForwardingEntity.m ├── Cartfile.resolved ├── ReactiveCocoaObjC ├── include │ ├── ReactiveCocoa.h │ └── ObjCRuntimeAliases.h └── ObjCRuntimeAliases.m ├── .gitignore ├── .gitmodules ├── ReactiveMapKitTests ├── Info.plist └── MKMapViewSpec.swift ├── ReactiveCocoa.xcworkspace └── contents.xcworkspacedata ├── script ├── update-version ├── build └── validate-playground.sh ├── ReactiveMapKit ├── Info.plist ├── MKLocalSearchRequest.swift └── MKMapView.swift ├── ReactiveMapKit.podspec ├── LICENSE.md ├── CONTRIBUTING.md ├── Package.swift ├── ReactiveCocoa.podspec ├── Package.resolved ├── .travis.yml ├── ReactiveCocoa.xcodeproj └── xcshareddata │ └── xcschemes │ └── ReactiveCocoa-watchOS.xcscheme └── Documentation └── DebuggingTechniques.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveCocoa/ReactiveSwift" ~> 6.2 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | #### Checklist 3 | - [ ] Updated CHANGELOG.md. 4 | -------------------------------------------------------------------------------- /Logo/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/header.png -------------------------------------------------------------------------------- /Logo/PNG/Docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/Docs.png -------------------------------------------------------------------------------- /Logo/PNG/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/logo.png -------------------------------------------------------------------------------- /Logo/Palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Palette.png -------------------------------------------------------------------------------- /ReactiveCocoaTests/ReactiveCocoaTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "MessageForwardingEntity.h" 2 | -------------------------------------------------------------------------------- /Logo/PNG/JoinSlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/JoinSlack.png -------------------------------------------------------------------------------- /Logo/PNG/flat-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/flat-logo.png -------------------------------------------------------------------------------- /ReactiveCocoaTests/test-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "item": 1 }, 3 | { "item": 2 }, 4 | { "item": 3 } 5 | ] 6 | -------------------------------------------------------------------------------- /Logo/Icons/flat-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Icons/flat-icon.png -------------------------------------------------------------------------------- /Logo/PNG/mono-dark-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/mono-dark-bg.png -------------------------------------------------------------------------------- /Logo/PNG/logo-unpadded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/logo-unpadded.png -------------------------------------------------------------------------------- /Logo/PNG/mono-light-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/PNG/mono-light-bg.png -------------------------------------------------------------------------------- /Logo/Icons/gradient-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Icons/gradient-icon.png -------------------------------------------------------------------------------- /Logo/Icons/mono-dark-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Icons/mono-dark-icon.png -------------------------------------------------------------------------------- /Logo/Icons/mono-light-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Icons/mono-light-icon.png -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "jspahrsummers/xcconfigs" "3d9d996" 2 | github "Quick/Quick" ~> 2.0 3 | github "Quick/Nimble" ~> 8.0.1 4 | -------------------------------------------------------------------------------- /Logo/Icons/mono-dark-icon-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Icons/mono-dark-icon-alt.png -------------------------------------------------------------------------------- /Logo/Icons/mono-light-icon-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/ReactiveCocoa/master/Logo/Icons/mono-light-icon-alt.png -------------------------------------------------------------------------------- /ReactiveCocoa/NSObject+ReactiveExtensionsProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | extension NSObject: ReactiveExtensionsProvider {} 5 | -------------------------------------------------------------------------------- /ReactiveCocoa-iOS.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ReactiveCocoa-macOS.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ReactiveCocoa-tvOS.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ReactiveCocoaObjCTestSupport/include/MessageForwardingEntity.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MessageForwardingEntity : NSObject 4 | @property(nonatomic) BOOL hasInvoked; 5 | @end 6 | -------------------------------------------------------------------------------- /ReactiveCocoa/include/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module ReactiveCocoa { 2 | umbrella header "ReactiveCocoa.h" 3 | private header "ObjCRuntimeAliases.h" 4 | 5 | export * 6 | module * { export * } 7 | } 8 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v8.0.5" 2 | github "Quick/Quick" "v2.2.0" 3 | github "ReactiveCocoa/ReactiveSwift" "6.2.0" 4 | github "jspahrsummers/xcconfigs" "3d9d99634cae6d586e272543d527681283b33eb0" 5 | -------------------------------------------------------------------------------- /ReactiveCocoa/Synchronizing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal func synchronized(_ token: AnyObject, execute: () throws -> Result) rethrows -> Result { 4 | objc_sync_enter(token) 5 | defer { objc_sync_exit(token) } 6 | return try execute() 7 | } 8 | -------------------------------------------------------------------------------- /ReactiveCocoaObjC/include/ReactiveCocoa.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for ReactiveCocoa. 4 | FOUNDATION_EXPORT double ReactiveCocoaVersionNumber; 5 | 6 | //! Project version string for ReactiveCocoa. 7 | FOUNDATION_EXPORT const unsigned char ReactiveCocoaVersionString[]; 8 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UITableView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UITableView { 6 | public var reloadData: BindingTarget<()> { 7 | return makeBindingTarget { base, _ in base.reloadData() } 8 | } 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /ReactiveCocoa-iOS.playground/Sources/PlaygroundUtility.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func scopedExample(_ exampleDescription: String, _ action: () -> Void) { 4 | print("\n--- \(exampleDescription) ---\n") 5 | action() 6 | } 7 | 8 | public enum PlaygroundError: Error { 9 | case example(String) 10 | } 11 | -------------------------------------------------------------------------------- /ReactiveCocoa-macOS.playground/Sources/PlaygroundUtility.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func scopedExample(_ exampleDescription: String, _ action: () -> Void) { 4 | print("\n--- \(exampleDescription) ---\n") 5 | action() 6 | } 7 | 8 | public enum PlaygroundError: Error { 9 | case example(String) 10 | } 11 | -------------------------------------------------------------------------------- /ReactiveCocoa-tvOS.playground/Sources/PlaygroundUtility.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func scopedExample(_ exampleDescription: String, _ action: () -> Void) { 4 | print("\n--- \(exampleDescription) ---\n") 5 | action() 6 | } 7 | 8 | public enum PlaygroundError: Error { 9 | case example(String) 10 | } 11 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UICollectionView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UICollectionView { 6 | public var reloadData: BindingTarget<()> { 7 | return makeBindingTarget { base, _ in base.reloadData() } 8 | } 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSTableView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSTableView { 6 | public var reloadData: BindingTarget<()> { 7 | return makeBindingTarget { base, _ in base.reloadData() } 8 | } 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceSeparator.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceSeparator { 6 | /// Sets the color of the separator. 7 | public var color: BindingTarget { 8 | return makeBindingTarget { $0.setColor($1) } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceController.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceController { 6 | /// Sets the title of the controller. 7 | public var title: BindingTarget { 8 | return makeBindingTarget { $0.setTitle($1) } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceDate.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceDate { 6 | /// Sets the color of the text of the date. 7 | public var textColor: BindingTarget { 8 | return makeBindingTarget { $0.setTextColor($1) } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSImageView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSImageView { 6 | /// Sets the currently displayed image 7 | public var image: BindingTarget { 8 | return makeBindingTarget { $0.image = $1 } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSCollectionView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSCollectionView { 6 | @available(macOS 10.11, *) 7 | public var reloadData: BindingTarget<()> { 8 | return makeBindingTarget { base, _ in base.reloadData() } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UINavigationBar.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UINavigationBar { 6 | /// Sets the barTintColor of the navigation bar. 7 | public var barTintColor: BindingTarget { 8 | return makeBindingTarget { $0.barTintColor = $1 } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIProgressView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIProgressView { 6 | /// Sets the relative progress to be reflected by the progress view. 7 | public var progress: BindingTarget { 8 | return makeBindingTarget { $0.progress = $1 } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/ReactiveCocoaTestsConfiguration.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | #if canImport(UIKit) 3 | import UIKit 4 | #endif 5 | 6 | class ReactiveCocoaTestsConfiguration: QuickConfiguration { 7 | override class func configure(_ configuration: Configuration) { 8 | #if canImport(UIKit) 9 | configuration.beforeSuite { 10 | UIControl._initialize() 11 | } 12 | #endif 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSSlider.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSSlider { 6 | 7 | // Provided for cross-platform compatibility 8 | 9 | public var value: BindingTarget { return floatValue } 10 | public var values: Signal { return floatValues } 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /ReactiveCocoaObjC/ObjCRuntimeAliases.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | const IMP _rac_objc_msgForward = _objc_msgForward; 5 | 6 | void _rac_objc_setAssociatedObject(const void* object, const void* key, id value, objc_AssociationPolicy policy) { 7 | __unsafe_unretained id obj = (__bridge typeof(obj)) object; 8 | objc_setAssociatedObject(obj, key, value, policy); 9 | } 10 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIApplication.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIApplication { 6 | /// Sets the number as the badge of the app icon in Springboard. 7 | public var applicationIconBadgeNumber: BindingTarget { 8 | return makeBindingTarget({ $0.applicationIconBadgeNumber = $1 }) 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/NSObject+ObjCRuntime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSObject { 4 | /// The class of the instance reported by the ObjC `-class:` message. 5 | /// 6 | /// - note: `type(of:)` might return the runtime subclass, while this property 7 | /// always returns the original class. 8 | @nonobjc internal var objcClass: AnyClass { 9 | return (self as AnyObject).objcClass 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIActivityIndicatorView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIActivityIndicatorView { 6 | /// Sets whether the activity indicator should be animating. 7 | public var isAnimating: BindingTarget { 8 | return makeBindingTarget { $1 ? $0.startAnimating() : $0.stopAnimating() } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceVolumeControl.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | @available(watchOSApplicationExtension 5.0, *) 6 | extension Reactive where Base: WKInterfaceVolumeControl { 7 | /// Sets the tint color of the volume control. 8 | public var tintColor: BindingTarget { 9 | return makeBindingTarget { $0.setTintColor($1) } 10 | } 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIFeedbackGenerator.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | @available(iOS 10.0, *) 6 | extension Reactive where Base: UIFeedbackGenerator { 7 | /// Prepares the feedback generator. 8 | public var prepare: BindingTarget<()> { 9 | return makeBindingTarget { generator, _ in 10 | generator.prepare() 11 | } 12 | } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /ReactiveCocoa/Shared/NSLayoutConstraint.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import ReactiveSwift 3 | 4 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 5 | import AppKit 6 | #else 7 | import UIKit 8 | #endif 9 | 10 | extension Reactive where Base: NSLayoutConstraint { 11 | 12 | /// Sets the constant. 13 | public var constant: BindingTarget { 14 | return makeBindingTarget { $0.constant = $1 } 15 | } 16 | 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIImpact​Feedback​Generator.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | @available(iOS 10.0, *) 6 | extension Reactive where Base: UIImpactFeedbackGenerator { 7 | /// Triggers the feedback. 8 | public var impactOccurred: BindingTarget<()> { 9 | return makeBindingTarget { generator, _ in 10 | generator.impactOccurred() 11 | } 12 | } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UISelection​Feedback​Generator.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | @available(iOS 10.0, *) 6 | extension Reactive where Base: UISelectionFeedbackGenerator { 7 | /// Triggers the feedback. 8 | public var selectionChanged: BindingTarget<()> { 9 | return makeBindingTarget { generator, _ in 10 | generator.selectionChanged() 11 | } 12 | } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UINotification​Feedback​Generator.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | @available(iOS 10.0, *) 6 | extension Reactive where Base: UINotificationFeedbackGenerator { 7 | /// Triggers the feedback. 8 | public var notificationOccurred: BindingTarget { 9 | return makeBindingTarget { $0.notificationOccurred($1) } 10 | } 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /ReactiveCocoaObjC/include/ObjCRuntimeAliases.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | extern const IMP _rac_objc_msgForward; 7 | 8 | /// A trampoline of `objc_setAssociatedObject` that is made to circumvent the 9 | /// reference counting calls in the imported version in Swift. 10 | void _rac_objc_setAssociatedObject(const void* object, const void* key, id _Nullable value, objc_AssociationPolicy policy); 11 | 12 | NS_ASSUME_NONNULL_END 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | PlaygroundUtility.remap 17 | *.xctimeline 18 | *.xcscmblueprint 19 | *.o 20 | 21 | # SwiftPM 22 | .build 23 | Packages 24 | 25 | # Carthage 26 | Carthage/Build 27 | 28 | # macOS 29 | .DS_Store 30 | 31 | # Jazzy 32 | docs 33 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceObject.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceObject { 6 | /// Sets the alpha value of the object. 7 | public var alpha: BindingTarget { 8 | return makeBindingTarget { $0.setAlpha($1) } 9 | } 10 | 11 | /// Sets whether the object is hidden. 12 | public var isHidden: BindingTarget { 13 | return makeBindingTarget { $0.setHidden($1) } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceTimer.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceTimer { 6 | /// Sets the start date of the timer. 7 | public var date: BindingTarget { 8 | return makeBindingTarget { $0.setDate($1) } 9 | } 10 | 11 | /// Sets the color of the text of the timer. 12 | public var textColor: BindingTarget { 13 | return makeBindingTarget { $0.setTextColor($1) } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSView { 6 | /// Sets the visibility of the view. 7 | public var isHidden: BindingTarget { 8 | return makeBindingTarget { $0.isHidden = $1 } 9 | } 10 | 11 | /// Sets the alpha value of the view. 12 | public var alphaValue: BindingTarget { 13 | return makeBindingTarget { $0.alphaValue = $1 } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIDatePicker.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIDatePicker { 6 | /// Sets the date of the date picker. 7 | public var date: BindingTarget { 8 | return makeBindingTarget { $0.date = $1 } 9 | } 10 | 11 | /// A signal of dates emitted by the date picker. 12 | public var dates: Signal { 13 | return mapControlEvents(.valueChanged) { $0.date } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIImageView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIImageView { 6 | /// Sets the image of the image view. 7 | public var image: BindingTarget { 8 | return makeBindingTarget { $0.image = $1 } 9 | } 10 | 11 | /// Sets the image of the image view for its highlighted state. 12 | public var highlightedImage: BindingTarget { 13 | return makeBindingTarget { $0.highlightedImage = $1 } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/Nimble"] 2 | path = Carthage/Checkouts/Nimble 3 | url = https://github.com/Quick/Nimble.git 4 | [submodule "Carthage/Checkouts/Quick"] 5 | path = Carthage/Checkouts/Quick 6 | url = https://github.com/Quick/Quick.git 7 | [submodule "Carthage/Checkouts/xcconfigs"] 8 | path = Carthage/Checkouts/xcconfigs 9 | url = https://github.com/jspahrsummers/xcconfigs.git 10 | [submodule "Carthage/Checkouts/ReactiveSwift"] 11 | path = Carthage/Checkouts/ReactiveSwift 12 | url = https://github.com/ReactiveCocoa/ReactiveSwift.git 13 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIKitReusableComponents.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import UIKit 3 | import ReactiveSwift 4 | 5 | @objc public protocol Reusable: class { 6 | func prepareForReuse() 7 | } 8 | 9 | extension Reactive where Base: NSObject, Base: Reusable { 10 | public var prepareForReuse: Signal<(), Never> { 11 | return trigger(for: #selector(base.prepareForReuse)) 12 | } 13 | } 14 | 15 | extension UITableViewCell: Reusable {} 16 | extension UITableViewHeaderFooterView: Reusable {} 17 | extension UICollectionReusableView: Reusable {} 18 | #endif 19 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceActivityRing.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | import HealthKit 5 | 6 | @available(watchOSApplicationExtension 2.2, *) 7 | extension Reactive where Base: WKInterfaceActivityRing { 8 | /// Sets the summary of the activity ring. 9 | /// 10 | /// - Parameter animated: Whether updates are animated. 11 | public func activitySummary(animated: Bool) -> BindingTarget { 12 | return makeBindingTarget { $0.setActivitySummary($1, animated: animated) } 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/AppKitReusableComponents.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSView { 6 | public var prepareForReuse: Signal<(), Never> { 7 | return trigger(for: #selector(base.prepareForReuse)) 8 | } 9 | } 10 | 11 | extension Reactive where Base: NSObject, Base: NSCollectionViewElement { 12 | @available(macOS 10.11, *) 13 | public var prepareForReuse: Signal<(), Never> { 14 | return trigger(for: #selector(base.prepareForReuse)) 15 | } 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UITabBarItem.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UITabBarItem { 6 | /// Sets the badge value of the tab bar item. 7 | public var badgeValue: BindingTarget { 8 | return makeBindingTarget { $0.badgeValue = $1 } 9 | } 10 | 11 | 12 | /// Sets the badge color of the tab bar item. 13 | @available(iOS 10, *) 14 | @available(tvOS 10, *) 15 | public var badgeColor: BindingTarget { 16 | return makeBindingTarget { $0.badgeColor = $1 } 17 | } 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/Swift4TestInteroperability.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | 4 | #if swift(>=4.0) 5 | internal typealias RACNSControlState = NSControl.StateValue 6 | internal let RACNSOnState = NSControl.StateValue.on 7 | internal let RACNSOffState = NSControl.StateValue.off 8 | internal let RACNSMixedState = NSControl.StateValue.mixed 9 | #else 10 | internal typealias RACNSControlState = Int 11 | internal let RACNSOnState = NSOnState 12 | internal let RACNSOffState = NSOffState 13 | internal let RACNSMixedState = NSMixedState 14 | #endif 15 | #endif 16 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UISegmentedControl.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UISegmentedControl { 6 | /// Changes the selected segment of the segmented control. 7 | public var selectedSegmentIndex: BindingTarget { 8 | return makeBindingTarget { $0.selectedSegmentIndex = $1 } 9 | } 10 | 11 | /// A signal of indexes of selections emitted by the segmented control. 12 | public var selectedSegmentIndexes: Signal { 13 | return mapControlEvents(.valueChanged) { $0.selectedSegmentIndex } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIBarItem.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIBarItem { 6 | /// Sets whether the bar item is enabled. 7 | public var isEnabled: BindingTarget { 8 | return makeBindingTarget { $0.isEnabled = $1 } 9 | } 10 | 11 | /// Sets image of bar item. 12 | public var image: BindingTarget { 13 | return makeBindingTarget { $0.image = $1 } 14 | } 15 | 16 | /// Sets the title of bar item. 17 | public var title: BindingTarget { 18 | return makeBindingTarget { $0.title = $1 } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIResponder.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIResponder { 6 | /// Asks UIKit to make this object the first responder in its window. 7 | public var becomeFirstResponder: BindingTarget<()> { 8 | return makeBindingTarget { base, _ in base.becomeFirstResponder() } 9 | } 10 | 11 | /// Notifies this object that it has been asked to relinquish its status as first responder in its window. 12 | public var resignFirstResponder: BindingTarget<()> { 13 | return makeBindingTarget { base, _ in base.resignFirstResponder() } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /Logo/README.md: -------------------------------------------------------------------------------- 1 | This folder contains brand assets. 2 | 3 | # Logo 4 | 5 | Four horizontal variations that include both the mark and the logotype. When 6 | using the logo in contexts where it's surrounded by other elements, leave 7 | a padding of about 10% of its height on each side. 8 | 9 | # Icon 10 | 11 | Four icon variations to be used on social media and other contexts where the 12 | horizontal logo wouldn't fit. 13 | 14 | # Colors 15 | 16 | Primary color: `#88CD79` 17 | Secondary color: `#41AD71` 18 | Tertiary color: `#4F6B97` 19 | 20 | ![](Palette.png) 21 | 22 | # Type 23 | 24 | Avenir Next, designed by Adrian Frutiger and Akira Kobayashi for Linotype. 25 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UILabel.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UILabel { 6 | /// Sets the text of the label. 7 | public var text: BindingTarget { 8 | return makeBindingTarget { $0.text = $1 } 9 | } 10 | 11 | /// Sets the attributed text of the label. 12 | public var attributedText: BindingTarget { 13 | return makeBindingTarget { $0.attributedText = $1 } 14 | } 15 | 16 | /// Sets the color of the text of the label. 17 | public var textColor: BindingTarget { 18 | return makeBindingTarget { $0.textColor = $1 } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceLabel.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceLabel { 6 | /// Sets the text of the label. 7 | public var text: BindingTarget { 8 | return makeBindingTarget { $0.setText($1) } 9 | } 10 | 11 | /// Sets the attributed text of the label. 12 | public var attributedText: BindingTarget { 13 | return makeBindingTarget { $0.setAttributedText($1) } 14 | } 15 | 16 | /// Sets the color of the text of the label. 17 | public var textColor: BindingTarget { 18 | return makeBindingTarget { $0.setTextColor($1) } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfacePicker.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfacePicker { 6 | /// Sets the list of items of the picker. 7 | public var items: BindingTarget<[WKPickerItem]?> { 8 | return makeBindingTarget { $0.setItems($1) } 9 | } 10 | 11 | /// Sets the selected item index of the picker. 12 | public var selectedItemIndex: BindingTarget { 13 | return makeBindingTarget { $0.setSelectedItemIndex($1) } 14 | } 15 | 16 | /// Sets whether the picker is enabled. 17 | public var isEnabled: BindingTarget { 18 | return makeBindingTarget { $0.setEnabled($1) } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /ReactiveCocoa/Deprecations+Removals.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | extension Action { 5 | @available(*, unavailable, message:"Use the `CocoaAction` initializers instead.") 6 | public var unsafeCocoaAction: CocoaAction { fatalError() } 7 | } 8 | 9 | extension Reactive where Base: NSObject { 10 | @available(*, deprecated, renamed: "producer(forKeyPath:)") 11 | public func values(forKeyPath keyPath: String) -> SignalProducer { 12 | return producer(forKeyPath: keyPath) 13 | } 14 | } 15 | 16 | #if os(watchOS) 17 | import WatchKit 18 | extension Reactive where Base: WKInterfaceButton { 19 | @available(*, deprecated, renamed: "title") 20 | public var text: BindingTarget { 21 | return title 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIResponderSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIResponderSpec: QuickSpec { 9 | override func spec() { 10 | it("should become and resign first responder") { 11 | let window = UIWindow(frame: .zero) 12 | let textField = UITextField(frame: .zero) 13 | window.addSubview(textField) 14 | 15 | expect(textField.isFirstResponder).to(beFalse()) 16 | textField.reactive.becomeFirstResponder <~ SignalProducer(value: ()) 17 | expect(textField.isFirstResponder).to(beTrue()) 18 | textField.reactive.resignFirstResponder <~ SignalProducer(value: ()) 19 | expect(textField.isFirstResponder).to(beFalse()) 20 | } 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceImage.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceImage { 6 | /// Sets the image. 7 | public var image: BindingTarget { 8 | return makeBindingTarget { $0.setImage($1) } 9 | } 10 | 11 | /// Sets the data of the image. 12 | public var imageData: BindingTarget { 13 | return makeBindingTarget { $0.setImageData($1) } 14 | } 15 | 16 | /// Sets the name of the image. 17 | public var imageNamed: BindingTarget { 18 | return makeBindingTarget { $0.setImageNamed($1) } 19 | } 20 | 21 | /// Sets the tint color of the template image. 22 | public var tintColor: BindingTarget { 23 | return makeBindingTarget { $0.setTintColor($1) } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /ReactiveMapKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReactiveCocoa/ObjC+Runtime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Search in `class` for any method that matches the supplied selector without 4 | /// propagating to the ancestors. 5 | /// 6 | /// - parameters: 7 | /// - class: The class to search the method in. 8 | /// - selector: The selector of the method. 9 | /// 10 | /// - returns: The matching method, or `nil` if none is found. 11 | internal func class_getImmediateMethod(_ `class`: AnyClass, _ selector: Selector) -> Method? { 12 | var total: UInt32 = 0 13 | 14 | if let methods = class_copyMethodList(`class`, &total) { 15 | defer { free(methods) } 16 | 17 | for index in 0 ..< Int(total) { 18 | let method = methods[index] 19 | 20 | if method_getName(method) == selector { 21 | return method 22 | } 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceSlider.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceSlider { 6 | /// Sets the value of the slider. 7 | public var value: BindingTarget { 8 | return makeBindingTarget { $0.setValue($1) } 9 | } 10 | 11 | /// Sets the number of steps of the slider. 12 | public var numberOfSteps: BindingTarget { 13 | return makeBindingTarget { $0.setNumberOfSteps($1) } 14 | } 15 | 16 | /// Sets the color of the slider. 17 | public var color: BindingTarget { 18 | return makeBindingTarget { $0.setColor($1) } 19 | } 20 | 21 | /// Sets whether the slider is enabled. 22 | public var isEnabled: BindingTarget { 23 | return makeBindingTarget { $0.setEnabled($1) } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceMovie.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceMovie { 6 | /// Sets the url of the movie. 7 | public var movieURL: BindingTarget { 8 | return makeBindingTarget { $0.setMovieURL($1) } 9 | } 10 | 11 | /// Sets the video gravity of the movie. 12 | public var videoGravity: BindingTarget { 13 | return makeBindingTarget { $0.setVideoGravity($1) } 14 | } 15 | 16 | /// Sets the poster image of the movie. 17 | public var posterImage: BindingTarget { 18 | return makeBindingTarget { $0.setPosterImage($1) } 19 | } 20 | 21 | /// Whether the movie loops. 22 | public var loops: BindingTarget { 23 | return makeBindingTarget { $0.setLoops($1) } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /ReactiveCocoa-iOS.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | > ## IMPORTANT: To use `ReactiveCocoa-iOS.playground`, please: 3 | 4 | 1. Retrieve the project dependencies using one of the following terminal commands from the ReactiveCocoa project root directory: 5 | - `git submodule update --init --recursive` 6 | **OR**, if you have [Carthage](https://github.com/Carthage/Carthage) installed 7 | - `carthage checkout --no-use-binaries` 8 | 1. Open `ReactiveCocoa.xcworkspace` 9 | 1. Build `ReactiveSwift-iOS` scheme 10 | 1. Build `ReactiveCocoa-iOS` scheme 11 | 1. Finally open the `ReactiveCocoa-iOS.playground` 12 | 1. Choose `View > Show Debug Area` 13 | */ 14 | 15 | import ReactiveCocoa 16 | import ReactiveSwift 17 | import UIKit 18 | 19 | /*: 20 | ## Sandbox 21 | 22 | A place where you can build your sand castles 🏖. 23 | */ 24 | -------------------------------------------------------------------------------- /ReactiveCocoa-tvOS.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | > ## IMPORTANT: To use `ReactiveCocoa-tvOS.playground`, please: 3 | 4 | 1. Retrieve the project dependencies using one of the following terminal commands from the ReactiveCocoa project root directory: 5 | - `git submodule update --init --recursive` 6 | **OR**, if you have [Carthage](https://github.com/Carthage/Carthage) installed 7 | - `carthage checkout --no-use-binaries` 8 | 1. Open `ReactiveCocoa.xcworkspace` 9 | 1. Build `ReactiveSwift-tvOS` scheme 10 | 1. Build `ReactiveCocoa-tvOS` scheme 11 | 1. Finally open the `ReactiveCocoa-tvOS.playground` 12 | 1. Choose `View > Show Debug Area` 13 | */ 14 | 15 | import ReactiveCocoa 16 | import ReactiveSwift 17 | import UIKit 18 | 19 | /*: 20 | ## Sandbox 21 | 22 | A place where you can build your sand castles 🏖. 23 | */ 24 | -------------------------------------------------------------------------------- /ReactiveCocoa.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReactiveCocoa-macOS.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | > ## IMPORTANT: To use `ReactiveCocoa-macOS.playground`, please: 3 | 4 | 1. Retrieve the project dependencies using one of the following terminal commands from the ReactiveCocoa project root directory: 5 | - `git submodule update --init --recursive` 6 | **OR**, if you have [Carthage](https://github.com/Carthage/Carthage) installed 7 | - `carthage checkout --no-use-binaries` 8 | 1. Open `ReactiveCocoa.xcworkspace` 9 | 1. Build `ReactiveSwift-macOS` scheme 10 | 1. Build `ReactiveCocoa-macOS` scheme 11 | 1. Finally open the `ReactiveCocoa-macOS.playground` 12 | 1. Choose `View > Show Debug Area` 13 | */ 14 | 15 | import ReactiveCocoa 16 | import ReactiveSwift 17 | import AppKit 18 | 19 | /*: 20 | ## Sandbox 21 | 22 | A place where you can build your sand castles 🏖. 23 | */ 24 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 6.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /script/update-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "$1" ]]; then 4 | echo "Please specify a version tag." 5 | exit 6 | fi 7 | 8 | PRERELEASE_STRIPPED=$(echo "$1" | perl -0777 -ne '/([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?/ and print "$1.$2.$3"') 9 | 10 | if [[ -z "$PRERELEASE_STRIPPED" ]]; then 11 | echo "The version tag is not semver compliant." 12 | exit 13 | fi 14 | 15 | CURRENT_TAG=$(perl -0777 -ne '/s.version([\s]+)=([\s]+)"(.+)"/ and print $3 and last' *.podspec) 16 | echo "Current tag: $CURRENT_TAG" 17 | 18 | perl -0777 -i -pe 's/s.version([\s]+)=([\s]+)"'${CURRENT_TAG}'"/s.version$1=$2"'${1}'"/' *.podspec 19 | perl -0777 -i -pe 's/g>'${CURRENT_TAG}'<\/str/g>'${PRERELEASE_STRIPPED}'<\/str/' */Info.plist 20 | perl -0777 -i -pe 's/g>'${CURRENT_TAG}'<\/str/g>'${PRERELEASE_STRIPPED}'<\/str/' */*/Info.plist 21 | sed -i '' '3i\ 22 | \ 23 | # '${1} CHANGELOG.md 24 | 25 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIStepper.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import UIKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: UIStepper { 6 | 7 | /// Sets the stepper's value. 8 | public var value: BindingTarget { 9 | return makeBindingTarget { $0.value = $1 } 10 | } 11 | 12 | /// Sets stepper's minimum value. 13 | public var minimumValue: BindingTarget { 14 | return makeBindingTarget { $0.minimumValue = $1 } 15 | } 16 | 17 | /// Sets stepper's maximum value. 18 | public var maximumValue: BindingTarget { 19 | return makeBindingTarget { $0.maximumValue = $1 } 20 | } 21 | 22 | /// A signal of double values emitted by the stepper upon each user's 23 | /// interaction. 24 | public var values: Signal { 25 | return mapControlEvents(.valueChanged) { $0.value } 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /ReactiveMapKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 6.0.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIGestureRecognizer { 6 | /// Create a signal which sends a `next` event for each gesture event 7 | /// 8 | /// - returns: A trigger signal. 9 | public var stateChanged: Signal { 10 | return Signal { observer, signalLifetime in 11 | let receiver = CocoaTarget(observer) { gestureRecognizer in 12 | return gestureRecognizer as! Base 13 | } 14 | base.addTarget(receiver, action: #selector(receiver.invoke)) 15 | 16 | let disposable = lifetime.ended.observeCompleted(observer.sendCompleted) 17 | 18 | signalLifetime.observeEnded { [weak base] in 19 | disposable?.dispose() 20 | base?.removeTarget(receiver, action: #selector(receiver.invoke)) 21 | } 22 | } 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UISegmentedControlSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UISegmentedControlSpec: QuickSpec { 9 | override func spec() { 10 | it("should accept changes from bindings to its selected segment index") { 11 | let s = UISegmentedControl(items: ["0", "1", "2"]) 12 | s.selectedSegmentIndex = UISegmentedControl.noSegment 13 | expect(s.numberOfSegments) == 3 14 | 15 | let (pipeSignal, observer) = Signal.pipe() 16 | s.reactive.selectedSegmentIndex <~ SignalProducer(pipeSignal) 17 | 18 | expect(s.selectedSegmentIndex) == UISegmentedControl.noSegment 19 | 20 | observer.send(value: 1) 21 | expect(s.selectedSegmentIndex) == 1 22 | 23 | observer.send(value: 2) 24 | expect(s.selectedSegmentIndex) == 2 25 | } 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /ReactiveMapKit/MKLocalSearchRequest.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | import ReactiveCocoa 3 | import MapKit 4 | 5 | private let defaultLocalSearchError = NSError(domain: "org.reactivecocoa.ReactiveCocoa.Reactivity.MKLocalSearchRequest", 6 | code: 1, 7 | userInfo: nil) 8 | @available(tvOS 9.2, *) 9 | extension Reactive where Base: MKLocalSearch.Request { 10 | /// A SignalProducer which performs an `MKLocalSearch`. 11 | public var search: SignalProducer { 12 | return SignalProducer {[base = self.base] observer, lifetime in 13 | let search = MKLocalSearch(request: base) 14 | search.start { response, error in 15 | if let response = response { 16 | observer.send(value: response) 17 | observer.sendCompleted() 18 | } else { 19 | observer.send(error: error ?? defaultLocalSearchError) 20 | } 21 | } 22 | lifetime.observeEnded(search.cancel) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/DeprecationsSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveCocoa 3 | import ReactiveSwift 4 | import Quick 5 | import Nimble 6 | 7 | class DeprecationsSpec: QuickSpec { 8 | override func spec() { 9 | describe("NSObject.reactive.values(forKeyPath:)") { 10 | class TestKVOObject: NSObject { 11 | @objc dynamic var value: Int = 0 12 | } 13 | 14 | it("should observe the initial value and changes for the key path") { 15 | let object = TestKVOObject() 16 | var values: [Int] = [] 17 | 18 | object.reactive.producer(forKeyPath: #keyPath(TestKVOObject.value)).startWithValues { value in 19 | values.append(value as! Int) 20 | } 21 | 22 | expect(values) == [0] 23 | 24 | object.value = 1 25 | expect(values) == [0, 1] 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSSegmentedControl.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSSegmentedControl { 6 | /// Changes the selected segment of the segmented control. 7 | public var selectedSegment: BindingTarget { 8 | return makeBindingTarget { $0.selectedSegment = $1 } 9 | } 10 | 11 | /// A signal of indexes of selections emitted by the segmented control. 12 | public var selectedSegments: Signal { 13 | return proxy.invoked.map { $0.selectedSegment } 14 | } 15 | 16 | /// The below are provided for cross-platform compatibility 17 | 18 | /// Changes the selected segment of the segmented control. 19 | public var selectedSegmentIndex: BindingTarget { return selectedSegment } 20 | /// A signal of indexes of selections emitted by the segmented control. 21 | public var selectedSegmentIndexes: Signal { return selectedSegments } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UISwitch.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UISwitch { 6 | /// The action to be triggered when the switch is changed. It also controls 7 | /// the enabled state of the switch 8 | public var toggled: CocoaAction? { 9 | get { 10 | return associatedAction.withValue { info in 11 | return info.flatMap { info in 12 | return info.controlEvents == .valueChanged ? info.action : nil 13 | } 14 | } 15 | } 16 | 17 | nonmutating set { 18 | setAction(newValue, for: .valueChanged) 19 | } 20 | } 21 | /// Sets the on-off state of the switch. 22 | public var isOn: BindingTarget { 23 | return makeBindingTarget { $0.isOn = $1 } 24 | } 25 | 26 | /// A signal of on-off states in `Bool` emitted by the switch. 27 | public var isOnValues: Signal { 28 | return mapControlEvents(.valueChanged) { $0.isOn } 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceSwitch.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceSwitch { 6 | /// Sets the title of the switch. 7 | public var title: BindingTarget { 8 | return makeBindingTarget { $0.setTitle($1) } 9 | } 10 | 11 | /// Sets the attributed title of the switch. 12 | public var attributedTitle: BindingTarget { 13 | return makeBindingTarget { $0.setAttributedTitle($1) } 14 | } 15 | 16 | /// Sets the color of the switch. 17 | public var color: BindingTarget { 18 | return makeBindingTarget { $0.setColor($1) } 19 | } 20 | 21 | /// Sets whether the switch is on. 22 | public var isOn: BindingTarget { 23 | return makeBindingTarget { $0.setOn($1) } 24 | } 25 | 26 | /// Sets whether the switch is enabled. 27 | public var isEnabled: BindingTarget { 28 | return makeBindingTarget { $0.setEnabled($1) } 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIView { 6 | /// Sets the alpha value of the view. 7 | public var alpha: BindingTarget { 8 | return makeBindingTarget { $0.alpha = $1 } 9 | } 10 | 11 | /// Sets whether the view is hidden. 12 | public var isHidden: BindingTarget { 13 | return makeBindingTarget { $0.isHidden = $1 } 14 | } 15 | 16 | /// Sets whether the view accepts user interactions. 17 | public var isUserInteractionEnabled: BindingTarget { 18 | return makeBindingTarget { $0.isUserInteractionEnabled = $1 } 19 | } 20 | 21 | /// Sets the background color of the view. 22 | public var backgroundColor: BindingTarget { 23 | return makeBindingTarget { $0.backgroundColor = $1 } 24 | } 25 | 26 | /// Sets the tintColor of the view 27 | public var tintColor: BindingTarget { 28 | return makeBindingTarget { $0.tintColor = $1 } 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UISlider.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import UIKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: UISlider { 6 | 7 | /// Sets slider's value. 8 | public var value: BindingTarget { 9 | return makeBindingTarget { $0.value = $1 } 10 | } 11 | 12 | /// Sets slider's minimum value. 13 | public var minimumValue: BindingTarget { 14 | return makeBindingTarget { $0.minimumValue = $1 } 15 | } 16 | 17 | /// Sets slider's maximum value. 18 | public var maximumValue: BindingTarget { 19 | return makeBindingTarget { $0.maximumValue = $1 } 20 | } 21 | 22 | /// A signal of float values emitted by the slider while being dragged by 23 | /// the user. 24 | /// 25 | /// - note: If slider's `isContinuous` property is `false` then values are 26 | /// sent only when user releases the slider. 27 | public var values: Signal { 28 | return mapControlEvents(.valueChanged) { $0.value } 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/NSViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import AppKit 7 | 8 | class NSViewSpec: QuickSpec { 9 | override func spec() { 10 | describe("NSView") { 11 | var view: NSView! 12 | 13 | beforeEach { 14 | view = NSView() 15 | } 16 | 17 | it("should allow binding of `isHidden` property") { 18 | let (hSignal, hSink) = Signal.pipe() 19 | expect(view.isHidden) == false 20 | 21 | view.reactive.isHidden <~ hSignal 22 | hSink.send(value: true) 23 | 24 | expect(view.isHidden) == true 25 | } 26 | 27 | it("should allow binding of `alphaValue` property") { 28 | let (avSignal, avSink) = Signal.pipe() 29 | expect(view.alphaValue) == 1.0 30 | 31 | view.reactive.alphaValue <~ avSignal 32 | avSink.send(value: 0.5) 33 | 34 | expect(view.alphaValue) == 0.5 35 | } 36 | } 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/TestError.swift: -------------------------------------------------------------------------------- 1 | import ReactiveCocoa 2 | import ReactiveSwift 3 | 4 | internal enum TestError: Int { 5 | case `default` = 0 6 | case error1 = 1 7 | case error2 = 2 8 | } 9 | 10 | extension TestError: Error { 11 | } 12 | 13 | 14 | internal extension SignalProducer { 15 | /// Halts if an error is emitted in the receiver signal. 16 | /// This is useful in tests to be able to just use `startWithNext` 17 | /// in cases where we know that an error won't be emitted. 18 | func assumeNoErrors() -> SignalProducer { 19 | return lift { $0.assumeNoErrors() } 20 | } 21 | } 22 | 23 | internal extension Signal { 24 | /// Halts if an error is emitted in the receiver signal. 25 | /// This is useful in tests to be able to just use `startWithNext` 26 | /// in cases where we know that an error won't be emitted. 27 | func assumeNoErrors() -> Signal { 28 | return mapError { error in 29 | fatalError("Unexpected error: \(error)") 30 | 31 | () 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ReactiveMapKit/MKMapView.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | import ReactiveCocoa 3 | import MapKit 4 | 5 | @available(tvOS 9.2, *) 6 | extension Reactive where Base: MKMapView { 7 | 8 | /// Sets the map type. 9 | public var mapType: BindingTarget { 10 | return makeBindingTarget { $0.mapType = $1 } 11 | } 12 | 13 | /// Sets if zoom is enabled for map. 14 | public var isZoomEnabled: BindingTarget { 15 | return makeBindingTarget { $0.isZoomEnabled = $1 } 16 | } 17 | 18 | /// Sets if scroll is enabled for map. 19 | public var isScrollEnabled: BindingTarget { 20 | return makeBindingTarget { $0.isScrollEnabled = $1 } 21 | } 22 | 23 | #if !os(tvOS) 24 | /// Sets if pitch is enabled for map. 25 | public var isPitchEnabled: BindingTarget { 26 | return makeBindingTarget { $0.isPitchEnabled = $1 } 27 | } 28 | 29 | /// Sets if rotation is enabled for map. 30 | public var isRotateEnabled: BindingTarget { 31 | return makeBindingTarget { $0.isRotateEnabled = $1 } 32 | } 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /ReactiveCocoa/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 10.2.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2014 GitHub. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceInlineMovie.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | @available(watchOSApplicationExtension 3.0, *) 6 | extension Reactive where Base: WKInterfaceInlineMovie { 7 | /// Sets the url of the movie. 8 | public var movieURL: BindingTarget { 9 | return makeBindingTarget { $0.setMovieURL($1) } 10 | } 11 | 12 | /// Sets the video gravity of the movie. 13 | public var videoGravity: BindingTarget { 14 | return makeBindingTarget { $0.setVideoGravity($1) } 15 | } 16 | 17 | /// Sets the poster image of the movie. 18 | public var posterImage: BindingTarget { 19 | return makeBindingTarget { $0.setPosterImage($1) } 20 | } 21 | 22 | /// Whether the movie loops. 23 | public var loops: BindingTarget { 24 | return makeBindingTarget { $0.setLoops($1) } 25 | } 26 | 27 | /// Whether the movie autoplays. 28 | public var autoplays: BindingTarget { 29 | return makeBindingTarget { $0.setAutoplays($1) } 30 | } 31 | } 32 | #endif 33 | -------------------------------------------------------------------------------- /ReactiveCocoaObjCTestSupport/MessageForwardingEntity.m: -------------------------------------------------------------------------------- 1 | #import "MessageForwardingEntity.h" 2 | #pragma GCC diagnostic ignored "-Wundeclared-selector" 3 | 4 | @implementation MessageForwardingEntity 5 | 6 | - (instancetype) init { 7 | if (self = [super init]) { 8 | self.hasInvoked = NO; 9 | } 10 | return self; 11 | } 12 | 13 | - (BOOL) respondsToSelector:(SEL)aSelector { 14 | if (aSelector == @selector(_rac_test_forwarding)) { 15 | return YES; 16 | } 17 | return [super respondsToSelector:aSelector]; 18 | } 19 | 20 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { 21 | if (aSelector == @selector(_rac_test_forwarding)) { 22 | return [NSMethodSignature signatureWithObjCTypes:"v@:"]; 23 | } 24 | return [super methodSignatureForSelector:aSelector]; 25 | } 26 | 27 | - (void)forwardInvocation:(NSInvocation *)anInvocation { 28 | if (anInvocation.selector == @selector(_rac_test_forwarding)) { 29 | [self setHasInvoked:YES]; 30 | return; 31 | } 32 | return [super forwardInvocation:anInvocation]; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIActivityIndicatorViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | import Quick 4 | import Nimble 5 | import ReactiveSwift 6 | import ReactiveCocoa 7 | 8 | class UIActivityIndicatorSpec: QuickSpec { 9 | override func spec() { 10 | var activityIndicatorView: UIActivityIndicatorView! 11 | weak var _activityIndicatorView: UIActivityIndicatorView? 12 | 13 | beforeEach { 14 | activityIndicatorView = UIActivityIndicatorView(frame: .zero) 15 | _activityIndicatorView = activityIndicatorView 16 | } 17 | 18 | afterEach { 19 | activityIndicatorView = nil 20 | expect(_activityIndicatorView).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its animating state") { 24 | let (pipeSignal, observer) = Signal.pipe() 25 | activityIndicatorView.reactive.isAnimating <~ SignalProducer(pipeSignal) 26 | 27 | observer.send(value: true) 28 | expect(activityIndicatorView.isAnimating) == true 29 | 30 | observer.send(value: false) 31 | expect(activityIndicatorView.isAnimating) == false 32 | } 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /ReactiveMapKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveMapKit" 3 | s.version = "10.2.0" 4 | s.summary = "MapKit bindings for ReactiveCocoa." 5 | s.description = <<-DESC 6 | Provide MapKit bindings for ReactiveCocoa. ReactiveCocoa (RAC) is a Cocoa framework built on top of ReactiveSwift. It provides APIs for using ReactiveSwift with Apple's Cocoa frameworks. 7 | DESC 8 | s.homepage = "https://github.com/ReactiveCocoa/ReactiveCocoa" 9 | s.license = { :type => "MIT", :file => "LICENSE.md" } 10 | s.author = "ReactiveCocoa" 11 | 12 | s.osx.deployment_target = "10.9" 13 | s.ios.deployment_target = "8.0" 14 | s.tvos.deployment_target = "9.0" 15 | 16 | s.source = { :git => "https://github.com/ReactiveCocoa/ReactiveCocoa.git", :tag => "#{s.version}" } 17 | s.source_files = "ReactiveMapKit/*.{swift,h,m}" 18 | 19 | s.dependency 'ReactiveCocoa', "#{s.version}" 20 | 21 | s.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS[config=Release]" => "$(inherited) -suppress-warnings" } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIProgressViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIProgressViewSpec: QuickSpec { 9 | override func spec() { 10 | var progressView: UIProgressView! 11 | weak var _progressView: UIProgressView? 12 | 13 | beforeEach { 14 | progressView = UIProgressView(frame: .zero) 15 | _progressView = progressView 16 | } 17 | 18 | afterEach { 19 | progressView = nil 20 | expect(_progressView).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its progress value") { 24 | let firstChange: Float = 0.5 25 | let secondChange: Float = 0.0 26 | 27 | progressView.progress = 1.0 28 | 29 | let (pipeSignal, observer) = Signal.pipe() 30 | progressView.reactive.progress <~ SignalProducer(pipeSignal) 31 | 32 | observer.send(value: firstChange) 33 | expect(progressView.progress) ≈ firstChange 34 | 35 | observer.send(value: secondChange) 36 | expect(progressView.progress) ≈ secondChange 37 | } 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/Shared/NSLayoutConstraintSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) || canImport(UIKit) 2 | 3 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 4 | import AppKit 5 | #elseif canImport(UIKit) 6 | import UIKit 7 | #endif 8 | 9 | import ReactiveSwift 10 | import ReactiveCocoa 11 | import Quick 12 | import Nimble 13 | 14 | class NSLayoutConstraintSpec: QuickSpec { 15 | override func spec() { 16 | var constraint: NSLayoutConstraint! 17 | weak var _constraint: NSLayoutConstraint? 18 | 19 | beforeEach { 20 | constraint = NSLayoutConstraint() 21 | _constraint = constraint 22 | } 23 | 24 | afterEach { 25 | constraint = nil 26 | expect(_constraint).to(beNil()) 27 | } 28 | 29 | it("should accept changes from bindings to its constant") { 30 | expect(constraint.constant).to(equal(0.0)) 31 | 32 | let (pipeSignal, observer) = Signal.pipe() 33 | 34 | constraint.reactive.constant <~ pipeSignal 35 | 36 | observer.send(value: 5.0) 37 | expect(constraint.constant) ≈ 5.0 38 | 39 | observer.send(value: -3.0) 40 | expect(constraint.constant) ≈ -3.0 41 | } 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2012 - 2016, GitHub, Inc.** 2 | **All rights reserved.** 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIRefreshControl.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIRefreshControl { 6 | /// Sets whether the refresh control should be refreshing. 7 | public var isRefreshing: BindingTarget { 8 | return makeBindingTarget { $1 ? $0.beginRefreshing() : $0.endRefreshing() } 9 | } 10 | 11 | /// Sets the attributed title of the refresh control. 12 | public var attributedTitle: BindingTarget { 13 | return makeBindingTarget { $0.attributedTitle = $1 } 14 | } 15 | 16 | /// The action to be triggered when the refresh control is refreshed. It 17 | /// also controls the enabled and refreshing states of the refresh control. 18 | public var refresh: CocoaAction? { 19 | get { 20 | return associatedAction.withValue { info in 21 | return info.flatMap { info in 22 | return info.controlEvents == .valueChanged ? info.action : nil 23 | } 24 | } 25 | } 26 | 27 | nonmutating set { 28 | let disposable = newValue.flatMap { isRefreshing <~ $0.isExecuting } 29 | setAction(newValue, for: .valueChanged, disposable: disposable) 30 | } 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love that you're interested in contributing to this project! 2 | 3 | To make the process as painless as possible, we have just a couple of guidelines 4 | that should make life easier for everyone involved. 5 | 6 | ## Prefer Pull Requests 7 | 8 | If you know exactly how to implement the feature being suggested or fix the bug 9 | being reported, please open a pull request instead of an issue. Pull requests are easier than 10 | patches or inline code blocks for discussing and merging the changes. 11 | 12 | If you can't make the change yourself, please open an issue after making sure 13 | that one isn't already logged. We are also happy to help you in our Slack room (ping [@ReactiveCocoa](https://twitter.com/ReactiveCocoa) for an invitation). 14 | 15 | ## Contributing Code 16 | 17 | Fork this repository, make it awesomer (preferably in a branch named for the 18 | topic), send a pull request! 19 | 20 | All code contributions should match our coding conventions ([Objective-c](https://github.com/github/objective-c-conventions) and [Swift](https://github.com/github/swift-style-guide)). If your particular case is not described in the coding convention, check the ReactiveCocoa codebase. 21 | 22 | Thanks for contributing! :boom::camel: 23 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUILD_DIRECTORY="build" 4 | CONFIGURATION=Release 5 | XCODE_WORKSPACE=ReactiveCocoa.xcworkspace 6 | 7 | if [[ -z $XCODE_SCHEME ]]; then 8 | echo "Error: \$XCODE_SCHEME is not set!" 9 | exit 1 10 | fi 11 | 12 | if [[ -z $XCODE_ACTION ]]; then 13 | echo "Error: \$XCODE_ACTION is not set!" 14 | exit 1 15 | fi 16 | 17 | if [[ -z $XCODE_SDK ]]; then 18 | echo "Error: \$XCODE_SDK is not set!" 19 | exit 1 20 | fi 21 | 22 | if [[ -z $XCODE_DESTINATION ]]; then 23 | echo "Error: \$XCODE_DESTINATION is not set!" 24 | exit 1 25 | fi 26 | 27 | set -o pipefail 28 | xcodebuild $XCODE_ACTION \ 29 | -workspace "$XCODE_WORKSPACE" \ 30 | -scheme "$XCODE_SCHEME" \ 31 | -sdk "$XCODE_SDK" \ 32 | -destination "$XCODE_DESTINATION" \ 33 | -derivedDataPath "${BUILD_DIRECTORY}" \ 34 | -configuration $CONFIGURATION \ 35 | ENABLE_TESTABILITY=YES \ 36 | GCC_GENERATE_DEBUGGING_SYMBOLS=NO \ 37 | RUN_CLANG_STATIC_ANALYZER=NO | xcpretty 38 | result=$? 39 | 40 | if [ "$result" -ne 0 ]; then 41 | exit $result 42 | fi 43 | 44 | # Compile code in playgrounds 45 | if [[ "$XCODE_PLAYGROUND_TARGET" ]]; then 46 | echo "Validating playground..." 47 | . script/validate-playground.sh 48 | fi 49 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSTextView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSTextView { 6 | private var notifications: Signal { 7 | #if swift(>=4.0) 8 | let name = NSTextView.didChangeNotification 9 | #else 10 | let name = Notification.Name.NSControlTextDidChange 11 | #endif 12 | 13 | return NotificationCenter.default 14 | .reactive 15 | .notifications(forName: name, object: base) 16 | .take(during: lifetime) 17 | } 18 | 19 | /// A signal of values in `String` from the text field upon any changes. 20 | public var continuousStringValues: Signal { 21 | return notifications 22 | .map { notification in 23 | let textView = notification.object as! NSTextView 24 | #if swift(>=4.0) 25 | return textView.string 26 | #else 27 | return textView.string ?? "" 28 | #endif 29 | } 30 | } 31 | 32 | /// A signal of values in `NSAttributedString` from the text field upon any 33 | /// changes. 34 | public var continuousAttributedStringValues: Signal { 35 | return notifications 36 | .map { ($0.object as! NSTextView).attributedString() } 37 | } 38 | 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /ReactiveCocoa/ReactiveSwift+Lifetime.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | 3 | extension Signal { 4 | /// Forward events from `self` until `object` deinitializes, at which point the 5 | /// returned signal will complete. 6 | /// 7 | /// - parameters: 8 | /// - object: An object of which the deinitialization would complete the returned 9 | /// `Signal`. Both Objective-C and native Swift objects are supported. 10 | /// 11 | /// - returns: A signal that will deliver events until `object` deinitializes. 12 | public func take(duringLifetimeOf object: AnyObject) -> Signal { 13 | return take(during: Lifetime.of(object)) 14 | } 15 | } 16 | 17 | extension SignalProducer { 18 | /// Forward events from `self` until `object` deinitializes, at which point the 19 | /// returned producer will complete. 20 | /// 21 | /// - parameters: 22 | /// - object: An object of which the deinitialization would complete the returned 23 | /// `Signal`. Both Objective-C and native Swift objects are supported. 24 | /// 25 | /// - returns: A producer that will deliver events until `object` deinitializes. 26 | public func take(duringLifetimeOf object: AnyObject) -> SignalProducer { 27 | return take(during: Lifetime.of(object)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceGroup.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceGroup { 6 | /// Sets the background color of the group. 7 | public var backgroundColor: BindingTarget { 8 | return makeBindingTarget { $0.setBackgroundColor($1) } 9 | } 10 | 11 | /// Sets the background image of the group. 12 | public var backgroundImage: BindingTarget { 13 | return makeBindingTarget { $0.setBackgroundImage($1) } 14 | } 15 | 16 | /// Sets the background image data of the group. 17 | public var backgroundImageData: BindingTarget { 18 | return makeBindingTarget { $0.setBackgroundImageData($1) } 19 | } 20 | 21 | /// Sets the background named image of the group. 22 | public var backgroundImageNamed: BindingTarget { 23 | return makeBindingTarget { $0.setBackgroundImageNamed($1) } 24 | } 25 | 26 | /// Sets the corner radius of the group. 27 | public var cornerRadius: BindingTarget { 28 | return makeBindingTarget { $0.setCornerRadius($1) } 29 | } 30 | 31 | /// Sets the content inset of the group. 32 | public var contentInset: BindingTarget { 33 | return makeBindingTarget { $0.setContentInset($1) } 34 | } 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/AppKitReusableComponentsSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import AppKit 7 | 8 | class ReusableComponentsSpec: QuickSpec { 9 | override func spec() { 10 | describe("NSTableCellView") { 11 | it("should send a `value` event when `prepareForReuse` is triggered") { 12 | let cell = NSTableCellView() 13 | 14 | var isTriggered = false 15 | cell.reactive.prepareForReuse.observeValues { 16 | isTriggered = true 17 | } 18 | 19 | expect(isTriggered) == false 20 | 21 | cell.prepareForReuse() 22 | expect(isTriggered) == true 23 | } 24 | } 25 | 26 | if #available(macOS 10.11, *) { 27 | describe("NSCollectionViewItem") { 28 | it("should send a `value` event when `prepareForReuse` is triggered") { 29 | let item = TestViewItem() 30 | 31 | var isTriggered = false 32 | item.reactive.prepareForReuse.observeValues { 33 | isTriggered = true 34 | } 35 | 36 | expect(isTriggered) == false 37 | 38 | item.prepareForReuse() 39 | expect(isTriggered) == true 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | private class TestViewItem: NSCollectionViewItem { 47 | override func loadView() { 48 | view = NSView() 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /script/validate-playground.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Bash script to lint the content of playgrounds 4 | # Heavily based on RxSwift's 5 | # https://github.com/ReactiveX/RxSwift/blob/master/scripts/validate-playgrounds.sh 6 | 7 | if [ -z "$BUILD_DIRECTORY" ]; then 8 | echo "\$BUILD_DIRECTORY is not set. Are you trying to run \`validate-playgrounds.sh\` without building RAC first?\n" 9 | echo "To validate the playground, run \`script/build\`." 10 | exit 1 11 | fi 12 | 13 | if [ -z "$PLAYGROUND" ]; then 14 | echo "\$PLAYGROUND is not set." 15 | exit 1 16 | fi 17 | 18 | if [ -z "$XCODE_PLAYGROUND_TARGET" ]; then 19 | echo "\$XCODE_PLAYGROUND_TARGET is not set." 20 | exit 1 21 | fi 22 | 23 | BUILD_DIR_PATH= 24 | 25 | if [ "$XCODE_SDK" == "macosx" ]; then 26 | BUILD_DIR_PATH=Products/${CONFIGURATION} 27 | else 28 | BUILD_DIR_PATH=Intermediates/CodeCoverage/Products/${CONFIGURATION}-${XCODE_SDK} 29 | fi 30 | 31 | SDK_ROOT=$(xcrun --sdk ${XCODE_SDK} --show-sdk-path) 32 | PAGES_PATH=${BUILD_DIRECTORY}/Build/${BUILD_DIR_PATH}/all-playground-pages.swift 33 | 34 | cat ${PLAYGROUND}/Sources/*.swift ${PLAYGROUND}/Pages/**/*.swift > ${PAGES_PATH} 35 | 36 | swift -v -target ${XCODE_PLAYGROUND_TARGET} -sdk ${SDK_ROOT} -D NOT_IN_PLAYGROUND -F ${BUILD_DIRECTORY}/Build/${BUILD_DIR_PATH} ${PAGES_PATH} > /dev/null 37 | result=$? 38 | 39 | # Cleanup 40 | rm -Rf $BUILD_DIRECTORY 41 | 42 | exit $result 43 | -------------------------------------------------------------------------------- /ReactiveCocoa/ObjC+Constants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Unavailable selectors in Swift. 4 | internal enum ObjCSelector { 5 | static let forwardInvocation = Selector((("forwardInvocation:"))) 6 | static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) 7 | static let getClass = Selector((("class"))) 8 | } 9 | 10 | // Method encoding of the unavailable selectors. 11 | internal enum ObjCMethodEncoding { 12 | static let forwardInvocation = extract("v@:@") 13 | static let methodSignatureForSelector = extract("v@::") 14 | static let getClass = extract("#@:") 15 | 16 | private static func extract(_ string: StaticString) -> UnsafePointer { 17 | return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) 18 | } 19 | } 20 | 21 | /// Objective-C type encoding. 22 | /// 23 | /// The enum does not cover all options, but only those that are expressive in 24 | /// Swift. 25 | internal enum ObjCTypeEncoding: Int8 { 26 | case char = 99 27 | case int = 105 28 | case short = 115 29 | case long = 108 30 | case longLong = 113 31 | 32 | case unsignedChar = 67 33 | case unsignedInt = 73 34 | case unsignedShort = 83 35 | case unsignedLong = 76 36 | case unsignedLongLong = 81 37 | 38 | case float = 102 39 | case double = 100 40 | 41 | case bool = 66 42 | 43 | case object = 64 44 | case type = 35 45 | case selector = 58 46 | 47 | case undefined = -1 48 | } 49 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/NSImageViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import AppKit 7 | 8 | class NSImageViewSpec: QuickSpec { 9 | override func spec() { 10 | var imageView: NSImageView! 11 | weak var _imageView: NSImageView? 12 | 13 | beforeEach { 14 | imageView = NSImageView(frame: .zero) 15 | _imageView = imageView 16 | } 17 | 18 | afterEach { 19 | imageView = nil 20 | expect(_imageView).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its enabling state") { 24 | imageView.isEnabled = false 25 | 26 | let (pipeSignal, observer) = Signal.pipe() 27 | imageView.reactive.isEnabled <~ SignalProducer(pipeSignal) 28 | 29 | observer.send(value: true) 30 | expect(imageView.isEnabled) == true 31 | 32 | observer.send(value: false) 33 | expect(imageView.isEnabled) == false 34 | } 35 | 36 | it("should accept changes from bindings to its image") { 37 | 38 | let (pipeSignal, observer) = Signal.pipe() 39 | imageView.reactive.image <~ SignalProducer(pipeSignal) 40 | 41 | let theImage = NSImage(named: NSImage.userName) 42 | 43 | observer.send(value: theImage) 44 | expect(imageView.image) == theImage 45 | 46 | observer.send(value: nil) 47 | expect(imageView.image).to(beNil()) 48 | } 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIDatePickerSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIDatePickerSpec: QuickSpec { 9 | override func spec() { 10 | var date: Date! 11 | var picker: UIDatePicker! 12 | weak var _picker: UIDatePicker? 13 | 14 | beforeEach { 15 | let formatter = DateFormatter() 16 | formatter.dateFormat = "MM/dd/YYYY" 17 | date = formatter.date(from: "11/29/1988")! 18 | 19 | picker = UIDatePicker(frame: .zero) 20 | _picker = picker 21 | } 22 | 23 | afterEach { 24 | picker = nil 25 | expect(_picker).to(beNil()) 26 | } 27 | 28 | it("should accept changes from bindings to its date value") { 29 | picker.reactive.date.action(date) 30 | expect(picker.date) == date 31 | } 32 | 33 | it("should emit user initiated changes to its date value") { 34 | let expectation = self.expectation(description: "Expected rac_date to send an event when picker's date value is changed by a UI event") 35 | defer { self.waitForExpectations(timeout: 2, handler: nil) } 36 | 37 | picker.reactive.dates.observeValues { changedDate in 38 | expect(changedDate) == date 39 | expectation.fulfill() 40 | } 41 | 42 | picker.date = date 43 | picker.isEnabled = true 44 | picker.isUserInteractionEnabled = true 45 | picker.sendActions(for: .valueChanged) 46 | } 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /ReactiveCocoa/WatchKit/WKInterfaceButton.swift: -------------------------------------------------------------------------------- 1 | #if canImport(WatchKit) 2 | import ReactiveSwift 3 | import WatchKit 4 | 5 | extension Reactive where Base: WKInterfaceButton { 6 | /// Sets the title of the button. 7 | public var title: BindingTarget { 8 | return makeBindingTarget { $0.setTitle($1) } 9 | } 10 | 11 | /// Sets the attributed title of the button. 12 | public var attributedTitle: BindingTarget { 13 | return makeBindingTarget { $0.setAttributedTitle($1) } 14 | } 15 | 16 | /// Sets the background color of the button. 17 | public var backgroundColor: BindingTarget { 18 | return makeBindingTarget { $0.setBackgroundColor($1) } 19 | } 20 | 21 | /// Sets the background image of the button. 22 | public var backgroundImage: BindingTarget { 23 | return makeBindingTarget { $0.setBackgroundImage($1) } 24 | } 25 | 26 | /// Sets the background image data of the button. 27 | public var backgroundImageData: BindingTarget { 28 | return makeBindingTarget { $0.setBackgroundImageData($1) } 29 | } 30 | 31 | /// Sets the background named image of the button. 32 | public var backgroundImageNamed: BindingTarget { 33 | return makeBindingTarget { $0.setBackgroundImageNamed($1) } 34 | } 35 | 36 | /// Sets whether the button is enabled. 37 | public var isEnabled: BindingTarget { 38 | return makeBindingTarget { $0.setEnabled($1) } 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSPopUpButton.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSPopUpButton { 6 | 7 | /// A signal of selected indexes 8 | public var selectedIndexes: Signal { 9 | return proxy.invoked.map { $0.indexOfSelectedItem } 10 | } 11 | 12 | /// Sets the button with an index. 13 | public var selectedIndex: BindingTarget { 14 | return makeBindingTarget { 15 | $0.selectItem(at: $1 ?? -1) 16 | } 17 | } 18 | 19 | /// A signal of selected title 20 | public var selectedTitles: Signal { 21 | return proxy.invoked.map { $0.titleOfSelectedItem }.skipNil() 22 | } 23 | 24 | /// Sets the button with title. 25 | /// note: emitting nil to this target will set selectedTitle to empty string 26 | public var selectedTitle: BindingTarget { 27 | return makeBindingTarget { 28 | $0.selectItem(withTitle: $1 ?? "") 29 | } 30 | } 31 | 32 | public var selectedItems: Signal { 33 | return proxy.invoked.map { $0.selectedItem }.skipNil() 34 | } 35 | 36 | 37 | /// A signal of selected tags 38 | public var selectedTags: Signal { 39 | return proxy.invoked.map { $0.selectedTag() } 40 | } 41 | 42 | /// Sets the selected tag 43 | public var selectedTag: BindingTarget { 44 | return makeBindingTarget { 45 | $0.selectItem(withTag: $1) 46 | } 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ReactiveCocoa", 7 | platforms: [ 8 | .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2) 9 | ], 10 | products: [ 11 | .library(name: "ReactiveCocoa", targets: ["ReactiveCocoa"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift", from: "6.2.0"), 15 | .package(url: "https://github.com/Quick/Quick.git", from: "2.0.0"), 16 | .package(url: "https://github.com/Quick/Nimble.git", from: "8.0.0"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "ReactiveCocoaObjC", 21 | dependencies: [], 22 | path: "ReactiveCocoaObjC"), 23 | 24 | .target( 25 | name: "ReactiveCocoa", 26 | dependencies: ["ReactiveSwift", "ReactiveCocoaObjC"], 27 | path: "ReactiveCocoa"), 28 | 29 | .target( 30 | name: "ReactiveCocoaObjCTestSupport", 31 | path: "ReactiveCocoaObjCTestSupport"), 32 | 33 | .testTarget( 34 | name: "ReactiveCocoaTests", 35 | dependencies: [ 36 | "ReactiveCocoa", 37 | "ReactiveCocoaObjCTestSupport", 38 | "Quick", 39 | "Nimble" 40 | ], 41 | path: "ReactiveCocoaTests"), 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIScrollView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIScrollView { 6 | /// Sets the content inset of the scroll view. 7 | public var contentInset: BindingTarget { 8 | return makeBindingTarget { $0.contentInset = $1 } 9 | } 10 | 11 | /// Sets the scroll indicator insets of the scroll view. 12 | public var scrollIndicatorInsets: BindingTarget { 13 | return makeBindingTarget { $0.scrollIndicatorInsets = $1 } 14 | } 15 | 16 | /// Sets whether scrolling the scroll view is enabled. 17 | public var isScrollEnabled: BindingTarget { 18 | return makeBindingTarget { $0.isScrollEnabled = $1 } 19 | } 20 | 21 | /// Sets the zoom scale of the scroll view. 22 | public var zoomScale: BindingTarget { 23 | return makeBindingTarget { $0.zoomScale = $1 } 24 | } 25 | 26 | /// Sets the minimum zoom scale of the scroll view. 27 | public var minimumZoomScale: BindingTarget { 28 | return makeBindingTarget { $0.minimumZoomScale = $1 } 29 | } 30 | 31 | /// Sets the maximum zoom scale of the scroll view. 32 | public var maximumZoomScale: BindingTarget { 33 | return makeBindingTarget { $0.maximumZoomScale = $1 } 34 | } 35 | 36 | #if os(iOS) 37 | /// Sets whether the scroll view scrolls to the top when the menu is tapped. 38 | public var scrollsToTop: BindingTarget { 39 | return makeBindingTarget { $0.scrollsToTop = $1 } 40 | } 41 | #endif 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIImageViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIImageViewSpec: QuickSpec { 9 | override func spec() { 10 | var imageView: UIImageView! 11 | weak var _imageView: UIImageView? 12 | 13 | beforeEach { 14 | imageView = UIImageView(frame: .zero) 15 | _imageView = imageView 16 | } 17 | 18 | afterEach { 19 | imageView = nil 20 | expect(_imageView).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its displaying image") { 24 | let firstChange = UIImage() 25 | let secondChange = UIImage() 26 | 27 | let (pipeSignal, observer) = Signal.pipe() 28 | imageView.reactive.image <~ SignalProducer(pipeSignal) 29 | 30 | observer.send(value: firstChange) 31 | expect(imageView.image) == firstChange 32 | 33 | observer.send(value: secondChange) 34 | expect(imageView.image) == secondChange 35 | } 36 | 37 | it("should accept changes from bindings to its displaying image when highlighted") { 38 | let firstChange = UIImage() 39 | let secondChange = UIImage() 40 | 41 | let (pipeSignal, observer) = Signal.pipe() 42 | imageView.reactive.highlightedImage <~ SignalProducer(pipeSignal) 43 | 44 | observer.send(value: firstChange) 45 | expect(imageView.highlightedImage) == firstChange 46 | 47 | observer.send(value: secondChange) 48 | expect(imageView.highlightedImage) == secondChange 49 | } 50 | } 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIKitReusableComponentsSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import UIKit 7 | 8 | class ReusableComponentsSpec: QuickSpec { 9 | override func spec() { 10 | describe("UITableViewCell") { 11 | it("should send a `value` event when `prepareForReuse` is triggered") { 12 | let cell = UITableViewCell() 13 | 14 | var isTriggered = false 15 | cell.reactive.prepareForReuse.observeValues { 16 | isTriggered = true 17 | } 18 | 19 | expect(isTriggered) == false 20 | 21 | cell.prepareForReuse() 22 | expect(isTriggered) == true 23 | } 24 | } 25 | 26 | describe("UITableViewHeaderFooterView") { 27 | it("should send a `value` event when `prepareForReuse` is triggered") { 28 | let cell = UITableViewHeaderFooterView() 29 | 30 | var isTriggered = false 31 | cell.reactive.prepareForReuse.observeValues { 32 | isTriggered = true 33 | } 34 | 35 | expect(isTriggered) == false 36 | 37 | cell.prepareForReuse() 38 | expect(isTriggered) == true 39 | } 40 | } 41 | 42 | describe("UICollectionReusableView") { 43 | it("should send a `value` event when `prepareForReuse` is triggered") { 44 | let cell = UICollectionReusableView() 45 | 46 | var isTriggered = false 47 | cell.reactive.prepareForReuse.observeValues { 48 | isTriggered = true 49 | } 50 | 51 | expect(isTriggered) == false 52 | 53 | cell.prepareForReuse() 54 | expect(isTriggered) == true 55 | } 56 | } 57 | } 58 | } 59 | #endif 60 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIButton.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIButton { 6 | /// The action to be triggered when the button is pressed. It also controls 7 | /// the enabled state of the button. 8 | public var pressed: CocoaAction? { 9 | get { 10 | return associatedAction.withValue { info in 11 | return info.flatMap { info in 12 | return info.controlEvents == pressEvent ? info.action : nil 13 | } 14 | } 15 | } 16 | 17 | nonmutating set { 18 | setAction(newValue, for: pressEvent) 19 | } 20 | } 21 | 22 | private var pressEvent: UIControl.Event { 23 | if #available(iOS 9.0, tvOS 9.0, *) { 24 | return .primaryActionTriggered 25 | } else { 26 | return .touchUpInside 27 | } 28 | } 29 | 30 | /// Sets the title of the button for its normal state. 31 | public var title: BindingTarget { 32 | return makeBindingTarget { $0.setTitle($1, for: .normal) } 33 | } 34 | 35 | /// Sets the title of the button for the specified state. 36 | public func title(for state: UIControl.State) -> BindingTarget { 37 | return makeBindingTarget { $0.setTitle($1, for: state) } 38 | } 39 | 40 | /// Sets the image of the button for the specified state. 41 | public func image(for state: UIControl.State) -> BindingTarget { 42 | return makeBindingTarget { $0.setImage($1, for: state) } 43 | } 44 | 45 | /// Sets the image of the button for the .normal state 46 | public var image: BindingTarget { 47 | return image(for: .normal) 48 | } 49 | } 50 | #endif 51 | -------------------------------------------------------------------------------- /ReactiveCocoa.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveCocoa" 3 | s.version = "10.2.0" 4 | s.summary = "Streams of values over time" 5 | s.description = <<-DESC 6 | ReactiveCocoa (RAC) is a Cocoa framework built on top of ReactiveSwift. It provides APIs for using ReactiveSwift with Apple's Cocoa frameworks. 7 | DESC 8 | s.homepage = "https://github.com/ReactiveCocoa/ReactiveCocoa" 9 | s.license = { :type => "MIT", :file => "LICENSE.md" } 10 | s.author = "ReactiveCocoa" 11 | 12 | s.osx.deployment_target = "10.9" 13 | s.ios.deployment_target = "8.0" 14 | s.tvos.deployment_target = "9.0" 15 | s.watchos.deployment_target = "2.0" 16 | 17 | s.source = { :git => "https://github.com/ReactiveCocoa/ReactiveCocoa.git", :tag => "#{s.version}" } 18 | s.source_files = "ReactiveCocoa/*.{swift,h,m}", "ReactiveCocoa/Shared/*.{swift}", "ReactiveCocoaObjC/**/*.{h,m}" 19 | s.public_header_files = "ReactiveCocoaObjC/include/ObjCRuntimeAliases.h" 20 | s.osx.source_files = "ReactiveCocoa/AppKit/*.{swift}" 21 | s.ios.source_files = "ReactiveCocoa/UIKit/*.{swift}", "ReactiveCocoa/UIKit/iOS/*.{swift}" 22 | s.tvos.source_files = "ReactiveCocoa/UIKit/*.{swift}" 23 | s.watchos.exclude_files = "ReactiveCocoa/Shared/*.{swift}" 24 | s.watchos.source_files = "ReactiveCocoa/WatchKit/*.{swift}" 25 | s.module_name = 'ReactiveCocoa' 26 | 27 | s.dependency 'ReactiveSwift', '~> 6.2' 28 | 29 | s.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS[config=Release]" => "$(inherited) -suppress-warnings" } 30 | s.swift_version = '5.0' 31 | end 32 | -------------------------------------------------------------------------------- /ReactiveCocoa/ObjC+Selector.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Selector { 4 | /// `self` as a pointer. It is uniqued across instances, similar to 5 | /// `StaticString`. 6 | internal var utf8Start: UnsafePointer { 7 | return unsafeBitCast(self, to: UnsafePointer.self) 8 | } 9 | 10 | /// An alias of `self`, used in method interception. 11 | internal var alias: Selector { 12 | return prefixing("rac0_") 13 | } 14 | 15 | /// An alias of `self`, used in method interception specifically for 16 | /// preserving (if found) an immediate implementation of `self` in the 17 | /// runtime subclass. 18 | internal var interopAlias: Selector { 19 | return prefixing("rac1_") 20 | } 21 | 22 | /// An alias of `self`, used for delegate proxies. 23 | internal var delegateProxyAlias: Selector { 24 | return prefixing("rac2_") 25 | } 26 | 27 | internal func prefixing(_ prefix: StaticString) -> Selector { 28 | let length = Int(strlen(utf8Start)) 29 | let prefixedLength = length + prefix.utf8CodeUnitCount 30 | 31 | let asciiPrefix = UnsafeRawPointer(prefix.utf8Start).assumingMemoryBound(to: Int8.self) 32 | 33 | let cString = UnsafeMutablePointer.allocate(capacity: prefixedLength + 1) 34 | defer { 35 | cString.deinitialize(count: prefixedLength + 1) 36 | cString.deallocate() 37 | } 38 | 39 | cString.initialize(from: asciiPrefix, count: prefix.utf8CodeUnitCount) 40 | (cString + prefix.utf8CodeUnitCount).initialize(from: utf8Start, count: length) 41 | (cString + prefixedLength).initialize(to: Int8(UInt8(ascii: "\0"))) 42 | 43 | return sel_registerName(cString) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIViewController.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIViewController { 6 | /// Set's the title of the view controller. 7 | public var title: BindingTarget { 8 | return makeBindingTarget({ $0.title = $1 }) 9 | } 10 | 11 | /// A signal that sends a value event every time `viewWillAppear` is invoked. 12 | public var viewWillAppear: Signal { 13 | return trigger(for: #selector(Base.viewWillAppear)) 14 | } 15 | 16 | /// A signal that sends a value event every time `viewDidAppear` is invoked. 17 | public var viewDidAppear: Signal { 18 | return trigger(for: #selector(Base.viewDidAppear)) 19 | } 20 | 21 | /// A signal that sends a value event every time `viewWillDisappear` is invoked. 22 | public var viewWillDisappear: Signal { 23 | return trigger(for: #selector(Base.viewWillDisappear)) 24 | } 25 | 26 | /// A signal that sends a value event every time `viewDidDisappear` is invoked. 27 | public var viewDidDisappear: Signal { 28 | return trigger(for: #selector(Base.viewDidDisappear)) 29 | } 30 | 31 | /// A signal that sends a value event every time `viewWillLayoutSubviews` is invoked. 32 | public var viewWillLayoutSubviews: Signal { 33 | return trigger(for: #selector(Base.viewWillLayoutSubviews)) 34 | } 35 | 36 | /// A signal that sends a value event every time `viewDidLayoutSubviews` is invoked. 37 | public var viewDidLayoutSubviews: Signal { 38 | return trigger(for: #selector(Base.viewDidLayoutSubviews)) 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CwlCatchException", 6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "7cd2f8cacc4d22f21bc0b2309c3b18acf7957b66", 10 | "version": "1.2.0" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c228db5d2ad1b01ebc84435e823e6cca4e3db98b", 19 | "version": "1.2.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "b02b00b30b6353632aa4a5fb6124f8147f7140c0", 28 | "version": "8.0.5" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f", 37 | "version": "2.2.0" 38 | } 39 | }, 40 | { 41 | "package": "ReactiveSwift", 42 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift", 43 | "state": { 44 | "branch": null, 45 | "revision": "e8703715afe26a8efbb2ecfdb3454d648f58d691", 46 | "version": "6.2.0" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UIBarButtonItem.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UIBarButtonItem { 6 | /// The current associated action of `self`. 7 | private var associatedAction: Atomic<(action: CocoaAction, disposable: Disposable?)?> { 8 | return associatedValue { _ in Atomic(nil) } 9 | } 10 | 11 | /// The action to be triggered when the button is pressed. It also controls 12 | /// the enabled state of the button. 13 | public var pressed: CocoaAction? { 14 | get { 15 | return associatedAction.value?.action 16 | } 17 | 18 | nonmutating set { 19 | base.target = newValue 20 | base.action = newValue.map { _ in CocoaAction.selector } 21 | 22 | associatedAction 23 | .swap(newValue.map { action in 24 | let disposable = isEnabled <~ action.isEnabled 25 | return (action, disposable) 26 | })? 27 | .disposable?.dispose() 28 | } 29 | } 30 | 31 | /// Sets the style of the bar button item. 32 | public var style: BindingTarget { 33 | return makeBindingTarget { $0.style = $1 } 34 | } 35 | 36 | /// Sets the width of the bar button item. 37 | public var width: BindingTarget { 38 | return makeBindingTarget { $0.width = $1 } 39 | } 40 | 41 | /// Sets the possible titles of the bar button item. 42 | public var possibleTitles: BindingTarget?> { 43 | return makeBindingTarget { $0.possibleTitles = $1 } 44 | } 45 | 46 | /// Sets the custom view of the bar button item. 47 | public var customView: BindingTarget { 48 | return makeBindingTarget { $0.customView = $1 } 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSTextField.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSTextField { 6 | private var notifications: Signal { 7 | #if swift(>=4.0) 8 | let name = NSControl.textDidChangeNotification 9 | #else 10 | let name = Notification.Name.NSControlTextDidChange 11 | #endif 12 | 13 | return NotificationCenter.default 14 | .reactive 15 | .notifications(forName: name, object: base) 16 | .take(during: lifetime) 17 | } 18 | 19 | /// A signal of values in `String` from the text field upon any changes. 20 | public var continuousStringValues: Signal { 21 | return notifications 22 | .map { ($0.object as! NSTextField).stringValue } 23 | } 24 | 25 | /// A signal of values in `NSAttributedString` from the text field upon any 26 | /// changes. 27 | public var continuousAttributedStringValues: Signal { 28 | return notifications 29 | .map { ($0.object as! NSTextField).attributedStringValue } 30 | } 31 | 32 | /// Wraps the `stringValue` binding target from NSControl for 33 | /// cross-platform compatibility 34 | public var text: BindingTarget { 35 | return stringValue 36 | } 37 | 38 | /// Wraps the `stringValue` binding target from NSControl for 39 | /// cross-platform compatibility 40 | public var attributedText: BindingTarget { 41 | return attributedStringValue 42 | } 43 | 44 | /// Sets the color of the text with an `NSColor`. 45 | public var textColor: BindingTarget { 46 | return makeBindingTarget { $0.textColor = $1 } 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /ReactiveCocoa/CocoaTarget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | /// A target that accepts action messages. 5 | internal final class CocoaTarget: NSObject { 6 | private enum State { 7 | case idle 8 | case sending(queue: [Value]) 9 | } 10 | 11 | private let observer: Signal.Observer 12 | private let transform: (Any?) -> Value 13 | 14 | private var state: State 15 | 16 | internal init(_ observer: Signal.Observer, transform: @escaping (Any?) -> Value) { 17 | self.observer = observer 18 | self.transform = transform 19 | self.state = .idle 20 | } 21 | 22 | /// Broadcast the action message to all observers. 23 | /// 24 | /// Reentrancy is supported, and the action message would be deferred until the 25 | /// delivery of the current message has completed. 26 | /// 27 | /// - note: It should only be invoked on the main queue. 28 | /// 29 | /// - parameters: 30 | /// - sender: The object which sends the action message. 31 | @objc internal func invoke(_ sender: Any?) { 32 | switch state { 33 | case .idle: 34 | state = .sending(queue: []) 35 | observer.send(value: transform(sender)) 36 | 37 | while case let .sending(values) = state { 38 | guard !values.isEmpty else { 39 | break 40 | } 41 | 42 | state = .sending(queue: Array(values.dropFirst())) 43 | observer.send(value: values[0]) 44 | } 45 | 46 | state = .idle 47 | 48 | case let .sending(values): 49 | state = .sending(queue: values + [transform(sender)]) 50 | } 51 | } 52 | } 53 | 54 | extension CocoaTarget where Value == Void { 55 | internal convenience init(_ observer: Signal<(), Never>.Observer) { 56 | self.init(observer, transform: { _ in }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/SwizzlingSpec.swift: -------------------------------------------------------------------------------- 1 | @testable import ReactiveCocoa 2 | import Foundation 3 | import Quick 4 | import Nimble 5 | 6 | class SwizzledObject: NSObject {} 7 | 8 | class SwizzlingSpec: QuickSpec { 9 | override func spec() { 10 | describe("runtime subclassing") { 11 | it("should swizzle the instance while still reporting the perceived class in `-class` and `+class`") { 12 | let object = SwizzledObject() 13 | expect(type(of: object)).to(beIdenticalTo(SwizzledObject.self)) 14 | 15 | let subclass: AnyClass = swizzleClass(object) 16 | expect(type(of: object)).to(beIdenticalTo(SwizzledObject.self)) 17 | expect(object.objcClass).to(beIdenticalTo(SwizzledObject.self)) 18 | expect(object_getClass(object)).to(beIdenticalTo(subclass)) 19 | 20 | let objcClass: AnyClass = (object as AnyObject).objcClass 21 | expect(objcClass).to(beIdenticalTo(SwizzledObject.self)) 22 | expect((objcClass as AnyObject).objcClass as Any?).to(beIdenticalTo(SwizzledObject.self as AnyClass)) 23 | 24 | expect(String(cString: class_getName(subclass))).to(contain("_RACSwift")) 25 | } 26 | 27 | it("should reuse the runtime subclass across instances") { 28 | let object = SwizzledObject() 29 | let subclass: AnyClass = swizzleClass(object) 30 | 31 | let object2 = SwizzledObject() 32 | let subclass2: AnyClass = swizzleClass(object2) 33 | 34 | expect(subclass).to(beIdenticalTo(subclass2)) 35 | } 36 | 37 | it("should return the known runtime subclass") { 38 | let object = SwizzledObject() 39 | let subclass: AnyClass = swizzleClass(object) 40 | let subclass2: AnyClass = swizzleClass(object) 41 | 42 | expect(subclass).to(beIdenticalTo(subclass2)) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/NSTableViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveCocoa 5 | import ReactiveSwift 6 | import AppKit 7 | 8 | final class NSTableViewSpec: QuickSpec { 9 | override func spec() { 10 | var tableView: TestTableView! 11 | 12 | beforeEach { 13 | tableView = TestTableView() 14 | } 15 | 16 | describe("reloadData") { 17 | var bindingSignal: Signal<(), Never>! 18 | var bindingObserver: Signal<(), Never>.Observer! 19 | 20 | var reloadDataCount = 0 21 | 22 | beforeEach { 23 | let (signal, observer) = Signal<(), Never>.pipe() 24 | (bindingSignal, bindingObserver) = (signal, observer) 25 | 26 | reloadDataCount = 0 27 | 28 | tableView.reloadDataSignal.observeValues { 29 | reloadDataCount += 1 30 | } 31 | } 32 | 33 | it("invokes reloadData whenever the bound signal sends a value") { 34 | tableView.reactive.reloadData <~ bindingSignal 35 | 36 | bindingObserver.send(value: ()) 37 | bindingObserver.send(value: ()) 38 | 39 | expect(reloadDataCount) == 2 40 | } 41 | } 42 | } 43 | } 44 | 45 | private final class TestTableView: NSTableView { 46 | let reloadDataSignal: Signal<(), Never> 47 | private let reloadDataObserver: Signal<(), Never>.Observer 48 | 49 | override init(frame: CGRect) { 50 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 51 | super.init(frame: frame) 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 56 | super.init(coder: aDecoder) 57 | } 58 | 59 | override func reloadData() { 60 | super.reloadData() 61 | reloadDataObserver.send(value: ()) 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UITableViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | final class UITableViewSpec: QuickSpec { 9 | override func spec() { 10 | var tableView: TestTableView! 11 | 12 | beforeEach { 13 | tableView = TestTableView() 14 | } 15 | 16 | describe("reloadData") { 17 | var bindingSignal: Signal<(), Never>! 18 | var bindingObserver: Signal<(), Never>.Observer! 19 | 20 | var reloadDataCount = 0 21 | 22 | beforeEach { 23 | let (signal, observer) = Signal<(), Never>.pipe() 24 | (bindingSignal, bindingObserver) = (signal, observer) 25 | 26 | reloadDataCount = 0 27 | 28 | tableView.reloadDataSignal.observeValues { 29 | reloadDataCount += 1 30 | } 31 | } 32 | 33 | it("invokes reloadData whenever the bound signal sends a value") { 34 | tableView.reactive.reloadData <~ bindingSignal 35 | 36 | bindingObserver.send(value: ()) 37 | bindingObserver.send(value: ()) 38 | 39 | expect(reloadDataCount) == 2 40 | } 41 | } 42 | } 43 | } 44 | 45 | private final class TestTableView: UITableView { 46 | let reloadDataSignal: Signal<(), Never> 47 | private let reloadDataObserver: Signal<(), Never>.Observer 48 | 49 | override init(frame: CGRect, style: UITableView.Style) { 50 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 51 | super.init(frame: frame, style: style) 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 56 | super.init(coder: aDecoder) 57 | } 58 | 59 | override func reloadData() { 60 | super.reloadData() 61 | reloadDataObserver.send(value: ()) 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSButton.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension Reactive where Base: NSButton { 6 | 7 | internal var associatedAction: Atomic<(action: CocoaAction, disposable: CompositeDisposable)?> { 8 | return associatedValue { _ in Atomic(nil) } 9 | } 10 | 11 | public var pressed: CocoaAction? { 12 | get { 13 | return associatedAction.value?.action 14 | } 15 | 16 | nonmutating set { 17 | associatedAction 18 | .modify { action in 19 | action?.disposable.dispose() 20 | action = newValue.map { action in 21 | let disposable = CompositeDisposable() 22 | disposable += isEnabled <~ action.isEnabled 23 | disposable += proxy.invoked.observeValues(action.execute) 24 | return (action, disposable) 25 | } 26 | } 27 | } 28 | } 29 | 30 | #if swift(>=4.0) 31 | /// A signal of integer states (On, Off, Mixed), emitted by the button. 32 | public var states: Signal { 33 | return proxy.invoked.map { $0.state } 34 | } 35 | 36 | /// Sets the button's state 37 | public var state: BindingTarget { 38 | return makeBindingTarget { $0.state = $1 } 39 | } 40 | #else 41 | /// A signal of integer states (On, Off, Mixed), emitted by the button. 42 | public var states: Signal { 43 | return proxy.invoked.map { $0.state } 44 | } 45 | 46 | /// Sets the button's state 47 | public var state: BindingTarget { 48 | return makeBindingTarget { $0.state = $1 } 49 | } 50 | #endif 51 | 52 | /// Sets the button's image 53 | public var image: BindingTarget { 54 | return makeBindingTarget { $0.image = $1 } 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIPickerView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import Foundation 3 | import ReactiveSwift 4 | import UIKit 5 | 6 | private class PickerViewDelegateProxy: DelegateProxy, UIPickerViewDelegate { 7 | @objc func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 8 | forwardee?.pickerView?(pickerView, didSelectRow: row, inComponent: component) 9 | } 10 | } 11 | 12 | extension Reactive where Base: UIPickerView { 13 | 14 | private var proxy: PickerViewDelegateProxy { 15 | return .proxy(for: base, 16 | setter: #selector(setter: base.delegate), 17 | getter: #selector(getter: base.delegate)) 18 | } 19 | 20 | /// Sets the selected row in the specified component, without animating the 21 | /// position. 22 | public func selectedRow(inComponent component: Int) -> BindingTarget { 23 | return makeBindingTarget { $0.selectRow($1, inComponent: component, animated: false) } 24 | } 25 | 26 | /// Reloads all components 27 | public var reloadAllComponents: BindingTarget<()> { 28 | return makeBindingTarget { base, _ in base.reloadAllComponents() } 29 | } 30 | 31 | /// Reloads the specified component 32 | public var reloadComponent: BindingTarget { 33 | return makeBindingTarget { $0.reloadComponent($1) } 34 | } 35 | 36 | /// Create a signal which sends a `value` event for each row selection 37 | /// 38 | /// - returns: A trigger signal. 39 | public var selections: Signal<(row: Int, component: Int), Never> { 40 | return proxy.intercept(#selector(UIPickerViewDelegate.pickerView(_:didSelectRow:inComponent:))) 41 | .map { (row: $0[1] as! Int, component: $0[2] as! Int) } 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UICollectionViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import Quick 3 | import Nimble 4 | import ReactiveCocoa 5 | import ReactiveSwift 6 | import UIKit 7 | 8 | final class UICollectionViewSpec: QuickSpec { 9 | override func spec() { 10 | var collectionView: TestCollectionView! 11 | 12 | beforeEach { 13 | collectionView = TestCollectionView() 14 | } 15 | 16 | describe("reloadData") { 17 | var bindingSignal: Signal<(), Never>! 18 | var bindingObserver: Signal<(), Never>.Observer! 19 | 20 | var reloadDataCount = 0 21 | 22 | beforeEach { 23 | let (signal, observer) = Signal<(), Never>.pipe() 24 | (bindingSignal, bindingObserver) = (signal, observer) 25 | 26 | reloadDataCount = 0 27 | 28 | collectionView.reloadDataSignal.observeValues { 29 | reloadDataCount += 1 30 | } 31 | } 32 | 33 | it("invokes reloadData whenever the bound signal sends a value") { 34 | collectionView.reactive.reloadData <~ bindingSignal 35 | 36 | bindingObserver.send(value: ()) 37 | bindingObserver.send(value: ()) 38 | 39 | expect(reloadDataCount) == 2 40 | } 41 | } 42 | } 43 | } 44 | 45 | private final class TestCollectionView: UICollectionView { 46 | let reloadDataSignal: Signal<(), Never> 47 | private let reloadDataObserver: Signal<(), Never>.Observer 48 | 49 | init() { 50 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 51 | super.init(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 56 | super.init(coder: aDecoder) 57 | } 58 | 59 | override func reloadData() { 60 | super.reloadData() 61 | reloadDataObserver.send(value: ()) 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIControl+EnableSendActionsForControlEvents.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | 4 | private func rac_swizzle() { 5 | let originalSelector = #selector(UIControl.sendAction(_:to:for:)) 6 | let swizzledSelector = #selector(UIControl.rac_sendAction(_:to:forEvent:)) 7 | 8 | let originalMethod = class_getInstanceMethod(UIControl.self, originalSelector)! 9 | let swizzledMethod = class_getInstanceMethod(UIControl.self, swizzledSelector)! 10 | 11 | let didAddMethod = class_addMethod(UIControl.self, 12 | originalSelector, 13 | method_getImplementation(swizzledMethod), 14 | method_getTypeEncoding(swizzledMethod)!) 15 | 16 | if didAddMethod { 17 | class_replaceMethod(UIControl.self, 18 | swizzledSelector, 19 | method_getImplementation(originalMethod), 20 | method_getTypeEncoding(originalMethod)!) 21 | } else { 22 | method_exchangeImplementations(originalMethod, swizzledMethod) 23 | } 24 | } 25 | 26 | /// Unfortunately, there's an apparent limitation in using `sendActionsForControlEvents` 27 | /// on unit-tests for any control besides `UIButton` which is very unfortunate since we 28 | /// want test our bindings for `UIDatePicker`, `UISwitch`, `UITextField` and others 29 | /// in the future. To be able to test them, we're now using swizzling to manually invoke 30 | /// the pair target+action. 31 | extension UIControl { 32 | static func _initialize() { 33 | rac_swizzle() 34 | } 35 | 36 | // MARK: - Method Swizzling 37 | 38 | @objc func rac_sendAction(_ action: Selector, to target: AnyObject?, forEvent event: UIEvent?) { 39 | _ = target?.perform(action, with: self) 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UISliderSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UISliderSpec: QuickSpec { 9 | override func spec() { 10 | var slider: UISlider! 11 | weak var _slider: UISlider? 12 | 13 | beforeEach { 14 | slider = UISlider() 15 | _slider = slider 16 | } 17 | 18 | afterEach { 19 | slider = nil 20 | expect(_slider).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its value") { 24 | expect(slider.value) == 0.0 25 | 26 | let (pipeSignal, observer) = Signal.pipe() 27 | 28 | slider.reactive.value <~ pipeSignal 29 | 30 | observer.send(value: 0.5) 31 | expect(slider.value) ≈ 0.5 32 | } 33 | 34 | it("should accept changes from bindings to its minimum value") { 35 | expect(slider.minimumValue) == 0.0 36 | 37 | let (pipeSignal, observer) = Signal.pipe() 38 | 39 | slider.reactive.minimumValue <~ pipeSignal 40 | 41 | observer.send(value: 0.3) 42 | expect(slider.minimumValue) ≈ 0.3 43 | } 44 | 45 | it("should accept changes from bindings to its maximum value") { 46 | expect(slider.maximumValue) == 1.0 47 | 48 | let (pipeSignal, observer) = Signal.pipe() 49 | 50 | slider.reactive.maximumValue <~ pipeSignal 51 | 52 | observer.send(value: 0.7) 53 | expect(slider.maximumValue) ≈ 0.7 54 | } 55 | 56 | it("should emit user's changes for its value") { 57 | slider.value = 0.25 58 | 59 | var updatedValue: Float? 60 | slider.reactive.values.observeValues { value in 61 | updatedValue = value 62 | } 63 | 64 | expect(updatedValue).to(beNil()) 65 | slider.sendActions(for: .valueChanged) 66 | expect(updatedValue) ≈ 0.25 67 | } 68 | } 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/NSCollectionViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveCocoa 5 | import ReactiveSwift 6 | import AppKit 7 | 8 | @available(macOS 10.11, *) 9 | final class NSCollectionViewSpec: QuickSpec { 10 | override func spec() { 11 | var collectionView: TestCollectionView! 12 | 13 | beforeEach { 14 | collectionView = TestCollectionView() 15 | } 16 | 17 | describe("reloadData") { 18 | var bindingSignal: Signal<(), Never>! 19 | var bindingObserver: Signal<(), Never>.Observer! 20 | 21 | var reloadDataCount = 0 22 | 23 | beforeEach { 24 | let (signal, observer) = Signal<(), Never>.pipe() 25 | (bindingSignal, bindingObserver) = (signal, observer) 26 | 27 | reloadDataCount = 0 28 | 29 | collectionView.reloadDataSignal.observeValues { 30 | reloadDataCount += 1 31 | } 32 | } 33 | 34 | it("invokes reloadData whenever the bound signal sends a value") { 35 | collectionView.reactive.reloadData <~ bindingSignal 36 | 37 | bindingObserver.send(value: ()) 38 | bindingObserver.send(value: ()) 39 | 40 | expect(reloadDataCount) == 2 41 | } 42 | } 43 | } 44 | } 45 | 46 | @available(macOS 10.11, *) 47 | private final class TestCollectionView: NSCollectionView { 48 | let reloadDataSignal: Signal<(), Never> 49 | private let reloadDataObserver: Signal<(), Never>.Observer 50 | 51 | override init(frame: CGRect) { 52 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 53 | super.init(frame: frame) 54 | } 55 | 56 | required init?(coder aDecoder: NSCoder) { 57 | (reloadDataSignal, reloadDataObserver) = Signal.pipe() 58 | super.init(coder: aDecoder) 59 | } 60 | 61 | override func reloadData() { 62 | super.reloadData() 63 | reloadDataObserver.send(value: ()) 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIStepperSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIStepperSpec: QuickSpec { 9 | override func spec() { 10 | var stepper: UIStepper! 11 | weak var _stepper: UIStepper? 12 | 13 | beforeEach { 14 | stepper = UIStepper() 15 | _stepper = stepper 16 | } 17 | 18 | afterEach { 19 | stepper = nil 20 | expect(_stepper).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its value") { 24 | expect(stepper.value) == 0.0 25 | 26 | let (pipeSignal, observer) = Signal.pipe() 27 | 28 | stepper.reactive.value <~ pipeSignal 29 | 30 | observer.send(value: 0.5) 31 | expect(stepper.value) ≈ 0.5 32 | } 33 | 34 | it("should accept changes from bindings to its minimum value") { 35 | expect(stepper.minimumValue) == 0.0 36 | 37 | let (pipeSignal, observer) = Signal.pipe() 38 | 39 | stepper.reactive.minimumValue <~ pipeSignal 40 | 41 | observer.send(value: 0.3) 42 | expect(stepper.minimumValue) ≈ 0.3 43 | } 44 | 45 | it("should accept changes from bindings to its maximum value") { 46 | expect(stepper.maximumValue) == 100.0 47 | 48 | let (pipeSignal, observer) = Signal.pipe() 49 | 50 | stepper.reactive.maximumValue <~ pipeSignal 51 | 52 | observer.send(value: 33.0) 53 | expect(stepper.maximumValue) ≈ 33.0 54 | } 55 | 56 | it("should emit user's changes for its value") { 57 | stepper.value = 0.25 58 | 59 | var updatedValue: Double? 60 | stepper.reactive.values.observeValues { value in 61 | updatedValue = value 62 | } 63 | 64 | expect(updatedValue).to(beNil()) 65 | stepper.sendActions(for: .valueChanged) 66 | expect(updatedValue) ≈ 0.25 67 | } 68 | } 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /ReactiveCocoa/ObjC+Messages.swift: -------------------------------------------------------------------------------- 1 | // Unavailable classes like `NSInvocation` can still be passed into Swift as 2 | // `AnyClass` and `AnyObject`, and receive messages as `AnyClass` and 3 | // `AnyObject` existentials. 4 | // 5 | // These `@objc` protocols host the method signatures so that they can be used 6 | // with `AnyObject`. 7 | 8 | import Foundation 9 | 10 | internal let NSInvocation: AnyClass = NSClassFromString("NSInvocation")! 11 | internal let NSMethodSignature: AnyClass = NSClassFromString("NSMethodSignature")! 12 | 13 | // Signatures defined in `@objc` protocols would be available for ObjC message 14 | // sending via `AnyObject`. 15 | @objc internal protocol ObjCClassReporting { 16 | // An alias for `-class`, which is unavailable in Swift. 17 | @objc(class) 18 | var objcClass: AnyClass! { get } 19 | 20 | @objc(methodSignatureForSelector:) 21 | func objcMethodSignature(for selector: Selector) -> AnyObject 22 | } 23 | 24 | // Methods of `NSInvocation`. 25 | @objc internal protocol ObjCInvocation { 26 | @objc(setSelector:) 27 | func objcSetSelector(_ selector: Selector) 28 | 29 | @objc(methodSignature) 30 | var objcMethodSignature: AnyObject { get } 31 | 32 | @objc(getArgument:atIndex:) 33 | func objcCopy(to buffer: UnsafeMutableRawPointer?, forArgumentAt index: Int) 34 | 35 | @objc(invoke) 36 | func objcInvoke() 37 | 38 | @objc(invocationWithMethodSignature:) 39 | static func objcInvocation(withMethodSignature signature: AnyObject) -> AnyObject 40 | } 41 | 42 | // Methods of `NSMethodSignature`. 43 | @objc internal protocol ObjCMethodSignature { 44 | @objc(numberOfArguments) 45 | var objcNumberOfArguments: UInt { get } 46 | 47 | @objc(getArgumentTypeAtIndex:) 48 | func objcArgumentType(at index: UInt) -> UnsafePointer 49 | 50 | @objc(signatureWithObjCTypes:) 51 | static func objcSignature(withObjCTypes typeEncoding: UnsafePointer) -> AnyObject 52 | } 53 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UITabBarItemSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UITabBarSpec: QuickSpec { 9 | override func spec() { 10 | var tabBarItem: UITabBarItem! 11 | weak var _tabBarItem: UITabBarItem? 12 | 13 | beforeEach { 14 | tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 1) 15 | _tabBarItem = tabBarItem 16 | } 17 | 18 | afterEach { 19 | tabBarItem = nil 20 | expect(_tabBarItem).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its badge value") { 24 | let firstChange = "first" 25 | let secondChange = "second" 26 | 27 | tabBarItem.badgeValue = "" 28 | 29 | let (pipeSignal, observer) = Signal.pipe() 30 | tabBarItem.reactive.badgeValue <~ pipeSignal 31 | 32 | observer.send(value: firstChange) 33 | expect(tabBarItem.badgeValue) == firstChange 34 | 35 | observer.send(value: secondChange) 36 | expect(tabBarItem.badgeValue) == secondChange 37 | 38 | observer.send(value: nil) 39 | expect(tabBarItem.badgeValue).to(beNil()) 40 | } 41 | 42 | if #available(iOS 10, *), #available(tvOS 10, *) { 43 | it("should accept changes from bindings to its badge color value") { 44 | let firstChange: UIColor = .red 45 | let secondChange: UIColor = .green 46 | 47 | tabBarItem.badgeColor = .blue 48 | 49 | let (pipeSignal, observer) = Signal.pipe() 50 | tabBarItem.reactive.badgeColor <~ pipeSignal 51 | 52 | observer.send(value: firstChange) 53 | expect(tabBarItem.badgeColor) == firstChange 54 | 55 | observer.send(value: secondChange) 56 | expect(tabBarItem.badgeColor) == secondChange 57 | 58 | observer.send(value: nil) 59 | expect(tabBarItem.badgeColor).to(beNil()) 60 | } 61 | } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AssociationSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import Nimble 4 | import Quick 5 | @testable import ReactiveCocoa 6 | 7 | class AssociationSpec: QuickSpec { 8 | override func spec() { 9 | it("should create and retrieve the same object") { 10 | let object = NSObject() 11 | let token = NSObject() 12 | 13 | var counter = 0 14 | 15 | func retrieve() -> NSObject { 16 | return object.reactive.associatedValue { _ in 17 | counter += 1 18 | return token 19 | } 20 | } 21 | 22 | expect(counter) == 0 23 | 24 | let firstResult = retrieve() 25 | expect(counter) == 1 26 | 27 | let secondResult = retrieve() 28 | expect(counter) == 1 29 | 30 | expect(firstResult).to(beIdenticalTo(secondResult)) 31 | expect(firstResult).to(beIdenticalTo(token)) 32 | } 33 | 34 | it("should support multiple keys per object") { 35 | let object = NSObject() 36 | let token = NSObject() 37 | let token2 = NSObject() 38 | 39 | var counter = 0 40 | var counter2 = 0 41 | 42 | 43 | func retrieve() -> NSObject { 44 | return object.reactive.associatedValue { _ in 45 | counter += 1 46 | return token 47 | } 48 | } 49 | 50 | func retrieve2() -> NSObject { 51 | return object.reactive.associatedValue(forKey: "customKey") { _ in 52 | counter2 += 1 53 | return token2 54 | } 55 | } 56 | 57 | expect(counter) == 0 58 | 59 | let firstResult = retrieve() 60 | expect(counter) == 1 61 | 62 | let secondResult = retrieve() 63 | expect(counter) == 1 64 | 65 | expect(firstResult).to(beIdenticalTo(secondResult)) 66 | expect(firstResult).to(beIdenticalTo(token)) 67 | 68 | expect(counter2) == 0 69 | 70 | let thirdResult = retrieve2() 71 | expect(counter2) == 1 72 | 73 | let forthResult = retrieve2() 74 | expect(counter2) == 1 75 | 76 | expect(thirdResult).to(beIdenticalTo(forthResult)) 77 | expect(thirdResult).to(beIdenticalTo(token2)) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UISwitchSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UISwitchSpec: QuickSpec { 9 | override func spec() { 10 | var toggle: UISwitch! 11 | weak var _toggle: UISwitch? 12 | 13 | beforeEach { 14 | toggle = UISwitch(frame: .zero) 15 | _toggle = toggle 16 | } 17 | 18 | afterEach { 19 | toggle = nil 20 | if #available(*, iOS 10.2) { 21 | expect(_toggle).to(beNil()) 22 | } 23 | } 24 | 25 | it("should accept changes from bindings to its `isOn` state") { 26 | toggle.isOn = false 27 | 28 | let (pipeSignal, observer) = Signal.pipe() 29 | toggle.reactive.isOn <~ SignalProducer(pipeSignal) 30 | 31 | observer.send(value: true) 32 | expect(toggle.isOn) == true 33 | 34 | observer.send(value: false) 35 | expect(toggle.isOn) == false 36 | } 37 | 38 | it("should emit user initiated changes to its `isOn` state") { 39 | var latestValue: Bool? 40 | toggle.reactive.isOnValues.observeValues { latestValue = $0 } 41 | 42 | toggle.isOn = true 43 | toggle.sendActions(for: .valueChanged) 44 | expect(latestValue!) == true 45 | } 46 | 47 | it("should execute the `toggled` action upon receiving a `valueChanged` action message.") { 48 | toggle.isOn = false 49 | toggle.isEnabled = true 50 | toggle.isUserInteractionEnabled = true 51 | 52 | let isOn = MutableProperty(false) 53 | let action = Action { isOn in 54 | return SignalProducer(value: isOn) 55 | } 56 | isOn <~ SignalProducer(action.values) 57 | 58 | toggle.reactive.toggled = CocoaAction(action) { return $0.isOn } 59 | 60 | expect(isOn.value) == false 61 | 62 | toggle.isOn = true 63 | toggle.sendActions(for: .valueChanged) 64 | expect(isOn.value) == true 65 | 66 | toggle.isOn = false 67 | toggle.sendActions(for: .valueChanged) 68 | expect(isOn.value) == false 69 | 70 | } 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | before_install: true 4 | install: true 5 | branches: 6 | only: 7 | - master 8 | - /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/ 9 | - /^hotfix-(\d+\.\d+\.\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/ 10 | cache: 11 | directories: 12 | - $HOME/Library/Caches/org.carthage.CarthageKit/dependencies 13 | - Carthage/Build 14 | jobs: 15 | include: 16 | - stage: unit tests 17 | osx_image: xcode10.2 18 | script: 19 | - XCODE_SCHEME=ReactiveCocoa-macOS 20 | XCODE_SDK=macosx 21 | XCODE_ACTION="build-for-testing test-without-building" 22 | XCODE_DESTINATION="arch=x86_64" 23 | XCODE_PLAYGROUND_TARGET="x86_64-apple-macosx10.10" 24 | PLAYGROUND="ReactiveCocoa-macOS.playground" 25 | script/build 26 | - XCODE_SCHEME=ReactiveCocoa-iOS 27 | XCODE_SDK=iphonesimulator 28 | XCODE_ACTION="build-for-testing test-without-building" 29 | XCODE_DESTINATION="platform=iOS Simulator,name=iPhone 6s" 30 | script/build 31 | - XCODE_SCHEME=ReactiveCocoa-tvOS 32 | XCODE_SDK=appletvsimulator 33 | XCODE_ACTION="build-for-testing test-without-building" 34 | XCODE_DESTINATION="platform=tvOS Simulator,name=Apple TV" 35 | script/build 36 | - XCODE_SCHEME=ReactiveCocoa-watchOS 37 | XCODE_SDK=watchsimulator 38 | XCODE_ACTION=build 39 | XCODE_DESTINATION="platform=watchOS Simulator,name=Apple Watch Series 3 - 38mm" 40 | script/build 41 | - stage: package manager tests 42 | install: gem update cocoapods 43 | script: 44 | - pod repo update 45 | - pod lib lint ReactiveCocoa.podspec --use-libraries 46 | - pod lib lint ReactiveMapKit.podspec --use-libraries --include-podspecs=ReactiveCocoa.podspec 47 | env: 48 | - JOB=PODSPEC 49 | - script: carthage build --cache-builds --no-skip-current 50 | - script: swift build 51 | -------------------------------------------------------------------------------- /ReactiveCocoa/NSObject+BindingTarget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | extension Reactive where Base: AnyObject { 5 | /// Creates a binding target which uses the lifetime of the object, and 6 | /// weakly references the object so that the supplied `action` is triggered 7 | /// only if the object has not deinitialized. 8 | /// 9 | /// - parameters: 10 | /// - scheduler: An optional scheduler that the binding target uses. If it 11 | /// is not specified, a UI scheduler would be used. 12 | /// - action: The action to consume values from the bindings. 13 | /// 14 | /// - returns: A binding target that holds no strong references to the 15 | /// object. 16 | public func makeBindingTarget(on scheduler: Scheduler = UIScheduler(), _ action: @escaping (Base, U) -> Void) -> BindingTarget { 17 | return BindingTarget(on: scheduler, lifetime: Lifetime.of(base)) { [weak base = self.base] value in 18 | if let base = base { 19 | action(base, value) 20 | } 21 | } 22 | } 23 | } 24 | 25 | #if swift(>=3.2) 26 | extension Reactive where Base: AnyObject { 27 | /// Creates a binding target that writes to the object with the given key path on a 28 | /// `UIScheduler`. 29 | /// 30 | /// - parameters: 31 | /// - keyPath: The key path to be written to. 32 | /// 33 | /// - returns: A binding target. 34 | public subscript(keyPath: ReferenceWritableKeyPath) -> BindingTarget { 35 | return BindingTarget(on: UIScheduler(), lifetime: Lifetime.of(base), object: base, keyPath: keyPath) 36 | } 37 | 38 | /// Creates a binding target that writes to the object with the given key path. 39 | /// 40 | /// - parameters: 41 | /// - keyPath: The key path to be written to. 42 | /// - scheduler: The scheduler to perform the write on. 43 | /// 44 | /// - returns: A binding target. 45 | public subscript(keyPath: ReferenceWritableKeyPath, on scheduler: Scheduler) -> BindingTarget { 46 | return BindingTarget(on: scheduler, lifetime: Lifetime.of(base), object: base, keyPath: keyPath) 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UILabelSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UILabelSpec: QuickSpec { 9 | override func spec() { 10 | var label: UILabel! 11 | weak var _label: UILabel? 12 | 13 | beforeEach { 14 | label = UILabel(frame: .zero) 15 | _label = label 16 | } 17 | 18 | afterEach { 19 | label = nil 20 | expect(_label).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its text value") { 24 | let firstChange = "first" 25 | let secondChange = "second" 26 | 27 | label.text = "" 28 | 29 | let (pipeSignal, observer) = Signal.pipe() 30 | label.reactive.text <~ SignalProducer(pipeSignal) 31 | 32 | observer.send(value: firstChange) 33 | expect(label.text) == firstChange 34 | 35 | observer.send(value: secondChange) 36 | expect(label.text) == secondChange 37 | 38 | observer.send(value: nil) 39 | expect(label.text).to(beNil()) 40 | } 41 | 42 | it("should accept changes from bindings to its attributed text value") { 43 | let firstChange = NSAttributedString(string: "first") 44 | let secondChange = NSAttributedString(string: "second") 45 | 46 | label.attributedText = NSAttributedString(string: "") 47 | 48 | let (pipeSignal, observer) = Signal.pipe() 49 | label.reactive.attributedText <~ SignalProducer(pipeSignal) 50 | 51 | observer.send(value: firstChange) 52 | expect(label.attributedText) == firstChange 53 | 54 | observer.send(value: secondChange) 55 | expect(label.attributedText) == secondChange 56 | } 57 | 58 | it("should accept changes from bindings to its text color value") { 59 | let firstChange = UIColor.red 60 | let secondChange = UIColor.black 61 | 62 | let label = UILabel(frame: .zero) 63 | 64 | let (pipeSignal, observer) = Signal.pipe() 65 | label.textColor = UIColor.black 66 | label.reactive.textColor <~ SignalProducer(pipeSignal) 67 | 68 | observer.send(value: firstChange) 69 | expect(label.textColor) == firstChange 70 | 71 | observer.send(value: secondChange) 72 | expect(label.textColor) == secondChange 73 | } 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIButtonSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import UIKit 7 | 8 | class UIButtonSpec: QuickSpec { 9 | override func spec() { 10 | var button: UIButton! 11 | weak var _button: UIButton? 12 | 13 | beforeEach { 14 | button = UIButton(frame: .zero) 15 | _button = button 16 | } 17 | 18 | afterEach { 19 | button = nil 20 | expect(_button).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its titles under different states") { 24 | let firstTitle = "First title" 25 | let secondTitle = "Second title" 26 | 27 | let (pipeSignal, observer) = Signal.pipe() 28 | button.reactive.title <~ SignalProducer(pipeSignal) 29 | button.setTitle("", for: .selected) 30 | button.setTitle("", for: .highlighted) 31 | 32 | observer.send(value: firstTitle) 33 | expect(button.title(for: UIControl.State())) == firstTitle 34 | expect(button.title(for: .highlighted)) == "" 35 | expect(button.title(for: .selected)) == "" 36 | 37 | observer.send(value: secondTitle) 38 | expect(button.title(for: UIControl.State())) == secondTitle 39 | expect(button.title(for: .highlighted)) == "" 40 | expect(button.title(for: .selected)) == "" 41 | } 42 | 43 | let pressedTest: (UIButton, UIControl.Event) -> Void = { button, event in 44 | button.isEnabled = true 45 | button.isUserInteractionEnabled = true 46 | 47 | let pressed = MutableProperty(false) 48 | let action = Action<(), Bool, Never> { _ in 49 | SignalProducer(value: true) 50 | } 51 | 52 | pressed <~ SignalProducer(action.values) 53 | 54 | button.reactive.pressed = CocoaAction(action) 55 | expect(pressed.value) == false 56 | 57 | button.sendActions(for: event) 58 | 59 | expect(pressed.value) == true 60 | } 61 | 62 | if #available(iOS 9.0, tvOS 9.0, *) { 63 | it("should execute the `pressed` action upon receiving a `primaryActionTriggered` action message.") { 64 | pressedTest(button, .primaryActionTriggered) 65 | } 66 | } else { 67 | it("should execute the `pressed` action upon receiving a `touchUpInside` action message.") { 68 | pressedTest(button, .touchUpInside) 69 | } 70 | } 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/SignalProducerNimbleMatchers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignalProducerNimbleMatchers.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Javier Soto on 1/25/15. 6 | // Copyright (c) 2015 GitHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import ReactiveSwift 12 | import Nimble 13 | 14 | public func sendValue(_ value: T?, sendError: E?, complete: Bool) -> Predicate> { 15 | return sendValues(value.map { [$0] } ?? [], sendError: sendError, complete: complete) 16 | } 17 | 18 | public func sendValues(_ values: [T], sendError maybeSendError: E?, complete: Bool) -> Predicate> { 19 | return Predicate> { actualExpression in 20 | precondition(maybeSendError == nil || !complete, "Signals can't both send an error and complete") 21 | guard let signalProducer = try actualExpression.evaluate() else { 22 | let message = ExpectationMessage.fail("The SignalProducer was not created.") 23 | .appendedBeNilHint() 24 | return PredicateResult(status: .fail, message: message) 25 | } 26 | 27 | var sentValues: [T] = [] 28 | var sentError: E? 29 | var signalCompleted = false 30 | 31 | signalProducer.start { event in 32 | switch event { 33 | case let .value(value): 34 | sentValues.append(value) 35 | case .completed: 36 | signalCompleted = true 37 | case let .failed(error): 38 | sentError = error 39 | default: 40 | break 41 | } 42 | } 43 | 44 | if sentValues != values { 45 | let message = ExpectationMessage.expectedCustomValueTo( 46 | "send values <\(values)>", 47 | "<\(sentValues)>" 48 | ) 49 | return PredicateResult(status: .doesNotMatch, message: message) 50 | } 51 | 52 | if sentError != maybeSendError { 53 | let message = ExpectationMessage.expectedCustomValueTo( 54 | "send error <\(String(describing: maybeSendError))>", 55 | "<\(String(describing: sentError))>" 56 | ) 57 | return PredicateResult(status: .doesNotMatch, message: message) 58 | } 59 | 60 | let completeMessage = complete ? 61 | "complete, but the producer did not complete" : 62 | "not to complete, but the producer completed" 63 | let message = ExpectationMessage.expectedTo(completeMessage) 64 | return PredicateResult(bool: signalCompleted == complete, message: message) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UITextField.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UITextField { 6 | /// Sets the text of the text field. 7 | public var text: BindingTarget { 8 | return makeBindingTarget { $0.text = $1 } 9 | } 10 | 11 | /// A signal of text values emitted by the text field upon end of editing. 12 | /// 13 | /// - note: To observe text values that change on all editing events, 14 | /// see `continuousTextValues`. 15 | public var textValues: Signal { 16 | return mapControlEvents([.editingDidEnd, .editingDidEndOnExit]) { $0.text ?? "" } 17 | } 18 | 19 | /// A signal of text values emitted by the text field upon any changes. 20 | /// 21 | /// - note: To observe text values only when editing ends, see `textValues`. 22 | public var continuousTextValues: Signal { 23 | return mapControlEvents(.allEditingEvents) { $0.text ?? "" } 24 | } 25 | 26 | /// Sets the attributed text of the text field. 27 | public var attributedText: BindingTarget { 28 | return makeBindingTarget { $0.attributedText = $1 } 29 | } 30 | 31 | /// Sets the placeholder text of the text field. 32 | public var placeholder: BindingTarget { 33 | return makeBindingTarget { $0.placeholder = $1 } 34 | } 35 | 36 | /// Sets the textColor of the text field. 37 | public var textColor: BindingTarget { 38 | return makeBindingTarget { $0.textColor = $1 } 39 | } 40 | 41 | /// A signal of attributed text values emitted by the text field upon end of editing. 42 | /// 43 | /// - note: To observe attributed text values that change on all editing events, 44 | /// see `continuousAttributedTextValues`. 45 | public var attributedTextValues: Signal { 46 | return mapControlEvents([.editingDidEnd, .editingDidEndOnExit]) { $0.attributedText ?? NSAttributedString() } 47 | } 48 | 49 | /// A signal of attributed text values emitted by the text field upon any changes. 50 | /// 51 | /// - note: To observe attributed text values only when editing ends, see `attributedTextValues`. 52 | public var continuousAttributedTextValues: Signal { 53 | return mapControlEvents(.allEditingEvents) { $0.attributedText ?? NSAttributedString() } 54 | } 55 | 56 | /// Sets the secure text entry attribute on the text field. 57 | public var isSecureTextEntry: BindingTarget { 58 | return makeBindingTarget { $0.isSecureTextEntry = $1 } 59 | } 60 | } 61 | #endif 62 | -------------------------------------------------------------------------------- /ReactiveCocoa/CocoaAction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | /// CocoaAction wraps an `Action` for use by a UI control (such as `NSControl` or 5 | /// `UIControl`). 6 | public final class CocoaAction: NSObject { 7 | /// The selector for message senders. 8 | public static var selector: Selector { 9 | return #selector(CocoaAction.execute(_:)) 10 | } 11 | 12 | /// Whether the action is enabled. 13 | /// 14 | /// This property will only change on the main thread. 15 | public let isEnabled: Property 16 | 17 | /// Whether the action is executing. 18 | /// 19 | /// This property will only change on the main thread. 20 | public let isExecuting: Property 21 | 22 | private let _execute: (Sender) -> Void 23 | 24 | /// Initialize a CocoaAction that invokes the given Action by mapping the 25 | /// sender to the input type of the Action. 26 | /// 27 | /// - parameters: 28 | /// - action: The Action. 29 | /// - inputTransform: A closure that maps Sender to the input type of the 30 | /// Action. 31 | public init(_ action: Action, _ inputTransform: @escaping (Sender) -> Input) { 32 | _execute = { sender in 33 | let producer = action.apply(inputTransform(sender)) 34 | producer.start() 35 | } 36 | 37 | isEnabled = Property(initial: action.isEnabled.value, 38 | then: action.isEnabled.producer.observe(on: UIScheduler())) 39 | isExecuting = Property(initial: action.isExecuting.value, 40 | then: action.isExecuting.producer.observe(on: UIScheduler())) 41 | 42 | super.init() 43 | } 44 | 45 | /// Initialize a CocoaAction that invokes the given Action. 46 | /// 47 | /// - parameters: 48 | /// - action: The Action. 49 | public convenience init(_ action: Action<(), Output, Error>) { 50 | self.init(action, { _ in }) 51 | } 52 | 53 | /// Initialize a CocoaAction that invokes the given Action with the given 54 | /// constant. 55 | /// 56 | /// - parameters: 57 | /// - action: The Action. 58 | /// - input: The constant value as the input to the action. 59 | public convenience init(_ action: Action, input: Input) { 60 | self.init(action, { _ in input }) 61 | } 62 | 63 | /// Attempt to execute the underlying action with the given input, subject 64 | /// to the behavior described by the initializer that was used. 65 | /// 66 | /// - parameters: 67 | /// - sender: The sender which initiates the attempt. 68 | @IBAction public func execute(_ sender: Any) { 69 | _execute(sender as! Sender) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIControlSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIControlSpec: QuickSpec { 9 | override func spec() { 10 | var control: UIControl! 11 | weak var _control: UIControl? 12 | 13 | beforeEach { 14 | control = UIControl(frame: .zero) 15 | _control = control 16 | } 17 | afterEach { 18 | control = nil 19 | expect(_control).to(beNil()) 20 | } 21 | 22 | it("should accept changes from bindings to its enabling state") { 23 | control.isEnabled = false 24 | 25 | let (pipeSignal, observer) = Signal.pipe() 26 | control.reactive.isEnabled <~ SignalProducer(pipeSignal) 27 | 28 | observer.send(value: true) 29 | expect(control.isEnabled) == true 30 | 31 | observer.send(value: false) 32 | expect(control.isEnabled) == false 33 | } 34 | 35 | it("should accept changes from bindings to its selecting state") { 36 | control.isSelected = false 37 | 38 | let (pipeSignal, observer) = Signal.pipe() 39 | control.reactive.isSelected <~ SignalProducer(pipeSignal) 40 | 41 | observer.send(value: true) 42 | expect(control.isSelected) == true 43 | 44 | observer.send(value: false) 45 | expect(control.isSelected) == false 46 | } 47 | 48 | it("should accept changes from bindings to its highlighting state") { 49 | control.isHighlighted = false 50 | 51 | let (pipeSignal, observer) = Signal.pipe() 52 | control.reactive.isHighlighted <~ SignalProducer(pipeSignal) 53 | 54 | observer.send(value: true) 55 | expect(control.isHighlighted) == true 56 | 57 | observer.send(value: false) 58 | expect(control.isHighlighted) == false 59 | } 60 | 61 | it("should accept changes from mutliple bindings to its states") { 62 | control.isSelected = false 63 | control.isEnabled = false 64 | 65 | let (pipeSignalSelected, observerSelected) = Signal.pipe() 66 | let (pipeSignalEnabled, observerEnabled) = Signal.pipe() 67 | control.reactive.isSelected <~ SignalProducer(pipeSignalSelected) 68 | control.reactive.isEnabled <~ SignalProducer(pipeSignalEnabled) 69 | 70 | observerSelected.send(value: true) 71 | observerEnabled.send(value: true) 72 | expect(control.isEnabled) == true 73 | expect(control.isSelected) == true 74 | 75 | observerSelected.send(value: false) 76 | expect(control.isEnabled) == true 77 | expect(control.isSelected) == false 78 | 79 | observerEnabled.send(value: false) 80 | expect(control.isEnabled) == false 81 | expect(control.isSelected) == false 82 | } 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /ReactiveMapKitTests/MKMapViewSpec.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | import ReactiveCocoa 3 | import ReactiveMapKit 4 | import Quick 5 | import Nimble 6 | import MapKit 7 | 8 | @available(tvOS 9.2, *) 9 | class MKMapViewSpec: QuickSpec { 10 | override func spec() { 11 | var mapView: MKMapView! 12 | weak var _mapView: MKMapView? 13 | 14 | beforeEach { 15 | mapView = MKMapView(frame: .zero) 16 | _mapView = mapView 17 | } 18 | 19 | afterEach { 20 | autoreleasepool { 21 | mapView = nil 22 | } 23 | // FIXME: SDK_ISSUE 24 | // 25 | // Temporarily disabled since the expectation keeps failing with 26 | // Xcode 8.3 and macOS Sierra 10.12.4. 27 | #if !os(macOS) 28 | // using toEventually(beNil()) here 29 | // since it takes time to release MKMapView 30 | expect(_mapView).toEventually(beNil()) 31 | #endif 32 | } 33 | 34 | it("should accept changes from bindings to its map type") { 35 | expect(mapView.mapType) == MKMapType.standard 36 | 37 | let (pipeSignal, observer) = Signal.pipe() 38 | 39 | mapView.reactive.mapType <~ pipeSignal 40 | 41 | observer.send(value: MKMapType.satellite) 42 | expect(mapView.mapType) == MKMapType.satellite 43 | 44 | observer.send(value: MKMapType.hybrid) 45 | expect(mapView.mapType) == MKMapType.hybrid 46 | } 47 | 48 | it("should accept changes from bindings to its zoom enabled state") { 49 | expect(mapView.isZoomEnabled) == true 50 | 51 | let (pipeSignal, observer) = Signal.pipe() 52 | 53 | mapView.reactive.isZoomEnabled <~ pipeSignal 54 | 55 | observer.send(value: false) 56 | expect(mapView.isZoomEnabled) == false 57 | } 58 | 59 | it("should accept changes from bindings to its scroll enabled state") { 60 | expect(mapView.isScrollEnabled) == true 61 | 62 | let (pipeSignal, observer) = Signal.pipe() 63 | 64 | mapView.reactive.isScrollEnabled <~ pipeSignal 65 | 66 | observer.send(value: false) 67 | expect(mapView.isScrollEnabled) == false 68 | } 69 | 70 | #if !os(tvOS) 71 | it("should accept changes from bindings to its pitch enabled state") { 72 | expect(mapView.isPitchEnabled) == true 73 | 74 | let (pipeSignal, observer) = Signal.pipe() 75 | 76 | mapView.reactive.isPitchEnabled <~ pipeSignal 77 | 78 | observer.send(value: false) 79 | expect(mapView.isPitchEnabled) == false 80 | } 81 | 82 | it("should accept changes from bindings to its rotate enabled state") { 83 | expect(mapView.isRotateEnabled) == true 84 | 85 | let (pipeSignal, observer) = Signal.pipe() 86 | 87 | mapView.reactive.isRotateEnabled <~ pipeSignal 88 | 89 | observer.send(value: false) 90 | expect(mapView.isRotateEnabled) == false 91 | } 92 | #endif 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIRefreshControlSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIRefreshControlSpec: QuickSpec { 9 | override func spec() { 10 | var refreshControl: UIRefreshControl! 11 | weak var _refreshControl: UIRefreshControl! 12 | 13 | beforeEach { 14 | refreshControl = UIRefreshControl() 15 | _refreshControl = refreshControl 16 | } 17 | 18 | afterEach { 19 | refreshControl = nil 20 | expect(_refreshControl).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its refreshing state") { 24 | let (pipeSignal, observer) = Signal.pipe() 25 | refreshControl.reactive.isRefreshing <~ SignalProducer(pipeSignal) 26 | 27 | observer.send(value: true) 28 | expect(refreshControl.isRefreshing) == true 29 | 30 | observer.send(value: false) 31 | expect(refreshControl.isRefreshing) == false 32 | } 33 | 34 | it("should accept changes from bindings to its attributed title state") { 35 | let (pipeSignal, observer) = Signal.pipe() 36 | refreshControl.reactive.attributedTitle <~ SignalProducer(pipeSignal) 37 | 38 | let string = NSAttributedString(string: "test") 39 | 40 | observer.send(value: nil) 41 | expect(refreshControl.attributedTitle).to(beNil()) 42 | 43 | observer.send(value: string) 44 | expect(refreshControl.attributedTitle) == string 45 | 46 | observer.send(value: nil) 47 | expect(refreshControl.attributedTitle).to(beNil()) 48 | } 49 | 50 | it("should execute the `refresh` action upon receiving a `valueChanged` action message.") { 51 | refreshControl.isEnabled = true 52 | refreshControl.isUserInteractionEnabled = true 53 | 54 | let refreshed = MutableProperty(false) 55 | let action = Action<(), Bool, Never> { _ in 56 | SignalProducer(value: true) 57 | } 58 | 59 | refreshed <~ SignalProducer(action.values) 60 | 61 | refreshControl.reactive.refresh = CocoaAction(action) 62 | expect(refreshed.value) == false 63 | 64 | refreshControl.sendActions(for: .valueChanged) 65 | expect(refreshed.value) == true 66 | } 67 | 68 | it("should set `isRefreshing` while `refresh` is executing.") { 69 | refreshControl.isEnabled = true 70 | refreshControl.isUserInteractionEnabled = true 71 | 72 | let action = Action<(), Bool, Never> { _ in 73 | SignalProducer(value: true).delay(1, on: QueueScheduler.main) 74 | } 75 | 76 | refreshControl.reactive.refresh = CocoaAction(action) 77 | expect(refreshControl.isRefreshing) == false 78 | 79 | refreshControl.sendActions(for: .valueChanged) 80 | expect(refreshControl.isRefreshing) == true 81 | 82 | expect(refreshControl.isRefreshing).toEventually(equal(false), timeout: 2) 83 | } 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIViewSpec: QuickSpec { 9 | override func spec() { 10 | var view: UIView! 11 | weak var _view: UIView? 12 | 13 | beforeEach { 14 | view = UIView(frame: .zero) 15 | _view = view 16 | } 17 | 18 | afterEach { 19 | view = nil 20 | expect(_view).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its hiding state") { 24 | view.isHidden = true 25 | 26 | let (pipeSignal, observer) = Signal.pipe() 27 | view.reactive.isHidden <~ SignalProducer(pipeSignal) 28 | 29 | observer.send(value: true) 30 | expect(view.isHidden) == true 31 | 32 | observer.send(value: false) 33 | expect(view.isHidden) == false 34 | } 35 | 36 | it("should accept changes from bindings to its alpha value") { 37 | view.alpha = 0.0 38 | 39 | let firstChange = CGFloat(0.5) 40 | let secondChange = CGFloat(0.7) 41 | 42 | let (pipeSignal, observer) = Signal.pipe() 43 | view.reactive.alpha <~ SignalProducer(pipeSignal) 44 | 45 | observer.send(value: firstChange) 46 | expect(view.alpha) ≈ firstChange 47 | 48 | observer.send(value: secondChange) 49 | expect(view.alpha) ≈ secondChange 50 | } 51 | 52 | it("should accept changes from bindings to its user interaction enabling state") { 53 | view.isUserInteractionEnabled = true 54 | 55 | let (pipeSignal, observer) = Signal.pipe() 56 | view.reactive.isUserInteractionEnabled <~ SignalProducer(pipeSignal) 57 | 58 | observer.send(value: true) 59 | expect(view.isUserInteractionEnabled) == true 60 | 61 | observer.send(value: false) 62 | expect(view.isUserInteractionEnabled) == false 63 | } 64 | 65 | it("should accept changes from bindings to its background color") { 66 | view.backgroundColor = .white 67 | 68 | let (pipeSignal, observer) = Signal.pipe() 69 | view.reactive.backgroundColor <~ SignalProducer(pipeSignal) 70 | 71 | observer.send(value: .yellow) 72 | expect(view.backgroundColor) == .yellow 73 | 74 | observer.send(value: .green) 75 | expect(view.backgroundColor) == .green 76 | 77 | observer.send(value: .red) 78 | expect(view.backgroundColor) == .red 79 | } 80 | 81 | it("should accept changes from bindings to its tint color") { 82 | view.tintColor = .white 83 | 84 | let (pipeSignal, observer) = Signal.pipe() 85 | view.reactive.tintColor <~ SignalProducer(pipeSignal) 86 | 87 | observer.send(value: .yellow) 88 | expect(view.tintColor) == .yellow 89 | 90 | observer.send(value: .green) 91 | expect(view.tintColor) == .green 92 | 93 | observer.send(value: .red) 94 | expect(view.tintColor) == .red 95 | } 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UINavigationItem.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | extension Reactive where Base: UINavigationItem { 6 | /// Sets the title of the navigation item. 7 | public var title: BindingTarget { 8 | return makeBindingTarget { $0.title = $1 } 9 | } 10 | 11 | /// Sets the title view of the navigation item. 12 | public var titleView: BindingTarget { 13 | return makeBindingTarget { $0.titleView = $1 } 14 | } 15 | 16 | #if os(iOS) 17 | /// Sets the prompt of the navigation item. 18 | public var prompt: BindingTarget { 19 | return makeBindingTarget { $0.prompt = $1 } 20 | } 21 | 22 | /// Sets the back button item of the navigation item. 23 | public var backBarButtonItem: BindingTarget { 24 | return makeBindingTarget { $0.backBarButtonItem = $1 } 25 | } 26 | 27 | /// Sets the `hidesBackButton` property of the navigation item. 28 | public var hidesBackButton: BindingTarget { 29 | return makeBindingTarget { $0.hidesBackButton = $1 } 30 | } 31 | #endif 32 | 33 | /// Sets the left bar button items of the navigation item. 34 | public var leftBarButtonItems: BindingTarget<[UIBarButtonItem]?> { 35 | return makeBindingTarget { $0.leftBarButtonItems = $1 } 36 | } 37 | 38 | /// Sets the right bar button items of the navigation item. 39 | public var rightBarButtonItems: BindingTarget<[UIBarButtonItem]?> { 40 | return makeBindingTarget { $0.rightBarButtonItems = $1 } 41 | } 42 | 43 | /// Sets the left bar button item of the navigation item. 44 | public var leftBarButtonItem: BindingTarget { 45 | return makeBindingTarget { $0.leftBarButtonItem = $1 } 46 | } 47 | 48 | /// Sets the right bar button item of the navigation item. 49 | public var rightBarButtonItem: BindingTarget { 50 | return makeBindingTarget { $0.rightBarButtonItem = $1 } 51 | } 52 | 53 | #if os(iOS) 54 | /// Sets the `leftItemsSupplementBackButton` property of the navigation item. 55 | @available(iOS 5.0, *) 56 | public var leftItemsSupplementBackButton: BindingTarget { 57 | return makeBindingTarget { $0.leftItemsSupplementBackButton = $1 } 58 | } 59 | 60 | /// Sets the large title display mode of the navigation item. 61 | @available(iOS 11.0, *) 62 | public var largeTitleDisplayMode: BindingTarget { 63 | return makeBindingTarget { $0.largeTitleDisplayMode = $1 } 64 | } 65 | 66 | /// Sets the search controller of the navigation item. 67 | @available(iOS 11.0, *) 68 | public var searchController: BindingTarget { 69 | return makeBindingTarget { $0.searchController = $1 } 70 | } 71 | 72 | /// Sets the `hidesSearchBarWhenScrolling` property of the navigation item. 73 | @available(iOS 11.0, *) 74 | public var hidesSearchBarWhenScrolling: BindingTarget { 75 | return makeBindingTarget { $0.hidesSearchBarWhenScrolling = $1 } 76 | } 77 | #endif 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /ReactiveCocoa/DynamicProperty.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | /// A typed mutable property view to a certain key path of an Objective-C object using 5 | /// Key-Value Coding and Key-Value Observing. 6 | /// 7 | /// Bindings towards a `DynamicProperty` would be directed to the underlying Objective-C 8 | /// object, and would not be affected by the deinitialization of the `DynamicProperty`. 9 | public final class DynamicProperty: MutablePropertyProtocol { 10 | private weak var object: NSObject? 11 | private let keyPath: String 12 | private let cache: Property 13 | private let transform: (Value) -> Any? 14 | 15 | /// The current value of the property, as read and written using Key-Value 16 | /// Coding. 17 | public var value: Value { 18 | get { return cache.value } 19 | set { object?.setValue(transform(newValue), forKeyPath: keyPath) } 20 | } 21 | 22 | /// The lifetime of the property. 23 | public var lifetime: Lifetime { 24 | return object?.reactive.lifetime ?? .empty 25 | } 26 | 27 | /// The binding target of the property. 28 | public var bindingTarget: BindingTarget { 29 | return BindingTarget(lifetime: lifetime) { [weak object, keyPath] value in 30 | object?.setValue(value, forKey: keyPath) 31 | } 32 | } 33 | 34 | /// A producer that will create a Key-Value Observer for the given object, 35 | /// send its initial value then all changes over time, and then complete 36 | /// when the observed object has deallocated. 37 | /// 38 | /// - important: This only works if the object given to init() is KVO-compliant. 39 | /// Most UI controls are not! 40 | public var producer: SignalProducer { 41 | return cache.producer 42 | } 43 | 44 | public var signal: Signal { 45 | return cache.signal 46 | } 47 | 48 | internal init(object: NSObject, keyPath: String, cache: Property, transform: @escaping (Value) -> Any?) { 49 | self.object = object 50 | self.keyPath = keyPath 51 | self.cache = cache 52 | self.transform = transform 53 | } 54 | 55 | /// Create a typed mutable view to the given key path of the given Objective-C object. 56 | /// The generic type `Value` can be any Swift type, and will be bridged to Objective-C 57 | /// via `Any`. 58 | /// 59 | /// - parameters: 60 | /// - object: The Objective-C object to be observed. 61 | /// - keyPath: The key path to observe. 62 | public convenience init(object: NSObject, keyPath: String) { 63 | self.init(object: object, keyPath: keyPath, cache: Property(object: object, keyPath: keyPath), transform: { $0 }) 64 | } 65 | } 66 | 67 | extension DynamicProperty where Value: OptionalProtocol { 68 | /// Create a typed mutable view to the given key path of the given Objective-C object. 69 | /// The generic type `Value` can be any Swift type, and will be bridged to Objective-C 70 | /// via `Any`. 71 | /// 72 | /// - parameters: 73 | /// - object: The Objective-C object to be observed. 74 | /// - keyPath: The key path to observe. 75 | public convenience init(object: NSObject, keyPath: String) { 76 | self.init(object: object, keyPath: keyPath, cache: Property(object: object, keyPath: keyPath), transform: { $0.optional }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/UITextView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(watchOS) 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | private class TextViewDelegateProxy: DelegateProxy, UITextViewDelegate { 6 | @objc func textViewDidChangeSelection(_ textView: UITextView) { 7 | forwardee?.textViewDidChangeSelection?(textView) 8 | } 9 | } 10 | 11 | extension Reactive where Base: UITextView { 12 | private var proxy: TextViewDelegateProxy { 13 | return .proxy(for: base, 14 | setter: #selector(setter: base.delegate), 15 | getter: #selector(getter: base.delegate)) 16 | } 17 | 18 | /// Sets the text of the text view. 19 | public var text: BindingTarget { 20 | return makeBindingTarget { $0.text = $1 } 21 | } 22 | 23 | private func textValues(forName name: NSNotification.Name) -> Signal { 24 | return NotificationCenter.default 25 | .reactive 26 | .notifications(forName: name, object: base) 27 | .take(during: lifetime) 28 | .map { ($0.object as! UITextView).text! } 29 | } 30 | 31 | /// A signal of text values emitted by the text view upon end of editing. 32 | /// 33 | /// - note: To observe text values that change on all editing events, 34 | /// see `continuousTextValues`. 35 | public var textValues: Signal { 36 | return textValues(forName: UITextView.textDidEndEditingNotification) 37 | } 38 | 39 | /// A signal of text values emitted by the text view upon any changes. 40 | /// 41 | /// - note: To observe text values only when editing ends, see `textValues`. 42 | public var continuousTextValues: Signal { 43 | return textValues(forName: UITextView.textDidChangeNotification) 44 | } 45 | 46 | /// Sets the attributed text of the text view. 47 | public var attributedText: BindingTarget { 48 | return makeBindingTarget { $0.attributedText = $1 } 49 | } 50 | 51 | private func attributedTextValues(forName name: NSNotification.Name) -> Signal { 52 | return NotificationCenter.default 53 | .reactive 54 | .notifications(forName: name, object: base) 55 | .take(during: lifetime) 56 | .map { ($0.object as! UITextView).attributedText! } 57 | } 58 | 59 | /// A signal of attributed text values emitted by the text view upon end of editing. 60 | /// 61 | /// - note: To observe attributed text values that change on all editing events, 62 | /// see `continuousAttributedTextValues`. 63 | public var attributedTextValues: Signal { 64 | return attributedTextValues(forName: UITextView.textDidEndEditingNotification) 65 | } 66 | 67 | /// A signal of attributed text values emitted by the text view upon any changes. 68 | /// 69 | /// - note: To observe text values only when editing ends, see `attributedTextValues`. 70 | public var continuousAttributedTextValues: Signal { 71 | return attributedTextValues(forName: UITextView.textDidChangeNotification) 72 | } 73 | 74 | /// A signal of range values emitted by the text view upon any selection change. 75 | public var selectedRangeValues: Signal { 76 | return proxy.intercept(#selector(UITextViewDelegate.textViewDidChangeSelection)) 77 | .map { [unowned base] in base.selectedRange } 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /ReactiveCocoa.xcodeproj/xcshareddata/xcschemes/ReactiveCocoa-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/CocoaActionSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | #endif 4 | import ReactiveSwift 5 | import Nimble 6 | import Quick 7 | import ReactiveCocoa 8 | 9 | class CocoaActionSpec: QuickSpec { 10 | override func spec() { 11 | var action: Action! 12 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 13 | var cocoaAction: CocoaAction! 14 | #else 15 | var cocoaAction: CocoaAction! 16 | #endif 17 | 18 | beforeEach { 19 | action = Action { value in SignalProducer(value: value + 1) } 20 | expect(action.isEnabled.value) == true 21 | 22 | cocoaAction = CocoaAction(action) { _ in 1 } 23 | expect(cocoaAction.isEnabled.value).toEventually(beTruthy()) 24 | } 25 | 26 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 27 | it("should be compatible with AppKit") { 28 | let control = NSControl(frame: NSZeroRect) 29 | control.target = cocoaAction 30 | control.action = CocoaAction.selector 31 | control.performClick(nil) 32 | } 33 | #elseif os(iOS) 34 | it("should be compatible with UIKit") { 35 | let control = UIControl(frame: .zero) 36 | control.addTarget(cocoaAction, action: CocoaAction.selector, for: .touchDown) 37 | control.sendActions(for: .touchDown) 38 | } 39 | #endif 40 | 41 | it("should emit changes for enabled") { 42 | var values: [Bool] = [] 43 | 44 | cocoaAction.isEnabled.producer 45 | .startWithValues { values.append($0) } 46 | 47 | expect(values) == [ true ] 48 | 49 | let result = action.apply(0).first() 50 | expect { try result?.get() } == 1 51 | expect(values).toEventually(equal([ true, false, true ])) 52 | 53 | _ = cocoaAction 54 | } 55 | 56 | it("should generate KVO notifications for executing") { 57 | var values: [Bool] = [] 58 | 59 | cocoaAction.isExecuting.producer 60 | .startWithValues { values.append($0) } 61 | 62 | expect(values) == [ false ] 63 | 64 | let result = action.apply(0).first() 65 | expect { try result?.get() } == 1 66 | expect(values).toEventually(equal([ false, true, false ])) 67 | 68 | _ = cocoaAction 69 | } 70 | 71 | it("should emit `isExecuting` changes only on the main thread") { 72 | var counter = 0 73 | 74 | cocoaAction.isExecuting.producer 75 | .startWithValues { _ in 76 | counter += Thread.current.isMainThread ? 1 : 0 77 | } 78 | 79 | expect(counter) == 1 80 | 81 | action.apply(0).start() 82 | expect(counter) == 3 83 | } 84 | 85 | it("should emit `isEnabled` changes only on the main thread") { 86 | var counter = 0 87 | 88 | cocoaAction.isEnabled.producer 89 | .startWithValues { _ in 90 | counter += Thread.current.isMainThread ? 1 : 0 91 | } 92 | 93 | expect(counter) == 1 94 | 95 | action.apply(0).start() 96 | expect(counter) == 3 97 | } 98 | 99 | context("lifetime") { 100 | it("CocoaAction should not create a retain cycle") { 101 | weak var weakAction = action 102 | expect(weakAction).notTo(beNil()) 103 | 104 | action = nil 105 | expect(weakAction).toNot(beNil()) 106 | 107 | cocoaAction = nil 108 | expect(weakAction).to(beNil()) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIViewControllerSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIViewControllerSpec: QuickSpec { 9 | override func spec() { 10 | var viewController: UIViewController! 11 | weak var _viewController: UIViewController? 12 | 13 | beforeEach { 14 | viewController = UIViewController() 15 | _viewController = viewController 16 | } 17 | 18 | afterEach { 19 | viewController = nil 20 | expect(_viewController).to(beNil()) 21 | } 22 | 23 | it("should accept changes from bindings to its title value") { 24 | let firstChange = "first" 25 | let secondChange = "second" 26 | 27 | viewController.title = "" 28 | 29 | let (pipeSignal, observer) = Signal.pipe() 30 | viewController.reactive.title <~ pipeSignal 31 | 32 | observer.send(value: firstChange) 33 | expect(viewController.title) == firstChange 34 | 35 | observer.send(value: secondChange) 36 | expect(viewController.title) == secondChange 37 | 38 | observer.send(value: nil) 39 | expect(viewController.title).to(beNil()) 40 | } 41 | 42 | it("should send a `value` event when `viewWillAppear` is invoked") { 43 | var isInvoked = false 44 | viewController.reactive.viewWillAppear.observeValues { 45 | isInvoked = true 46 | } 47 | 48 | expect(isInvoked) == false 49 | 50 | viewController.viewWillAppear(false) 51 | expect(isInvoked) == true 52 | } 53 | 54 | it("should send a `value` event when `viewDidAppear` is invoked") { 55 | var isInvoked = false 56 | viewController.reactive.viewDidAppear.observeValues { 57 | isInvoked = true 58 | } 59 | 60 | expect(isInvoked) == false 61 | 62 | viewController.viewDidAppear(false) 63 | expect(isInvoked) == true 64 | } 65 | 66 | it("should send a `value` event when `viewWillDisappear` is invoked") { 67 | var isInvoked = false 68 | viewController.reactive.viewWillDisappear.observeValues { 69 | isInvoked = true 70 | } 71 | 72 | expect(isInvoked) == false 73 | 74 | viewController.viewWillDisappear(false) 75 | expect(isInvoked) == true 76 | } 77 | 78 | it("should send a `value` event when `viewDidDisappear` is invoked") { 79 | var isInvoked = false 80 | viewController.reactive.viewDidDisappear.observeValues { 81 | isInvoked = true 82 | } 83 | 84 | expect(isInvoked) == false 85 | 86 | viewController.viewDidDisappear(false) 87 | expect(isInvoked) == true 88 | } 89 | 90 | it("should send a `value` event when `viewWillLayoutSubviews` is invoked") { 91 | var isInvoked = false 92 | viewController.reactive.viewWillLayoutSubviews.observeValues { 93 | isInvoked = true 94 | } 95 | 96 | expect(isInvoked) == false 97 | 98 | viewController.viewWillLayoutSubviews() 99 | expect(isInvoked) == true 100 | } 101 | 102 | it("should send a `value` event when `viewDidLayoutSubviews` is invoked") { 103 | var isInvoked = false 104 | viewController.reactive.viewDidLayoutSubviews.observeValues { 105 | isInvoked = true 106 | } 107 | 108 | expect(isInvoked) == false 109 | 110 | viewController.viewDidLayoutSubviews() 111 | expect(isInvoked) == true 112 | } 113 | } 114 | } 115 | #endif 116 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIGestureRecognizerSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UIGestureRecognizerSpec: QuickSpec { 9 | override func spec() { 10 | var gestureRecognizer: TestTapGestureRecognizer! 11 | weak var _gestureRecognizer: TestTapGestureRecognizer? 12 | 13 | beforeEach { 14 | gestureRecognizer = TestTapGestureRecognizer() 15 | _gestureRecognizer = gestureRecognizer 16 | } 17 | 18 | afterEach { 19 | gestureRecognizer = nil 20 | expect(_gestureRecognizer).to(beNil()) 21 | } 22 | 23 | it("should send a value when the gesture state changes") { 24 | let signal = gestureRecognizer.reactive.stateChanged 25 | 26 | var counter = 0 27 | signal.observeValues { _ in counter += 1 } 28 | 29 | expect(counter) == 0 30 | gestureRecognizer.fireGestureEvent(.possible) 31 | 32 | expect(counter) == 1 33 | 34 | gestureRecognizer.fireGestureEvent( .began) 35 | expect(counter) == 2 36 | 37 | gestureRecognizer.fireGestureEvent(.changed) 38 | expect(counter) == 3 39 | 40 | gestureRecognizer.fireGestureEvent(.ended) 41 | expect(counter) == 4 42 | } 43 | 44 | it("should send it's gesture recognizer in signal") { 45 | let signal = gestureRecognizer.reactive.stateChanged 46 | var counter = 0 47 | signal.observeValues { signalGestureRecognizer in 48 | if signalGestureRecognizer === gestureRecognizer{ 49 | counter += 1 50 | } 51 | } 52 | gestureRecognizer.fireGestureEvent( .began) 53 | expect(counter) == 1 54 | } 55 | 56 | it("should send it's gesture recognizer with the fired state") { 57 | let signal = gestureRecognizer.reactive.stateChanged 58 | weak var signalGestureRecognizer: TestTapGestureRecognizer? 59 | signal.observeValues { recognizer in 60 | signalGestureRecognizer = recognizer 61 | } 62 | 63 | gestureRecognizer.fireGestureEvent(.possible) 64 | expect(signalGestureRecognizer?.state) == .possible 65 | 66 | gestureRecognizer.fireGestureEvent( .began) 67 | expect(signalGestureRecognizer?.state) == .began 68 | 69 | gestureRecognizer.fireGestureEvent(.changed) 70 | expect(signalGestureRecognizer?.state) == .changed 71 | 72 | gestureRecognizer.fireGestureEvent(.ended) 73 | expect(signalGestureRecognizer?.state) == .ended 74 | } 75 | } 76 | } 77 | 78 | private final class TestTapGestureRecognizer: UITapGestureRecognizer { 79 | private struct TargetActionPair { 80 | let target: AnyObject 81 | let action: Selector 82 | } 83 | 84 | private var targetActionPair: TargetActionPair? 85 | private var forceState: UIGestureRecognizer.State = .ended 86 | 87 | fileprivate override var state: UIGestureRecognizer.State { 88 | get { return forceState } 89 | set { } 90 | } 91 | 92 | fileprivate override func addTarget(_ target: Any, action: Selector) { 93 | targetActionPair = TargetActionPair(target: target as AnyObject, action: action) 94 | } 95 | 96 | fileprivate func fireGestureEvent(_ state: UIGestureRecognizer.State) { 97 | guard let targetAction = self.targetActionPair else { return } 98 | forceState = state 99 | _ = targetAction.target.perform(targetAction.action, with: self) 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/NSControl.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | extension NSControl: ActionMessageSending {} 6 | 7 | extension Reactive where Base: NSControl { 8 | /// Sets whether the control is enabled. 9 | public var isEnabled: BindingTarget { 10 | return makeBindingTarget { $0.isEnabled = $1 } 11 | } 12 | 13 | /// Sets the value of the control with an `NSAttributedString`. 14 | public var attributedStringValue: BindingTarget { 15 | return makeBindingTarget { $0.attributedStringValue = $1 } 16 | } 17 | 18 | /// A signal of values in `NSAttributedString`, emitted by the control. 19 | public var attributedStringValues: Signal { 20 | return proxy.invoked.map { $0.attributedStringValue } 21 | } 22 | 23 | /// Sets the value of the control with a `Bool`. 24 | public var boolValue: BindingTarget { 25 | #if swift(>=4.0) 26 | return makeBindingTarget { $0.integerValue = $1 ? NSControl.StateValue.on.rawValue : NSControl.StateValue.off.rawValue } 27 | #else 28 | return makeBindingTarget { $0.integerValue = $1 ? NSOnState : NSOffState } 29 | #endif 30 | } 31 | 32 | /// A signal of values in `Bool`, emitted by the control. 33 | public var boolValues: Signal { 34 | #if swift(>=4.0) 35 | return proxy.invoked.map { $0.integerValue != NSControl.StateValue.off.rawValue } 36 | #else 37 | return proxy.invoked.map { $0.integerValue != NSOffState } 38 | #endif 39 | } 40 | 41 | /// Sets the value of the control with a `Double`. 42 | public var doubleValue: BindingTarget { 43 | return makeBindingTarget { $0.doubleValue = $1 } 44 | } 45 | 46 | /// A signal of values in `Double`, emitted by the control. 47 | public var doubleValues: Signal { 48 | return proxy.invoked.map { $0.doubleValue } 49 | } 50 | 51 | /// Sets the value of the control with a `Float`. 52 | public var floatValue: BindingTarget { 53 | return makeBindingTarget { $0.floatValue = $1 } 54 | } 55 | 56 | /// A signal of values in `Float`, emitted by the control. 57 | public var floatValues: Signal { 58 | return proxy.invoked.map { $0.floatValue } 59 | } 60 | 61 | /// Sets the value of the control with an `Int32`. 62 | public var intValue: BindingTarget { 63 | return makeBindingTarget { $0.intValue = $1 } 64 | } 65 | 66 | /// A signal of values in `Int32`, emitted by the control. 67 | public var intValues: Signal { 68 | return proxy.invoked.map { $0.intValue } 69 | } 70 | 71 | /// Sets the value of the control with an `Int`. 72 | public var integerValue: BindingTarget { 73 | return makeBindingTarget { $0.integerValue = $1 } 74 | } 75 | 76 | /// A signal of values in `Int`, emitted by the control. 77 | public var integerValues: Signal { 78 | return proxy.invoked.map { $0.integerValue } 79 | } 80 | 81 | /// Sets the value of the control. 82 | public var objectValue: BindingTarget { 83 | return makeBindingTarget { $0.objectValue = $1 } 84 | } 85 | 86 | /// A signal of values in `Any?`, emitted by the control. 87 | public var objectValues: Signal { 88 | return proxy.invoked.map { $0.objectValue } 89 | } 90 | 91 | /// Sets the value of the control with a `String`. 92 | public var stringValue: BindingTarget { 93 | return makeBindingTarget { $0.stringValue = $1 } 94 | } 95 | 96 | /// A signal of values in `String`, emitted by the control. 97 | public var stringValues: Signal { 98 | return proxy.invoked.map { $0.stringValue } 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /ReactiveCocoa/AppKit/ActionProxy.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | import ReactiveSwift 4 | 5 | internal final class ActionProxy: NSObject { 6 | internal weak var target: AnyObject? 7 | internal var action: Selector? 8 | internal let invoked: Signal 9 | 10 | private let observer: Signal.Observer 11 | private unowned let owner: Owner 12 | 13 | internal init(owner: Owner, lifetime: Lifetime) { 14 | self.owner = owner 15 | (invoked, observer) = Signal.pipe() 16 | lifetime.ended.observeCompleted(observer.sendCompleted) 17 | } 18 | 19 | // In AppKit, action messages always have only one parameter. 20 | @objc func invoke(_ sender: Any?) { 21 | if let action = action { 22 | if let app = NSApp { 23 | app.sendAction(action, to: target, from: sender) 24 | } else { 25 | _ = target?.perform(action, with: sender) 26 | } 27 | } 28 | 29 | observer.send(value: owner) 30 | } 31 | } 32 | 33 | private let hasSwizzledKey = AssociationKey(default: false) 34 | 35 | @objc internal protocol ActionMessageSending: class { 36 | weak var target: AnyObject? { get set } 37 | var action: Selector? { get set } 38 | } 39 | 40 | extension Reactive where Base: NSObject, Base: ActionMessageSending { 41 | internal var proxy: ActionProxy { 42 | let key = AssociationKey?>((#function as StaticString)) 43 | 44 | return synchronized(base) { 45 | if let proxy = base.associations.value(forKey: key) { 46 | return proxy 47 | } 48 | 49 | let superclass: AnyClass = class_getSuperclass(swizzleClass(base))! 50 | 51 | let proxy = ActionProxy(owner: base, lifetime: lifetime) 52 | proxy.target = base.target 53 | proxy.action = base.action 54 | 55 | // Set the proxy as the new delegate with all dynamic interception bypassed 56 | // by directly invoking setters in the original class. 57 | typealias TargetSetter = @convention(c) (NSObject, Selector, AnyObject?) -> Void 58 | typealias ActionSetter = @convention(c) (NSObject, Selector, Selector?) -> Void 59 | 60 | let setTargetImpl = class_getMethodImplementation(superclass, #selector(setter: base.target)) 61 | unsafeBitCast(setTargetImpl, to: TargetSetter.self)(base, #selector(setter: base.target), proxy) 62 | 63 | let setActionImpl = class_getMethodImplementation(superclass, #selector(setter: base.action)) 64 | unsafeBitCast(setActionImpl, to: ActionSetter.self)(base, #selector(setter: base.action), #selector(proxy.invoke(_:))) 65 | 66 | base.associations.setValue(proxy, forKey: key) 67 | 68 | let newTargetSetterImpl: @convention(block) (NSObject, AnyObject?) -> Void = { object, target in 69 | if let proxy = object.associations.value(forKey: key) { 70 | proxy.target = target 71 | } else { 72 | let impl = class_getMethodImplementation(superclass, #selector(setter: self.base.target)) 73 | unsafeBitCast(impl, to: TargetSetter.self)(object, #selector(setter: self.base.target), target) 74 | } 75 | } 76 | 77 | let newActionSetterImpl: @convention(block) (NSObject, Selector?) -> Void = { object, action in 78 | if let proxy = object.associations.value(forKey: key) { 79 | proxy.action = action 80 | } else { 81 | let impl = class_getMethodImplementation(superclass, #selector(setter: self.base.action)) 82 | unsafeBitCast(impl, to: ActionSetter.self)(object, #selector(setter: self.base.action), action) 83 | } 84 | } 85 | 86 | // Swizzle the instance only after setting up the proxy. 87 | base.swizzle((#selector(setter: base.target), newTargetSetterImpl), 88 | (#selector(setter: base.action), newActionSetterImpl), 89 | key: hasSwizzledKey) 90 | 91 | return proxy 92 | } 93 | } 94 | } 95 | #endif 96 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UITextViewSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import UIKit 5 | import Quick 6 | import Nimble 7 | 8 | class UITextViewSpec: QuickSpec { 9 | override func spec() { 10 | var textView: UITextView! 11 | weak var _textView: UITextView? 12 | 13 | #if swift(>=4.0) 14 | let attributes: [NSAttributedString.Key: Any] = [ 15 | .font: UIFont.systemFont(ofSize: 18), 16 | .foregroundColor: UIColor.red 17 | ] 18 | #else 19 | let attributes = [ 20 | NSFontAttributeName: UIFont.systemFont(ofSize: 18), 21 | NSForegroundColorAttributeName: UIColor.red 22 | ] 23 | #endif 24 | 25 | beforeEach { 26 | autoreleasepool { 27 | textView = UITextView(frame: .zero) 28 | _textView = textView 29 | } 30 | } 31 | 32 | afterEach { 33 | autoreleasepool { 34 | textView = nil 35 | } 36 | expect(_textView).toEventually(beNil()) 37 | } 38 | 39 | it("should emit user initiated changes to its text value when the editing ends") { 40 | textView.text = "Test" 41 | 42 | var latestValue: String? 43 | textView.reactive.textValues.observeValues { text in 44 | latestValue = text 45 | } 46 | 47 | NotificationCenter.default.post(name: UITextView.textDidEndEditingNotification, object: textView) 48 | expect(latestValue) == textView.text 49 | } 50 | 51 | it("should emit user initiated changes to its text value continuously") { 52 | textView.text = "Test" 53 | 54 | var latestValue: String? 55 | textView.reactive.continuousTextValues.observeValues { text in 56 | latestValue = text 57 | } 58 | 59 | NotificationCenter.default.post(name: UITextView.textDidChangeNotification, object: textView) 60 | expect(latestValue) == textView.text 61 | } 62 | 63 | it("should accept changes from bindings to its attributed text value") { 64 | let firstChange = NSAttributedString(string: "first", attributes: attributes) 65 | let secondChange = NSAttributedString(string: "second", attributes: attributes) 66 | 67 | textView.attributedText = NSAttributedString(string: "") 68 | 69 | let (pipeSignal, observer) = Signal.pipe() 70 | textView.reactive.attributedText <~ SignalProducer(pipeSignal) 71 | 72 | observer.send(value: firstChange) 73 | expect(textView.attributedText) == firstChange 74 | 75 | observer.send(value: secondChange) 76 | expect(textView.attributedText) == secondChange 77 | } 78 | 79 | it("should emit user initiated changes to its attributed text value when the editing ends") { 80 | textView.attributedText = NSAttributedString(string: "Test", attributes: attributes) 81 | 82 | var latestValue: NSAttributedString? 83 | textView.reactive.attributedTextValues.observeValues { attributedText in 84 | latestValue = attributedText 85 | } 86 | 87 | NotificationCenter.default.post(name: UITextView.textDidEndEditingNotification, object: textView) 88 | expect(latestValue) == textView.attributedText 89 | } 90 | 91 | it("should emit user initiated changes to its attributed text value continuously") { 92 | textView.attributedText = NSAttributedString(string: "Test", attributes: attributes) 93 | 94 | var latestValue: NSAttributedString? 95 | textView.reactive.continuousAttributedTextValues.observeValues { attributedText in 96 | latestValue = attributedText 97 | } 98 | 99 | NotificationCenter.default.post(name: UITextView.textDidChangeNotification, object: textView) 100 | expect(latestValue) == textView.attributedText 101 | } 102 | 103 | it("should emit user initiated changes for selection") { 104 | var latestValue: NSRange! 105 | textView.reactive.selectedRangeValues.observeValues { 106 | latestValue = $0 107 | } 108 | 109 | textView.text = "Test" 110 | textView.selectedRange = NSRange(location: 1, length: 2) 111 | 112 | textView.delegate!.textViewDidChangeSelection!(textView) 113 | expect(latestValue.location) == 1 114 | expect(latestValue.length) == 2 115 | } 116 | } 117 | } 118 | #endif 119 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/UIKit/UIBarButtonItemSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import UIKit 7 | 8 | class UIBarButtonItemSpec: QuickSpec { 9 | override func spec() { 10 | var barButtonItem: UIBarButtonItem! 11 | weak var _barButtonItem: UIBarButtonItem? 12 | 13 | beforeEach { 14 | barButtonItem = UIBarButtonItem() 15 | _barButtonItem = barButtonItem 16 | } 17 | 18 | afterEach { 19 | barButtonItem = nil 20 | expect(_barButtonItem).to(beNil()) 21 | } 22 | 23 | it("should not be retained with the presence of a `pressed` action") { 24 | let action = Action<(),(),Never> { SignalProducer(value: ()) } 25 | barButtonItem.reactive.pressed = CocoaAction(action) 26 | } 27 | 28 | it("should accept changes from bindings to its enabling state") { 29 | let (pipeSignal, observer) = Signal.pipe() 30 | barButtonItem.reactive.isEnabled <~ SignalProducer(pipeSignal) 31 | 32 | observer.send(value: false) 33 | expect(barButtonItem.isEnabled) == false 34 | 35 | observer.send(value: true) 36 | expect(barButtonItem.isEnabled) == true 37 | } 38 | 39 | it("should accept changes from bindings to its title") { 40 | let (pipeSignal, observer) = Signal.pipe() 41 | barButtonItem.reactive.title <~ SignalProducer(pipeSignal) 42 | 43 | observer.send(value: "title") 44 | expect(barButtonItem.title) == "title" 45 | 46 | observer.send(value: nil) 47 | expect(barButtonItem.title).to(beNil()) 48 | } 49 | 50 | it("should accept changes from bindings to its image") { 51 | let (pipeSignal, observer) = Signal.pipe() 52 | barButtonItem.reactive.image <~ SignalProducer(pipeSignal) 53 | 54 | let image = UIImage() 55 | expect(image).notTo(beNil()) 56 | 57 | observer.send(value: image) 58 | expect(barButtonItem.image) == image 59 | 60 | observer.send(value: nil) 61 | expect(barButtonItem.image).to(beNil()) 62 | } 63 | 64 | it("should accept changes from bindings to its style") { 65 | let (pipeSignal, observer) = Signal.pipe() 66 | barButtonItem.reactive.style <~ SignalProducer(pipeSignal) 67 | 68 | observer.send(value: .done) 69 | expect(barButtonItem.style) == .done 70 | 71 | observer.send(value: .plain) 72 | expect(barButtonItem.style) == .plain 73 | } 74 | 75 | it("should accept changes from bindings to its width") { 76 | let (pipeSignal, observer) = Signal.pipe() 77 | barButtonItem.reactive.width <~ SignalProducer(pipeSignal) 78 | 79 | observer.send(value: 42.0) 80 | expect(barButtonItem.width) == 42.0 81 | 82 | observer.send(value: 320.0) 83 | expect(barButtonItem.width) == 320.0 84 | 85 | observer.send(value: 0.0) 86 | expect(barButtonItem.width) == 0.0 87 | } 88 | 89 | it("should accept changes from bindings to its possible titles") { 90 | let (pipeSignal, observer) = Signal?, Never>.pipe() 91 | barButtonItem.reactive.possibleTitles <~ SignalProducer(pipeSignal) 92 | 93 | let possibleTitles = Set(["Unread (123,456,789)", "Unread"]) 94 | observer.send(value: possibleTitles) 95 | expect(barButtonItem.possibleTitles) == possibleTitles 96 | 97 | observer.send(value: nil) 98 | expect(barButtonItem.possibleTitles).to(beNil()) 99 | } 100 | 101 | it("should accept changes from bindings to its custom view") { 102 | let firstChange = UIView() 103 | firstChange.accessibilityIdentifier = "first" 104 | 105 | let secondChange = UIView() 106 | secondChange.accessibilityIdentifier = "second" 107 | 108 | barButtonItem.customView = nil 109 | 110 | let (pipeSignal, observer) = Signal.pipe() 111 | barButtonItem.reactive.customView <~ pipeSignal 112 | 113 | observer.send(value: firstChange) 114 | expect(barButtonItem.customView) == firstChange 115 | 116 | observer.send(value: secondChange) 117 | expect(barButtonItem.customView) == secondChange 118 | 119 | observer.send(value: nil) 120 | expect(barButtonItem.customView).to(beNil()) 121 | } 122 | } 123 | } 124 | #endif 125 | -------------------------------------------------------------------------------- /ReactiveCocoa/UIKit/iOS/UIKeyboard.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && !os(tvOS) && !os(watchOS) 2 | import UIKit 3 | import ReactiveSwift 4 | 5 | /// The type of system keyboard events. 6 | public enum KeyboardEvent { 7 | case willShow 8 | case didShow 9 | case willHide 10 | case didHide 11 | case willChangeFrame 12 | case didChangeFrame 13 | 14 | /// The name of the notification to observe system keyboard events. 15 | fileprivate var notificationName: Notification.Name { 16 | switch self { 17 | case .willShow: 18 | return UIResponder.keyboardWillShowNotification 19 | case .didShow: 20 | return UIResponder.keyboardDidShowNotification 21 | case .willHide: 22 | return UIResponder.keyboardWillHideNotification 23 | case .didHide: 24 | return UIResponder.keyboardDidHideNotification 25 | case .willChangeFrame: 26 | return UIResponder.keyboardWillChangeFrameNotification 27 | case .didChangeFrame: 28 | return UIResponder.keyboardDidChangeFrameNotification 29 | } 30 | } 31 | } 32 | 33 | /// The context of an upcoming change in the frame of the system keyboard. 34 | public struct KeyboardChangeContext { 35 | private let base: [AnyHashable: Any] 36 | 37 | /// The event type of the system keyboard. 38 | public let event: KeyboardEvent 39 | 40 | /// The current frame of the system keyboard. 41 | public var beginFrame: CGRect { 42 | return base[UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect 43 | } 44 | 45 | /// The final frame of the system keyboard. 46 | public var endFrame: CGRect { 47 | return base[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect 48 | } 49 | 50 | /// The animation curve which the system keyboard will use to animate the 51 | /// change in its frame. 52 | public var animationCurve: UIView.AnimationCurve { 53 | let value = base[UIResponder.keyboardAnimationCurveUserInfoKey] as! NSNumber 54 | return UIView.AnimationCurve(rawValue: value.intValue)! 55 | } 56 | 57 | /// The duration in which the system keyboard expects to animate the change in 58 | /// its frame. 59 | public var animationDuration: Double { 60 | return base[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double 61 | } 62 | 63 | /// Indicates whether the change is triggered locally. Used in iPad 64 | /// multitasking, where all foreground apps would be notified of any changes 65 | /// in the system keyboard's frame. 66 | @available(iOS 9.0, *) 67 | public var isLocal: Bool { 68 | return base[UIResponder.keyboardIsLocalUserInfoKey] as! Bool 69 | } 70 | 71 | fileprivate init(userInfo: [AnyHashable: Any], event: KeyboardEvent) { 72 | base = userInfo 73 | self.event = event 74 | } 75 | } 76 | 77 | extension Reactive where Base: NotificationCenter { 78 | /// Create a `Signal` that notifies whenever the system keyboard announce specified event. 79 | /// 80 | /// - parameters: 81 | /// - event: The type of system keyboard event to observe. 82 | /// 83 | /// - returns: A `Signal` that emits the context of system keyboard event. 84 | public func keyboard(_ event: KeyboardEvent) -> Signal { 85 | return notifications(forName: event.notificationName) 86 | .map { notification in KeyboardChangeContext(userInfo: notification.userInfo!, event: event) } 87 | } 88 | 89 | /// Create a `Signal` that notifies whenever the system keyboard announces specified events. 90 | /// 91 | /// - parameters: 92 | /// - first: First type of system keyboard event to observe. 93 | /// - second: Second type of system keyboard event to observe. 94 | /// - tail: Rest of the types of system keyboard events to observe. 95 | /// 96 | /// - returns: A `Signal` that emits the context of system keyboard events. 97 | public func keyboard(_ first: KeyboardEvent, _ second: KeyboardEvent, _ tail: KeyboardEvent...) -> Signal { 98 | let events = [first, second] + tail 99 | return .merge(events.map(keyboard)) 100 | } 101 | 102 | /// Create a `Signal` that notifies whenever the system keyboard announces an 103 | /// upcoming change in its frame. 104 | /// 105 | /// - returns: A `Signal` that emits the context of every change in the 106 | /// system keyboard's frame. 107 | public var keyboardChange: Signal { 108 | return keyboard(.willChangeFrame) 109 | } 110 | } 111 | #endif 112 | -------------------------------------------------------------------------------- /Documentation/DebuggingTechniques.md: -------------------------------------------------------------------------------- 1 | # Debugging Techniques 2 | 3 | This document lists debugging techniques and infrastructure helpful for debugging ReactiveCocoa applications. 4 | 5 | #### Use of unresolved operator '<~' or not found in RAC 5 6 | 7 | Since the split into ReactiveCocoa and ReactiveSwift, you'll need to `import ReactiveSwift` as well when using classes or operators that are implemented in ReactiveSwift. 8 | 9 | #### Unscrambling Swift compiler errors 10 | 11 | Type inferrence can be a source of hard-to-debug compiler errors. There are two potential places to be wrong when type inferrence used: 12 | 13 | 1. Definition of type inferred variable 14 | 2. Consumption of type inferred variable 15 | 16 | In both cases errors are related to incorrect assumptions about type. Such issues are common for ReactiveCocoa applications as it is all about operations over data and related types. The current state of the Swift compiler can cause misleading type errors, especially when error happens in the middle of a signal chain. 17 | 18 | Below is an example of type-error scenario: 19 | 20 | ```swift 21 | SignalProducer(value:42) 22 | .on(value: { answer in 23 | return _ 24 | }) 25 | .startWithCompleted { 26 | print("Completed.") 27 | } 28 | ``` 29 | 30 | The code above will not compile with the following error on the `.startWithCompleted` call `error: cannot convert value of type 'Disposable' to closure result type '()'. To find the actual compile error, the chain needs to be broken apart. Add explicit definitions of closure types on each of the steps: 31 | 32 | ```swift 33 | let initialProducer = SignalProducer.init(value:42) 34 | let sideEffectProducer = initialProducer.on(value: { (answer: Int) in 35 | return _ 36 | }) 37 | let disposable = sideEffectProducer.startWithCompleted { 38 | print("Completed.") 39 | } 40 | ``` 41 | 42 | The code above will not compile too, but with the error `error: cannot convert value of type '(Int) -> _' to expected argument type '((Int) -> Void)?'` on definition of `on` closure. This gives enough of information to locate unexpected `return _` since `on` closure should not have any return value. 43 | 44 | #### Debugging event streams 45 | 46 | As mentioned in the README, stream debugging can be quite difficut and tedious, so we provide the `logEvents` operator. In its simplest form: 47 | 48 | ```swift 49 | let searchString = textField.reactive.continuousTextValues 50 | .throttle(0.5, on: QueueScheduler.main) 51 | .logEvents() 52 | ``` 53 | 54 | This will print to the standard output the events. For most use cases, this is enough and will greatly help you understand your flow. 55 | The biggest problem with this approach, is that it will continue to ouput in Release mode. This leaves with you with two options: 56 | 57 | 1. Comment out the operator: `//.logEvents()`. This is the simpleste approach, but it's error prone, since you will eventually forget to do this. 58 | 2. Pass your own function and manipulate the output as you see fit. This is the recommended approach. 59 | 60 | Let's see how this would look like if we didn't want to print in Release mode: 61 | 62 | ```swift 63 | func debugLog(identifier: String, event: String, fileName: String, functionName: String, lineNumber: Int) { 64 | // Don't forget to set up the DEBUG symbol (http://stackoverflow.com/a/24112024/491239) 65 | #if DEBUG 66 | print(event) 67 | #endif 68 | } 69 | ``` 70 | 71 | You would then: 72 | 73 | ```swift 74 | let searchString = textField.reactive.continuousTextValues 75 | .throttle(0.5, on: QueueScheduler.main) 76 | .logEvents(logger: debugLog) 77 | ``` 78 | 79 | We also provide the `identifier` parameter. This is useful when you are debugging multiple streams and you don't want to get lost: 80 | 81 | ```swift 82 | let searchString = textField.reactive.continuousTextValues 83 | .throttle(0.5, on: QueueScheduler.main) 84 | .logEvents(identifier: "✨My awesome stream ✨") 85 | ``` 86 | 87 | There also cases, especially with [hot signals][Signal], when there is simply too much output. For those, you can specify which events you are interested in: 88 | 89 | ```swift 90 | let searchString = textField.reactive.continuousTextValues 91 | .throttle(0.5, on: QueueScheduler.main) 92 | .logEvents(events: [.disposed]) 93 | ``` 94 | 95 | [Signal]: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Sources/Signal.swift 96 | 97 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/NSPopUpButtonSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveCocoa 5 | import ReactiveSwift 6 | import AppKit 7 | 8 | final class NSPopUpButtonSpec: QuickSpec { 9 | override func spec() { 10 | describe("NSPopUpButton") { 11 | var button: NSPopUpButton! 12 | var window: NSWindow! 13 | weak var _button: NSButton? 14 | let testTitles = (0..<100).map { $0.description } 15 | 16 | beforeEach { 17 | window = NSWindow() 18 | button = NSPopUpButton(frame: .zero) 19 | _button = button 20 | for (i, title) in testTitles.enumerated() { 21 | let item = NSMenuItem(title: title, action: nil, keyEquivalent: "") 22 | item.tag = 1000 + i 23 | 24 | button.menu?.addItem(item) 25 | } 26 | 27 | window.contentView?.addSubview(button) 28 | } 29 | 30 | afterEach { 31 | autoreleasepool { 32 | button.removeFromSuperview() 33 | button = nil 34 | } 35 | expect(_button).to(beNil()) 36 | } 37 | 38 | it("should emit selected index changes") { 39 | var values = [Int]() 40 | button.reactive.selectedIndexes.observeValues { values.append($0) } 41 | 42 | button.menu?.performActionForItem(at: 1) 43 | button.menu?.performActionForItem(at: 99) 44 | 45 | expect(values) == [1, 99] 46 | } 47 | 48 | it("should emit selected title changes") { 49 | var values = [String]() 50 | button.reactive.selectedTitles.observeValues { values.append($0) } 51 | 52 | button.menu?.performActionForItem(at: 1) 53 | button.menu?.performActionForItem(at: 99) 54 | 55 | expect(values) == ["1", "99"] 56 | } 57 | 58 | it("should accept changes from its bindings to its index values") { 59 | let (signal, observer) = Signal.pipe() 60 | button.reactive.selectedIndex <~ SignalProducer(signal) 61 | 62 | observer.send(value: 1) 63 | expect(button.indexOfSelectedItem) == 1 64 | 65 | observer.send(value: 99) 66 | expect(button.indexOfSelectedItem) == 99 67 | 68 | observer.send(value: nil) 69 | expect(button.indexOfSelectedItem) == -1 70 | expect(button.selectedItem?.title).to(beNil()) 71 | } 72 | 73 | it("should accept changes from its bindings to its title values") { 74 | let (signal, observer) = Signal.pipe() 75 | button.reactive.selectedTitle <~ SignalProducer(signal) 76 | 77 | observer.send(value: "1") 78 | expect(button.selectedItem?.title) == "1" 79 | 80 | observer.send(value: "99") 81 | expect(button.selectedItem?.title) == "99" 82 | 83 | observer.send(value: nil) 84 | expect(button.selectedItem?.title).to(beNil()) 85 | expect(button.indexOfSelectedItem) == -1 86 | } 87 | 88 | it("should emit selected item changes") { 89 | var values = [NSMenuItem]() 90 | button.reactive.selectedItems.observeValues { values.append($0) } 91 | 92 | button.menu?.performActionForItem(at: 1) 93 | button.menu?.performActionForItem(at: 99) 94 | 95 | let titles = values.map { $0.title } 96 | expect(titles) == ["1", "99"] 97 | } 98 | 99 | it("should emit selected tag changes") { 100 | var values = [Int]() 101 | button.reactive.selectedTags.observeValues { values.append($0) } 102 | 103 | button.menu?.performActionForItem(at: 1) 104 | button.menu?.performActionForItem(at: 99) 105 | 106 | expect(values) == [1001, 1099] 107 | } 108 | 109 | it("should accept changes from its bindings to its tag values") { 110 | let (signal, observer) = Signal.pipe() 111 | button.reactive.selectedTag <~ SignalProducer(signal) 112 | 113 | observer.send(value: 1001) 114 | expect(button.selectedItem?.tag) == 1001 115 | expect(button.indexOfSelectedItem) == 1 116 | 117 | observer.send(value: 1099) 118 | expect(button.selectedItem?.tag) == 1099 119 | expect(button.indexOfSelectedItem) == 99 120 | 121 | observer.send(value: 1042) 122 | expect(button.selectedItem?.tag) == 1042 123 | expect(button.indexOfSelectedItem) == 42 124 | 125 | // Sending an invalid tag number doesn't change the selection 126 | observer.send(value: testTitles.count + 1) 127 | expect(button.selectedItem?.tag) == 1042 128 | expect(button.indexOfSelectedItem) == 42 129 | } 130 | } 131 | } 132 | } 133 | #endif 134 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/BindingTargetSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import Quick 5 | import Nimble 6 | 7 | private class Object: NSObject { 8 | var value: Int = 0 9 | 10 | func increment() { 11 | value += 1 12 | } 13 | } 14 | 15 | private class NativeObject: ReactiveExtensionsProvider { 16 | var value: Int = 0 17 | 18 | func increment() { 19 | value += 1 20 | } 21 | } 22 | 23 | class BindingTargetSpec: QuickSpec { 24 | override func spec() { 25 | describe("arbitrary binding target on Objective-C object") { 26 | it("should call the action") { 27 | let object = Object() 28 | let target = object.reactive.makeBindingTarget { (object: Object, nothing: Void) -> Void in 29 | object.increment() 30 | } 31 | expect(object.value) == 0 32 | 33 | let (signal, observer) = Signal<(), Never>.pipe() 34 | target <~ signal 35 | expect(object.value) == 0 36 | 37 | observer.send(value: ()) 38 | expect(object.value) == 1 39 | 40 | observer.send(value: ()) 41 | expect(object.value) == 2 42 | } 43 | 44 | it("should call the action on the given scheduler") { 45 | let scheduler = TestScheduler() 46 | 47 | let object = Object() 48 | let target = object.reactive.makeBindingTarget(on: scheduler) { (object: Object, nothing: Void) -> Void in 49 | object.increment() 50 | } 51 | expect(object.value) == 0 52 | 53 | let (signal, observer) = Signal<(), Never>.pipe() 54 | target <~ signal 55 | observer.send(value: ()) 56 | expect(object.value) == 0 57 | 58 | scheduler.run() 59 | expect(object.value) == 1 60 | 61 | observer.send(value: ()) 62 | expect(object.value) == 1 63 | 64 | scheduler.run() 65 | expect(object.value) == 2 66 | } 67 | } 68 | 69 | describe("arbitrary binding target on native object") { 70 | it("should call the action") { 71 | let object = NativeObject() 72 | let target = object.reactive.makeBindingTarget { (object: NativeObject, nothing: Void) -> Void in 73 | object.increment() 74 | } 75 | expect(object.value) == 0 76 | 77 | let (signal, observer) = Signal<(), Never>.pipe() 78 | target <~ signal 79 | expect(object.value) == 0 80 | 81 | observer.send(value: ()) 82 | expect(object.value) == 1 83 | 84 | observer.send(value: ()) 85 | expect(object.value) == 2 86 | } 87 | 88 | it("should call the action on the given scheduler") { 89 | let scheduler = TestScheduler() 90 | 91 | let object = NativeObject() 92 | let target = object.reactive.makeBindingTarget(on: scheduler) { (object: NativeObject, nothing: Void) -> Void in 93 | object.increment() 94 | } 95 | expect(object.value) == 0 96 | 97 | let (signal, observer) = Signal<(), Never>.pipe() 98 | target <~ signal 99 | observer.send(value: ()) 100 | expect(object.value) == 0 101 | 102 | scheduler.run() 103 | expect(object.value) == 1 104 | 105 | observer.send(value: ()) 106 | expect(object.value) == 1 107 | 108 | scheduler.run() 109 | expect(object.value) == 2 110 | } 111 | } 112 | 113 | 114 | #if swift(>=3.2) 115 | describe("key path binding target") { 116 | it("should update the value") { 117 | let object = Object() 118 | expect(object.value) == 0 119 | 120 | let property = MutableProperty(1) 121 | object.reactive[\.value] <~ property 122 | expect(object.value) == 1 123 | 124 | property.value = 2 125 | expect(object.value) == 2 126 | } 127 | 128 | it("should update the value on the given scheduler") { 129 | let scheduler = TestScheduler() 130 | 131 | let object = Object() 132 | let property = MutableProperty(1) 133 | object.reactive[\.value, on: scheduler] <~ property 134 | expect(object.value) == 0 135 | 136 | scheduler.run() 137 | expect(object.value) == 1 138 | 139 | property.value = 2 140 | expect(object.value) == 1 141 | 142 | scheduler.run() 143 | expect(object.value) == 2 144 | } 145 | 146 | it("should work for native swift objects") { 147 | let object = NativeObject() 148 | expect(object.value) == 0 149 | 150 | let property = MutableProperty(1) 151 | object.reactive[\.value] <~ property 152 | expect(object.value) == 1 153 | 154 | property.value = 2 155 | expect(object.value) == 2 156 | } 157 | } 158 | #endif 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /ReactiveCocoaTests/AppKit/NSButtonSpec.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import Quick 3 | import Nimble 4 | import ReactiveSwift 5 | import ReactiveCocoa 6 | import AppKit 7 | 8 | class NSButtonSpec: QuickSpec { 9 | override func spec() { 10 | var button: NSButton! 11 | weak var _button: NSButton? 12 | 13 | var window: NSWindow! 14 | 15 | beforeEach { 16 | button = NSButton(frame: .zero) 17 | _button = button 18 | window = NSWindow() 19 | window.contentView?.addSubview(button) 20 | } 21 | 22 | afterEach { 23 | autoreleasepool { 24 | button.removeFromSuperview() 25 | button = nil 26 | } 27 | expect(_button).to(beNil()) 28 | } 29 | 30 | it("should accept changes from bindings to its enabling state") { 31 | button.isEnabled = false 32 | 33 | let (pipeSignal, observer) = Signal.pipe() 34 | button.reactive.isEnabled <~ SignalProducer(pipeSignal) 35 | 36 | observer.send(value: true) 37 | expect(button.isEnabled) == true 38 | 39 | observer.send(value: false) 40 | expect(button.isEnabled) == false 41 | } 42 | 43 | it("should accept changes from bindings to its state") { 44 | button.allowsMixedState = true 45 | button.state = RACNSOffState 46 | 47 | let (pipeSignal, observer) = Signal.pipe() 48 | button.reactive.state <~ SignalProducer(pipeSignal) 49 | 50 | observer.send(value: RACNSOffState) 51 | expect(button.state) == RACNSOffState 52 | 53 | observer.send(value: RACNSMixedState) 54 | expect(button.state) == RACNSMixedState 55 | 56 | observer.send(value: RACNSOnState) 57 | expect(button.state) == RACNSOnState 58 | } 59 | 60 | it("should send along state changes") { 61 | button.setButtonType(.pushOnPushOff) 62 | button.allowsMixedState = false 63 | button.state = RACNSOffState 64 | 65 | let state = MutableProperty(RACNSOffState) 66 | state <~ button.reactive.states 67 | 68 | button.performClick(nil) 69 | expect(state.value) == RACNSOnState 70 | 71 | button.performClick(nil) 72 | expect(state.value) == RACNSOffState 73 | 74 | button.allowsMixedState = true 75 | 76 | button.performClick(nil) 77 | expect(state.value) == RACNSMixedState 78 | 79 | button.performClick(nil) 80 | expect(state.value) == RACNSOnState 81 | 82 | button.performClick(nil) 83 | expect(state.value) == RACNSOffState 84 | 85 | } 86 | 87 | if #available(OSX 10.11, *) { 88 | it("should send along state changes embedded within NSStackView") { 89 | 90 | let window = NSWindow() 91 | 92 | let button1 = NSButton() 93 | let button2 = NSButton() 94 | 95 | button1.setButtonType(.pushOnPushOff) 96 | button1.allowsMixedState = false 97 | button1.state = RACNSOffState 98 | 99 | button2.setButtonType(.pushOnPushOff) 100 | button2.allowsMixedState = false 101 | button2.state = RACNSOnState 102 | 103 | let stackView = NSStackView() 104 | stackView.addArrangedSubview(button1) 105 | stackView.addArrangedSubview(button2) 106 | 107 | window.contentView?.addSubview(stackView) 108 | 109 | let state = MutableProperty(RACNSOffState) 110 | state <~ button1.reactive.states 111 | state <~ button2.reactive.states 112 | 113 | button1.performClick(nil) 114 | expect(state.value) == RACNSOnState 115 | 116 | button2.performClick(nil) 117 | expect(state.value) == RACNSOffState 118 | 119 | autoreleasepool { 120 | button1.removeFromSuperview() 121 | button2.removeFromSuperview() 122 | stackView.removeFromSuperview() 123 | } 124 | } 125 | } 126 | 127 | it("should execute the `pressed` action upon receiving a click") { 128 | button.isEnabled = true 129 | 130 | let pressed = MutableProperty(false) 131 | 132 | let (executionSignal, observer) = Signal.pipe() 133 | let action = Action<(), Bool, Never> { _ in 134 | SignalProducer(executionSignal) 135 | } 136 | 137 | pressed <~ SignalProducer(action.values) 138 | button.reactive.pressed = CocoaAction(action) 139 | expect(pressed.value) == false 140 | 141 | button.performClick(nil) 142 | expect(button.isEnabled) == false 143 | 144 | observer.send(value: true) 145 | observer.sendCompleted() 146 | 147 | expect(button.isEnabled) == true 148 | expect(pressed.value) == true 149 | } 150 | } 151 | } 152 | #endif 153 | --------------------------------------------------------------------------------