├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── master.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .jazzy.yaml ├── .swiftlint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── CodeOfConduct.md ├── Documentation ├── APIContracts.md ├── BasicOperators.md ├── DebuggingTechniques.md ├── Example.OnlineSearch.md ├── FrameworkOverview.md ├── ReactivePrimitives.md ├── RxCheatsheet.md └── RxComparison.md ├── LICENSE.md ├── Logo ├── AF │ ├── Docs.afdesign │ ├── JoinSlack.afdesign │ └── ReactiveSwift.afdesign ├── Icons │ ├── Swift.png │ ├── docset-icon.png │ └── docset-icon@2x.png ├── PNG │ ├── Docs.png │ ├── JoinSlack.png │ ├── logo-Swift-unpadded.png │ └── logo-Swift.png ├── Palette.png ├── README.md ├── SVG │ └── logo-Swift.svg └── header.png ├── Package.resolved ├── Package.swift ├── README.md ├── ReactiveSwift-UIExamples.playground ├── Pages │ └── ValidatingProperty.xcplaygroundpage │ │ ├── Contents.swift │ │ └── Sources │ │ ├── FormView.swift │ │ ├── StdlibExtensions.swift │ │ └── UIKitExtensions.swift ├── Sources │ └── PlaygroundUtility.swift └── contents.xcplayground ├── ReactiveSwift.playground ├── Pages │ ├── Property.xcplaygroundpage │ │ └── Contents.swift │ ├── Sandbox.xcplaygroundpage │ │ └── Contents.swift │ ├── Signal.xcplaygroundpage │ │ └── Contents.swift │ └── SignalProducer.xcplaygroundpage │ │ └── Contents.swift ├── Sources │ └── PlaygroundUtility.swift └── contents.xcplayground ├── ReactiveSwift.podspec ├── ReactiveSwift.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── ReactiveSwift-iOS.xcscheme │ ├── ReactiveSwift-macOS.xcscheme │ ├── ReactiveSwift-tvOS.xcscheme │ ├── ReactiveSwift-watchOS.xcscheme │ └── ReactiveSwift-xrOS.xcscheme ├── ReactiveSwift.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources ├── Action.swift ├── Atomic.swift ├── Bag.swift ├── Deprecations+Removals.swift ├── Disposable.swift ├── Event.swift ├── EventLogger.swift ├── Flatten.swift ├── FoundationExtensions.swift ├── Info.plist ├── Lifetime.swift ├── Observers │ ├── AttemptMap.swift │ ├── Collect.swift │ ├── CollectEvery.swift │ ├── CombinePrevious.swift │ ├── CompactMap.swift │ ├── Debounce.swift │ ├── Delay.swift │ ├── Dematerialize.swift │ ├── DematerializeResults.swift │ ├── Filter.swift │ ├── LazyMap.swift │ ├── Map.swift │ ├── MapError.swift │ ├── Materialize.swift │ ├── MaterializeAsResult.swift │ ├── ObserveOn.swift │ ├── Observer.swift │ ├── Operators.swift │ ├── Reduce.swift │ ├── ScanMap.swift │ ├── SkipFirst.swift │ ├── SkipRepeats.swift │ ├── SkipWhile.swift │ ├── TakeFirst.swift │ ├── TakeLast.swift │ ├── TakeUntil.swift │ ├── TakeWhile.swift │ ├── Throttle.swift │ ├── UnaryAsyncOperator.swift │ └── UniqueValues.swift ├── Optional.swift ├── Property.swift ├── Reactive.swift ├── ReactiveSwift.h ├── ResultExtensions.swift ├── Scheduler.swift ├── Signal.Observer.swift ├── Signal.swift ├── SignalProducer.swift ├── UnidirectionalBinding.swift ├── UninhabitedTypeGuards.swift └── ValidatingProperty.swift ├── Tests ├── LinuxMain.swift └── ReactiveSwiftTests │ ├── ActionSpec.swift │ ├── AtomicSpec.swift │ ├── BagSpec.swift │ ├── DateSchedulerAsyncTestCase.swift │ ├── DeprecationSpec.swift │ ├── DisposableSpec.swift │ ├── FlattenSpec.swift │ ├── FoundationExtensionsSpec.swift │ ├── Info.plist │ ├── LifetimeSpec.swift │ ├── PropertySpec.swift │ ├── QueueScheduler+Factory.swift │ ├── ReactiveExtensionsSpec.swift │ ├── SchedulerSpec.swift │ ├── SignalLifetimeSpec.swift │ ├── SignalProducerLiftingSpec.swift │ ├── SignalProducerNimbleMatchers.swift │ ├── SignalProducerSpec.swift │ ├── SignalSpec.swift │ ├── TestError.swift │ ├── TestLogger.swift │ ├── TestSchedulerAsyncTestCase.swift │ ├── UnidirectionalBindingSpec.swift │ └── ValidatingPropertySpec.swift └── script ├── build ├── carthage ├── feed.xml.template ├── gen-docs ├── update-version └── validate-playground.sh /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | #### Checklist 3 | - [ ] Updated CHANGELOG.md. 4 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | tags: 6 | - '[0-9]+\.[0-9]+\.[0-9]+' 7 | - '[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+' 8 | pull_request: 9 | types: [labeled] 10 | branches: 11 | - master 12 | 13 | env: 14 | DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer 15 | 16 | name: Verification | Release 17 | jobs: 18 | carthage: 19 | if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.label.name == 'ci:verify' ) }} 20 | name: Carthage Verification 21 | runs-on: macos-15 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Recover cached dependencies 26 | uses: actions/cache@v4 27 | id: dependency-cache 28 | with: 29 | path: ~/Library/Caches/org.carthage.CarthageKit 30 | key: 4-carthage-verification-${{ runner.os }}-${{ hashFiles('Cartfile.resolved') }} 31 | - name: Carthage verification 32 | run: | 33 | carthage checkout 34 | carthage build --cache-builds --no-skip-current --use-xcframeworks 35 | 36 | swiftpm-macos: 37 | if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.label.name == 'ci:verify' ) }} 38 | name: SwiftPM macOS Verification 39 | runs-on: macos-15 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | - name: Pull dependencies 44 | run: | 45 | swift package resolve 46 | - name: Test via SwiftPM 47 | run: | 48 | swift --version 49 | swift build 50 | 51 | cocoapods: 52 | if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.label.name == 'ci:verify' ) }} 53 | name: CocoaPods Verification 54 | runs-on: macos-15 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | - name: CocoaPods verification 59 | run: | 60 | pod repo update 61 | pod lib lint --use-libraries 62 | 63 | release-github: 64 | if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} 65 | name: GitHub Release 66 | runs-on: macos-15 67 | needs: [swiftpm-macos, cocoapods, carthage] 68 | steps: 69 | - name: git checkout 70 | uses: actions/checkout@v4 71 | - name: create release 72 | uses: softprops/action-gh-release@v2 73 | 74 | release-cocoapods: 75 | if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} 76 | name: CocoaPods Release 77 | runs-on: macos-15 78 | needs: [swiftpm-macos, cocoapods, carthage] 79 | steps: 80 | - name: git checkout 81 | uses: actions/checkout@v4 82 | - name: pod trunk push 83 | env: 84 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 85 | run: pod trunk push -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Test 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: macos-15 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | destination: [macOS, iOS, tvOS, watchOS] 11 | env: 12 | DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Recover cached dependencies 17 | uses: actions/cache@v4 18 | id: dependency-cache 19 | with: 20 | path: Carthage/Checkouts 21 | key: carthage-${{ hashFiles('Cartfile.resolved') }} 22 | - name: Pull dependencies 23 | if: steps.dependency-cache.outputs.cache-hit != 'true' 24 | run: | 25 | carthage checkout 26 | - name: Test via xcodebuild 27 | run: | 28 | ACTION=test 29 | DESTINATION=unknown 30 | SCHEME=unknown 31 | 32 | case "${{ matrix.destination }}" in 33 | "iOS") 34 | DESTINATION="platform=iOS Simulator,name=iPhone 16" 35 | SCHEME=ReactiveSwift-iOS 36 | ;; 37 | "tvOS") 38 | DESTINATION="platform=tvOS Simulator,name=Apple TV" 39 | SCHEME=ReactiveSwift-tvOS 40 | ;; 41 | "watchOS") 42 | ACTION=build 43 | DESTINATION="platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)" 44 | SCHEME=ReactiveSwift-watchOS 45 | ;; 46 | "macCatalyst") 47 | DESTINATION="platform=macOS,variant=Mac Catalyst" 48 | SCHEME=ReactiveSwift-iOS 49 | ;; 50 | "macOS") 51 | DESTINATION="platform=macOS,arch=x86_64" 52 | SCHEME=ReactiveSwift-macOS 53 | ;; 54 | *) 55 | echo "Unknown destination." 56 | exit 1 57 | ;; 58 | esac 59 | 60 | xcodebuild clean ${ACTION} \ 61 | -destination "${DESTINATION}" \ 62 | -scheme ${SCHEME} \ 63 | -workspace ReactiveSwift.xcworkspace \ 64 | CODE_SIGN_IDENTITY="" \ 65 | CODE_SIGNING_REQUIRED=NO \ 66 | ONLY_ACTIVE_ARCH=YES 67 | 68 | swiftpm-linux: 69 | strategy: 70 | matrix: 71 | swift: ["5.9"] 72 | 73 | name: SwiftPM Linux 74 | runs-on: ubuntu-22.04 75 | steps: 76 | - name: Setup Swift version 77 | uses: swift-actions/setup-swift@v2 78 | with: 79 | swift-version: ${{ matrix.swift }} 80 | - name: Checkout 81 | uses: actions/checkout@v4 82 | - name: Pull dependencies 83 | run: | 84 | swift package resolve 85 | - name: Test via SwiftPM 86 | run: | 87 | swift --version 88 | swift build 89 | swift test 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | build/* 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | !default.xcworkspace 15 | xcuserdata 16 | profile 17 | *.moved-aside 18 | PlaygroundUtility.remap 19 | *.xctimeline 20 | *.xcscmblueprint 21 | *.o 22 | 23 | # SwiftPM 24 | .build 25 | Packages 26 | .swiftpm 27 | 28 | # Carthage 29 | Carthage/Build 30 | 31 | # Jazzy 32 | docs 33 | -------------------------------------------------------------------------------- /.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/xcconfigs/xcconfigs.git 10 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | custom_categories: 2 | - name: Getting Started 3 | children: 4 | - ReactivePrimitives 5 | - BasicOperators 6 | - Example.OnlineSearch 7 | - RxComparison 8 | - name: Advanced Topics 9 | children: 10 | - APIContracts 11 | - DebuggingTechniques 12 | - name: Signal 13 | children: 14 | - Signal 15 | - SignalProtocol 16 | - FlattenStrategy 17 | - name: SignalProducer 18 | children: 19 | - SignalProducer 20 | - SignalProducerProtocol 21 | - name: Property 22 | children: 23 | - Property 24 | - PropertyProtocol 25 | - MutableProperty 26 | - MutablePropertyProtocol 27 | - ValidatingProperty 28 | - name: Action 29 | children: 30 | - Action 31 | - ActionError 32 | - name: Bindings 33 | children: 34 | - BindingSource 35 | - BindingTarget 36 | - BindingTargetProvider 37 | - name: Scheduler Protocols 38 | children: 39 | - DateScheduler 40 | - Scheduler 41 | - name: Schedulers 42 | children: 43 | - ImmediateScheduler 44 | - QueueScheduler 45 | - TestScheduler 46 | - UIScheduler 47 | - name: Reactive Extensions 48 | children: 49 | - Reactive 50 | - ReactiveExtensionsProvider 51 | - name: Lifetime 52 | children: 53 | - Lifetime 54 | - name: Disposable 55 | children: 56 | - AnyDisposable 57 | - ActionDisposable 58 | - Disposable 59 | - CompositeDisposable 60 | - ScopedDisposable 61 | - SerialDisposable 62 | - SimpleDisposable 63 | - +=(_:_:) 64 | - name: Debugging 65 | children: 66 | - EventLogger 67 | - LoggingEvent 68 | - name: Utilities 69 | children: 70 | - Atomic 71 | - Bag 72 | - Optional 73 | - OptionalProtocol 74 | theme: fullwidth 75 | skip_undocumented: true 76 | readme: README.md 77 | documentation: Documentation/*.md 78 | hide_documentation_coverage: true 79 | xcodebuild_arguments: 80 | - -workspace 81 | - ReactiveSwift.xcworkspace 82 | - -scheme 83 | - ReactiveSwift-macOS 84 | author: ReactiveCocoa 85 | author_url: https://reactivecocoa.io/ 86 | github_url: https://github.com/ReactiveCocoa/ReactiveSwift/ 87 | dash_url: http://reactivecocoa.io/reactiveswift/docs/ReactiveSwift.xml 88 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - opening_brace 3 | included: 4 | - Sources 5 | - Tests 6 | 7 | trailing_comma: 8 | mandatory_comma: true 9 | 10 | line_length: 200 11 | -------------------------------------------------------------------------------- /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 | 24 | ## Documenting Code 25 | 26 | Please follow these guidelines when documenting code using [Xcode's markup](https://developer.apple.com/library/mac/documentation/Xcode/Reference/xcode_markup_formatting_ref/): 27 | 28 | - Expand lines up to 80 characters per line. If the line extends beyond 80 characters the next line must be indented at the previous markup delimiter's colon position + 1 space: 29 | 30 | ``` 31 | /// DO: 32 | /// For the sake of the demonstration we will use the lorem ipsum text here. 33 | /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ullamcorper 34 | /// tempor dolor a cras amet. 35 | /// 36 | /// - returns: Cras a convallis dolor, sed pellentesque mi. Integer suscipit 37 | /// fringilla turpis in bibendum volutpat. 38 | /// ... 39 | /// DON'T 40 | /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ullamcorper tempor dolor a cras amet. 41 | /// 42 | /// - returns: Cras a convallis dolor, sed pellentesque mi. Integer suscipit fringilla turpis in bibendum volutpat. 43 | /// ... 44 | /// DON'T II 45 | /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ullamcorper 46 | /// tempor dolor a cras amet. 47 | /// 48 | /// - returns: Cras a convallis dolor, sed pellentesque mi. Integer suscipit 49 | /// fringilla turpis in bibendum volutpat. 50 | ``` 51 | 52 | - Always use the `parameters` delimiter instead of the `parameter` delimiter even if a function has only one parameter: 53 | 54 | ``` 55 | /// DO: 56 | /// - parameters: 57 | /// - foo: Instance of `Foo`. 58 | /// DON'T: 59 | /// - parameter foo: Instance of `Foo`. 60 | ``` 61 | 62 | - Do not add a `return` delimiter to an initializer's markup: 63 | 64 | ``` 65 | /// DO: 66 | /// Initialises instance of `Foo` with given arguments. 67 | init(withBar bar: Bar = Bar.defaultBar()) { 68 | ... 69 | /// DON'T: 70 | /// Initialises instance of `Foo` with given arguments. 71 | /// 72 | /// - returns: Initialized `Foo` with default `Bar` 73 | init(withBar bar: Bar = Bar.defaultBar()) { 74 | ... 75 | ``` 76 | 77 | - Treat parameter declaration as a separate sentence/paragraph: 78 | 79 | ``` 80 | /// DO: 81 | /// - parameters: 82 | /// - foo: A foo for the function. 83 | /// ... 84 | /// DON'T: 85 | /// - parameters: 86 | /// - foo: foo for the function; 87 | ``` 88 | 89 | - Add one line between markup delimiters and no whitespace lines between a function's `parameters`: 90 | 91 | ``` 92 | /// DO: 93 | /// - note: This is an amazing function. 94 | /// 95 | /// - parameters: 96 | /// - foo: Instance of `Foo`. 97 | /// - bar: Instance of `Bar`. 98 | /// 99 | /// - returns: Something magical. 100 | /// ... 101 | /// DON'T: 102 | /// - note: Don't forget to breathe, it's important! 😎 103 | /// - parameters: 104 | /// - foo: Instance of `Foo`. 105 | /// - bar: Instance of `Bar`. 106 | /// - returns: Something claustrophobic. 107 | ``` 108 | 109 | - Use code voice to highlight symbols in descriptions, parameter definitions, return statments and elsewhere. 110 | 111 | ``` 112 | /// DO: 113 | /// Create instance of `Foo` by passing it `Bar`. 114 | /// 115 | /// - parameters: 116 | /// - bar: Instance of `Bar`. 117 | /// ... 118 | /// DON'T: 119 | /// Create instance of Foo by passing it Bar. 120 | /// 121 | /// - parameters: 122 | /// - bar: Instance of Bar. 123 | ``` 124 | 125 | - The `precondition`, `parameters` and `return` delimiters should come last in that order in the markup block before the method's signature. 126 | 127 | ``` 128 | /// DO: 129 | /// Create instance of `Foo` by passing it `Bar`. 130 | /// 131 | /// - note: The `foo` is not retained by the receiver. 132 | /// 133 | /// - parameters: 134 | /// - foo: Instance of `Foo`. 135 | init(foo: Foo) { 136 | /* ... */ 137 | /// DON'T 138 | /// Create counter that will count down from `number`. 139 | /// 140 | /// - parameters: 141 | /// - number: Number to count down from. 142 | /// 143 | /// - precondition: `number` must be non-negative. 144 | init(count: Int) 145 | ``` 146 | 147 | - Use first person's active voice in present simple tense. 148 | 149 | ``` 150 | /// DO: 151 | /// Do something magical and return pixie dust from `self`. 152 | /// 153 | /// DON'T: 154 | /// Does something magical and returns pixie dust from `self`. 155 | ``` 156 | 157 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Cartfile -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "xcconfigs/xcconfigs" ~> 1.1 2 | github "Quick/Quick" ~> 7.0.0 3 | github "Quick/Nimble" ~> 13.0.0 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v13.2.1" 2 | github "Quick/Quick" "v7.4.1" 3 | github "xcconfigs/xcconfigs" "1.1" 4 | -------------------------------------------------------------------------------- /CodeOfConduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting members of [the ReactiveSwift team](https://github.com/orgs/ReactiveCocoa/teams/reactiveswift). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Documentation/DebuggingTechniques.md: -------------------------------------------------------------------------------- 1 | # Debugging Techniques 2 | 3 | This document lists debugging techniques and infrastructure helpful for debugging ReactiveCocoa applications. 4 | 5 | #### Unscrambling Swift compiler errors 6 | 7 | Type inferrence can be a source of hard-to-debug compiler errors. There are two potential places to be wrong when type inferrence used: 8 | 9 | 1. Definition of type inferred variable 10 | 2. Consumption of type inferred variable 11 | 12 | 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. 13 | 14 | Below is an example of type-error scenario: 15 | 16 | ```swift 17 | SignalProducer(value:42) 18 | .on(value: { answer in 19 | return _ 20 | }) 21 | .startWithCompleted { 22 | print("Completed.") 23 | } 24 | ``` 25 | 26 | 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: 27 | 28 | ```swift 29 | let initialProducer = SignalProducer.init(value:42) 30 | let sideEffectProducer = initialProducer.on(value: { (answer: Int) in 31 | return _ 32 | }) 33 | let disposable = sideEffectProducer.startWithCompleted { 34 | print("Completed.") 35 | } 36 | ``` 37 | 38 | 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. 39 | 40 | #### Debugging event streams 41 | 42 | As mentioned in the README, stream debugging can be quite difficut and tedious, so we provide the `logEvents` operator. In its simplest form: 43 | 44 | ```swift 45 | let property = MutableProperty("") 46 | ... 47 | let searchString = property.producer 48 | .throttle(0.5, on: QueueScheduler.main) 49 | .logEvents() 50 | ``` 51 | 52 | This will print to the standard output the events. For most use cases, this is enough and will greatly help you understand your flow. 53 | The biggest problem with this approach, is that it will continue to ouput in Release mode. This leaves with you with two options: 54 | 55 | 1. Comment out the operator: `//.logEvents()`. This is the simpleste approach, but it's error prone, since you will eventually forget to do this. 56 | 2. Pass your own function and manipulate the output as you see fit. This is the recommended approach. 57 | 58 | Let's see how this would look like if we didn't want to print in Release mode: 59 | 60 | ```swift 61 | func debugLog(identifier: String, event: String, fileName: String, functionName: String, lineNumber: Int) { 62 | // Don't forget to set up the DEBUG symbol (http://stackoverflow.com/a/24112024/491239) 63 | #if DEBUG 64 | print(event) 65 | #endif 66 | } 67 | ``` 68 | 69 | You would then: 70 | 71 | ```swift 72 | let property = MutableProperty("") 73 | ... 74 | let searchString = property.producer 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 property = MutableProperty("") 83 | ... 84 | let searchString = property.producer 85 | .throttle(0.5, on: QueueScheduler.main) 86 | .logEvents(identifier: "✨My awesome stream ✨") 87 | ``` 88 | 89 | There also cases, specially with [hot signals][Signal], when there is simply too much output. For those, you can specify which events you are interested in: 90 | 91 | ```swift 92 | let property = MutableProperty("") 93 | ... 94 | let searchString = property.producer 95 | .throttle(0.5, on: QueueScheduler.main) 96 | .logEvents(events: [.disposed]) // This will happen when `property` is released 97 | ``` 98 | 99 | [Signal]: ../Sources/Signal.swift 100 | 101 | -------------------------------------------------------------------------------- /Documentation/Example.OnlineSearch.md: -------------------------------------------------------------------------------- 1 | # Example: Online Searching 2 | 3 | Let’s say you have a text field, and whenever the user types something into it, 4 | you want to make a network request which searches for that query. 5 | 6 | _Please note that the following examples use Cocoa extensions in [ReactiveCocoa][] for illustration._ 7 | 8 | #### Observing text edits 9 | 10 | The first step is to observe edits to the text field, using a RAC extension to 11 | `UITextField` specifically for this purpose: 12 | 13 | ```swift 14 | let searchStrings = textField.reactive.continuousTextValues 15 | ``` 16 | 17 | This gives us a [Signal][] which sends values of type `String?`. 18 | 19 | #### Making network requests 20 | 21 | With each string, we want to execute a network request. ReactiveSwift offers an 22 | `URLSession` extension for doing exactly that: 23 | 24 | ```swift 25 | let searchResults = searchStrings 26 | .flatMap(.latest) { (query: String?) -> SignalProducer<(Data, URLResponse), AnyError> in 27 | let request = self.makeSearchRequest(escapedQuery: query) 28 | return URLSession.shared.reactive.data(with: request) 29 | } 30 | .map { (data, response) -> [SearchResult] in 31 | let string = String(data: data, encoding: .utf8)! 32 | return self.searchResults(fromJSONString: string) 33 | } 34 | .observe(on: UIScheduler()) 35 | ``` 36 | 37 | This has transformed our producer of `String`s into a producer of `Array`s 38 | containing the search results, which will be forwarded on the main thread 39 | (using the [`UIScheduler`][Schedulers]). 40 | 41 | Additionally, [`flatMap(.latest)`][flatMapLatest] here ensures that _only one search_—the 42 | latest—is allowed to be running. If the user types another character while the 43 | network request is still in flight, it will be cancelled before starting a new 44 | one. Just think of how much code that would take to do by hand! 45 | 46 | #### Receiving the results 47 | 48 | Since the source of search strings is a `Signal` which has a hot signal semantic, 49 | the transformations we applied are automatically evaluated whenever new values are 50 | emitted from `searchStrings`. 51 | 52 | Therefore, we can simply observe the signal using `Signal.observe(_:)`: 53 | 54 | ```swift 55 | searchResults.observe { event in 56 | switch event { 57 | case let .value(results): 58 | print("Search results: \(results)") 59 | 60 | case let .failed(error): 61 | print("Search error: \(error)") 62 | 63 | case .completed, .interrupted: 64 | break 65 | } 66 | } 67 | ``` 68 | 69 | Here, we watch for the `Value` [event][Events], which contains our results, and 70 | just log them to the console. This could easily do something else instead, like 71 | update a table view or a label on screen. 72 | 73 | #### Handling failures 74 | 75 | In this example so far, any network error will generate a `Failed` 76 | [event][Events], which will terminate the event stream. Unfortunately, this 77 | means that future queries won’t even be attempted. 78 | 79 | To remedy this, we need to decide what to do with failures that occur. The 80 | quickest solution would be to log them, then ignore them: 81 | 82 | ```swift 83 | .flatMap(.latest) { (query: String) -> SignalProducer<(Data, URLResponse), AnyError> in 84 | let request = self.makeSearchRequest(escapedQuery: query) 85 | 86 | return URLSession.shared.reactive 87 | .data(with: request) 88 | .flatMapError { error in 89 | print("Network error occurred: \(error)") 90 | return SignalProducer.empty 91 | } 92 | } 93 | ``` 94 | 95 | By replacing failures with the `empty` event stream, we’re able to effectively 96 | ignore them. 97 | 98 | However, it’s probably more appropriate to retry at least a couple of times 99 | before giving up. Conveniently, there’s a [`retry`][retry] operator to do exactly that! 100 | 101 | Our improved `searchResults` producer might look like this: 102 | 103 | ```swift 104 | let searchResults = searchStrings 105 | .flatMap(.latest) { (query: String) -> SignalProducer<(Data, URLResponse), AnyError> in 106 | let request = self.makeSearchRequest(escapedQuery: query) 107 | 108 | return URLSession.shared.reactive 109 | .data(with: request) 110 | .retry(upTo: 2) 111 | .flatMapError { error in 112 | print("Network error occurred: \(error)") 113 | return SignalProducer.empty 114 | } 115 | } 116 | .map { (data, response) -> [SearchResult] in 117 | let string = String(data: data, encoding: .utf8)! 118 | return self.searchResults(fromJSONString: string) 119 | } 120 | .observe(on: UIScheduler()) 121 | ``` 122 | 123 | #### Throttling requests 124 | 125 | Now, let’s say you only want to actually perform the search periodically, 126 | to minimize traffic. 127 | 128 | ReactiveCocoa has a declarative `throttle` operator that we can apply to our 129 | search strings: 130 | 131 | ```swift 132 | let searchStrings = textField.reactive.continuousTextValues 133 | .throttle(0.5, on: QueueScheduler.main) 134 | ``` 135 | 136 | This prevents values from being sent less than 0.5 seconds apart. 137 | 138 | To do this manually would require significant state, and end up much harder to 139 | read! With ReactiveCocoa, we can use just one operator to incorporate _time_ into 140 | our event stream. 141 | 142 | #### Debugging event streams 143 | 144 | Due to its nature, a stream's stack trace might have dozens of frames, which, more often than not, can make debugging a very frustrating activity. 145 | A naive way of debugging, is by injecting side effects into the stream, like so: 146 | 147 | ```swift 148 | let searchString = textField.reactive.continuousTextValues 149 | .throttle(0.5, on: QueueScheduler.main) 150 | .on(event: { print ($0) }) // the side effect 151 | ``` 152 | 153 | This will print the stream's [events][Events], while preserving the original stream behaviour. Both [`SignalProducer`][SignalProducer] 154 | and [`Signal`][Signal] provide the `logEvents` operator, that will do this automatically for you: 155 | 156 | ```swift 157 | let searchString = textField.reactive.continuousTextValues 158 | .throttle(0.5, on: QueueScheduler.main) 159 | .logEvents() 160 | ``` 161 | 162 | For more information and advance usage, check the [Debugging Techniques][] document. 163 | 164 | [SignalProducer]: ReactivePrimitives.md#signalproducer-deferred-work-that-creates-a-stream-of-values 165 | [Schedulers]: FrameworkOverview.md#Schedulers 166 | [Signal]: ReactivePrimitives.md#signal-a-unidirectional-stream-of-events 167 | [Events]: ReactivePrimitives.md#event-the-basic-transfer-unit-of-an-event-stream 168 | [Debugging Techniques]: DebuggingTechniques.md 169 | 170 | [retry]: BasicOperators.md#retrying 171 | [flatMapLatest]: BasicOperators.md#combining-latest-values 172 | 173 | [ReactiveCocoa]: https://github.com/ReactiveCocoa/ReactiveCocoa/#readme -------------------------------------------------------------------------------- /Documentation/ReactivePrimitives.md: -------------------------------------------------------------------------------- 1 | # Core Reactive Primitives 2 | 3 | 1. [`Signal`](#signal-a-unidirectional-stream-of-events) 4 | 1. [`Event`](#event-the-basic-transfer-unit-of-an-event-stream) 5 | 1. [`SignalProducer`](#signalproducer-deferred-work-that-creates-a-stream-of-values) 6 | 1. [`Property`](#property-an-observable-box-that-always-holds-a-value) 7 | 1. [`Action`](#action-a-serialized-worker-with-a-preset-action) 8 | 1. [`Lifetime`](#lifetime-limits-the-scope-of-an-observation) 9 | 10 | #### `Signal`: a unidirectional stream of events. 11 | The owner of a `Signal` has unilateral control of the event stream. Observers may register their interests in the future events at any time, but the observation would have no side effect on the stream or its owner. 12 | 13 | A `Signal` is like a live TV feed — you can observe and react to the content, but you cannot have a side effect on the live feed or the TV station. 14 | 15 | ```swift 16 | let channel: Signal = tvStation.channelOne 17 | channel.observeValues { program in ... } 18 | ``` 19 | 20 | *See also: [The `Signal` overview](FrameworkOverview.md#signals), [The `Signal` contract](APIContracts.md#the-signal-contract), [The `Signal` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Signal.html)* 21 | 22 | 23 | #### `Event`: the basic transfer unit of an event stream. 24 | A `Signal` may have any arbitrary number of events carrying a value, followed by an eventual terminal event of a specific reason. 25 | 26 | An `Event` is like a frame in a one-time live feed — seas of data frames carry the visual and audio data, but the feed would eventually be terminated with a special frame to indicate "end of stream". 27 | 28 | *See also: [The `Event` overview](FrameworkOverview.md#events), [The `Event` contract](APIContracts.md#the-event-contract), [The `Event` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Signal/Event.html)* 29 | 30 | #### `SignalProducer`: deferred work that creates a stream of values. 31 | `SignalProducer` defers work — of which the output is represented as a stream of values — until it is started. For every invocation to start the `SignalProducer`, a new `Signal` is created and the deferred work is subsequently invoked. 32 | 33 | A `SignalProducer` is like an on-demand streaming service — even though the episode is streamed like a live TV feed, you can choose what you watch, when to start watching and when to interrupt it. 34 | 35 | 36 | ```swift 37 | let frames: SignalProducer = vidStreamer.streamAsset(id: tvShowId) 38 | let interrupter = frames.start { frame in ... } 39 | interrupter.dispose() 40 | ``` 41 | 42 | *See also: [The `SignalProducer` overview](FrameworkOverview.md#signal-producers), [The `SignalProducer` contract](APIContracts.md#the-signalproducer-contract), [The `SignalProducer` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Structs/SignalProducer.html)* 43 | 44 | #### `Property`: an observable box that always holds a value. 45 | `Property` is a variable that can be observed for its changes. In other words, it is a stream of values with a stronger guarantee than `Signal` — the latest value is always available, and the stream will never fail. 46 | 47 | A `Property` is like the continuously updated current time offset of a video playback — the playback is always at a certain time offset at any time, and it would be updated by the playback logic as the playback continues. 48 | 49 | ```swift 50 | let currentTime: Property = video.currentTime 51 | print("Current time offset: \(currentTime.value)") 52 | currentTime.signal.observeValues { timeBar.timeLabel.text = "\($0)" } 53 | ``` 54 | 55 | *See also: [The `Property` overview](FrameworkOverview.md#properties), [The `Property` contract](APIContracts.md#the-property-contract), [The property API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Property.html)* 56 | 57 | #### `Action`: a serialized worker with a preset action. 58 | When invoked with an input, an `Action` applies the input and the latest state to the preset action, and pushes the output to any interested parties. 59 | 60 | An `Action` is like an automatic vending machine — after inserting coins and choosing an option, the machine processes the order and eventually outputs your desired snack. Notice that you cannot have the machine serve two customers concurrently. 61 | 62 | ```swift 63 | // Purchase from the vending machine with a specific option. 64 | vendingMachine.purchase 65 | .apply(snackId) 66 | .startWithResult { result 67 | switch result { 68 | case let .success(snack): 69 | print("Snack: \(snack)") 70 | 71 | case let .failure(error): 72 | // Out of stock? Insufficient fund? 73 | print("Transaction aborted: \(error)") 74 | } 75 | } 76 | 77 | // The vending machine. 78 | class VendingMachine { 79 | let purchase: Action 80 | let coins: MutableProperty 81 | 82 | // The vending machine is connected with a sales recorder. 83 | init(_ salesRecorder: SalesRecorder) { 84 | coins = MutableProperty(0) 85 | purchase = Action(state: coins, enabledIf: { $0 > 0 }) { coins, snackId in 86 | return SignalProducer { observer, _ in 87 | // The sales magic happens here. 88 | // Fetch a snack based on its id 89 | } 90 | } 91 | 92 | // The sales recorders are notified for any successful sales. 93 | purchase.values.observeValues(salesRecorder.record) 94 | } 95 | } 96 | ``` 97 | 98 | *See also: [The `Action` overview](FrameworkOverview.md#actions), [The `Action` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Action.html)* 99 | 100 | #### `Lifetime`: limits the scope of an observation 101 | It doesn't make sense for a `Signal` or `SignalProducer` to continue emitting values if there are no longer any observers. 102 | Consider the video stream: once you stop watching the video, the stream can be automatically closed by providing a `Lifetime`: 103 | 104 | ```swift 105 | class VideoPlayer { 106 | private let (lifetime, token) = Lifetime.make() 107 | 108 | func play() { 109 | let frames: SignalProducer = ... 110 | frames.take(during: lifetime).start { frame in ... } 111 | } 112 | } 113 | ``` 114 | 115 | *See also: [The `Lifetime` overview](FrameworkOverview.md#lifetimes), [The `Lifetime` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Lifetime.html)* 116 | -------------------------------------------------------------------------------- /Documentation/RxComparison.md: -------------------------------------------------------------------------------- 1 | # How does ReactiveSwift relate to RxSwift? 2 | RxSwift is a Swift implementation of the [ReactiveX][] (Rx) APIs. While ReactiveCocoa 3 | was inspired and heavily influenced by Rx, ReactiveSwift is an opinionated 4 | implementation of [functional reactive programming][], and _intentionally_ not a 5 | direct port like [RxSwift][]. 6 | 7 | ReactiveSwift differs from RxSwift/ReactiveX where doing so: 8 | 9 | * Results in a simpler API 10 | * Addresses common sources of confusion 11 | * Matches closely to Swift, and sometimes Cocoa, conventions 12 | 13 | The following are a few important differences, along with their rationales. 14 | 15 | ### Signals and SignalProducers (“hot” and “cold” observables) 16 | 17 | One of the most confusing aspects of Rx is that of [“hot”, “cold”, and “warm” 18 | observables](http://introtorx.com/Content/v1.0.10621.0/14_HotAndColdObservables.html) (event streams). 19 | 20 | In short, given just a method or function declaration like this, in C#: 21 | 22 | ```csharp 23 | IObservable Search(string query) 24 | ``` 25 | 26 | … it is **impossible to tell** whether subscribing to (observing) that 27 | `IObservable` will involve side effects. If it _does_ involve side effects, it’s 28 | also impossible to tell whether _each subscription_ has a side effect, or if only 29 | the first one does. 30 | 31 | This example is contrived, but it demonstrates **a real, pervasive problem** 32 | that makes it extremely hard to understand Rx code (and pre-3.0 ReactiveCocoa 33 | code) at a glance. 34 | 35 | **ReactiveSwift** addresses this by distinguishing side effects with the separate 36 | [`Signal`][Signal] and [`SignalProducer`][SignalProducer] types. Although this 37 | means there’s another type to learn about, it improves code clarity and helps 38 | communicate intent much better. 39 | 40 | In other words, **ReactiveSwift’s changes here are [simple, not 41 | easy](http://www.infoq.com/presentations/Simple-Made-Easy)**. 42 | 43 | ### Typed errors 44 | 45 | When [Signals][Signal] and [SignalProducers][SignalProducer] are allowed to [fail][Events] in ReactiveSwift, 46 | the kind of error must be specified in the type system. For example, 47 | `Signal` is a signal of integer values that may fail with an error 48 | of type `AnyError`. 49 | 50 | More importantly, RAC allows the special type `Never` to be used instead, 51 | which _statically guarantees_ that an event stream is not allowed to send a 52 | failure. **This eliminates many bugs caused by unexpected failure events.** 53 | 54 | In Rx systems with types, event streams only specify the type of their 55 | values—not the type of their errors—so this sort of guarantee is impossible. 56 | 57 | ### Naming 58 | 59 | In most versions of Rx, Streams over time are known as `Observable`s, which 60 | parallels the `Enumerable` type in .NET. Additionally, most operations in Rx.NET 61 | borrow names from [LINQ](https://msdn.microsoft.com/en-us/library/bb397926.aspx), 62 | which uses terms reminiscent of relational databases, like `Select` and `Where`. 63 | 64 | **ReactiveSwift**, on the other hand, focuses on being a native Swift citizen 65 | first and foremost, following the [Swift API Guidelines][] as appropriate. Other 66 | naming differences are typically inspired by significantly better alternatives 67 | from [Haskell](https://www.haskell.org) or [Elm](http://elm-lang.org) (which is the primary source for the “signal” terminology). 68 | 69 | ### UI programming 70 | 71 | Rx is basically agnostic as to how it’s used. Although UI programming with Rx is 72 | very common, it has few features tailored to that particular case. 73 | 74 | ReactiveSwift takes a lot of inspiration from [ReactiveUI](http://reactiveui.net/), 75 | including the basis for [Actions][]. 76 | 77 | Unlike ReactiveUI, which unfortunately cannot directly change Rx to make it more 78 | friendly for UI programming, **ReactiveSwift has been improved many times 79 | specifically for this purpose**—even when it means diverging further from Rx. 80 | 81 | [Actions]: FrameworkOverview.md#actions 82 | [Events]: FrameworkOverview.md#events 83 | [Schedulers]: FrameworkOverview.md#schedulers 84 | [SignalProducer]: FrameworkOverview.md#signal-producers 85 | [Signal]: FrameworkOverview.md#signals 86 | [functional reactive programming]: https://en.wikipedia.org/wiki/Functional_reactive_programming 87 | [ReactiveX]: https://reactivex.io/ 88 | [RxSwift]: https://github.com/ReactiveX/RxSwift/#readme 89 | [Swift API Guidelines]: https://swift.org/documentation/api-design-guidelines/ 90 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Logo/AF/Docs.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/AF/Docs.afdesign -------------------------------------------------------------------------------- /Logo/AF/JoinSlack.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/AF/JoinSlack.afdesign -------------------------------------------------------------------------------- /Logo/AF/ReactiveSwift.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/AF/ReactiveSwift.afdesign -------------------------------------------------------------------------------- /Logo/Icons/Swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/Icons/Swift.png -------------------------------------------------------------------------------- /Logo/Icons/docset-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/Icons/docset-icon.png -------------------------------------------------------------------------------- /Logo/Icons/docset-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/Icons/docset-icon@2x.png -------------------------------------------------------------------------------- /Logo/PNG/Docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/PNG/Docs.png -------------------------------------------------------------------------------- /Logo/PNG/JoinSlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/PNG/JoinSlack.png -------------------------------------------------------------------------------- /Logo/PNG/logo-Swift-unpadded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/PNG/logo-Swift-unpadded.png -------------------------------------------------------------------------------- /Logo/PNG/logo-Swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/PNG/logo-Swift.png -------------------------------------------------------------------------------- /Logo/Palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/Palette.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Logo/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/ReactiveSwift/7f733497761379030d1b82e36d5b7c3deabbf01b/Logo/header.png -------------------------------------------------------------------------------- /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": "3ef6999c73b6938cc0da422f2c912d0158abb0a0", 10 | "version": "2.2.0" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "2ef56b2caf25f55fa7eef8784c30d5a767550f54", 19 | "version": "2.2.1" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "efe11bbca024b57115260709b5c05e01131470d0", 28 | "version": "13.2.1" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "6d01974d236f598633cac58280372c0c8cfea5bc", 37 | "version": "7.4.1" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ReactiveSwift", 6 | platforms: [ 7 | .macOS(.v10_13), .iOS(.v11), .tvOS(.v11), .watchOS(.v4) 8 | ], 9 | products: [ 10 | .library(name: "ReactiveSwift", targets: ["ReactiveSwift"]), 11 | .library(name: "ReactiveSwift-Dynamic", type: .dynamic, targets: ["ReactiveSwift"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/Quick/Quick.git", from: "7.0.0"), 15 | .package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"), 16 | ], 17 | targets: [ 18 | .target(name: "ReactiveSwift", dependencies: [], path: "Sources"), 19 | .testTarget(name: "ReactiveSwiftTests", dependencies: ["ReactiveSwift", "Quick", "Nimble"]), 20 | ], 21 | swiftLanguageVersions: [.v5] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ReactiveSwift

3 | Streams of values over time. Tailored for Swift.

4 | Latest ReactiveSwift Documentation Join the ReactiveSwift Slack community. 5 |

6 |
7 | 8 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](#carthage) [![CocoaPods compatible](https://img.shields.io/cocoapods/v/ReactiveSwift.svg)](#cocoapods) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-orange.svg)](#swift-package-manager) [![GitHub release](https://img.shields.io/github/release/ReactiveCocoa/ReactiveSwift.svg)](https://github.com/ReactiveCocoa/ReactiveSwift/releases) ![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg) ![platforms](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS%20%7C%20Linux-lightgrey.svg) 9 | 10 | 🚄 [Release Roadmap](#release-roadmap) 11 | 12 | ## Getting Started 13 | 14 | Learn about the **[Core Reactive Primitives][]** in ReactiveSwift, and **[Basic Operators][]** available offered by these primitives. 15 | 16 | ### Extended modules 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | 51 |
ModuleRepositoryDescription
ReactiveCocoa 27 | ReactiveCocoa/ReactiveCocoa 28 |
29 | 30 |

Extend Cocoa frameworks and Objective-C runtime APIs with ReactiveSwift bindings and extensions.

Loop 36 | ReactiveCocoa/Loop 37 |
38 | 39 |

Composable unidirectional data flow with ReactiveSwift.

ReactiveSwift Composable Architecture 45 | trading-point/reactiveswift-composable-architecture 46 |
47 | 48 |

The Pointfree Composable Architecture using ReactiveSwift instead of Combine.

52 | 53 | ## What is ReactiveSwift in a nutshell? 54 | __ReactiveSwift__ offers composable, declarative and flexible primitives that are built around the grand concept of ___streams of values over time___. 55 | 56 | These primitives can be used to uniformly represent common Cocoa and generic programming patterns that are fundamentally an act of observation, e.g. delegate pattern, callback closures, notifications, control actions, responder chain events, [futures/promises](https://en.wikipedia.org/wiki/Futures_and_promises) and [key-value observing](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html) (KVO). 57 | 58 | Because all of these different mechanisms can be represented in the _same_ way, 59 | it’s easy to declaratively compose them together, with less spaghetti 60 | code and state to bridge the gap. 61 | 62 | ## References 63 | 64 | 1. **[API Reference][]** 65 | 66 | 1. **[API Contracts][]** 67 | 68 | Contracts of the ReactiveSwift primitives, Best Practices with ReactiveSwift, and Guidelines on implementing custom operators. 69 | 70 | 1. **[Debugging Techniques][]** 71 | 72 | 1. **[RxSwift Migration Cheatsheet][]** 73 | 74 | ## Installation 75 | 76 | ReactiveSwift supports macOS 10.13+, iOS 11.0+, watchOS 4.0+, tvOS 11.0+ and Linux. 77 | 78 | #### Carthage 79 | 80 | If you use [Carthage][] to manage your dependencies, simply add 81 | ReactiveSwift to your `Cartfile`: 82 | 83 | ``` 84 | github "ReactiveCocoa/ReactiveSwift" ~> 6.1 85 | ``` 86 | 87 | If you use Carthage to build your dependencies, make sure you have added `ReactiveSwift.framework` to the "_Linked Frameworks and Libraries_" section of your target, and have included them in your Carthage framework copying build phase. 88 | 89 | #### CocoaPods 90 | 91 | If you use [CocoaPods][] to manage your dependencies, simply add 92 | ReactiveSwift to your `Podfile`: 93 | 94 | ``` 95 | pod 'ReactiveSwift', '~> 6.1' 96 | ``` 97 | 98 | #### Swift Package Manager 99 | 100 | If you use Swift Package Manager, simply add ReactiveSwift as a dependency 101 | of your package in `Package.swift`: 102 | 103 | ``` 104 | .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "6.1.0") 105 | ``` 106 | 107 | #### Git submodule 108 | 109 | 1. Add the ReactiveSwift repository as a [submodule][] of your 110 | application’s repository. 111 | 1. Run `git submodule update --init --recursive` from within the ReactiveCocoa folder. 112 | 1. Drag and drop `ReactiveSwift.xcodeproj` into your application’s Xcode 113 | project or workspace. 114 | 1. On the “General” tab of your application target’s settings, add 115 | `ReactiveSwift.framework` to the “Embedded Binaries” section. 116 | 1. If your application target does not contain Swift code at all, you should also 117 | set the `EMBEDDED_CONTENT_CONTAINS_SWIFT` build setting to “Yes”. 118 | 119 | ## Playground 120 | 121 | We also provide a Playground, so you can get used to ReactiveCocoa's operators. In order to start using it: 122 | 123 | 1. Clone the ReactiveSwift repository. 124 | 1. Retrieve the project dependencies using one of the following terminal commands from the ReactiveSwift project root directory: 125 | - `git submodule update --init --recursive` **OR**, if you have [Carthage][] installed 126 | - `carthage checkout` 127 | 1. Open `ReactiveSwift.xcworkspace` 128 | 1. Build `ReactiveSwift-macOS` scheme 129 | 1. Finally open the `ReactiveSwift.playground` 130 | 1. Choose `View > Show Debug Area` 131 | 132 | ## Have a question? 133 | If you need any help, please visit our [GitHub issues][] or [Stack Overflow][]. Feel free to file an issue if you do not manage to find any solution from the archives. 134 | 135 | ## Release Roadmap 136 | **Current Stable Release:**
[![GitHub release](https://img.shields.io/github/release/ReactiveCocoa/ReactiveSwift.svg)](https://github.com/ReactiveCocoa/ReactiveSwift/releases) 137 | 138 | ### Plan of Record 139 | #### ABI stability release 140 | ReactiveSwift has no plan to declare ABI and module stability at the moment. It will continue to be offered as a source only dependency for the foreseeable future. 141 | 142 | [Core Reactive Primitives]: Documentation/ReactivePrimitives.md 143 | [Basic Operators]: Documentation/BasicOperators.md 144 | [How does ReactiveSwift relate to RxSwift?]: Documentation/RxComparison.md 145 | [API Contracts]: Documentation/APIContracts.md 146 | [API Reference]: http://reactivecocoa.io/reactiveswift/docs/latest/ 147 | [Debugging Techniques]: Documentation/DebuggingTechniques.md 148 | [RxSwift Migration Cheatsheet]: Documentation/RxCheatsheet.md 149 | [Online Searching]: Documentation/Example.OnlineSearch.md 150 | [_UI Examples_ playground]: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/ReactiveSwift-UIExamples.playground/Pages/ValidatingProperty.xcplaygroundpage/Contents.swift 151 | 152 | [`Action`]: Documentation/ReactivePrimitives.md#action-a-serialized-worker-with-a-preset-action 153 | [`SignalProducer`]: Documentation/ReactivePrimitives.md#signalproducer-deferred-work-that-creates-a-stream-of-values 154 | [`Signal`]: Documentation/ReactivePrimitives.md#signal-a-unidirectional-stream-of-events 155 | [`Property`]: Documentation/ReactivePrimitives.md#property-an-observable-box-that-always-holds-a-value 156 | 157 | [ReactiveCocoa]: https://github.com/ReactiveCocoa/ReactiveCocoa/#readme 158 | 159 | [Carthage]: https://github.com/Carthage/Carthage/#readme 160 | [CocoaPods]: https://cocoapods.org/ 161 | [submodule]: https://git-scm.com/docs/git-submodule 162 | 163 | [GitHub issues]: https://github.com/ReactiveCocoa/ReactiveSwift/issues?q=is%3Aissue+label%3Aquestion+ 164 | [Stack Overflow]: http://stackoverflow.com/questions/tagged/reactive-cocoa 165 | 166 | [Looking for the Objective-C API?]: https://github.com/ReactiveCocoa/ReactiveObjC/#readme 167 | [Still using Swift 2.x?]: https://github.com/ReactiveCocoa/ReactiveCocoa/tree/v4.0.0 168 | -------------------------------------------------------------------------------- /ReactiveSwift-UIExamples.playground/Pages/ValidatingProperty.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | > ## IMPORTANT: To use `ReactiveSwift-UIExamples.playground`, please: 3 | 4 | 1. Retrieve the project dependencies using one of the following terminal commands from the ReactiveSwift project root directory: 5 | - `git submodule update --init` 6 | **OR**, if you have [Carthage](https://github.com/Carthage/Carthage) installed 7 | - `carthage checkout` 8 | 1. Open `ReactiveSwift.xcworkspace` 9 | 1. Build `ReactiveSwift-iOS` scheme 10 | 1. Finally open the `ReactiveSwift-UIExamples.playground` through the workspace. 11 | 1. Choose `View > Assistant Editor > Show Assistant Editor` 12 | 1. If you cannot see the playground live view, make sure the Timeline view has been selected for the Assistant Editor. 13 | */ 14 | import ReactiveSwift 15 | import UIKit 16 | import PlaygroundSupport 17 | 18 | final class ViewModel { 19 | struct FormError: Error { 20 | let reason: String 21 | 22 | static let invalidEmail = FormError(reason: "The address must end with `@reactivecocoa.io`.") 23 | static let mismatchEmail = FormError(reason: "The e-mail addresses do not match.") 24 | static let usernameUnavailable = FormError(reason: "The username has been taken.") 25 | } 26 | 27 | let email: ValidatingProperty 28 | let emailConfirmation: ValidatingProperty 29 | let termsAccepted: MutableProperty 30 | 31 | let submit: Action<(), (), FormError> 32 | 33 | let reasons: Signal 34 | 35 | init(userService: UserService) { 36 | // email: ValidatingProperty 37 | email = ValidatingProperty("") { input in 38 | return input.hasSuffix("@reactivecocoa.io") ? .valid : .invalid(.invalidEmail) 39 | } 40 | 41 | // emailConfirmation: ValidatingProperty 42 | emailConfirmation = ValidatingProperty("", with: email) { input, email in 43 | return input == email ? .valid : .invalid(.mismatchEmail) 44 | } 45 | 46 | // termsAccepted: MutableProperty 47 | termsAccepted = MutableProperty(false) 48 | 49 | // `validatedEmail` is a property which holds the validated email address if 50 | // the entire form is valid, or it holds `nil` otherwise. 51 | // 52 | // The condition used in the `map` transform is: 53 | // 1. `emailConfirmation` passes validation: `!email.isInvalid`; and 54 | // 2. `termsAccepted` is asserted: `accepted`. 55 | let validatedEmail: Property = Property 56 | .combineLatest(emailConfirmation.result, termsAccepted) 57 | .map { email, accepted -> String? in 58 | return !email.isInvalid && accepted ? email.value! : nil 59 | } 60 | 61 | // The action to be invoked when the submit button is pressed. 62 | // 63 | // Recall that `submit` is an `Action` with no input, i.e. `Input == ()`. 64 | // `Action` provides a special initializer in this case: `init(state:)`. 65 | // It takes a property of optionals — in our case, `validatedEmail` — and 66 | // would disable whenever the property holds `nil`. 67 | submit = Action(unwrapping: validatedEmail) { (email: String) in 68 | let username = email.stripSuffix("@reactivecocoa.io")! 69 | 70 | return userService.canUseUsername(username) 71 | .promoteError(FormError.self) 72 | .attemptMap { Result<(), FormError>($0 ? () : nil, failWith: .usernameUnavailable) } 73 | } 74 | 75 | // `reason` is an aggregate of latest validation error for the UI to display. 76 | reasons = Property.combineLatest(email.result, emailConfirmation.result) 77 | .signal 78 | .debounce(0.1, on: QueueScheduler.main) 79 | .map { [$0, $1].flatMap { $0.error?.reason }.joined(separator: "\n") } 80 | } 81 | } 82 | 83 | final class ViewController: UIViewController { 84 | private let viewModel: ViewModel 85 | private var formView: FormView! 86 | 87 | override func viewDidLoad() { 88 | super.viewDidLoad() 89 | 90 | // Initialize the interactive controls. 91 | formView.emailField.text = viewModel.email.value 92 | formView.emailConfirmationField.text = viewModel.emailConfirmation.value 93 | formView.termsSwitch.isOn = false 94 | 95 | // Setup bindings with the interactive controls. 96 | viewModel.email <~ formView.emailField.reactive 97 | .continuousTextValues.skipNil() 98 | 99 | viewModel.emailConfirmation <~ formView.emailConfirmationField.reactive 100 | .continuousTextValues.skipNil() 101 | 102 | viewModel.termsAccepted <~ formView.termsSwitch.reactive 103 | .isOnValues 104 | 105 | // Setup bindings with the invalidation reason label. 106 | formView.reasonLabel.reactive.text <~ viewModel.reasons 107 | 108 | // Setup the Action binding with the submit button. 109 | formView.submitButton.reactive.pressed = CocoaAction(viewModel.submit) 110 | } 111 | 112 | override func loadView() { 113 | formView = FormView() 114 | view = formView 115 | } 116 | 117 | init(_ viewModel: ViewModel) { 118 | self.viewModel = viewModel 119 | super.init(nibName: nil, bundle: nil) 120 | } 121 | 122 | required init?(coder aDecoder: NSCoder) { 123 | fatalError() 124 | } 125 | } 126 | 127 | final class UserService { 128 | let (requestSignal, requestObserver) = Signal.pipe() 129 | 130 | func canUseUsername(_ string: String) -> SignalProducer { 131 | return SignalProducer { observer, disposable in 132 | self.requestObserver.send(value: string) 133 | observer.send(value: true) 134 | observer.sendCompleted() 135 | } 136 | } 137 | } 138 | 139 | func main() { 140 | let userService = UserService() 141 | let viewModel = ViewModel(userService: userService) 142 | let viewController = ViewController(viewModel) 143 | let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 350)) 144 | window.rootViewController = viewController 145 | 146 | PlaygroundPage.current.liveView = window 147 | PlaygroundPage.current.needsIndefiniteExecution = true 148 | 149 | window.makeKeyAndVisible() 150 | 151 | // Setup console messages. 152 | userService.requestSignal.observeValues { 153 | print("UserService.requestSignal: Username `\($0)`.") 154 | } 155 | 156 | viewModel.submit.completed.observeValues { 157 | print("ViewModel.submit: execution producer has completed.") 158 | } 159 | 160 | viewModel.email.result.signal.observeValues { 161 | print("ViewModel.email: Validation result - \($0 != nil ? "\($0!)" : "No validation has ever been performed.")") 162 | } 163 | 164 | viewModel.emailConfirmation.result.signal.observeValues { 165 | print("ViewModel.emailConfirmation: Validation result - \($0 != nil ? "\($0!)" : "No validation has ever been performed.")") 166 | } 167 | } 168 | 169 | main() 170 | -------------------------------------------------------------------------------- /ReactiveSwift-UIExamples.playground/Pages/ValidatingProperty.xcplaygroundpage/Sources/FormView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import ReactiveSwift 4 | 5 | public class FormView: UIView { 6 | let _lifetime = Lifetime.Token() 7 | lazy var lifetime: Lifetime = Lifetime(self._lifetime) 8 | 9 | private let disposable = SerialDisposable() 10 | 11 | public let emailField = UITextField() 12 | public let emailConfirmationField = UITextField() 13 | public let termsSwitch = UISwitch() 14 | public let submitButton = UIButton(type: .system) 15 | public let reasonLabel = UILabel() 16 | 17 | convenience init() { 18 | self.init(frame: CGRect(x: 0, y: 0, width: 300, height: 350)) 19 | 20 | backgroundColor = .white 21 | 22 | addSubview(emailField) 23 | addSubview(emailConfirmationField) 24 | addSubview(termsSwitch) 25 | addSubview(submitButton) 26 | addSubview(reasonLabel) 27 | 28 | let labelFont = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize) 29 | 30 | // Email Field. 31 | let emailLabel = UILabel(frame: CGRect(x: 20, y: 20, width: 260, height: 20)) 32 | emailLabel.font = labelFont 33 | emailLabel.text = "E-mail" 34 | addSubview(emailLabel) 35 | 36 | emailField.borderStyle = .roundedRect 37 | emailField.frame.origin = CGPoint(x: 20, y: 40) 38 | emailField.frame.size = CGSize(width: 260, height: 30) 39 | emailField.autocapitalizationType = .none 40 | 41 | // Email Confirmation Field. 42 | let emailConfirmationLabel = UILabel(frame: CGRect(x: 20, y: 80, width: 260, height: 20)) 43 | emailConfirmationLabel.font = labelFont 44 | emailConfirmationLabel.text = "Confirm E-mail" 45 | addSubview(emailConfirmationLabel) 46 | 47 | emailConfirmationField.borderStyle = .roundedRect 48 | emailConfirmationField.frame.origin = CGPoint(x: 20, y: 100) 49 | emailConfirmationField.frame.size = CGSize(width: 260, height: 30) 50 | emailConfirmationField.autocapitalizationType = .none 51 | 52 | // Accept Terms Switch 53 | let termsSwitchLabel = UILabel(frame: CGRect(x: 80, y: 155, width: 200, height: 20)) 54 | termsSwitchLabel.font = labelFont 55 | termsSwitchLabel.text = "Accept Terms and Conditions" 56 | addSubview(termsSwitchLabel) 57 | 58 | termsSwitch.frame.origin = CGPoint(x: 20, y: 150) 59 | 60 | // Submit Button 61 | submitButton.titleLabel!.font = labelFont 62 | submitButton.setBackgroundColor(submitButton.tintColor, for: .normal) 63 | submitButton.setBackgroundColor(UIColor(white: 0.85, alpha: 1.0), for: .disabled) 64 | submitButton.setTitleColor(.white, for: .normal) 65 | submitButton.setTitle("Submit", for: .normal) 66 | submitButton.frame.origin = CGPoint(x: 20, y: 200) 67 | submitButton.frame.size = CGSize(width: 260, height: 30) 68 | 69 | // Reason Label 70 | reasonLabel.frame.origin = CGPoint(x: 20, y: 250) 71 | reasonLabel.frame.size = CGSize(width: 260, height: 80) 72 | reasonLabel.numberOfLines = 0 73 | reasonLabel.font = labelFont 74 | } 75 | } 76 | 77 | // http://stackoverflow.com/questions/26600980/how-do-i-set-uibutton-background-color-forstate-uicontrolstate-highlighted-in-s 78 | extension UIButton { 79 | func setBackgroundColor(_ color: UIColor, for state: UIControlState) { 80 | UIGraphicsBeginImageContext(CGSize(width: 1, height: 1)) 81 | let context = UIGraphicsGetCurrentContext()! 82 | context.setFillColor(color.cgColor) 83 | context.fill(CGRect(x: 0, y: 0, width: 1, height: 1)) 84 | let colorImage = UIGraphicsGetImageFromCurrentImageContext() 85 | UIGraphicsEndImageContext() 86 | self.setBackgroundImage(colorImage, for: state) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ReactiveSwift-UIExamples.playground/Pages/ValidatingProperty.xcplaygroundpage/Sources/StdlibExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | public func stripSuffix(_ suffix: String) -> String? { 5 | if let range = range(of: suffix) { 6 | return substring(with: startIndex ..< range.lowerBound) 7 | } 8 | return nil 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ReactiveSwift-UIExamples.playground/Pages/ValidatingProperty.xcplaygroundpage/Sources/UIKitExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import ReactiveSwift 4 | 5 | // These extensions mimics the ReactiveCocoa API, but not in a complete way. 6 | // 7 | // For production use, check the ReactiveCocoa framework: 8 | // https://github.com/ReactiveCocoa/ReactiveCocoa/ 9 | 10 | private let lifetimeKey = UnsafeMutablePointer.allocate(capacity: 1) 11 | private let pressedKey = UnsafeMutablePointer.allocate(capacity: 1) 12 | 13 | public final class CocoaTarget { 14 | let _execute: (Any) -> Void 15 | 16 | init(_ body: @escaping (Any) -> Void) { 17 | _execute = body 18 | } 19 | 20 | @objc func execute(_ sender: Any) { 21 | _execute(sender) 22 | } 23 | } 24 | 25 | public final class CocoaAction { 26 | let _execute: () -> Void 27 | let isEnabled: Property 28 | 29 | public init(_ action: Action<(), Output, Error>) { 30 | _execute = { action.apply().start() } 31 | isEnabled = action.isEnabled 32 | } 33 | 34 | @objc func execute(_ sender: Any) { 35 | _execute() 36 | } 37 | } 38 | 39 | extension NSObject: ReactiveExtensionsProvider {} 40 | 41 | extension Reactive where Base: NSObject { 42 | public var lifetime: Lifetime { 43 | if let (lifetime, _) = objc_getAssociatedObject(base, lifetimeKey) as! (Lifetime, Lifetime.Token)? { 44 | return lifetime 45 | } 46 | 47 | let token = Lifetime.Token() 48 | let lifetime = Lifetime(token) 49 | objc_setAssociatedObject(base, lifetimeKey, (lifetime, token), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 50 | 51 | return lifetime 52 | } 53 | } 54 | 55 | extension Reactive where Base: UILabel { 56 | public var text: BindingTarget { 57 | return BindingTarget(lifetime: lifetime) { [weak base] value in 58 | base?.text = value 59 | } 60 | } 61 | } 62 | 63 | extension Reactive where Base: UISwitch { 64 | public var isOnValues: Signal { 65 | return Signal { observer, lifetime in 66 | let target = CocoaTarget { observer.send(value: ($0 as! UISwitch).isOn) } 67 | base.addTarget(target, action: #selector(target.execute), for: .valueChanged) 68 | lifetime.observeEnded { _ = target } 69 | } 70 | } 71 | } 72 | 73 | extension Reactive where Base: UITextField { 74 | public var continuousTextValues: Signal { 75 | return Signal { observer, lifetime in 76 | let target = CocoaTarget { observer.send(value: ($0 as! UITextField).text) } 77 | base.addTarget(target, action: #selector(target.execute), for: .editingChanged) 78 | lifetime.observeEnded { _ = target } 79 | } 80 | } 81 | } 82 | 83 | extension Reactive where Base: UIControl { 84 | public var isEnabled: BindingTarget { 85 | return BindingTarget(lifetime: lifetime) { [weak base] value in 86 | base?.isEnabled = value 87 | } 88 | } 89 | } 90 | 91 | extension Reactive where Base: UIButton { 92 | public var pressed: CocoaAction? { 93 | get { 94 | if let (action, _) = objc_getAssociatedObject(base, pressedKey) as! (CocoaAction?, SerialDisposable)? { 95 | return action 96 | } 97 | return nil 98 | } 99 | 100 | nonmutating set { 101 | let disposable: SerialDisposable = { 102 | if let (_, disposable) = objc_getAssociatedObject(base, pressedKey) as! (CocoaAction?, SerialDisposable)? { 103 | return disposable 104 | } 105 | 106 | let d = SerialDisposable() 107 | objc_setAssociatedObject(base, pressedKey, (nil, d) as (CocoaAction?, SerialDisposable), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 108 | return d 109 | }() 110 | 111 | if let newAction = newValue { 112 | base.addTarget(newAction, action: #selector(newAction.execute), for: .touchUpInside) 113 | let d = CompositeDisposable() 114 | d += isEnabled <~ newAction.isEnabled.producer 115 | d += { _ = newAction } 116 | disposable.inner = d 117 | } else { 118 | disposable.inner = nil 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ReactiveSwift-UIExamples.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 | -------------------------------------------------------------------------------- /ReactiveSwift-UIExamples.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ReactiveSwift.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | > # IMPORTANT: To use `ReactiveSwift.playground`, please: 3 | 4 | 1. Retrieve the project dependencies using one of the following terminal commands from the ReactiveSwift project root directory: 5 | - `git submodule update --init` 6 | **OR**, if you have [Carthage](https://github.com/Carthage/Carthage) installed 7 | - `carthage checkout` 8 | 1. Open `ReactiveSwift.xcworkspace` 9 | 1. Build `ReactiveSwift-macOS` scheme 10 | 1. Finally open the `ReactiveSwift.playground` 11 | 1. Choose `View > Show Debug Area` 12 | */ 13 | 14 | import ReactiveSwift 15 | import Foundation 16 | 17 | /*: 18 | ## Sandbox 19 | 20 | A place where you can build your sand castles 🏖. 21 | */ 22 | 23 | 24 | -------------------------------------------------------------------------------- /ReactiveSwift.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 | -------------------------------------------------------------------------------- /ReactiveSwift.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ReactiveSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveSwift" 3 | # Version goes here and will be used to access the git tag later on, once we have a first release. 4 | s.version = "7.2.0" 5 | s.summary = "Streams of values over time" 6 | s.description = <<-DESC 7 | ReactiveSwift is a Swift framework inspired by Functional Reactive Programming. It provides APIs for composing and transforming streams of values over time. 8 | DESC 9 | s.homepage = "https://github.com/ReactiveCocoa/ReactiveSwift" 10 | s.license = { :type => "MIT", :file => "LICENSE.md" } 11 | s.author = "ReactiveCocoa" 12 | 13 | s.ios.deployment_target = "12.0" 14 | s.osx.deployment_target = "10.13" 15 | s.watchos.deployment_target = "4.0" 16 | s.tvos.deployment_target = "12.0" 17 | 18 | s.source = { :git => "https://github.com/ReactiveCocoa/ReactiveSwift.git", :tag => "#{s.version}" } 19 | # Directory glob for all Swift files 20 | s.source_files = ["Sources/*.{swift}", "Sources/**/*.{swift}"] 21 | 22 | s.pod_target_xcconfig = { 23 | 'APPLICATION_EXTENSION_API_ONLY' => 'YES', 24 | "OTHER_SWIFT_FLAGS[config=Release]" => "$(inherited) -suppress-warnings" 25 | } 26 | 27 | s.cocoapods_version = ">= 1.7.0" 28 | s.swift_versions = ["5.2", "5.3" "5.4", "5.5", "5.6", "5.7", "5.8", "5.9"] 29 | end 30 | -------------------------------------------------------------------------------- /ReactiveSwift.xcodeproj/xcshareddata/xcschemes/ReactiveSwift-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 77 | 83 | 84 | 85 | 86 | 87 | 97 | 98 | 104 | 105 | 106 | 107 | 113 | 114 | 120 | 121 | 122 | 123 | 125 | 126 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /ReactiveSwift.xcodeproj/xcshareddata/xcschemes/ReactiveSwift-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 77 | 83 | 84 | 85 | 86 | 87 | 97 | 98 | 104 | 105 | 106 | 107 | 113 | 114 | 116 | 117 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /ReactiveSwift.xcodeproj/xcshareddata/xcschemes/ReactiveSwift-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 77 | 83 | 84 | 85 | 86 | 87 | 97 | 98 | 104 | 105 | 106 | 107 | 113 | 114 | 120 | 121 | 122 | 123 | 125 | 126 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /ReactiveSwift.xcodeproj/xcshareddata/xcschemes/ReactiveSwift-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /ReactiveSwift.xcodeproj/xcshareddata/xcschemes/ReactiveSwift-xrOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /ReactiveSwift.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ReactiveSwift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Bag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bag.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Justin Spahr-Summers on 2014-07-10. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | /// An unordered, non-unique collection of values of type `Element`. 10 | public struct Bag { 11 | /// A uniquely identifying token for removing a value that was inserted into a 12 | /// Bag. 13 | public struct Token { 14 | fileprivate let value: UInt64 15 | } 16 | 17 | fileprivate var elements: ContiguousArray 18 | fileprivate var tokens: ContiguousArray 19 | 20 | private var nextToken: Token 21 | 22 | public init() { 23 | elements = ContiguousArray() 24 | tokens = ContiguousArray() 25 | nextToken = Token(value: 0) 26 | } 27 | 28 | public init(_ elements: S) where S.Iterator.Element == Element { 29 | self.elements = ContiguousArray(elements) 30 | self.nextToken = Token(value: UInt64(self.elements.count)) 31 | self.tokens = ContiguousArray(0.. Token { 41 | let token = nextToken 42 | 43 | // Practically speaking, this would overflow only if we have 101% uptime and we 44 | // manage to call `insert(_:)` every 1 ns for 500+ years non-stop. 45 | nextToken = Token(value: token.value &+ 1) 46 | 47 | elements.append(value) 48 | tokens.append(token.value) 49 | 50 | return token 51 | } 52 | 53 | /// Remove a value, given the token returned from `insert()`. 54 | /// 55 | /// - note: If the value has already been removed, nothing happens. 56 | /// 57 | /// - parameters: 58 | /// - token: A token returned from a call to `insert()`. 59 | @discardableResult 60 | public mutating func remove(using token: Token) -> Element? { 61 | // Given that tokens are always added to the end of the array and have a monotonically 62 | // increasing value, this list is always sorted, so we can use a binary search to improve 63 | // performance if this list gets large. 64 | guard let index = binarySearch(tokens, value: token.value) else { 65 | return nil 66 | } 67 | 68 | tokens.remove(at: index) 69 | return elements.remove(at: index) 70 | } 71 | 72 | /// Perform a binary search on a sorted array returning the index of a value. 73 | /// 74 | /// - parameters: 75 | /// - input: The sorted array to search for `value` 76 | /// - value: The value to find in the sorted `input` array 77 | /// 78 | /// - returns: The index of the `value` or `nil` 79 | private func binarySearch(_ input:ContiguousArray, value: UInt64) -> Int? { 80 | var lower = 0 81 | var upper = input.count - 1 82 | 83 | while (true) { 84 | let current = (lower + upper)/2 85 | if(input[current] == value) { 86 | return current 87 | } 88 | 89 | if (lower > upper) { 90 | return nil 91 | } 92 | 93 | if (input[current] > value) { 94 | upper = current - 1 95 | } else { 96 | lower = current + 1 97 | } 98 | } 99 | } 100 | } 101 | 102 | extension Bag: RandomAccessCollection { 103 | public var startIndex: Int { 104 | return elements.startIndex 105 | } 106 | 107 | public var endIndex: Int { 108 | return elements.endIndex 109 | } 110 | 111 | public subscript(index: Int) -> Element { 112 | return elements[index] 113 | } 114 | 115 | public func makeIterator() -> Iterator { 116 | return Iterator(elements.makeIterator()) 117 | } 118 | 119 | /// An iterator of `Bag`. 120 | public struct Iterator: IteratorProtocol { 121 | private var base: ContiguousArray.Iterator 122 | 123 | fileprivate init(_ base: ContiguousArray.Iterator) { 124 | self.base = base 125 | } 126 | 127 | public mutating func next() -> Element? { 128 | return base.next() 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/Deprecations+Removals.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | 4 | // MARK: Unavailable methods in ReactiveSwift 3.0. 5 | extension Signal { 6 | @available(*, unavailable, message:"Use the `Signal.init` that accepts a two-argument generator.") 7 | public convenience init(_ generator: (Observer) -> Disposable?) { fatalError() } 8 | } 9 | 10 | extension Lifetime { 11 | @discardableResult 12 | @available(*, unavailable, message:"Use `observeEnded(_:)` with a method reference to `dispose()` instead.") 13 | public func add(_ d: Disposable?) -> Disposable? { fatalError() } 14 | } 15 | 16 | // MARK: Deprecated types 17 | -------------------------------------------------------------------------------- /Sources/EventLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventLogger.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Rui Peres on 30/04/2016. 6 | // Copyright © 2016 GitHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A namespace for logging event types. 12 | public enum LoggingEvent { 13 | public enum Signal: String, CaseIterable { 14 | case value, completed, failed, terminated, disposed, interrupted 15 | 16 | @available(*, deprecated, message:"Use `allCases` instead.") 17 | public static var allEvents: Set { Set(allCases) } 18 | } 19 | 20 | public enum SignalProducer: String, CaseIterable { 21 | case starting, started, value, completed, failed, terminated, disposed, interrupted 22 | 23 | @available(*, deprecated, message:"Use `allCases` instead.") 24 | public static var allEvents: Set { Set(allCases) } 25 | } 26 | } 27 | 28 | public func defaultEventLog(identifier: String, event: String, fileName: String, functionName: String, lineNumber: Int) { 29 | print("[\(identifier)] \(event) fileName: \(fileName), functionName: \(functionName), lineNumber: \(lineNumber)") 30 | } 31 | 32 | /// A type that represents an event logging function. 33 | /// Signature is: 34 | /// - identifier 35 | /// - event 36 | /// - fileName 37 | /// - functionName 38 | /// - lineNumber 39 | public typealias EventLogger = ( 40 | _ identifier: String, 41 | _ event: String, 42 | _ fileName: String, 43 | _ functionName: String, 44 | _ lineNumber: Int 45 | ) -> Void 46 | 47 | // See https://bugs.swift.org/browse/SR-6796. 48 | fileprivate struct LogContext { 49 | let events: Set 50 | let identifier: String 51 | let fileName: String 52 | let functionName: String 53 | let lineNumber: Int 54 | let logger: EventLogger 55 | 56 | func log(_ event: Event) -> ((T) -> Void)? { 57 | return event.logIfNeeded(events: self.events) { event in 58 | self.logger(self.identifier, event, self.fileName, self.functionName, self.lineNumber) 59 | } 60 | } 61 | 62 | func log(_ event: Event) -> (() -> Void)? { 63 | return event.logIfNeededNoArg(events: self.events) { event in 64 | self.logger(self.identifier, event, self.fileName, self.functionName, self.lineNumber) 65 | } 66 | } 67 | } 68 | 69 | extension Signal { 70 | /// Logs all events that the receiver sends. By default, it will print to 71 | /// the standard output. 72 | /// 73 | /// - parameters: 74 | /// - identifier: a string to identify the Signal firing events. 75 | /// - events: Types of events to log. 76 | /// - fileName: Name of the file containing the code which fired the 77 | /// event. 78 | /// - functionName: Function where event was fired. 79 | /// - lineNumber: Line number where event was fired. 80 | /// - logger: Logger that logs the events. 81 | /// 82 | /// - returns: Signal that, when observed, logs the fired events. 83 | public func logEvents(identifier: String = "", events: Set = Set(LoggingEvent.Signal.allCases), fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, logger: @escaping EventLogger = defaultEventLog) -> Signal { 84 | let logContext = LogContext(events: events, 85 | identifier: identifier, 86 | fileName: fileName, 87 | functionName: functionName, 88 | lineNumber: lineNumber, 89 | logger: logger) 90 | 91 | return self.on( 92 | failed: logContext.log(.failed), 93 | completed: logContext.log(.completed), 94 | interrupted: logContext.log(.interrupted), 95 | terminated: logContext.log(.terminated), 96 | disposed: logContext.log(.disposed), 97 | value: logContext.log(.value) 98 | ) 99 | } 100 | } 101 | 102 | extension SignalProducer { 103 | /// Logs all events that the receiver sends. By default, it will print to 104 | /// the standard output. 105 | /// 106 | /// - parameters: 107 | /// - identifier: a string to identify the SignalProducer firing events. 108 | /// - events: Types of events to log. 109 | /// - fileName: Name of the file containing the code which fired the 110 | /// event. 111 | /// - functionName: Function where event was fired. 112 | /// - lineNumber: Line number where event was fired. 113 | /// - logger: Logger that logs the events. 114 | /// 115 | /// - returns: Signal producer that, when started, logs the fired events. 116 | public func logEvents(identifier: String = "", 117 | events: Set = Set(LoggingEvent.SignalProducer.allCases), 118 | fileName: String = #file, 119 | functionName: String = #function, 120 | lineNumber: Int = #line, 121 | logger: @escaping EventLogger = defaultEventLog 122 | ) -> SignalProducer { 123 | let logContext = LogContext(events: events, 124 | identifier: identifier, 125 | fileName: fileName, 126 | functionName: functionName, 127 | lineNumber: lineNumber, 128 | logger: logger) 129 | 130 | return self.on( 131 | starting: logContext.log(.starting), 132 | started: logContext.log(.started), 133 | failed: logContext.log(.failed), 134 | completed: logContext.log(.completed), 135 | interrupted: logContext.log(.interrupted), 136 | terminated: logContext.log(.terminated), 137 | disposed: logContext.log(.disposed), 138 | value: logContext.log(.value) 139 | ) 140 | } 141 | } 142 | 143 | private protocol LoggingEventProtocol: Hashable, RawRepresentable {} 144 | extension LoggingEvent.Signal: LoggingEventProtocol {} 145 | extension LoggingEvent.SignalProducer: LoggingEventProtocol {} 146 | 147 | private extension LoggingEventProtocol { 148 | // FIXME: See https://bugs.swift.org/browse/SR-6796 and the discussion in https://github.com/apple/swift/pull/14477. 149 | // Due to differences in the type checker, this method cannot 150 | // overload the generic `logIfNeeded`, or otherwise it would lead to 151 | // infinite recursion with Swift 4.0.x. 152 | func logIfNeededNoArg(events: Set, logger: @escaping (String) -> Void) -> (() -> Void)? { 153 | return (self.logIfNeeded(events: events, logger: logger) as ((()) -> Void)?) 154 | .map { closure in 155 | { closure(()) } 156 | } 157 | } 158 | 159 | func logIfNeeded(events: Set, logger: @escaping (String) -> Void) -> ((T) -> Void)? { 160 | guard events.contains(self) else { 161 | return nil 162 | } 163 | 164 | return { value in 165 | if value is Void { 166 | logger("\(self.rawValue)") 167 | } else { 168 | logger("\(self.rawValue) \(value)") 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoundationExtensions.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Justin Spahr-Summers on 2014-10-19. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if canImport(FoundationNetworking) 11 | import FoundationNetworking 12 | #endif 13 | import Dispatch 14 | 15 | #if os(Linux) 16 | import let CDispatch.NSEC_PER_USEC 17 | import let CDispatch.NSEC_PER_SEC 18 | #endif 19 | 20 | extension NotificationCenter: ReactiveExtensionsProvider {} 21 | 22 | extension Reactive where Base: NotificationCenter { 23 | /// Returns a Signal to observe posting of the specified notification. 24 | /// 25 | /// - parameters: 26 | /// - name: name of the notification to observe 27 | /// - object: an instance which sends the notifications 28 | /// 29 | /// - returns: A Signal of notifications posted that match the given criteria. 30 | /// 31 | /// - note: The signal does not terminate naturally. Observers must be 32 | /// explicitly disposed to avoid leaks. 33 | public func notifications(forName name: Notification.Name?, object: AnyObject? = nil) -> Signal { 34 | return Signal { [base = self.base] observer, lifetime in 35 | let notificationObserver = base.addObserver(forName: name, object: object, queue: nil) { notification in 36 | observer.send(value: notification) 37 | } 38 | 39 | lifetime.observeEnded { 40 | base.removeObserver(notificationObserver) 41 | } 42 | } 43 | } 44 | } 45 | 46 | private let defaultSessionError = NSError(domain: "org.reactivecocoa.ReactiveSwift.Reactivity.URLSession.dataWithRequest", 47 | code: 1, 48 | userInfo: nil) 49 | 50 | extension URLSession: ReactiveExtensionsProvider {} 51 | 52 | extension Reactive where Base: URLSession { 53 | /// Returns a SignalProducer which performs the work associated with an 54 | /// `NSURLSession` 55 | /// 56 | /// - parameters: 57 | /// - request: A request that will be performed when the producer is 58 | /// started 59 | /// 60 | /// - returns: A producer that will execute the given request once for each 61 | /// invocation of `start()`. 62 | /// 63 | /// - note: This method will not send an error event in the case of a server 64 | /// side error (i.e. when a response with status code other than 65 | /// 200...299 is received). 66 | public func data(with request: URLRequest) -> SignalProducer<(Data, URLResponse), Error> { 67 | return SignalProducer { [base = self.base] observer, lifetime in 68 | let task = base.dataTask(with: request) { data, response, error in 69 | if let data = data, let response = response { 70 | observer.send(value: (data, response)) 71 | observer.sendCompleted() 72 | } else { 73 | observer.send(error: error ?? defaultSessionError) 74 | } 75 | } 76 | 77 | lifetime.observeEnded(task.cancel) 78 | task.resume() 79 | } 80 | } 81 | } 82 | 83 | extension Date { 84 | internal func addingTimeInterval(_ interval: DispatchTimeInterval) -> Date { 85 | return addingTimeInterval(interval.timeInterval) 86 | } 87 | } 88 | 89 | extension DispatchTimeInterval { 90 | internal var timeInterval: TimeInterval { 91 | switch self { 92 | case let .seconds(s): 93 | return TimeInterval(s) 94 | case let .milliseconds(ms): 95 | return TimeInterval(TimeInterval(ms) / 1000.0) 96 | case let .microseconds(us): 97 | return TimeInterval(Int64(us)) * TimeInterval(NSEC_PER_USEC) / TimeInterval(NSEC_PER_SEC) 98 | case let .nanoseconds(ns): 99 | return TimeInterval(ns) / TimeInterval(NSEC_PER_SEC) 100 | case .never: 101 | return .infinity 102 | @unknown default: 103 | return .infinity 104 | } 105 | } 106 | 107 | // This was added purely so that our test scheduler to "go backwards" in 108 | // time. See `TestScheduler.rewind(by interval: DispatchTimeInterval)`. 109 | internal static prefix func -(lhs: DispatchTimeInterval) -> DispatchTimeInterval { 110 | switch lhs { 111 | case let .seconds(s): 112 | return .seconds(-s) 113 | case let .milliseconds(ms): 114 | return .milliseconds(-ms) 115 | case let .microseconds(us): 116 | return .microseconds(-us) 117 | case let .nanoseconds(ns): 118 | return .nanoseconds(-ns) 119 | case .never: 120 | return .never 121 | @unknown default: 122 | return .never 123 | } 124 | } 125 | 126 | /// Scales a time interval by the given scalar specified in `rhs`. 127 | /// 128 | /// - returns: Scaled interval in minimal appropriate unit 129 | internal static func *(lhs: DispatchTimeInterval, rhs: Double) -> DispatchTimeInterval { 130 | let seconds = lhs.timeInterval * rhs 131 | var result: DispatchTimeInterval = .never 132 | if let integerTimeInterval = Int(exactly: (seconds * 1000 * 1000 * 1000).rounded()) { 133 | result = .nanoseconds(integerTimeInterval) 134 | } else if let integerTimeInterval = Int(exactly: (seconds * 1000 * 1000).rounded()) { 135 | result = .microseconds(integerTimeInterval) 136 | } else if let integerTimeInterval = Int(exactly: (seconds * 1000).rounded()) { 137 | result = .milliseconds(integerTimeInterval) 138 | } else if let integerTimeInterval = Int(exactly: (seconds).rounded()) { 139 | result = .seconds(integerTimeInterval) 140 | } 141 | return result 142 | } 143 | } 144 | 145 | extension TimeInterval { 146 | 147 | internal var dispatchTimeInterval: DispatchTimeInterval { 148 | .nanoseconds(Int(self * TimeInterval(NSEC_PER_SEC))) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/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 | 6.5.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 | -------------------------------------------------------------------------------- /Sources/Lifetime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents the lifetime of an object, and provides a hook to observe when 4 | /// the object deinitializes. 5 | public final class Lifetime { 6 | private let disposables: CompositeDisposable 7 | 8 | /// A signal that sends a `completed` event when the lifetime ends. 9 | /// 10 | /// - note: Consider using `Lifetime.observeEnded` if only a closure observer 11 | /// is to be attached. 12 | public var ended: Signal { 13 | return Signal { observer, lifetime in 14 | lifetime += (disposables += observer.sendCompleted) 15 | } 16 | } 17 | 18 | /// A flag indicating whether the lifetime has ended. 19 | public var hasEnded: Bool { 20 | return disposables.isDisposed 21 | } 22 | 23 | /// Initialize a `Lifetime` object with the supplied composite disposable. 24 | /// 25 | /// - parameters: 26 | /// - signal: The composite disposable. 27 | internal init(_ disposables: CompositeDisposable) { 28 | self.disposables = disposables 29 | } 30 | 31 | /// Initialize a `Lifetime` from a lifetime token, which is expected to be 32 | /// associated with an object. 33 | /// 34 | /// - important: The resulting lifetime object does not retain the lifetime 35 | /// token. 36 | /// 37 | /// - parameters: 38 | /// - token: A lifetime token for detecting the deinitialization of the 39 | /// associated object. 40 | public convenience init(_ token: Token) { 41 | self.init(token.disposables) 42 | } 43 | 44 | /// Observe the termination of `self`. 45 | /// 46 | /// - parameters: 47 | /// - action: The action to be invoked when `self` ends. 48 | /// 49 | /// - returns: A disposable that detaches `action` from the lifetime, or `nil` 50 | /// if `lifetime` has already ended. 51 | @discardableResult 52 | public func observeEnded(_ action: @escaping () -> Void) -> Disposable? { 53 | return disposables += action 54 | } 55 | 56 | /// Add the given disposable as an observer of `self`. 57 | /// 58 | /// - parameters: 59 | /// - disposable: The disposable to be disposed of when `self` ends. 60 | /// 61 | /// - returns: A disposable that detaches `disposable` from the lifetime, or `nil` 62 | /// if `lifetime` has already ended. 63 | @discardableResult 64 | public static func += (lifetime: Lifetime, disposable: Disposable?) -> Disposable? { 65 | guard let dispose = disposable?.dispose else { return nil } 66 | return lifetime.observeEnded(dispose) 67 | } 68 | } 69 | 70 | extension Lifetime { 71 | /// Factory method for creating a `Lifetime` and its associated `Token`. 72 | /// 73 | /// - returns: A `(lifetime, token)` tuple. 74 | public static func make() -> (lifetime: Lifetime, token: Token) { 75 | let token = Token() 76 | return (Lifetime(token), token) 77 | } 78 | 79 | /// A `Lifetime` that has already ended. 80 | public static let empty: Lifetime = { 81 | let disposables = CompositeDisposable() 82 | disposables.dispose() 83 | return Lifetime(disposables) 84 | }() 85 | } 86 | 87 | extension Lifetime { 88 | /// A token object which completes its associated `Lifetime` when 89 | /// it deinitializes, or when `dispose()` is called. 90 | /// 91 | /// It is generally used in conjunction with `Lifetime` as a private 92 | /// deinitialization trigger. 93 | /// 94 | /// ``` 95 | /// class MyController { 96 | /// private let (lifetime, token) = Lifetime.make() 97 | /// } 98 | /// ``` 99 | public final class Token { 100 | fileprivate let disposables: CompositeDisposable 101 | 102 | public init() { 103 | disposables = CompositeDisposable() 104 | } 105 | 106 | public func dispose() { 107 | disposables.dispose() 108 | } 109 | 110 | deinit { 111 | dispose() 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/Observers/AttemptMap.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class AttemptMap: Observer { 3 | let downstream: Observer 4 | let transform: (InputValue) -> Result 5 | 6 | init(downstream: Observer, transform: @escaping (InputValue) -> Result) { 7 | self.downstream = downstream 8 | self.transform = transform 9 | } 10 | 11 | override func receive(_ value: InputValue) { 12 | switch transform(value) { 13 | case let .success(value): 14 | downstream.receive(value) 15 | case let .failure(error): 16 | downstream.terminate(.failed(error)) 17 | } 18 | } 19 | 20 | override func terminate(_ termination: Termination) { 21 | downstream.terminate(termination) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Observers/Collect.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class Collect: Observer { 3 | let downstream: Observer<[Value], Error> 4 | let modify: (_ collected: inout [Value], _ latest: Value) -> [Value]? 5 | 6 | private var values: [Value] = [] 7 | private var hasReceivedValues = false 8 | 9 | convenience init(downstream: Observer<[Value], Error>, shouldEmit: @escaping (_ collected: [Value], _ latest: Value) -> Bool) { 10 | self.init(downstream: downstream, modify: { collected, latest in 11 | if shouldEmit(collected, latest) { 12 | defer { collected = [latest] } 13 | return collected 14 | } 15 | 16 | collected.append(latest) 17 | return nil 18 | }) 19 | } 20 | 21 | convenience init(downstream: Observer<[Value], Error>, shouldEmit: @escaping (_ collected: [Value]) -> Bool) { 22 | self.init(downstream: downstream, modify: { collected, latest in 23 | collected.append(latest) 24 | 25 | if shouldEmit(collected) { 26 | defer { collected.removeAll(keepingCapacity: true) } 27 | return collected 28 | } 29 | 30 | return nil 31 | }) 32 | } 33 | 34 | private init(downstream: Observer<[Value], Error>, modify: @escaping (_ collected: inout [Value], _ latest: Value) -> [Value]?) { 35 | self.downstream = downstream 36 | self.modify = modify 37 | } 38 | 39 | override func receive(_ value: Value) { 40 | if let outgoing = modify(&values, value) { 41 | downstream.receive(outgoing) 42 | } 43 | 44 | if !hasReceivedValues { 45 | hasReceivedValues = true 46 | } 47 | } 48 | 49 | override func terminate(_ termination: Termination) { 50 | if case .completed = termination { 51 | if !values.isEmpty { 52 | downstream.receive(values) 53 | values.removeAll() 54 | } else if !hasReceivedValues { 55 | downstream.receive([]) 56 | } 57 | } 58 | 59 | downstream.terminate(termination) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Observers/CollectEvery.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | extension Operators { 4 | internal final class CollectEvery: UnaryAsyncOperator { 5 | let interval: DispatchTimeInterval 6 | let discardWhenCompleted: Bool 7 | let targetWithClock: DateScheduler 8 | 9 | private let state: Atomic> 10 | private let timerDisposable = SerialDisposable() 11 | 12 | init( 13 | downstream: Observer<[Value], Error>, 14 | downstreamLifetime: Lifetime, 15 | target: DateScheduler, 16 | interval: DispatchTimeInterval, 17 | skipEmpty: Bool, 18 | discardWhenCompleted: Bool 19 | ) { 20 | self.interval = interval 21 | self.discardWhenCompleted = discardWhenCompleted 22 | self.targetWithClock = target 23 | self.state = Atomic(CollectEveryState(skipEmpty: skipEmpty)) 24 | 25 | super.init(downstream: downstream, downstreamLifetime: downstreamLifetime, target: target) 26 | 27 | downstreamLifetime += timerDisposable 28 | 29 | let initialDate = targetWithClock.currentDate.addingTimeInterval(interval) 30 | timerDisposable.inner = targetWithClock.schedule(after: initialDate, interval: interval, leeway: interval * 0.1) { 31 | let (currentValues, isCompleted) = self.state.modify { ($0.collect(), $0.isCompleted) } 32 | 33 | if let currentValues = currentValues { 34 | self.unscheduledSend(currentValues) 35 | } 36 | 37 | if isCompleted { 38 | self.unscheduledTerminate(.completed) 39 | } 40 | } 41 | } 42 | 43 | override func receive(_ value: Value) { 44 | state.modify { $0.values.append(value) } 45 | } 46 | 47 | override func terminate(_ termination: Termination) { 48 | guard isActive else { return } 49 | 50 | if case .completed = termination, !discardWhenCompleted { 51 | state.modify { $0.isCompleted = true } 52 | } else { 53 | timerDisposable.dispose() 54 | super.terminate(termination) 55 | } 56 | } 57 | } 58 | } 59 | 60 | private struct CollectEveryState { 61 | let skipEmpty: Bool 62 | var values: [Value] = [] 63 | var isCompleted: Bool = false 64 | 65 | init(skipEmpty: Bool) { 66 | self.skipEmpty = skipEmpty 67 | } 68 | 69 | var hasValues: Bool { 70 | return !values.isEmpty || !skipEmpty 71 | } 72 | 73 | mutating func collect() -> [Value]? { 74 | guard hasValues else { return nil } 75 | defer { values.removeAll() } 76 | return values 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Observers/CombinePrevious.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class CombinePrevious: Observer { 3 | let downstream: Observer<(Value, Value), Error> 4 | var previous: Value? 5 | 6 | init(downstream: Observer<(Value, Value), Error>, initial: Value?) { 7 | self.downstream = downstream 8 | self.previous = initial 9 | } 10 | 11 | override func receive(_ value: Value) { 12 | if let previous = previous { 13 | downstream.receive((previous, value)) 14 | } 15 | 16 | previous = value 17 | } 18 | 19 | override func terminate(_ termination: Termination) { 20 | downstream.terminate(termination) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Observers/CompactMap.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class CompactMap: Observer { 3 | let downstream: Observer 4 | let transform: (InputValue) -> OutputValue? 5 | 6 | init(downstream: Observer, transform: @escaping (InputValue) -> OutputValue?) { 7 | self.downstream = downstream 8 | self.transform = transform 9 | } 10 | 11 | override func receive(_ value: InputValue) { 12 | if let output = transform(value) { 13 | downstream.receive(output) 14 | } 15 | } 16 | 17 | override func terminate(_ termination: Termination) { 18 | downstream.terminate(termination) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Observers/Debounce.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Operators { 4 | internal final class Debounce: UnaryAsyncOperator { 5 | let interval: TimeInterval 6 | let discardWhenCompleted: Bool 7 | let targetWithClock: DateScheduler 8 | 9 | private let state: Atomic> = Atomic(DebounceState()) 10 | private let schedulerDisposable = SerialDisposable() 11 | 12 | init( 13 | downstream: Observer, 14 | downstreamLifetime: Lifetime, 15 | target: DateScheduler, 16 | interval: TimeInterval, 17 | discardWhenCompleted: Bool 18 | ) { 19 | precondition(interval >= 0) 20 | 21 | self.interval = interval 22 | self.discardWhenCompleted = discardWhenCompleted 23 | self.targetWithClock = target 24 | 25 | super.init(downstream: downstream, downstreamLifetime: downstreamLifetime, target: target) 26 | 27 | downstreamLifetime += schedulerDisposable 28 | } 29 | 30 | override func receive(_ value: Value) { 31 | let now = targetWithClock.currentDate 32 | 33 | state.modify { state in 34 | state.lastUpdated = now 35 | state.pendingValue = value 36 | } 37 | let targetDate = now.addingTimeInterval(interval) 38 | schedulerDisposable.inner = targetWithClock.schedule(after: targetDate) { 39 | if let pendingValue = self.state.modify({ $0.retrieve() }) { 40 | self.unscheduledSend(pendingValue) 41 | } 42 | } 43 | } 44 | 45 | override func terminate(_ termination: Termination) { 46 | guard isActive else { return } 47 | schedulerDisposable.dispose() 48 | 49 | if case .completed = termination { 50 | let pending: (value: Value?, lastUpdated: Date) = state.modify { state in 51 | return (state.retrieve(), state.lastUpdated) 52 | } 53 | 54 | if !discardWhenCompleted, let pendingValue = pending.value { 55 | targetWithClock.schedule(after: pending.lastUpdated.addingTimeInterval(interval)) { 56 | self.unscheduledSend(pendingValue) 57 | super.terminate(.completed) 58 | } 59 | } else { 60 | super.terminate(.completed) 61 | } 62 | } else { 63 | super.terminate(termination) 64 | } 65 | } 66 | } 67 | } 68 | 69 | private struct DebounceState { 70 | var lastUpdated: Date = .distantPast 71 | var pendingValue: Value? 72 | 73 | mutating func retrieve() -> Value? { 74 | defer { pendingValue = nil } 75 | return pendingValue 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Observers/Delay.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Operators { 4 | internal final class Delay: UnaryAsyncOperator { 5 | let interval: TimeInterval 6 | let targetWithClock: DateScheduler 7 | 8 | init( 9 | downstream: Observer, 10 | downstreamLifetime: Lifetime, 11 | target: DateScheduler, 12 | interval: TimeInterval 13 | ) { 14 | precondition(interval >= 0) 15 | 16 | self.interval = interval 17 | self.targetWithClock = target 18 | super.init(downstream: downstream, downstreamLifetime: downstreamLifetime, target: target) 19 | } 20 | 21 | 22 | override func receive(_ value: Value) { 23 | guard isActive else { return } 24 | 25 | targetWithClock.schedule(after: computeNextDate()) { 26 | self.unscheduledSend(value) 27 | } 28 | } 29 | 30 | override func terminate(_ termination: Termination) { 31 | if case .completed = termination { 32 | targetWithClock.schedule(after: computeNextDate()) { 33 | super.terminate(.completed) 34 | } 35 | } else { 36 | super.terminate(termination) 37 | } 38 | } 39 | 40 | private func computeNextDate() -> Date { 41 | targetWithClock.currentDate.addingTimeInterval(interval) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Observers/Dematerialize.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class Dematerialize: Observer where Event: EventProtocol { 3 | let downstream: Observer 4 | 5 | init(downstream: Observer) { 6 | self.downstream = downstream 7 | } 8 | 9 | override func receive(_ event: Event) { 10 | switch event.event { 11 | case let .value(value): 12 | downstream.receive(value) 13 | case .completed: 14 | downstream.terminate(.completed) 15 | case .interrupted: 16 | downstream.terminate(.interrupted) 17 | case let .failed(error): 18 | downstream.terminate(.failed(error)) 19 | } 20 | } 21 | 22 | override func terminate(_ termination: Termination) { 23 | switch termination { 24 | case .completed: 25 | downstream.terminate(.completed) 26 | case .interrupted: 27 | downstream.terminate(.interrupted) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Observers/DematerializeResults.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class DematerializeResults: Observer where Result: ResultProtocol { 3 | let downstream: Observer 4 | 5 | init(downstream: Observer) { 6 | self.downstream = downstream 7 | } 8 | 9 | override func receive(_ value: Result) { 10 | switch value.result { 11 | case let .success(value): 12 | downstream.receive(value) 13 | case let .failure(error): 14 | downstream.terminate(.failed(error)) 15 | } 16 | } 17 | 18 | override func terminate(_ termination: Termination) { 19 | switch termination { 20 | case .completed: 21 | downstream.terminate(.completed) 22 | case .interrupted: 23 | downstream.terminate(.interrupted) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Observers/Filter.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class Filter: Observer { 3 | let downstream: Observer 4 | let predicate: (Value) -> Bool 5 | 6 | init(downstream: Observer, predicate: @escaping (Value) -> Bool) { 7 | self.downstream = downstream 8 | self.predicate = predicate 9 | } 10 | 11 | override func receive(_ value: Value) { 12 | if predicate(value) { 13 | downstream.receive(value) 14 | } 15 | } 16 | 17 | override func terminate(_ termination: Termination) { 18 | downstream.terminate(termination) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Observers/LazyMap.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class LazyMap: UnaryAsyncOperator { 3 | let transform: (Value) -> NewValue 4 | let box = Atomic(nil) 5 | let valueDisposable = SerialDisposable() 6 | 7 | init( 8 | downstream: Observer, 9 | downstreamLifetime: Lifetime, 10 | target: Scheduler, 11 | transform: @escaping (Value) -> NewValue 12 | ) { 13 | self.transform = transform 14 | super.init(downstream: downstream, downstreamLifetime: downstreamLifetime, target: target) 15 | 16 | downstreamLifetime += valueDisposable 17 | } 18 | 19 | override func receive(_ value: Value) { 20 | // Schedule only when there is no prior outstanding value. 21 | if box.swap(value) == nil { 22 | valueDisposable.inner = target.schedule { 23 | if let value = self.box.swap(nil) { 24 | self.unscheduledSend(self.transform(value)) 25 | } 26 | } 27 | } 28 | } 29 | 30 | override func terminate(_ termination: Termination) { 31 | if case .interrupted = termination { 32 | // `interrupted` immediately cancels any scheduled value. 33 | // 34 | // On the other hand, completion and failure does not cancel anything, and is scheduled to run after any 35 | // scheduled value. `valueDisposable` will naturally be disposed by `downstreamLifetime` as soon as the 36 | // downstream has processed the termination. 37 | valueDisposable.dispose() 38 | } 39 | 40 | super.terminate(termination) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Observers/Map.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class Map: Observer { 3 | let downstream: Observer 4 | let transform: (InputValue) -> OutputValue 5 | 6 | init(downstream: Observer, transform: @escaping (InputValue) -> OutputValue) { 7 | self.downstream = downstream 8 | self.transform = transform 9 | } 10 | 11 | override func receive(_ value: InputValue) { 12 | downstream.receive(transform(value)) 13 | } 14 | 15 | override func terminate(_ termination: Termination) { 16 | downstream.terminate(termination) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Observers/MapError.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class MapError: Observer { 3 | let downstream: Observer 4 | let transform: (InputError) -> OutputError 5 | 6 | init(downstream: Observer, transform: @escaping (InputError) -> OutputError) { 7 | self.downstream = downstream 8 | self.transform = transform 9 | } 10 | 11 | override func receive(_ value: Value) { 12 | downstream.receive(value) 13 | } 14 | 15 | override func terminate(_ termination: Termination) { 16 | switch termination { 17 | case .completed: 18 | downstream.terminate(.completed) 19 | case let .failed(error): 20 | downstream.terminate(.failed(transform(error))) 21 | case .interrupted: 22 | downstream.terminate(.interrupted) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Observers/Materialize.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class Materialize: Observer { 3 | let downstream: Observer.Event, Never> 4 | 5 | init(downstream: Observer.Event, Never>) { 6 | self.downstream = downstream 7 | } 8 | 9 | override func receive(_ value: Value) { 10 | downstream.receive(.value(value)) 11 | } 12 | 13 | override func terminate(_ termination: Termination) { 14 | downstream.receive(Signal.Event(termination)) 15 | 16 | switch termination { 17 | case .completed, .failed: 18 | downstream.terminate(.completed) 19 | case .interrupted: 20 | downstream.terminate(.interrupted) 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Observers/MaterializeAsResult.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class MaterializeAsResult: Observer { 3 | let downstream: Observer, Never> 4 | 5 | init(downstream: Observer, Never>) { 6 | self.downstream = downstream 7 | } 8 | 9 | override func receive(_ value: Value) { 10 | downstream.receive(.success(value)) 11 | } 12 | 13 | override func terminate(_ termination: Termination) { 14 | switch termination { 15 | case .completed: 16 | downstream.terminate(.completed) 17 | case let .failed(error): 18 | downstream.receive(.failure(error)) 19 | downstream.terminate(.completed) 20 | case .interrupted: 21 | downstream.terminate(.interrupted) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Observers/ObserveOn.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class ObserveOn: UnaryAsyncOperator { 3 | override func receive(_ value: Value) { 4 | target.schedule { 5 | guard !self.downstreamLifetime.hasEnded else { return } 6 | self.unscheduledSend(value) 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Observers/Observer.swift: -------------------------------------------------------------------------------- 1 | open class Observer { 2 | public init() {} 3 | 4 | open func receive(_ value: Value) { fatalError() } 5 | open func terminate(_ termination: Termination) { fatalError() } 6 | } 7 | 8 | extension Observer { 9 | internal func assumeUnboundDemand() -> Signal.Observer { 10 | Signal.Observer(self.process) 11 | } 12 | 13 | internal func callAsFunction(_ event: Signal.Event) { 14 | process(event) 15 | } 16 | 17 | fileprivate func process(_ event: Signal.Event) { 18 | switch event { 19 | case let .value(value): 20 | receive(value) 21 | case let .failed(error): 22 | terminate(.failed(error)) 23 | case .completed: 24 | terminate(.completed) 25 | case .interrupted: 26 | terminate(.interrupted) 27 | } 28 | } 29 | } 30 | 31 | public enum Termination { 32 | case failed(Error) 33 | case completed 34 | case interrupted 35 | } 36 | 37 | extension Signal.Event { 38 | init(_ termination: Termination) { 39 | switch termination { 40 | case .completed: 41 | self = .completed 42 | case .interrupted: 43 | self = .interrupted 44 | case let .failed(error): 45 | self = .failed(error) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Observers/Operators.swift: -------------------------------------------------------------------------------- 1 | internal enum Operators {} 2 | -------------------------------------------------------------------------------- /Sources/Observers/Reduce.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class Reduce: Observer { 3 | let downstream: Observer 4 | let nextPartialResult: (inout Result, Value) -> Void 5 | var accumulator: Result 6 | 7 | init(downstream: Observer, initial: Result, nextPartialResult: @escaping (inout Result, Value) -> Void) { 8 | self.downstream = downstream 9 | self.accumulator = initial 10 | self.nextPartialResult = nextPartialResult 11 | } 12 | 13 | override func receive(_ value: Value) { 14 | nextPartialResult(&accumulator, value) 15 | } 16 | 17 | override func terminate(_ termination: Termination) { 18 | if case .completed = termination { 19 | downstream.receive(accumulator) 20 | } 21 | 22 | downstream.terminate(termination) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Observers/ScanMap.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class ScanMap: Observer { 3 | let downstream: Observer 4 | let next: (inout State, Value) -> Result 5 | var accumulator: State 6 | 7 | init(downstream: Observer, initial: State, next: @escaping (inout State, Value) -> Result) { 8 | self.downstream = downstream 9 | self.accumulator = initial 10 | self.next = next 11 | } 12 | 13 | override func receive(_ value: Value) { 14 | let result = next(&accumulator, value) 15 | downstream.receive(result) 16 | } 17 | 18 | override func terminate(_ termination: Termination) { 19 | downstream.terminate(termination) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Observers/SkipFirst.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class SkipFirst: Observer { 3 | let downstream: Observer 4 | let count: Int 5 | var skipped: Int = 0 6 | 7 | init(downstream: Observer, count: Int) { 8 | precondition(count >= 1) 9 | 10 | self.downstream = downstream 11 | self.count = count 12 | } 13 | 14 | override func receive(_ value: Value) { 15 | if skipped < count { 16 | skipped += 1 17 | } else { 18 | downstream.receive(value) 19 | } 20 | } 21 | 22 | override func terminate(_ termination: Termination) { 23 | downstream.terminate(termination) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Observers/SkipRepeats.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class SkipRepeats: Observer { 3 | let downstream: Observer 4 | let isEquivalent: (Value, Value) -> Bool 5 | 6 | var previous: Value? = nil 7 | 8 | init(downstream: Observer, isEquivalent: @escaping (Value, Value) -> Bool) { 9 | self.downstream = downstream 10 | self.isEquivalent = isEquivalent 11 | } 12 | 13 | override func receive(_ value: Value) { 14 | let isRepeating = previous.map { isEquivalent($0, value) } ?? false 15 | previous = value 16 | 17 | if !isRepeating { 18 | downstream.receive(value) 19 | } 20 | } 21 | 22 | override func terminate(_ termination: Termination) { 23 | downstream.terminate(termination) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Observers/SkipWhile.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class SkipWhile: Observer { 3 | let downstream: Observer 4 | let shouldContinueToSkip: (Value) -> Bool 5 | var isSkipping = true 6 | 7 | init(downstream: Observer, shouldContinueToSkip: @escaping (Value) -> Bool) { 8 | self.downstream = downstream 9 | self.shouldContinueToSkip = shouldContinueToSkip 10 | } 11 | 12 | override func receive(_ value: Value) { 13 | isSkipping = isSkipping && shouldContinueToSkip(value) 14 | 15 | if !isSkipping { 16 | downstream.receive(value) 17 | } 18 | } 19 | 20 | override func terminate(_ termination: Termination) { 21 | downstream.terminate(termination) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Observers/TakeFirst.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class TakeFirst: Observer { 3 | let downstream: Observer 4 | let count: Int 5 | var taken: Int = 0 6 | 7 | init(downstream: Observer, count: Int) { 8 | precondition(count >= 1) 9 | 10 | self.downstream = downstream 11 | self.count = count 12 | } 13 | 14 | override func receive(_ value: Value) { 15 | if taken < count { 16 | taken += 1 17 | downstream.receive(value) 18 | } 19 | 20 | if taken == count { 21 | downstream.terminate(.completed) 22 | } 23 | } 24 | 25 | override func terminate(_ termination: Termination) { 26 | downstream.terminate(termination) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Observers/TakeLast.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class TakeLast: Observer { 3 | let downstream: Observer 4 | let count: Int 5 | var buffer: [Value] = [] 6 | 7 | init(downstream: Observer, count: Int) { 8 | precondition(count >= 1) 9 | 10 | self.downstream = downstream 11 | self.count = count 12 | 13 | buffer.reserveCapacity(count) 14 | } 15 | 16 | override func receive(_ value: Value) { 17 | // To avoid exceeding the reserved capacity of the buffer, 18 | // we remove then add. Remove elements until we have room to 19 | // add one more. 20 | while (buffer.count + 1) > count { 21 | buffer.remove(at: 0) 22 | } 23 | 24 | buffer.append(value) 25 | } 26 | 27 | override func terminate(_ termination: Termination) { 28 | if case .completed = termination { 29 | buffer.forEach(downstream.receive) 30 | buffer = [] 31 | } 32 | 33 | downstream.terminate(termination) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Observers/TakeUntil.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class TakeUntil: Observer { 3 | let downstream: Observer 4 | let shouldContinue: (Value) -> Bool 5 | 6 | init(downstream: Observer, shouldContinue: @escaping (Value) -> Bool) { 7 | self.downstream = downstream 8 | self.shouldContinue = shouldContinue 9 | } 10 | 11 | override func receive(_ value: Value) { 12 | downstream.receive(value) 13 | 14 | if !shouldContinue(value) { 15 | downstream.terminate(.completed) 16 | } 17 | } 18 | 19 | override func terminate(_ termination: Termination) { 20 | downstream.terminate(termination) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Observers/TakeWhile.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class TakeWhile: Observer { 3 | let downstream: Observer 4 | let shouldContinue: (Value) -> Bool 5 | 6 | init(downstream: Observer, shouldContinue: @escaping (Value) -> Bool) { 7 | self.downstream = downstream 8 | self.shouldContinue = shouldContinue 9 | } 10 | 11 | override func receive(_ value: Value) { 12 | if !shouldContinue(value) { 13 | downstream.terminate(.completed) 14 | } else { 15 | downstream.receive(value) 16 | } 17 | } 18 | 19 | override func terminate(_ termination: Termination) { 20 | downstream.terminate(termination) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Observers/Throttle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Operators { 4 | internal final class Throttle: UnaryAsyncOperator { 5 | let interval: TimeInterval 6 | let targetWithClock: DateScheduler 7 | 8 | private let state: Atomic> = Atomic(ThrottleState()) 9 | private let schedulerDisposable = SerialDisposable() 10 | 11 | init( 12 | downstream: Observer, 13 | downstreamLifetime: Lifetime, 14 | target: DateScheduler, 15 | interval: TimeInterval 16 | ) { 17 | precondition(interval >= 0) 18 | 19 | self.interval = interval 20 | self.targetWithClock = target 21 | super.init(downstream: downstream, downstreamLifetime: downstreamLifetime, target: target) 22 | 23 | downstreamLifetime += schedulerDisposable 24 | } 25 | 26 | override func receive(_ value: Value) { 27 | let scheduleDate: Date = state.modify { state in 28 | state.pendingValue = value 29 | 30 | let proposedScheduleDate: Date 31 | if let previousDate = state.previousDate, previousDate <= targetWithClock.currentDate { 32 | proposedScheduleDate = previousDate.addingTimeInterval(interval) 33 | } else { 34 | proposedScheduleDate = targetWithClock.currentDate 35 | } 36 | 37 | return proposedScheduleDate < targetWithClock.currentDate ? targetWithClock.currentDate : proposedScheduleDate 38 | } 39 | 40 | schedulerDisposable.inner = targetWithClock.schedule(after: scheduleDate) { 41 | guard self.isActive else { return } 42 | 43 | if let pendingValue = self.state.modify({ $0.retrieveValue(date: scheduleDate) }) { 44 | self.unscheduledSend(pendingValue) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | private struct ThrottleState { 52 | var previousDate: Date? 53 | var pendingValue: Value? 54 | 55 | mutating func retrieveValue(date: Date) -> Value? { 56 | defer { 57 | if pendingValue != nil { 58 | pendingValue = nil 59 | previousDate = date 60 | } 61 | } 62 | return pendingValue 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Observers/UnaryAsyncOperator.swift: -------------------------------------------------------------------------------- 1 | internal class UnaryAsyncOperator: Observer { 2 | let downstreamLifetime: Lifetime 3 | let target: Scheduler 4 | 5 | /// Whether or not the downstream observer can still receive values or termination. 6 | /// 7 | /// - note: This is a thread-safe atomic read. So you can use it in any part of `receive(_:)` or `terminate(_:)` to 8 | /// attempt to early out before expensive scheduling or computation. 9 | var isActive: Bool { state.is(.active) } 10 | 11 | // Direct access is discouraged for subclasses by keeping this private. 12 | private let downstream: Observer 13 | private let state: UnsafeAtomicState 14 | 15 | public init( 16 | downstream: Observer, 17 | downstreamLifetime: Lifetime, 18 | target: Scheduler 19 | ) { 20 | self.downstream = downstream 21 | self.downstreamLifetime = downstreamLifetime 22 | self.target = target 23 | self.state = UnsafeAtomicState(.active) 24 | 25 | super.init() 26 | 27 | downstreamLifetime.observeEnded { 28 | if self.state.tryTransition(from: .active, to: .terminated) { 29 | target.schedule { 30 | downstream.terminate(.interrupted) 31 | } 32 | } 33 | } 34 | } 35 | 36 | deinit { 37 | state.deinitialize() 38 | } 39 | 40 | open override func receive(_ value: InputValue) { fatalError() } 41 | 42 | /// Send a value to the downstream without any implicit scheduling on `target`. 43 | /// 44 | /// - important: Subclasses must invoke this only after having hopped onto the target scheduler. 45 | final func unscheduledSend(_ value: OutputValue) { 46 | downstream.receive(value) 47 | } 48 | 49 | /// Signal termination to the downstream without any implicit scheduling on `target`. 50 | /// 51 | /// - important: Subclasses must invoke this only after having hopped onto the target scheduler. 52 | final func unscheduledTerminate(_ termination: Termination) { 53 | if self.state.tryTransition(from: .active, to: .terminated) { 54 | if case .completed = termination { 55 | self.onCompleted() 56 | } 57 | 58 | self.downstream.terminate(termination) 59 | } 60 | } 61 | 62 | open override func terminate(_ termination: Termination) { 63 | // The atomic transition here must happen **after** we hop onto the target scheduler. This is to preserve the timing 64 | // behaviour observed in previous versions of ReactiveSwift. 65 | 66 | target.schedule { 67 | self.unscheduledTerminate(termination) 68 | } 69 | } 70 | 71 | open func onCompleted() {} 72 | } 73 | 74 | private enum AsyncOperatorState: Int32 { 75 | case active 76 | case terminated 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Observers/UniqueValues.swift: -------------------------------------------------------------------------------- 1 | extension Operators { 2 | internal final class UniqueValues: Observer { 3 | let downstream: Observer 4 | let extract: (Value) -> Identity 5 | 6 | var seenIdentities: Set = [] 7 | 8 | init(downstream: Observer, extract: @escaping (Value) -> Identity) { 9 | self.downstream = downstream 10 | self.extract = extract 11 | } 12 | 13 | override func receive(_ value: Value) { 14 | let identity = extract(value) 15 | let (inserted, _) = seenIdentities.insert(identity) 16 | 17 | if inserted { 18 | downstream.receive(value) 19 | } 20 | } 21 | 22 | override func terminate(_ termination: Termination) { 23 | downstream.terminate(termination) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Neil Pankey on 6/24/15. 6 | // Copyright (c) 2015 GitHub. All rights reserved. 7 | // 8 | 9 | /// An optional protocol for use in type constraints. 10 | public protocol OptionalProtocol: ExpressibleByNilLiteral { 11 | /// The type contained in the optional. 12 | associatedtype Wrapped 13 | 14 | init(reconstructing value: Wrapped?) 15 | 16 | /// Extracts an optional from the receiver. 17 | var optional: Wrapped? { get } 18 | } 19 | 20 | extension Optional: OptionalProtocol { 21 | public var optional: Wrapped? { 22 | return self 23 | } 24 | 25 | public init(reconstructing value: Wrapped?) { 26 | self = value 27 | } 28 | } 29 | 30 | extension Signal { 31 | /// Turns each value into an Optional. 32 | internal func optionalize() -> Signal { 33 | return map(Optional.init) 34 | } 35 | } 36 | 37 | extension SignalProducer { 38 | /// Turns each value into an Optional. 39 | internal func optionalize() -> SignalProducer { 40 | return lift { $0.optionalize() } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Reactive.swift: -------------------------------------------------------------------------------- 1 | /// Describes a provider of reactive extensions. 2 | /// 3 | /// - note: `ReactiveExtensionsProvider` does not indicate whether a type is 4 | /// reactive. It is intended for extensions to types that are not owned 5 | /// by the module in order to avoid name collisions and return type 6 | /// ambiguities. 7 | public protocol ReactiveExtensionsProvider {} 8 | 9 | extension ReactiveExtensionsProvider { 10 | /// A proxy which hosts reactive extensions for `self`. 11 | public var reactive: Reactive { 12 | return Reactive(self) 13 | } 14 | 15 | /// A proxy which hosts static reactive extensions for the type of `self`. 16 | public static var reactive: Reactive.Type { 17 | return Reactive.self 18 | } 19 | } 20 | 21 | /// A proxy which hosts reactive extensions of `Base`. 22 | public struct Reactive { 23 | /// The `Base` instance the extensions would be invoked with. 24 | public let base: Base 25 | 26 | /// Construct a proxy 27 | /// 28 | /// - parameters: 29 | /// - base: The object to be proxied. 30 | fileprivate init(_ base: Base) { 31 | self.base = base 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ReactiveSwift.h: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveSwift.h 3 | // ReactiveSwift 4 | // 5 | // Created by Matt Diephouse on 8/15/16. 6 | // Copyright (c) 2016 the ReactiveSwift contributors. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ReactiveSwift. 12 | FOUNDATION_EXPORT double ReactiveSwiftVersionNumber; 13 | 14 | //! Project version string for ReactiveSwift. 15 | FOUNDATION_EXPORT const unsigned char ReactiveSwiftVersionString[]; 16 | -------------------------------------------------------------------------------- /Sources/ResultExtensions.swift: -------------------------------------------------------------------------------- 1 | extension Result: SignalProducerConvertible { 2 | public var producer: SignalProducer { 3 | return .init(result: self) 4 | } 5 | 6 | internal var value: Success? { 7 | switch self { 8 | case let .success(value): return value 9 | case .failure: return nil 10 | } 11 | } 12 | 13 | internal var error: Failure? { 14 | switch self { 15 | case .success: return nil 16 | case let .failure(error): return error 17 | } 18 | } 19 | } 20 | 21 | /// A protocol that can be used to constrain associated types as `Result`. 22 | internal protocol ResultProtocol { 23 | associatedtype Success 24 | associatedtype Failure: Swift.Error 25 | 26 | init(success: Success) 27 | init(failure: Failure) 28 | 29 | var result: Result { get } 30 | } 31 | 32 | extension Result: ResultProtocol { 33 | internal init(success: Success) { 34 | self = .success(success) 35 | } 36 | 37 | internal init(failure: Failure) { 38 | self = .failure(failure) 39 | } 40 | 41 | internal var result: Result { 42 | return self 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Signal.Observer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observer.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Andy Matuschak on 10/2/15. 6 | // Copyright © 2015 GitHub. All rights reserved. 7 | // 8 | 9 | extension Signal { 10 | /// An Observer is a simple wrapper around a function which can receive Events 11 | /// (typically from a Signal). 12 | public final class Observer: ReactiveSwift.Observer { 13 | public typealias Action = (Event) -> Void 14 | private let _send: Action 15 | 16 | /// Whether the observer should send an `interrupted` event as it deinitializes. 17 | private let interruptsOnDeinit: Bool 18 | 19 | /// An initializer that accepts a closure accepting an event for the 20 | /// observer. 21 | /// 22 | /// - parameters: 23 | /// - action: A closure to lift over received event. 24 | /// - interruptsOnDeinit: `true` if the observer should send an `interrupted` 25 | /// event as it deinitializes. `false` otherwise. 26 | internal init(action: @escaping Action, interruptsOnDeinit: Bool) { 27 | self._send = action 28 | self.interruptsOnDeinit = interruptsOnDeinit 29 | } 30 | 31 | /// An initializer that accepts a closure accepting an event for the 32 | /// observer. 33 | /// 34 | /// - parameters: 35 | /// - action: A closure to lift over received event. 36 | public init(_ action: @escaping Action) { 37 | self._send = action 38 | self.interruptsOnDeinit = false 39 | } 40 | 41 | /// An initializer that accepts closures for different event types. 42 | /// 43 | /// - parameters: 44 | /// - value: Optional closure executed when a `value` event is observed. 45 | /// - failed: Optional closure that accepts an `Error` parameter when a 46 | /// failed event is observed. 47 | /// - completed: Optional closure executed when a `completed` event is 48 | /// observed. 49 | /// - interruped: Optional closure executed when an `interrupted` event is 50 | /// observed. 51 | public convenience init( 52 | value: ((Value) -> Void)? = nil, 53 | failed: ((Error) -> Void)? = nil, 54 | completed: (() -> Void)? = nil, 55 | interrupted: (() -> Void)? = nil 56 | ) { 57 | self.init { event in 58 | switch event { 59 | case let .value(v): 60 | value?(v) 61 | 62 | case let .failed(error): 63 | failed?(error) 64 | 65 | case .completed: 66 | completed?() 67 | 68 | case .interrupted: 69 | interrupted?() 70 | } 71 | } 72 | } 73 | 74 | internal convenience init(mappingInterruptedToCompleted observer: Signal.Observer) { 75 | self.init { event in 76 | switch event { 77 | case .value, .completed, .failed: 78 | observer.send(event) 79 | case .interrupted: 80 | observer.sendCompleted() 81 | } 82 | } 83 | } 84 | 85 | deinit { 86 | if interruptsOnDeinit { 87 | // Since `Signal` would ensure that only one terminal event would ever be 88 | // sent for any given `Signal`, we do not need to assert any condition 89 | // here. 90 | _send(.interrupted) 91 | } 92 | } 93 | 94 | public override func receive(_ value: Value) { 95 | send(value: value) 96 | } 97 | 98 | public override func terminate(_ termination: Termination) { 99 | switch termination { 100 | case let .failed(error): 101 | send(error: error) 102 | case .completed: 103 | sendCompleted() 104 | case .interrupted: 105 | sendInterrupted() 106 | } 107 | } 108 | 109 | /// Puts an event into `self`. 110 | public func send(_ event: Event) { 111 | _send(event) 112 | } 113 | 114 | /// Puts a `value` event into `self`. 115 | /// 116 | /// - parameters: 117 | /// - value: A value sent with the `value` event. 118 | public func send(value: Value) { 119 | _send(.value(value)) 120 | } 121 | 122 | /// Puts a failed event into `self`. 123 | /// 124 | /// - parameters: 125 | /// - error: An error object sent with failed event. 126 | public func send(error: Error) { 127 | _send(.failed(error)) 128 | } 129 | 130 | /// Puts a `completed` event into `self`. 131 | public func sendCompleted() { 132 | _send(.completed) 133 | } 134 | 135 | /// Puts an `interrupted` event into `self`. 136 | public func sendInterrupted() { 137 | _send(.interrupted) 138 | } 139 | } 140 | } 141 | 142 | /// FIXME: Cannot be placed in `Deprecations+Removal.swift` if compiling with 143 | /// Xcode 9.2. 144 | extension Signal.Observer { 145 | /// An action that will be performed upon arrival of the event. 146 | @available(*, unavailable, renamed:"send(_:)") 147 | public var action: Action { fatalError() } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/UnidirectionalBinding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | 4 | precedencegroup BindingPrecedence { 5 | associativity: right 6 | 7 | // Binds tighter than assignment but looser than everything else 8 | higherThan: AssignmentPrecedence 9 | } 10 | 11 | infix operator <~ : BindingPrecedence 12 | 13 | #if swift(>=5.7) 14 | /// Describes a source which can be bound. 15 | public protocol BindingSource: SignalProducerConvertible where Error == Never {} 16 | #else 17 | /// Describes a source which can be bound. 18 | public protocol BindingSource: SignalProducerConvertible where Error == Never {} 19 | #endif 20 | extension Signal: BindingSource where Error == Never {} 21 | extension SignalProducer: BindingSource where Error == Never {} 22 | 23 | #if swift(>=5.7) 24 | /// Describes an entity which be bound towards. 25 | public protocol BindingTargetProvider { 26 | associatedtype Value 27 | 28 | var bindingTarget: BindingTarget { get } 29 | } 30 | #else 31 | /// Describes an entity which be bound towards. 32 | public protocol BindingTargetProvider { 33 | associatedtype Value 34 | 35 | var bindingTarget: BindingTarget { get } 36 | } 37 | #endif 38 | 39 | extension BindingTargetProvider { 40 | /// Binds a source to a target, updating the target's value to the latest 41 | /// value sent by the source. 42 | /// 43 | /// - note: The binding will automatically terminate when the target is 44 | /// deinitialized, or when the source sends a `completed` event. 45 | /// 46 | /// ```` 47 | /// let property = MutableProperty(0) 48 | /// let signal = Signal({ /* do some work after some time */ }) 49 | /// property <~ signal 50 | /// ```` 51 | /// 52 | /// ```` 53 | /// let property = MutableProperty(0) 54 | /// let signal = Signal({ /* do some work after some time */ }) 55 | /// let disposable = property <~ signal 56 | /// ... 57 | /// // Terminates binding before property dealloc or signal's 58 | /// // `completed` event. 59 | /// disposable.dispose() 60 | /// ```` 61 | /// 62 | /// - parameters: 63 | /// - target: A target to be bound to. 64 | /// - source: A source to bind. 65 | /// 66 | /// - returns: A disposable that can be used to terminate binding before the 67 | /// deinitialization of the target or the source's `completed` 68 | /// event. 69 | @discardableResult 70 | public static func <~ 71 | 72 | (provider: Self, source: Source) -> Disposable? 73 | where Source.Value == Value 74 | { 75 | return source.producer 76 | .take(during: provider.bindingTarget.lifetime) 77 | .startWithValues(provider.bindingTarget.action) 78 | } 79 | 80 | /// Binds a source to a target, updating the target's value to the latest 81 | /// value sent by the source. 82 | /// 83 | /// - note: The binding will automatically terminate when the target is 84 | /// deinitialized, or when the source sends a `completed` event. 85 | /// 86 | /// ```` 87 | /// let property = MutableProperty(0) 88 | /// let signal = Signal({ /* do some work after some time */ }) 89 | /// property <~ signal 90 | /// ```` 91 | /// 92 | /// ```` 93 | /// let property = MutableProperty(0) 94 | /// let signal = Signal({ /* do some work after some time */ }) 95 | /// let disposable = property <~ signal 96 | /// ... 97 | /// // Terminates binding before property dealloc or signal's 98 | /// // `completed` event. 99 | /// disposable.dispose() 100 | /// ```` 101 | /// 102 | /// - parameters: 103 | /// - target: A target to be bound to. 104 | /// - source: A source to bind. 105 | /// 106 | /// - returns: A disposable that can be used to terminate binding before the 107 | /// deinitialization of the target or the source's `completed` 108 | /// event. 109 | @discardableResult 110 | public static func <~ 111 | 112 | (provider: Self, source: Source) -> Disposable? 113 | where Value == Source.Value? 114 | { 115 | return provider <~ source.producer.optionalize() 116 | } 117 | } 118 | 119 | extension Signal.Observer { 120 | /// Binds a source to a target, updating the target's value to the latest 121 | /// value sent by the source. 122 | /// 123 | /// - note: Only `value` events will be forwarded to the Observer. 124 | /// The binding will automatically terminate when the target is 125 | /// deinitialized, or when the source sends a `completed` event. 126 | /// 127 | /// - parameters: 128 | /// - target: A target to be bound to. 129 | /// - source: A source to bind. 130 | /// 131 | /// - returns: A disposable that can be used to terminate binding before the 132 | /// deinitialization of the target or the source's `completed` 133 | /// event. 134 | @discardableResult 135 | public static func <~ 136 | 137 | (observer: Signal.Observer, source: Source) -> Disposable 138 | where Source.Value == Value 139 | { 140 | return source.producer.startWithValues { [weak observer] in 141 | observer?.send(value: $0) 142 | } 143 | } 144 | } 145 | 146 | /// A binding target that can be used with the `<~` operator. 147 | public struct BindingTarget: BindingTargetProvider { 148 | public let lifetime: Lifetime 149 | public let action: (Value) -> Void 150 | 151 | public var bindingTarget: BindingTarget { 152 | return self 153 | } 154 | 155 | /// Creates a binding target which consumes values on the specified scheduler. 156 | /// 157 | /// If no scheduler is specified, the binding target would consume the value 158 | /// immediately. 159 | /// 160 | /// - parameters: 161 | /// - scheduler: The scheduler on which the `action` consumes the values. 162 | /// - lifetime: The expected lifetime of any bindings towards `self`. 163 | /// - action: The action to consume values. 164 | public init(on scheduler: Scheduler = ImmediateScheduler(), lifetime: Lifetime, action: @escaping (Value) -> Void) { 165 | self.lifetime = lifetime 166 | 167 | if scheduler is ImmediateScheduler { 168 | self.action = action 169 | } else { 170 | self.action = { value in 171 | scheduler.schedule { 172 | action(value) 173 | } 174 | } 175 | } 176 | } 177 | 178 | /// Creates a binding target which consumes values on the specified scheduler. 179 | /// 180 | /// If no scheduler is specified, the binding target would consume the value 181 | /// immediately. 182 | /// 183 | /// - parameters: 184 | /// - scheduler: The scheduler on which the key path consumes the values. 185 | /// - lifetime: The expected lifetime of any bindings towards `self`. 186 | /// - object: The object to consume values. 187 | /// - keyPath: The key path of the object that consumes values. 188 | public init(on scheduler: Scheduler = ImmediateScheduler(), lifetime: Lifetime, object: Object, keyPath: WritableKeyPath) { 189 | self.init(on: scheduler, lifetime: lifetime) { [weak object] in object?[keyPath: keyPath] = $0 } 190 | } 191 | } 192 | 193 | extension Optional: BindingTargetProvider where Wrapped: BindingTargetProvider { 194 | public typealias Value = Wrapped.Value 195 | 196 | public var bindingTarget: BindingTarget { 197 | switch self { 198 | case let .some(provider): 199 | return provider.bindingTarget 200 | case .none: 201 | return BindingTarget(lifetime: .empty, action: { _ in }) 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Quick 3 | 4 | @testable import ReactiveSwiftTests 5 | 6 | Quick.QCKMain([ 7 | ActionSpec.self, 8 | AtomicSpec.self, 9 | BagSpec.self, 10 | DisposableSpec.self, 11 | DeprecationSpec.self, 12 | FlattenSpec.self, 13 | FoundationExtensionsSpec.self, 14 | LifetimeSpec.self, 15 | PropertySpec.self, 16 | SchedulerSpec.self, 17 | SignalLifetimeSpec.self, 18 | SignalProducerLiftingSpec.self, 19 | SignalProducerSpec.self, 20 | SignalSpec.self, 21 | ]) 22 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/AtomicSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicSpec.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Justin Spahr-Summers on 2014-07-13. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | import ReactiveSwift 12 | 13 | class AtomicSpec: QuickSpec { 14 | override class func spec() { 15 | var atomic: Atomic! 16 | 17 | beforeEach { 18 | atomic = Atomic(1) 19 | } 20 | 21 | it("should read and write the value directly") { 22 | expect(atomic.value) == 1 23 | 24 | atomic.value = 2 25 | expect(atomic.value) == 2 26 | } 27 | 28 | it("should swap the value atomically") { 29 | expect(atomic.swap(2)) == 1 30 | expect(atomic.value) == 2 31 | } 32 | 33 | it("should modify the value atomically") { 34 | atomic.modify { $0 += 1 } 35 | expect(atomic.value) == 2 36 | } 37 | 38 | it("should perform an action with the value") { 39 | let result: Bool = atomic.withValue { $0 == 1 } 40 | expect(result) == true 41 | expect(atomic.value) == 1 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/BagSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BagSpec.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Justin Spahr-Summers on 2014-07-13. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | import ReactiveSwift 12 | 13 | class BagSpec: QuickSpec { 14 | override class func spec() { 15 | var bag = Bag() 16 | 17 | beforeEach { 18 | bag = Bag() 19 | } 20 | 21 | it("should insert values") { 22 | bag.insert("foo") 23 | bag.insert("bar") 24 | bag.insert("buzz") 25 | 26 | expect(bag).to(contain("foo")) 27 | expect(bag).to(contain("bar")) 28 | expect(bag).to(contain("buzz")) 29 | expect(bag).toNot(contain("fuzz")) 30 | expect(bag).toNot(contain("foobar")) 31 | } 32 | 33 | it("should remove values given the token from insertion") { 34 | let a = bag.insert("foo") 35 | let b = bag.insert("bar") 36 | let c = bag.insert("buzz") 37 | 38 | bag.remove(using: b) 39 | expect(bag).to(contain("foo")) 40 | expect(bag).toNot(contain("bar")) 41 | expect(bag).to(contain("buzz")) 42 | 43 | bag.remove(using: a) 44 | expect(bag).toNot(contain("foo")) 45 | expect(bag).toNot(contain("bar")) 46 | expect(bag).to(contain("buzz")) 47 | 48 | bag.remove(using: c) 49 | expect(bag).toNot(contain("foo")) 50 | expect(bag).toNot(contain("bar")) 51 | expect(bag).toNot(contain("buzz")) 52 | } 53 | 54 | it("should create bag with initial values") { 55 | let values = ["foo", "bar", "buzz"] 56 | var bag = Bag(values) 57 | 58 | let a = bag.insert("fuzz") 59 | 60 | expect(bag).to(contain("foo")) 61 | expect(bag).to(contain("bar")) 62 | expect(bag).to(contain("buzz")) 63 | expect(bag).to(contain("fuzz")) 64 | 65 | bag.remove(using: a) 66 | expect(bag).to(contain("foo")) 67 | expect(bag).to(contain("bar")) 68 | expect(bag).to(contain("buzz")) 69 | expect(bag).toNot(contain("fuzz")) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/DateSchedulerAsyncTestCase.swift: -------------------------------------------------------------------------------- 1 | #if canImport(_Concurrency) && compiler(>=5.5.2) 2 | import ReactiveSwift 3 | import XCTest 4 | 5 | // !!!: Using XCTest as only from Quick 6 are asynchronous contexts available (we're on Quick 4 at the time of writing) 6 | 7 | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, *) 8 | final class DateSchedulerAsyncTestCase: XCTestCase { 9 | var scheduler: TestScheduler! 10 | var startDate: Date! 11 | 12 | override func setUpWithError() throws { 13 | try super.setUpWithError() 14 | 15 | startDate = Date() 16 | scheduler = TestScheduler(startDate: startDate) 17 | XCTAssert(scheduler.currentDate == startDate) 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | scheduler = nil 22 | startDate = nil 23 | 24 | try super.tearDownWithError() 25 | } 26 | 27 | func test_sleepFor_shouldSleepForTheDefinedIntervalBeforeReturning() async throws { 28 | let task = Task { 29 | try await scheduler.sleep(for: .seconds(5)) 30 | XCTAssertEqual(scheduler.currentDate, startDate.addingTimeInterval(5)) 31 | } 32 | 33 | XCTAssertEqual(scheduler.currentDate, startDate) 34 | await scheduler.advance(by: .seconds(5)) 35 | 36 | let _ = await task.result 37 | } 38 | 39 | func test_sleepUntil_shouldSleepForTheDefinedIntervalBeforeReturning() async throws { 40 | let sleepDate = startDate.addingTimeInterval(5) 41 | 42 | let task = Task { 43 | try await scheduler.sleep(until: sleepDate) 44 | XCTAssertEqual(scheduler.currentDate, sleepDate) 45 | } 46 | 47 | XCTAssertEqual(scheduler.currentDate, startDate) 48 | await scheduler.advance(by: .seconds(5)) 49 | 50 | let _ = await task.result 51 | } 52 | 53 | func test_timer_shouldSendTheCurrentDateAtTheGivenInterval() async throws { 54 | let expectation = self.expectation(description: "timer") 55 | expectation.expectedFulfillmentCount = 3 56 | 57 | let startDate = scheduler.currentDate 58 | let tick1 = startDate.addingTimeInterval(1) 59 | let tick2 = startDate.addingTimeInterval(2) 60 | let tick3 = startDate.addingTimeInterval(3) 61 | 62 | let dates = Atomic<[Date]>([]) 63 | 64 | let task = Task { [dates] in 65 | for await date in scheduler.timer(interval: .seconds(1)) { 66 | XCTAssertEqual(scheduler.currentDate, date) 67 | dates.modify { $0.append(date) } 68 | expectation.fulfill() 69 | } 70 | } 71 | 72 | await scheduler.advance(by: .milliseconds(900)) 73 | XCTAssertEqual(dates.value, []) 74 | 75 | await scheduler.advance(by: .seconds(1)) 76 | XCTAssertEqual(dates.value, [tick1]) 77 | 78 | await scheduler.advance() 79 | XCTAssertEqual(dates.value, [tick1]) 80 | 81 | await scheduler.advance(by: .milliseconds(200)) 82 | XCTAssertEqual(dates.value, [tick1, tick2]) 83 | 84 | await scheduler.advance(by: .seconds(1)) 85 | XCTAssertEqual(dates.value, [tick1, tick2, tick3]) 86 | 87 | task.cancel() // cancel the timer 88 | 89 | #if swift(>=5.8) 90 | await fulfillment(of: [expectation], timeout: 0.1) 91 | #else 92 | await waitForExpectations(timeout: 0.1) 93 | #endif 94 | } 95 | } 96 | #endif 97 | 98 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/DeprecationSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReactiveSwift 4 | 5 | class DeprecationSpec: QuickSpec { 6 | override class func spec() {} 7 | } 8 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/DisposableSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisposableSpec.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Justin Spahr-Summers on 2014-07-13. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | import ReactiveSwift 12 | 13 | class DisposableSpec: QuickSpec { 14 | override class func spec() { 15 | describe("SimpleDisposable") { 16 | it("should set disposed to true") { 17 | let disposable = AnyDisposable() 18 | expect(disposable.isDisposed) == false 19 | 20 | disposable.dispose() 21 | expect(disposable.isDisposed) == true 22 | } 23 | } 24 | 25 | describe("ActionDisposable") { 26 | it("should run the given action upon disposal") { 27 | var didDispose = false 28 | let disposable = AnyDisposable { 29 | didDispose = true 30 | } 31 | 32 | expect(didDispose) == false 33 | expect(disposable.isDisposed) == false 34 | 35 | disposable.dispose() 36 | expect(didDispose) == true 37 | expect(disposable.isDisposed) == true 38 | } 39 | } 40 | 41 | describe("CompositeDisposable") { 42 | var disposable = CompositeDisposable() 43 | 44 | beforeEach { 45 | disposable = CompositeDisposable() 46 | } 47 | 48 | it("should ignore the addition of nil") { 49 | disposable.add(nil) 50 | return 51 | } 52 | 53 | it("should dispose of added disposables") { 54 | let simpleDisposable = AnyDisposable() 55 | disposable += simpleDisposable 56 | 57 | var didDispose = false 58 | disposable += { 59 | didDispose = true 60 | } 61 | 62 | expect(simpleDisposable.isDisposed) == false 63 | expect(didDispose) == false 64 | expect(disposable.isDisposed) == false 65 | 66 | disposable.dispose() 67 | expect(simpleDisposable.isDisposed) == true 68 | expect(didDispose) == true 69 | expect(disposable.isDisposed) == true 70 | } 71 | 72 | it("should not dispose of removed disposables") { 73 | let simpleDisposable = AnyDisposable() 74 | let handle = disposable += simpleDisposable 75 | 76 | // We should be allowed to call this any number of times. 77 | handle?.dispose() 78 | handle?.dispose() 79 | expect(simpleDisposable.isDisposed) == false 80 | 81 | disposable.dispose() 82 | expect(simpleDisposable.isDisposed) == false 83 | } 84 | 85 | it("should create with initial disposables") { 86 | let disposable1 = AnyDisposable() 87 | let disposable2 = AnyDisposable() 88 | let disposable3 = AnyDisposable() 89 | 90 | let compositeDisposable = CompositeDisposable([disposable1, disposable2, disposable3]) 91 | 92 | expect(disposable1.isDisposed) == false 93 | expect(disposable2.isDisposed) == false 94 | expect(disposable3.isDisposed) == false 95 | 96 | compositeDisposable.dispose() 97 | 98 | expect(disposable1.isDisposed) == true 99 | expect(disposable2.isDisposed) == true 100 | expect(disposable3.isDisposed) == true 101 | } 102 | } 103 | 104 | describe("ScopedDisposable") { 105 | it("should be initialized with an instance of `Disposable` protocol type") { 106 | let d: Disposable = AnyDisposable() 107 | let scoped = ScopedDisposable(d) 108 | expect(type(of: scoped) == ScopedDisposable.self) == true 109 | } 110 | 111 | it("should dispose of the inner disposable upon deinitialization") { 112 | let simpleDisposable = AnyDisposable() 113 | 114 | func runScoped() { 115 | let scopedDisposable = ScopedDisposable(simpleDisposable) 116 | expect(simpleDisposable.isDisposed) == false 117 | expect(scopedDisposable.isDisposed) == false 118 | } 119 | 120 | expect(simpleDisposable.isDisposed) == false 121 | 122 | runScoped() 123 | expect(simpleDisposable.isDisposed) == true 124 | } 125 | } 126 | 127 | describe("SerialDisposable") { 128 | var disposable: SerialDisposable! 129 | 130 | beforeEach { 131 | disposable = SerialDisposable() 132 | } 133 | 134 | it("should dispose of the inner disposable") { 135 | let simpleDisposable = AnyDisposable() 136 | disposable.inner = simpleDisposable 137 | 138 | expect(disposable.inner).notTo(beNil()) 139 | expect(simpleDisposable.isDisposed) == false 140 | expect(disposable.isDisposed) == false 141 | 142 | disposable.dispose() 143 | expect(disposable.inner).to(beNil()) 144 | expect(simpleDisposable.isDisposed) == true 145 | expect(disposable.isDisposed) == true 146 | } 147 | 148 | it("should dispose of the previous disposable when swapping innerDisposable") { 149 | let oldDisposable = AnyDisposable() 150 | let newDisposable = AnyDisposable() 151 | 152 | disposable.inner = oldDisposable 153 | expect(oldDisposable.isDisposed) == false 154 | expect(newDisposable.isDisposed) == false 155 | 156 | disposable.inner = newDisposable 157 | expect(oldDisposable.isDisposed) == true 158 | expect(newDisposable.isDisposed) == false 159 | expect(disposable.isDisposed) == false 160 | 161 | disposable.inner = nil 162 | expect(newDisposable.isDisposed) == true 163 | expect(disposable.isDisposed) == false 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoundationExtensionsSpec.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Neil Pankey on 5/22/15. 6 | // Copyright (c) 2015 GitHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Dispatch 11 | import Nimble 12 | import Quick 13 | @testable import ReactiveSwift 14 | 15 | extension Notification.Name { 16 | static let racFirst = Notification.Name(rawValue: "rac_notifications_test") 17 | static let racAnother = Notification.Name(rawValue: "rac_notifications_another") 18 | } 19 | 20 | class FoundationExtensionsSpec: QuickSpec { 21 | override class func spec() { 22 | describe("NotificationCenter.reactive.notifications") { 23 | let center = NotificationCenter.default 24 | 25 | it("should send notifications on the signal") { 26 | let signal = center.reactive.notifications(forName: .racFirst) 27 | 28 | var notif: Notification? = nil 29 | let disposable = signal.observeValues { notif = $0 } 30 | 31 | center.post(name: .racAnother, object: nil) 32 | expect(notif).to(beNil()) 33 | 34 | center.post(name: .racFirst, object: nil) 35 | expect(notif?.name) == .racFirst 36 | 37 | notif = nil 38 | disposable?.dispose() 39 | 40 | center.post(name: .racFirst, object: nil) 41 | expect(notif).to(beNil()) 42 | } 43 | 44 | it("should be disposed of if it is not reachable and no observer is attached") { 45 | weak var signal: Signal? 46 | var isDisposed = false 47 | 48 | let disposable: Disposable? = { 49 | let innerSignal = center.reactive.notifications(forName: nil) 50 | .on(disposed: { isDisposed = true }) 51 | 52 | signal = innerSignal 53 | return innerSignal.observe { _ in } 54 | }() 55 | 56 | expect(isDisposed) == false 57 | expect(signal).to(beNil()) 58 | 59 | disposable?.dispose() 60 | 61 | expect(isDisposed) == true 62 | expect(signal).to(beNil()) 63 | } 64 | 65 | it("should be not disposed of if it still has one or more active observers") { 66 | weak var signal: Signal? 67 | var isDisposed = false 68 | 69 | let disposable: Disposable? = { 70 | let innerSignal = center.reactive.notifications(forName: nil) 71 | .on(disposed: { isDisposed = true }) 72 | 73 | signal = innerSignal 74 | innerSignal.observe { _ in } 75 | return innerSignal.observe { _ in } 76 | }() 77 | 78 | expect(isDisposed) == false 79 | expect(signal).to(beNil()) 80 | 81 | disposable?.dispose() 82 | 83 | expect(isDisposed) == false 84 | expect(signal).to(beNil()) 85 | } 86 | } 87 | 88 | describe("DispatchTimeInterval") { 89 | it("should scale time values as expected") { 90 | expect((DispatchTimeInterval.seconds(1) * 0.1).timeInterval).to(beCloseTo(DispatchTimeInterval.milliseconds(100).timeInterval)) 91 | expect((DispatchTimeInterval.milliseconds(100) * 0.1).timeInterval).to(beCloseTo(DispatchTimeInterval.microseconds(10000).timeInterval)) 92 | 93 | expect((DispatchTimeInterval.seconds(5) * 0.5).timeInterval).to(beCloseTo(DispatchTimeInterval.milliseconds(2500).timeInterval)) 94 | expect((DispatchTimeInterval.seconds(1) * 0.25).timeInterval).to(beCloseTo(DispatchTimeInterval.milliseconds(250).timeInterval)) 95 | } 96 | 97 | it("should not introduce integer overflow upon scale") { 98 | expect((DispatchTimeInterval.seconds(Int.max) * 0.01).timeInterval).to(beCloseTo(10 * DispatchTimeInterval.milliseconds(Int.max).timeInterval, within: 1)) 99 | expect((DispatchTimeInterval.milliseconds(Int.max) * 0.01).timeInterval).to(beCloseTo(10 * DispatchTimeInterval.microseconds(Int.max).timeInterval, within: 1)) 100 | expect((DispatchTimeInterval.microseconds(Int.max) * 0.01).timeInterval).to(beCloseTo(10 * DispatchTimeInterval.nanoseconds(Int.max).timeInterval, within: 1)) 101 | expect((DispatchTimeInterval.seconds(Int.max) * 10).timeInterval) == Double.infinity 102 | } 103 | 104 | it("should produce the expected TimeInterval values") { 105 | expect(DispatchTimeInterval.seconds(1).timeInterval).to(beCloseTo(1.0)) 106 | expect(DispatchTimeInterval.milliseconds(1).timeInterval).to(beCloseTo(0.001)) 107 | expect(DispatchTimeInterval.microseconds(1).timeInterval).to(beCloseTo(0.000001, within: 0.0000001)) 108 | expect(DispatchTimeInterval.nanoseconds(1).timeInterval).to(beCloseTo(0.000000001, within: 0.0000000001)) 109 | 110 | expect(DispatchTimeInterval.milliseconds(500).timeInterval).to(beCloseTo(0.5)) 111 | expect(DispatchTimeInterval.milliseconds(250).timeInterval).to(beCloseTo(0.25)) 112 | expect(DispatchTimeInterval.never.timeInterval) == Double.infinity 113 | } 114 | 115 | it("should negate as you'd hope") { 116 | expect((-DispatchTimeInterval.seconds(1)).timeInterval).to(beCloseTo(-1.0)) 117 | expect((-DispatchTimeInterval.milliseconds(1)).timeInterval).to(beCloseTo(-0.001)) 118 | expect((-DispatchTimeInterval.microseconds(1)).timeInterval).to(beCloseTo(-0.000001, within: 0.0000001)) 119 | expect((-DispatchTimeInterval.nanoseconds(1)).timeInterval).to(beCloseTo(-0.000000001, within: 0.0000000001)) 120 | expect((-DispatchTimeInterval.never).timeInterval) == Double.infinity 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/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.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/LifetimeSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReactiveSwift 4 | 5 | final class LifetimeSpec: QuickSpec { 6 | override class func spec() { 7 | describe("Lifetime") { 8 | it("should complete its lifetime ended signal when the token deinitializes") { 9 | let object = MutableReference(TestObject()) 10 | 11 | var isCompleted = false 12 | 13 | object.value!.lifetime.ended.observeCompleted { isCompleted = true } 14 | expect(isCompleted) == false 15 | 16 | object.value = nil 17 | expect(isCompleted) == true 18 | } 19 | 20 | it("should complete its lifetime ended signal when the token is disposed of") { 21 | let object = MutableReference(TestObject()) 22 | 23 | var isCompleted = false 24 | 25 | object.value!.lifetime.ended.observeCompleted { isCompleted = true } 26 | expect(isCompleted) == false 27 | 28 | object.value!.disposeToken() 29 | expect(isCompleted) == true 30 | } 31 | 32 | it("should complete its lifetime ended signal even if the lifetime object is being retained") { 33 | let object = MutableReference(TestObject()) 34 | let lifetime = object.value!.lifetime 35 | 36 | var isCompleted = false 37 | 38 | lifetime.ended.observeCompleted { isCompleted = true } 39 | expect(isCompleted) == false 40 | 41 | object.value = nil 42 | expect(isCompleted) == true 43 | } 44 | 45 | it("should provide a convenience factory method") { 46 | let lifetime: Lifetime 47 | var token: Lifetime.Token 48 | 49 | (lifetime, token) = Lifetime.make() 50 | 51 | var isEnded = false 52 | lifetime.observeEnded { isEnded = true } 53 | 54 | token = Lifetime.Token() 55 | _ = token 56 | 57 | expect(isEnded) == true 58 | } 59 | 60 | it("should notify its observers when the underlying token deinitializes") { 61 | let object = MutableReference(TestObject()) 62 | 63 | var isEnded = false 64 | 65 | object.value!.lifetime.observeEnded { isEnded = true } 66 | expect(isEnded) == false 67 | 68 | object.value = nil 69 | expect(isEnded) == true 70 | } 71 | 72 | it("should notify its observers of the deinitialization of the underlying token even if the `Lifetime` object is retained") { 73 | let object = MutableReference(TestObject()) 74 | let lifetime = object.value!.lifetime 75 | 76 | var isEnded = false 77 | 78 | lifetime.observeEnded { isEnded = true } 79 | expect(isEnded) == false 80 | 81 | object.value = nil 82 | expect(isEnded) == true 83 | } 84 | 85 | it("should notify its observers of its deinitialization if it has already ended") { 86 | var isEnded = false 87 | 88 | Lifetime.empty.observeEnded { isEnded = true } 89 | expect(isEnded) == true 90 | } 91 | } 92 | } 93 | } 94 | 95 | internal final class MutableReference { 96 | var value: Value? 97 | init(_ value: Value?) { 98 | self.value = value 99 | } 100 | } 101 | 102 | internal final class TestObject { 103 | private let token = Lifetime.Token() 104 | var lifetime: Lifetime { return Lifetime(token) } 105 | 106 | func disposeToken() { 107 | token.dispose() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/QueueScheduler+Factory.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | import Foundation 3 | 4 | extension QueueScheduler { 5 | static func makeForTesting(file: String = #file, line: UInt = #line) -> QueueScheduler { 6 | let file = URL(string: file)?.lastPathComponent ?? "" 7 | let label = "reactiveswift:\(file):\(line)" 8 | 9 | return QueueScheduler(name: label) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/ReactiveExtensionsSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | @testable import ReactiveSwift 4 | 5 | private final class TestExtensionProvider: ReactiveExtensionsProvider { 6 | let instanceProperty = "instance" 7 | static let staticProperty = "static" 8 | } 9 | 10 | extension Reactive where Base: TestExtensionProvider { 11 | var instanceProperty: SignalProducer { 12 | return SignalProducer(value: base.instanceProperty) 13 | } 14 | 15 | static var staticProperty: SignalProducer { 16 | return SignalProducer(value: Base.staticProperty) 17 | } 18 | } 19 | 20 | final class ReactiveExtensionsSpec: QuickSpec { 21 | override class func spec() { 22 | describe("ReactiveExtensions") { 23 | it("allows reactive extensions of instances") { 24 | expect(TestExtensionProvider().reactive.instanceProperty.first()?.value) == "instance" 25 | } 26 | 27 | it("allows reactive extensions of types") { 28 | expect(TestExtensionProvider.reactive.staticProperty.first()?.value) == "static" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/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) -> Matcher> { 15 | return sendValues(value.map { [$0] } ?? [], sendError: sendError, complete: complete) 16 | } 17 | 18 | public func sendValues(_ values: [T], sendError maybeSendError: E?, complete: Bool) -> Matcher> { 19 | return Matcher> { 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 MatcherResult(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 | actual: "<\(sentValues)>" 48 | ) 49 | return MatcherResult(status: .doesNotMatch, message: message) 50 | } 51 | 52 | if sentError != maybeSendError { 53 | let message = ExpectationMessage.expectedCustomValueTo( 54 | "send error <\(String(describing: maybeSendError))>", 55 | actual: "<\(String(describing: sentError))>" 56 | ) 57 | return MatcherResult(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 MatcherResult(bool: signalCompleted == complete, message: message) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/TestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestError.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Almas Sapargali on 1/26/15. 6 | // Copyright (c) 2015 GitHub. All rights reserved. 7 | // 8 | 9 | import ReactiveSwift 10 | 11 | internal enum TestError: Int { 12 | case `default` = 0 13 | case error1 = 1 14 | case error2 = 2 15 | } 16 | 17 | extension TestError: Error { 18 | } 19 | 20 | internal extension SignalProducer { 21 | /// Halts if an error is emitted in the receiver signal. 22 | /// This is useful in tests to be able to just use `startWithNext` 23 | /// in cases where we know that an error won't be emitted. 24 | func assumeNoErrors() -> SignalProducer { 25 | return self.lift { $0.assumeNoErrors() } 26 | } 27 | } 28 | 29 | internal extension Signal { 30 | /// Halts if an error is emitted in the receiver signal. 31 | /// This is useful in tests to be able to just use `startWithNext` 32 | /// in cases where we know that an error won't be emitted. 33 | func assumeNoErrors() -> Signal { 34 | return self.mapError { error in 35 | fatalError("Unexpected error: \(error)") 36 | 37 | () 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/TestLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestLogger.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Rui Peres on 29/04/2016. 6 | // Copyright © 2016 GitHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import ReactiveSwift 11 | 12 | final class TestLogger { 13 | fileprivate var expectations: [(String) -> Void] 14 | 15 | init(expectations: [(String) -> Void]) { 16 | self.expectations = expectations 17 | } 18 | } 19 | 20 | extension TestLogger { 21 | func logEvent(_ identifier: String, event: String, fileName: String, functionName: String, lineNumber: Int) { 22 | expectations.removeFirst()("[\(identifier)] \(event)") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/TestSchedulerAsyncTestCase.swift: -------------------------------------------------------------------------------- 1 | #if canImport(_Concurrency) && compiler(>=5.5.2) 2 | import ReactiveSwift 3 | import XCTest 4 | 5 | // !!!: Using XCTest as only from Quick 6 are asynchronous contexts available (we're on Quick 4 at the time of writing) 6 | 7 | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, *) 8 | final class TestSchedulerAsyncTestCase: XCTestCase { 9 | var scheduler: TestScheduler! 10 | var startDate: Date! 11 | 12 | override func setUpWithError() throws { 13 | try super.setUpWithError() 14 | 15 | startDate = Date() 16 | scheduler = TestScheduler(startDate: startDate) 17 | XCTAssert(scheduler.currentDate == startDate) 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | scheduler = nil 22 | startDate = nil 23 | 24 | try super.tearDownWithError() 25 | } 26 | 27 | func test_advance_shouldRunEnqueuedActionsImmediately() async { 28 | var string = "" 29 | 30 | scheduler.schedule { 31 | string += "foo" 32 | XCTAssert(Thread.isMainThread) 33 | } 34 | 35 | scheduler.schedule { 36 | string += "bar" 37 | XCTAssert(Thread.isMainThread) 38 | } 39 | 40 | XCTAssert(string.isEmpty) 41 | 42 | await scheduler.advance() 43 | 44 | // How much dates are allowed to differ when they should be "equal." 45 | let dateComparisonDelta = 0.00001 46 | 47 | XCTAssertLessThanOrEqual( 48 | scheduler.currentDate.timeIntervalSince1970 - startDate.timeIntervalSince1970, 49 | dateComparisonDelta 50 | ) 51 | 52 | XCTAssertEqual(string, "foobar") 53 | } 54 | 55 | func test_advanceByDispatchTimeInterval_shouldRunEnqueuedActionsWhenPastTheTargetDate() async { 56 | var string = "" 57 | 58 | scheduler.schedule(after: .seconds(15)) { 59 | string += "bar" 60 | XCTAssert(Thread.isMainThread) 61 | XCTAssertEqual(self.scheduler.currentDate, self.startDate!.addingTimeInterval(15)) 62 | } 63 | 64 | scheduler.schedule(after: .seconds(5)) { 65 | string += "foo" 66 | XCTAssert(Thread.isMainThread) 67 | XCTAssertEqual(self.scheduler.currentDate, self.startDate!.addingTimeInterval(5)) 68 | } 69 | 70 | XCTAssert(string.isEmpty) 71 | 72 | await scheduler.advance(by: .seconds(10)) 73 | XCTAssertEqual(scheduler.currentDate, startDate.addingTimeInterval(10)) 74 | XCTAssertEqual(string, "foo") 75 | 76 | await scheduler.advance(by: .seconds(10)) 77 | XCTAssertEqual(scheduler.currentDate, startDate.addingTimeInterval(20)) 78 | XCTAssertEqual(string, "foobar") 79 | } 80 | 81 | func test_advanceByTimeInterval_shouldRunEnqueuedActionsWhenPastTheTargetDate() async { 82 | var string = "" 83 | 84 | scheduler.schedule(after: .seconds(15)) { 85 | string += "bar" 86 | XCTAssert(Thread.isMainThread) 87 | XCTAssertEqual(self.scheduler.currentDate, self.startDate!.addingTimeInterval(15)) 88 | } 89 | 90 | scheduler.schedule(after: .seconds(5)) { 91 | string += "foo" 92 | XCTAssert(Thread.isMainThread) 93 | XCTAssertEqual(self.scheduler.currentDate, self.startDate!.addingTimeInterval(5)) 94 | } 95 | 96 | XCTAssert(string.isEmpty) 97 | 98 | await scheduler.advance(by: 10) 99 | XCTAssertEqual(scheduler.currentDate, startDate.addingTimeInterval(10)) 100 | XCTAssertEqual(string, "foo") 101 | 102 | await scheduler.advance(by: 10) 103 | XCTAssertEqual(scheduler.currentDate, startDate.addingTimeInterval(20)) 104 | XCTAssertEqual(string, "foobar") 105 | } 106 | 107 | func test_run_shouldRunAllEnqueuedActionsInOrder() async { 108 | var string = "" 109 | 110 | scheduler.schedule(after: .seconds(15)) { 111 | string += "bar" 112 | XCTAssert(Thread.isMainThread) 113 | } 114 | 115 | scheduler.schedule(after: .seconds(5)) { 116 | string += "foo" 117 | XCTAssert(Thread.isMainThread) 118 | } 119 | 120 | scheduler.schedule { 121 | string += "fuzzbuzz" 122 | XCTAssert(Thread.isMainThread) 123 | } 124 | 125 | XCTAssert(string.isEmpty) 126 | 127 | await scheduler.run() 128 | XCTAssertEqual(scheduler.currentDate, Date.distantFuture) 129 | XCTAssertEqual(string, "fuzzbuzzfoobar") 130 | } 131 | } 132 | #endif 133 | -------------------------------------------------------------------------------- /Tests/ReactiveSwiftTests/UnidirectionalBindingSpec.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | import Nimble 4 | import Quick 5 | @testable import ReactiveSwift 6 | 7 | private class Object { 8 | var value: Int = 0 9 | } 10 | 11 | class UnidirectionalBindingSpec: QuickSpec { 12 | override class func spec() { 13 | describe("BindingTarget") { 14 | var token: Lifetime.Token! 15 | var lifetime: Lifetime! 16 | 17 | beforeEach { 18 | token = Lifetime.Token() 19 | lifetime = Lifetime(token) 20 | } 21 | 22 | describe("closure binding target") { 23 | var target: BindingTarget? 24 | var optionalTarget: BindingTarget? 25 | var value: Int? 26 | 27 | beforeEach { 28 | target = BindingTarget(lifetime: lifetime, action: { value = $0 }) 29 | optionalTarget = BindingTarget(lifetime: lifetime, action: { value = $0 }) 30 | value = nil 31 | } 32 | 33 | describe("non-optional target") { 34 | it("should pass through the lifetime") { 35 | expect(target!.lifetime).to(beIdenticalTo(lifetime)) 36 | } 37 | 38 | it("should trigger the supplied setter") { 39 | expect(value).to(beNil()) 40 | 41 | target!.action(1) 42 | expect(value) == 1 43 | } 44 | 45 | it("should accept bindings from properties") { 46 | expect(value).to(beNil()) 47 | 48 | let property = MutableProperty(1) 49 | target! <~ property 50 | expect(value) == 1 51 | 52 | property.value = 2 53 | expect(value) == 2 54 | } 55 | } 56 | 57 | describe("target of optional value") { 58 | it("should pass through the lifetime") { 59 | expect(optionalTarget!.lifetime).to(beIdenticalTo(lifetime)) 60 | } 61 | 62 | it("should trigger the supplied setter") { 63 | expect(value).to(beNil()) 64 | 65 | optionalTarget!.action(1) 66 | expect(value) == 1 67 | } 68 | 69 | it("should accept bindings from properties") { 70 | expect(value).to(beNil()) 71 | 72 | let property = MutableProperty(1) 73 | optionalTarget! <~ property 74 | expect(value) == 1 75 | 76 | property.value = 2 77 | expect(value) == 2 78 | } 79 | } 80 | 81 | describe("optional LHS binding with non-nil LHS at runtime") { 82 | it("should pass through the lifetime") { 83 | expect(target.bindingTarget.lifetime).to(beIdenticalTo(lifetime)) 84 | } 85 | 86 | it("should accept bindings from properties") { 87 | expect(value).to(beNil()) 88 | 89 | let property = MutableProperty(1) 90 | optionalTarget <~ property 91 | expect(value) == 1 92 | 93 | property.value = 2 94 | expect(value) == 2 95 | } 96 | } 97 | 98 | describe("optional LHS binding with nil LHS at runtime") { 99 | it("should pass through the empty lifetime") { 100 | let nilTarget: BindingTarget? = nil 101 | expect(nilTarget.bindingTarget.lifetime).to(beIdenticalTo(Lifetime.empty)) 102 | } 103 | } 104 | } 105 | 106 | describe("key path binding target") { 107 | var target: BindingTarget! 108 | var object: Object! 109 | 110 | beforeEach { 111 | object = Object() 112 | target = BindingTarget(lifetime: lifetime, object: object, keyPath: \.value) 113 | } 114 | 115 | it("should pass through the lifetime") { 116 | expect(target.lifetime).to(beIdenticalTo(lifetime)) 117 | } 118 | 119 | it("should trigger the supplied setter") { 120 | expect(object.value) == 0 121 | 122 | target.action(1) 123 | expect(object.value) == 1 124 | } 125 | 126 | it("should accept bindings from properties") { 127 | expect(object.value) == 0 128 | 129 | let property = MutableProperty(1) 130 | target <~ property 131 | expect(object.value) == 1 132 | 133 | property.value = 2 134 | expect(object.value) == 2 135 | } 136 | } 137 | 138 | it("should not deadlock on the same queue") { 139 | var value: Int? 140 | 141 | let target = BindingTarget(on: UIScheduler(), 142 | lifetime: lifetime, 143 | action: { value = $0 }) 144 | 145 | let property = MutableProperty(1) 146 | target <~ property 147 | expect(value) == 1 148 | } 149 | 150 | it("should not deadlock on the main thread even if the context was switched to a different queue") { 151 | var value: Int? 152 | 153 | let queue = DispatchQueue(label: #file) 154 | 155 | let target = BindingTarget(on: UIScheduler(), 156 | lifetime: lifetime, 157 | action: { value = $0 }) 158 | 159 | let property = MutableProperty(1) 160 | 161 | queue.sync { 162 | _ = target <~ property 163 | } 164 | 165 | expect(value).toEventually(equal(1)) 166 | } 167 | 168 | it("should not deadlock even if the value is originated from the same queue indirectly") { 169 | var value: Int? 170 | 171 | let key = DispatchSpecificKey() 172 | DispatchQueue.main.setSpecific(key: key, value: ()) 173 | 174 | let mainQueueCounter = Atomic(0) 175 | 176 | let setter: (Int) -> Void = { 177 | value = $0 178 | mainQueueCounter.modify { $0 += DispatchQueue.getSpecific(key: key) != nil ? 1 : 0 } 179 | } 180 | 181 | let target = BindingTarget(on: UIScheduler(), 182 | lifetime: lifetime, 183 | action: setter) 184 | 185 | let scheduler = QueueScheduler.makeForTesting() 186 | 187 | let property = MutableProperty(1) 188 | target <~ property.producer 189 | .start(on: scheduler) 190 | .observe(on: scheduler) 191 | 192 | expect(value).toEventually(equal(1)) 193 | expect(mainQueueCounter.value).toEventually(equal(1)) 194 | 195 | property.value = 2 196 | expect(value).toEventually(equal(2)) 197 | expect(mainQueueCounter.value).toEventually(equal(2)) 198 | } 199 | 200 | describe("observer binding operator") { 201 | it("should forward values to observer") { 202 | let targetPipe = Signal.pipe() 203 | let sourcePipe = Signal.pipe() 204 | let targetProperty = Property(initial: nil, then: targetPipe.output) 205 | targetPipe.input <~ sourcePipe.output 206 | expect(targetProperty.value).to(beNil()) 207 | sourcePipe.input.send(value: 1) 208 | expect(targetProperty.value).to(equal(1)) 209 | } 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE=ReactiveSwift.xcworkspace 4 | BUILD_DIRECTORY="build" 5 | CONFIGURATION=Release 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 $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 \ 38 | ${XCODE_ARGS} | xcpretty 39 | result=$? 40 | 41 | if [ "$result" -ne 0 ]; then 42 | exit $result 43 | fi 44 | 45 | # Compile code in playgrounds 46 | if [[ $XCODE_SDK = "macosx" && -n "$XCODE_PLAYGROUND_TARGET" ]]; then 47 | echo "SDK is $XCODE_SDK, validating playground..." 48 | . script/validate-playground.sh 49 | fi 50 | -------------------------------------------------------------------------------- /script/carthage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Adopted from https://github.com/Carthage/Carthage/issues/3019#issuecomment-665136323 4 | 5 | # carthage.sh 6 | # Usage example: ./carthage.sh build --platform iOS 7 | 8 | set -euo pipefail 9 | 10 | xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) 11 | trap 'rm -f "$xcconfig"' INT TERM HUP EXIT 12 | 13 | # For Xcode 12+ make sure EXCLUDED_ARCHS is set to arm architectures otherwise 14 | # the build will fail on lipo due to duplicate architectures. 15 | XCODE_VERSION_MAJ=1300 # should mirror XCODE_VERSION_MAJOR - update accordingly 16 | 17 | echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_${XCODE_VERSION_MAJ} = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig 18 | echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig 19 | 20 | export XCODE_XCCONFIG_FILE="$xcconfig" 21 | carthage "$@" 22 | -------------------------------------------------------------------------------- /script/feed.xml.template: -------------------------------------------------------------------------------- 1 | 2 | FRAMEWORK_VERSION 3 | http://reactivecocoa.io/reactiveswift/docs/latest/docsets/ReactiveSwift.tgz 4 | 5 | -------------------------------------------------------------------------------- /script/gen-docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "$TRAVIS_TAG" ]]; then 4 | echo "Not a tag build. Abort." 5 | exit 0 6 | fi 7 | 8 | if [[ -z "$RAC_CIBOT_TOKEN" ]]; then 9 | echo "CI bot token is undefined. Abort." 10 | exit 0 11 | fi 12 | 13 | if [[ -z "$RAC_CIBOT_USERNAME" ]]; then 14 | echo "CI bot username is undefined. Abort." 15 | exit 0 16 | fi 17 | 18 | echo "Documentation builder. The tag is \`${TRAVIS_TAG}\`." 19 | 20 | rvm use 2.3 21 | gem install jazzy 22 | jazzy 23 | 24 | if [[ $? -ne 0 ]]; then 25 | echo "Jazzy has failed. Aborting the push." 26 | exit 1 27 | fi 28 | 29 | brew install hub 30 | export GITHUB_TOKEN=${RAC_CIBOT_TOKEN} 31 | 32 | # Clone the master branch of the website. 33 | git clone -b master --single-branch https://${RAC_CIBOT_TOKEN}@github.com/ReactiveCocoa/ReactiveCocoa.github.io.git RACSite 34 | cd ./RACSite 35 | 36 | # Fork the repo without fetching it. 37 | hub fork --no-remote 38 | git remote add cibot https://${RAC_CIBOT_TOKEN}@github.com/${RAC_CIBOT_USERNAME}/ReactiveCocoa.github.io.git 39 | git checkout master 40 | 41 | # Create a new branch for the release. 42 | git checkout -b ras-${TRAVIS_TAG} 43 | 44 | DOC_PATH=./reactiveswift/docs/${TRAVIS_TAG} 45 | 46 | # Copy the generated docs. 47 | mkdir -p ${DOC_PATH} 48 | cp -r ../docs/. ${DOC_PATH} 49 | 50 | # Copy the icons, and archive the docset. 51 | rm ${DOC_PATH}/docsets/ReactiveSwift.tgz 52 | 53 | DOCSET_IMG_PATH=${DOC_PATH}/docsets/ReactiveSwift.docset/Contents/Resources/Documents/Logo/PNG 54 | mkdir -p ${DOCSET_IMG_PATH} 55 | cp ../Logo/PNG/logo-Swift.png ${DOCSET_IMG_PATH}/logo-Swift.png 56 | cp ../Logo/PNG/JoinSlack.png ${DOCSET_IMG_PATH}/JoinSlack.png 57 | cp ../Logo/PNG/Docs.png ${DOCSET_IMG_PATH}/Docs.png 58 | cp ../Logo/Icons/docset-icon.png ${DOC_PATH}/docsets/ReactiveSwift.docset/icon.png 59 | cp ../Logo/Icons/docset-icon@2x.png ${DOC_PATH}/docsets/ReactiveSwift.docset/icon@2x.png 60 | tar --exclude='.DS_Store' -cvzf ${DOC_PATH}/docsets/ReactiveSwift.tgz -C ${DOC_PATH}/docsets/ . 61 | rm -rf ${DOC_PATH}/docsets/ReactiveSwift.docset/ 62 | 63 | # Copy image assets used by README.md. 64 | mkdir -p ${DOC_PATH}/Logo/PNG/ 65 | cp ../Logo/PNG/logo-Swift.png ${DOC_PATH}/Logo/PNG/logo-Swift.png 66 | cp ../Logo/PNG/JoinSlack.png ${DOC_PATH}/Logo/PNG/JoinSlack.png 67 | cp ../Logo/PNG/Docs.png ${DOC_PATH}/Logo/PNG/Docs.png 68 | 69 | # Fix all readme links. 70 | perl -0777 -i -pe 's/"Documentation\/([a-zA-Z0-9-_\.]+)\.md/"\L$1\.html/g' ${DOC_PATH}/*.html 71 | perl -0777 -i -pe 's/"(a-zA-Z0-9-_\.]+)\.md/"\L$1\.html/g' ${DOC_PATH}/*.html 72 | 73 | git add ${DOC_PATH} 74 | 75 | # Ensure Jekyll is not running in `docs`. 76 | touch ./reactiveswift/docs/.nojekyll 77 | git add ./reactiveswift/docs/.nojekyll 78 | 79 | # Update the `latest` symlink. 80 | ln -sfn ${TRAVIS_TAG}/ reactiveswift/docs/latest 81 | git add ./reactiveswift/docs/latest 82 | 83 | # Update the docset feed. 84 | rm ./reactiveswift/docs/ReactiveSwift.xml 85 | /bin/cp -f ../script/feed.xml.template ./reactiveswift/docs/ReactiveSwift.xml 86 | sed -i -- "s/FRAMEWORK_VERSION/${TRAVIS_TAG}/g" ./reactiveswift/docs/ReactiveSwift.xml 87 | git add ./reactiveswift/docs/ReactiveSwift.xml 88 | 89 | # Commit and push to the fork. 90 | git commit -m "Documentation: ReactiveSwift ${TRAVIS_TAG}" 91 | git push -u cibot ras-${TRAVIS_TAG} 92 | 93 | # Open a pull request in the main repo. 94 | hub pull-request -m "Documentation: ReactiveSwift ${TRAVIS_TAG}" -h "${RAC_CIBOT_USERNAME}:ras-${TRAVIS_TAG}" 95 | 96 | cd .. 97 | rm -rf ./RACSite 98 | -------------------------------------------------------------------------------- /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' *.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 | -------------------------------------------------------------------------------- /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 "$XCODE_PLAYGROUND_TARGET" ]; then 14 | echo "\$XCODE_PLAYGROUND_TARGET is not set." 15 | exit 1 16 | fi 17 | 18 | PAGES_PATH=${BUILD_DIRECTORY}/Build/Products/${CONFIGURATION}/all-playground-pages.swift 19 | 20 | validate () { 21 | echo "Validating \$1..." 22 | cat $1/Sources/*.swift $1/Pages/**/*.swift > ${PAGES_PATH} 23 | swift -v -target ${XCODE_PLAYGROUND_TARGET} -D NOT_IN_PLAYGROUND -F ${BUILD_DIRECTORY}/Build/Products/${CONFIGURATION} ${PAGES_PATH} > /dev/null 24 | result=$? 25 | } 26 | 27 | validate ReactiveSwift.playground 28 | validate ReactiveSwift-UIExamples.playground 29 | 30 | # Cleanup 31 | rm -Rf $BUILD_DIRECTORY 32 | 33 | exit $result 34 | --------------------------------------------------------------------------------