├── .gitmodules ├── Assets └── logo.png ├── Playground.playground ├── Pages │ ├── Working with UI.xcplaygroundpage │ │ ├── timeline.xctimeline │ │ └── Contents.swift │ ├── Exploration.xcplaygroundpage │ │ └── Contents.swift │ ├── Creating Signals.xcplaygroundpage │ │ └── Contents.swift │ ├── Observing Signals.xcplaygroundpage │ │ └── Contents.swift │ └── Transforming Signals.xcplaygroundpage │ │ └── Contents.swift └── contents.xcplayground ├── Tests ├── LinuxMain.swift └── ReactiveKitTests │ ├── Scheduler.swift │ ├── PublishedTests.swift │ ├── Stress.swift │ ├── CombineTests.swift │ └── PropertyTests.swift ├── ReactiveKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── ReactiveKit.xcscmblueprint └── xcshareddata │ ├── xcbaselines │ └── ECBCCDD91BEB6B9B00723476.xcbaseline │ │ ├── FB4F14DE-1778-47BF-9AF1-A000D430FBBA.plist │ │ ├── 15721443-CEB9-478C-919D-711F1A7CCA56.plist │ │ └── Info.plist │ └── xcschemes │ ├── ReactiveKit-tvOS.xcscheme │ ├── ReactiveKit-macOS.xcscheme │ ├── ReactiveKit-watchOS.xcscheme │ └── ReactiveKit-iOS.xcscheme ├── ReactiveKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── ReactiveKit.xcscmblueprint ├── Package.swift ├── .gitignore ├── Supporting Files ├── TestsInfo.plist ├── Info.plist └── ReactiveKit.h ├── ReactiveKit.podspec ├── .github └── workflows │ └── swift.yml ├── LICENSE ├── .travis.yml └── Sources ├── Atomic.swift ├── Cancellable.swift ├── Subscription.swift ├── Subscribers ├── Completion.swift ├── Demand.swift ├── Sink.swift └── Accumulator.swift ├── Lock.swift ├── Subscriber.swift ├── Published.swift ├── Publishers ├── Deferred.swift └── Empty.swift ├── Scheduler.swift ├── Deallocatable.swift ├── ObservableObject.swift ├── SignalProtocol+Sequence.swift ├── SignalProtocol+Threading.swift ├── SignalProtocol+Optional.swift ├── SignalProtocol+Event.swift ├── Signal.Event.swift ├── Reactive.swift ├── Combine.swift ├── SignalProtocol+Async.swift ├── Property.swift ├── ExecutionContext.swift ├── SignalProtocol+Result.swift ├── Observer.swift ├── Connectable.swift ├── SignalProtocol.swift ├── LoadingProperty.swift ├── SignalProtocol+Transforming.swift ├── Subjects.swift ├── Bindable.swift ├── SignalProtocol+ErrorHandling.swift └── SignalProtocol+Utilities.swift /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeclarativeHub/ReactiveKit/HEAD/Assets/logo.png -------------------------------------------------------------------------------- /Playground.playground/Pages/Working with UI.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ReactiveKitTests 3 | 4 | XCTMain([ 5 | testCase(SignalTests.allTests), 6 | testCase(PropertyTests.allTests), 7 | testCase(SubjectTests.allTests), 8 | ]) 9 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReactiveKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ReactiveKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Playground.playground/Pages/Exploration.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import ReactiveKit 4 | import PlaygroundSupport 5 | 6 | //: Explore ReactiveKit here 7 | 8 | enum MyError: Error { 9 | case unknown 10 | } 11 | 12 | let a = Signal(sequence: 0...4, interval: 0.5) 13 | let b = SafeSignal(sequence: 0...2, interval: 2) 14 | 15 | b.concat(with: b) 16 | 17 | -------------------------------------------------------------------------------- /Playground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ReactiveKit", 6 | platforms: [ 7 | .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) 8 | ], 9 | products: [ 10 | .library(name: "ReactiveKit", targets: ["ReactiveKit"]) 11 | ], 12 | targets: [ 13 | .target(name: "ReactiveKit", path: "Sources"), 14 | .testTarget(name: "ReactiveKitTests", dependencies: ["ReactiveKit"]) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcbaselines/ECBCCDD91BEB6B9B00723476.xcbaseline/FB4F14DE-1778-47BF-9AF1-A000D430FBBA.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | SignalTests 8 | 9 | testPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.078778 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcbaselines/ECBCCDD91BEB6B9B00723476.xcbaseline/15721443-CEB9-478C-919D-711F1A7CCA56.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | SignalTests 8 | 9 | testPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.077 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | maxPercentRelativeStandardDeviation 18 | 20 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | .DS_Store 20 | 21 | # CocoaPods 22 | # 23 | # We recommend against adding the Pods directory to your .gitignore. However 24 | # you should judge for yourself, the pros and cons are mentioned at: 25 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 26 | # 27 | # Pods/ 28 | 29 | # Carthage 30 | # 31 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 32 | # Carthage/Checkouts 33 | 34 | Carthage/Build 35 | 36 | # Swift Package Manager 37 | 38 | .build 39 | .swiftpm -------------------------------------------------------------------------------- /Supporting Files/TestsInfo.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 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ReactiveKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveKit" 3 | s.version = "3.19.1" 4 | s.summary = "A Swift Reactive Programming Framework" 5 | s.description = "ReactiveKit is a Swift framework for reactive and functional reactive programming." 6 | s.homepage = "https://github.com/DeclarativeHub/ReactiveKit" 7 | s.license = 'MIT' 8 | s.author = { "Srdan Rasic" => "srdan.rasic@gmail.com" } 9 | s.source = { :git => "https://github.com/DeclarativeHub/ReactiveKit.git", :tag => "v3.19.1" } 10 | 11 | s.ios.deployment_target = '8.0' 12 | s.osx.deployment_target = '10.11' 13 | s.watchos.deployment_target = '2.0' 14 | s.tvos.deployment_target = '9.0' 15 | 16 | s.source_files = 'Sources/**/*.swift', 'ReactiveKit/*.{h,m}' 17 | s.requires_arc = true 18 | s.swift_version = '5.0' 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | linux: 9 | name: Test on Linux 10 | runs-on: ubuntu-latest 11 | container: 12 | image: swift:latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Show verion 16 | run: swift --version 17 | - name: Build 18 | run: swift build -v 19 | - name: Run tests 20 | run: swift test -v 21 | 22 | macOS: 23 | name: Test on macOS 24 | runs-on: macOS-latest 25 | steps: 26 | - uses: actions/checkout@v1 27 | - name: Build 28 | run: swift build -v 29 | - name: Run tests 30 | run: swift test -v 31 | 32 | android: 33 | name: Test on Android 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Test Swift Package on Android 38 | uses: skiptools/swift-android-action@v2 39 | -------------------------------------------------------------------------------- /Supporting Files/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 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ReactiveKit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Tests/ReactiveKitTests/Scheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scheduler.swift 3 | // ReactiveKit-Tests 4 | // 5 | // Created by Srdan Rasic on 01/01/2020. 6 | // Copyright © 2020 DeclarativeHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveKit 11 | 12 | /// A scheduler that buffers actions and enables their manual execution through `run` methods. 13 | class Scheduler: ReactiveKit.Scheduler { 14 | 15 | private var availableRuns = 0 16 | private var scheduledBlocks: [() -> Void] = [] 17 | private(set) var numberOfRuns = 0 18 | 19 | func schedule(_ action: @escaping () -> Void) { 20 | scheduledBlocks.append(action) 21 | tryRun() 22 | } 23 | 24 | func runOne() { 25 | guard availableRuns < Int.max else { return } 26 | availableRuns += 1 27 | tryRun() 28 | } 29 | 30 | func runRemaining() { 31 | availableRuns = Int.max 32 | tryRun() 33 | } 34 | 35 | private func tryRun() { 36 | while availableRuns > 0 && scheduledBlocks.count > 0 { 37 | let block = scheduledBlocks.removeFirst() 38 | block() 39 | numberOfRuns += 1 40 | availableRuns -= 1 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10.2 3 | 4 | jobs: 5 | include: 6 | - stage: "Xcode" 7 | name: "Run tests on iOS" 8 | script: xcrun xcodebuild test -destination "platform=iOS Simulator,OS=12.2,name=iPhone X" -workspace "ReactiveKit.xcworkspace" -scheme "ReactiveKit-iOS" 9 | after_success: 'bash <(curl -s https://codecov.io/bash)' 10 | - 11 | name: "Build for macOS" 12 | script: xcrun xcodebuild build -destination "platform=macOS" -workspace "ReactiveKit.xcworkspace" -scheme "ReactiveKit-macOS" 13 | - 14 | name: "Build for tvOS" 15 | script: xcrun xcodebuild build -destination "platform=tvOS Simulator,OS=12.2,name=Apple TV 4K" -workspace "ReactiveKit.xcworkspace" -scheme "ReactiveKit-tvOS" 16 | - 17 | name: "Build for watchOS" 18 | script: xcrun xcodebuild build -destination "platform=watchOS Simulator,OS=5.2,name=Apple Watch Series 4 - 44mm" -workspace "ReactiveKit.xcworkspace" -scheme "ReactiveKit-watchOS" 19 | 20 | - stage: "Swift Package Manager" 21 | name: "Run Tests" 22 | script: swift test 23 | 24 | - stage: "CocoaPods" 25 | name: "Lint Podspec" 26 | script: pod lib lint 27 | -------------------------------------------------------------------------------- /Tests/ReactiveKitTests/PublishedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PublishedTests.swift 3 | // ReactiveKit-iOS 4 | // 5 | // Created by Ibrahim Koteish on 15/12/2019. 6 | // Copyright © 2019 DeclarativeHub. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import ReactiveKit 11 | 12 | #if compiler(>=5.1) 13 | 14 | class PublishedTests: XCTestCase { 15 | 16 | class User: ReactiveKit.ObservableObject { 17 | @ReactiveKit.Published var id: Int 18 | init(id: Int) { self.id = id } 19 | } 20 | 21 | func testPublished() { 22 | 23 | let user = User(id: 0) 24 | 25 | let objectSubscriber = Subscribers.Accumulator() 26 | let propertySubscriber = Subscribers.Accumulator() 27 | 28 | user.objectWillChange.subscribe(objectSubscriber) 29 | user.$id.subscribe(propertySubscriber) 30 | 31 | XCTAssertEqual(user.id, 0) 32 | 33 | user.id = 1 34 | user.id = 2 35 | user.id = 3 36 | 37 | XCTAssertEqual(propertySubscriber.values, [0, 1, 2, 3]) 38 | XCTAssertFalse(propertySubscriber.isFinished) 39 | 40 | XCTAssertEqual(objectSubscriber.values.count, 3) 41 | XCTAssertFalse(objectSubscriber.isFinished) 42 | } 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/Atomic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Atomic.swift 3 | // ReactiveKit 4 | // 5 | // Created by Srdan Rasic on 06/11/2019. 6 | // Copyright © 2019 DeclarativeHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class Atomic { 12 | 13 | private var _value: T 14 | private let lock: NSLocking 15 | 16 | init(_ value: T, lock: NSLocking = NSRecursiveLock()) { 17 | self._value = value 18 | self.lock = lock 19 | } 20 | 21 | var value: T { 22 | get { 23 | lock.lock() 24 | let value = _value 25 | lock.unlock() 26 | return value 27 | } 28 | set { 29 | lock.lock() 30 | _value = newValue 31 | lock.unlock() 32 | } 33 | } 34 | 35 | func mutate(_ block: (inout T) -> Void) { 36 | lock.lock() 37 | block(&_value) 38 | lock.unlock() 39 | } 40 | 41 | func mutateAndRead(_ block: (T) -> T) -> T { 42 | lock.lock() 43 | let newValue = block(_value) 44 | _value = newValue 45 | lock.unlock() 46 | return newValue 47 | } 48 | 49 | func readAndMutate(_ block: (T) -> T) -> T { 50 | lock.lock() 51 | let oldValue = _value 52 | _value = block(_value) 53 | lock.unlock() 54 | return oldValue 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Cancellable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol Cancellable { 28 | func cancel() 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Subscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol Subscription: Cancellable { 28 | func request(_ demand: Subscribers.Demand) 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Subscribers/Completion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public enum Subscribers { 28 | 29 | public enum Completion where Failure: Error { 30 | case finished 31 | case failure(Failure) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Supporting Files/ReactiveKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2015 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | #import 26 | 27 | //! Project version number for ReactiveKit. 28 | FOUNDATION_EXPORT double ReactiveKitVersionNumber; 29 | 30 | //! Project version string for ReactiveKit. 31 | FOUNDATION_EXPORT const unsigned char ReactiveKitVersionString[]; 32 | -------------------------------------------------------------------------------- /Sources/Lock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension NSLock { 28 | 29 | public convenience init(name: String) { 30 | self.init() 31 | self.name = name 32 | } 33 | } 34 | 35 | extension NSRecursiveLock { 36 | 37 | public convenience init(name: String) { 38 | self.init() 39 | self.name = name 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Subscriber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol Subscriber { 28 | 29 | associatedtype Input 30 | associatedtype Failure: Error 31 | 32 | func receive(subscription: Subscription) 33 | 34 | func receive(_ input: Self.Input) -> Subscribers.Demand 35 | 36 | func receive(completion: Subscribers.Completion) 37 | } 38 | 39 | extension Subscriber where Self.Input == Void { 40 | 41 | public func receive() -> Subscribers.Demand { 42 | return receive(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Playground.playground/Pages/Creating Signals.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import ReactiveKit 5 | import PlaygroundSupport 6 | 7 | PlaygroundPage.current.needsIndefiniteExecution = true 8 | 9 | //: # Creating Signals 10 | //: Uncomment the `observe { ... }` line to explore the behaviour! 11 | 12 | SafeSignal(just: "Jim") 13 | //.observe { print($0) } 14 | 15 | SafeSignal(just: "Jim after 1 second", after: 1) 16 | //.observe { print($0) } 17 | 18 | SafeSignal(sequence: [1, 2, 3]) 19 | //.observe { print($0) } 20 | 21 | SafeSignal(sequence: [1, 2, 3], interval: 1) 22 | //.observe { print($0) } 23 | 24 | SafeSignal(sequence: 1..., interval: 1) 25 | //.observe { print($0) } 26 | 27 | SafeSignal(performing: { 28 | (0...1000).reduce(0, +) 29 | }) 30 | //.observe { print($0) } 31 | 32 | Signal(evaluating: { 33 | if let file = try? String(contentsOf: URL(fileURLWithPath: "list.txt")) { 34 | return .success(file) 35 | } else { 36 | return .failure(NSError(domain: "No such file", code: 0, userInfo: nil)) 37 | } 38 | }) 39 | //.observe { print($0) } 40 | 41 | Signal(catching: { 42 | try String(contentsOf: URL(string: "https://pokeapi.co/api/v2/pokemon/ditto/")!, encoding: .utf8) 43 | }) 44 | //.observe { print($0) } 45 | 46 | Signal { observer in 47 | observer.receive(1) 48 | observer.receive(2) 49 | observer.receive(completion: .finished) 50 | return BlockDisposable { 51 | print("disposed") 52 | } 53 | } 54 | //.observe { print($0) } 55 | 56 | var didTapReload: () -> Void = {} 57 | let reloadTaps = Signal(takingOver: &didTapReload) 58 | 59 | reloadTaps 60 | //.observeNext { print("reload") } 61 | 62 | didTapReload() 63 | didTapReload() 64 | 65 | //: [Next](@next) 66 | -------------------------------------------------------------------------------- /ReactiveKit.xcworkspace/xcshareddata/ReactiveKit.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "C1F6A63EF115019142AF6E3E8A173E32E3445DB3", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "D78924967B5ECB0A1D20E197FAB93ACBFA47F0D1" : 9223372036854775807, 8 | "C1F6A63EF115019142AF6E3E8A173E32E3445DB3" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "380029EF-6AB5-47E0-9552-F3F289E07C83", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "D78924967B5ECB0A1D20E197FAB93ACBFA47F0D1" : "..\/..", 13 | "C1F6A63EF115019142AF6E3E8A173E32E3445DB3" : "ReactiveKit\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "ReactiveKit", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "ReactiveKit.xcworkspace", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:ReactiveKit\/ReactiveKit.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "C1F6A63EF115019142AF6E3E8A173E32E3445DB3" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/SwiftBond\/Bond.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "D78924967B5ECB0A1D20E197FAB93ACBFA47F0D1" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Sources/Published.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Published.swift 3 | // ReactiveKit 4 | // 5 | // Created by Srdan Rasic on 07/12/2019. 6 | // Copyright © 2019 DeclarativeHub. All rights reserved. 7 | // 8 | 9 | #if compiler(>=5.1) 10 | 11 | @propertyWrapper 12 | public struct Published { 13 | 14 | private let publisher: Publisher 15 | private let willChangeSubject = PassthroughSubject() 16 | 17 | public init(wrappedValue: Value) { 18 | publisher = Publisher(wrappedValue) 19 | } 20 | 21 | /// A publisher for properties used with the `@Published` attribute. 22 | public struct Publisher: SignalProtocol { 23 | public typealias Element = Value 24 | public typealias Error = Never 25 | 26 | fileprivate let property: Property 27 | 28 | public func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 29 | self.property.observe(with: observer) 30 | } 31 | 32 | fileprivate init(_ output: Element) { 33 | self.property = Property(output) 34 | } 35 | } 36 | 37 | public var wrappedValue: Value { 38 | get { self.publisher.property.value } 39 | nonmutating set { 40 | self.willChangeSubject.send() 41 | self.publisher.property.value = newValue 42 | } 43 | } 44 | 45 | public var projectedValue: Publisher { 46 | get { publisher } 47 | } 48 | } 49 | 50 | protocol _MutablePropertyWrapper { 51 | var willChange: Signal { mutating get } 52 | } 53 | 54 | extension Published: _MutablePropertyWrapper { 55 | 56 | var willChange: Signal { 57 | mutating get { 58 | return willChangeSubject.toSignal() 59 | } 60 | } 61 | } 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/Publishers/Deferred.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deferred.swift 3 | // GlovoCourier 4 | // 5 | // Created by Ibrahim Koteish on 01/12/2019. 6 | // Copyright © 2019 Glovo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A signal that awaits subscription before running the supplied closure 12 | /// to create a signal for the new subscriber. 13 | public struct Deferred: SignalProtocol { 14 | 15 | /// The kind of values published by this signal. 16 | public typealias Element = DeferredSignal.Element 17 | 18 | /// The kind of errors this signal might publish. 19 | /// 20 | /// Use `Never` if this `signal` does not publish errors. 21 | public typealias Error = DeferredSignal.Error 22 | 23 | /// The closure to execute when it receives a subscription. 24 | /// 25 | /// The signal returned by this closure immediately 26 | /// receives the incoming subscription. 27 | public let signalFactory: () -> DeferredSignal 28 | 29 | /// Creates a deferred signal. 30 | /// 31 | /// - Parameter signalFactory: The closure to execute 32 | /// when calling `observe(with:)`. 33 | public init(signalFactory: @escaping () -> DeferredSignal) { 34 | self.signalFactory = signalFactory 35 | } 36 | 37 | /// This function is called to attach the specified `Observer` 38 | /// to this `Signal` by `observe(with:)` 39 | /// 40 | /// - Parameters: 41 | /// - observer: The observer to attach to this `Signal`. 42 | /// once attached it can begin to receive values. 43 | public func observe( 44 | with observer: @escaping (Signal.Event) -> Void) 45 | -> Disposable 46 | { 47 | let deferredSignal = signalFactory() 48 | return deferredSignal.observe(with: observer) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Subscribers/Demand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension Subscribers { 28 | 29 | public struct Demand: Equatable, Hashable { 30 | 31 | public let value: Int 32 | 33 | private init(value: Int) { 34 | self.value = value 35 | } 36 | 37 | public static let unlimited: Subscribers.Demand = .init(value: Int.max) 38 | 39 | @available(*, unavailable, message: "Not supported yet.") 40 | public static func max(_ value: Int) -> Subscribers.Demand { 41 | return .init(value: value) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/ReactiveKitTests/Stress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Common.swift 3 | // ReactiveKit 4 | // 5 | // Created by Srdan Rasic on 14/04/16. 6 | // Copyright © 2016 Srdan Rasic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import ReactiveKit 11 | 12 | extension SignalProtocol { 13 | 14 | func stress( 15 | with sendElements: [(Int) -> Void], 16 | queuesCount: Int = 3, 17 | eventsCount: Int = 3000, 18 | timeout: Double = 2, 19 | expectation: XCTestExpectation 20 | ) -> Disposable { 21 | 22 | let dispatchQueues = Array((0..( 45 | with subjects: [S], 46 | queuesCount: Int = 3, 47 | eventsCount: Int = 3000, 48 | timeout: Double = 2, 49 | expectation: XCTestExpectation 50 | ) -> Disposable where S.Element == Int { 51 | return stress( 52 | with: subjects.map { subject in { event in subject.send(event) } }, 53 | queuesCount: queuesCount, 54 | eventsCount: eventsCount, 55 | timeout: timeout, 56 | expectation: expectation 57 | ) 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /Sources/Scheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import Dispatch 27 | 28 | /// A protocol that defines when and how to execute a closure. 29 | public protocol Scheduler { 30 | 31 | /// Performs the action at the next possible opportunity. 32 | func schedule(_ action: @escaping () -> Void) 33 | } 34 | 35 | extension ExecutionContext: Scheduler { 36 | 37 | @inlinable 38 | public func schedule(_ action: @escaping () -> Void) { 39 | context(action) 40 | } 41 | } 42 | 43 | extension DispatchQueue: Scheduler { 44 | 45 | @inlinable 46 | public func schedule(_ action: @escaping () -> Void) { 47 | async(execute: action) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/ReactiveKitTests/CombineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineTests.swift 3 | // ReactiveKit-Tests 4 | // 5 | // Created by Srdan Rasic on 18/01/2020. 6 | // Copyright © 2020 DeclarativeHub. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | 11 | import XCTest 12 | import ReactiveKit 13 | import Combine 14 | 15 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | class CombineTests: XCTestCase { 17 | 18 | func testPublisherToSignal() { 19 | let publisher = Combine.PassthroughSubject() 20 | let subscriber = ReactiveKit.Subscribers.Accumulator() 21 | publisher.toSignal().subscribe(subscriber) 22 | publisher.send(0) 23 | publisher.send(1) 24 | publisher.send(2) 25 | publisher.send(completion: .failure(.error)) 26 | publisher.send(3) 27 | XCTAssertEqual(subscriber.values, [0, 1, 2]) 28 | XCTAssertFalse(subscriber.isFinished) 29 | XCTAssertTrue(subscriber.isFailure) 30 | } 31 | 32 | func testSignalToPublisher() { 33 | let publisher = ReactiveKit.PassthroughSubject() 34 | var receivedValues: [Int] = [] 35 | var receivedCompletion: Combine.Subscribers.Completion? = nil 36 | 37 | let cancellable = publisher.toPublisher().sink( 38 | receiveCompletion: { (completion) in 39 | receivedCompletion = completion 40 | }, receiveValue: { value in 41 | receivedValues.append(value) 42 | } 43 | ) 44 | 45 | publisher.send(0) 46 | publisher.send(1) 47 | publisher.send(2) 48 | publisher.send(completion: .failure(.error)) 49 | publisher.send(3) 50 | 51 | XCTAssertEqual(receivedValues, [0, 1, 2]) 52 | XCTAssertEqual(receivedCompletion, .failure(.error)) 53 | 54 | cancellable.cancel() 55 | } 56 | } 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/Deallocatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deallocatable.swift 3 | // ReactiveKit 4 | // 5 | // Created by Srdan Rasic on 17/03/2017. 6 | // Copyright © 2017 Srdan Rasic. All rights reserved. 7 | // 8 | 9 | /// A type that notifies about its own deallocation. 10 | /// 11 | /// `Deallocatable` can be used as a binding target. For example, 12 | /// instead of observing a signal, one can bind it to a `Deallocatable`. 13 | /// 14 | /// class View: Deallocatable { ... } 15 | /// 16 | /// let view: View = ... 17 | /// let signal: SafeSignal = ... 18 | /// 19 | /// signal.bind(to: view) { view, number in 20 | /// view.display(number) 21 | /// } 22 | public protocol Deallocatable: class { 23 | 24 | /// A signal that fires `completed` event when the receiver is deallocated. 25 | var deallocated: SafeSignal { get } 26 | } 27 | 28 | /// A type that provides a dispose bag. 29 | /// `DisposeBagProvider` conforms to `Deallocatable` out of the box. 30 | public protocol DisposeBagProvider: Deallocatable { 31 | 32 | /// A `DisposeBag` that can be used to dispose observations and bindings. 33 | var bag: DisposeBag { get } 34 | } 35 | 36 | extension DisposeBagProvider { 37 | 38 | /// A signal that fires `completed` event when the receiver is deallocated. 39 | public var deallocated: SafeSignal { 40 | return bag.deallocated 41 | } 42 | } 43 | 44 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 45 | 46 | import ObjectiveC.runtime 47 | 48 | extension NSObject: DisposeBagProvider { 49 | 50 | private struct AssociatedKeys { 51 | static var DisposeBagKey = "DisposeBagKey" 52 | } 53 | 54 | /// A `DisposeBag` that can be used to dispose observations and bindings. 55 | public var bag: DisposeBag { 56 | if let disposeBag = objc_getAssociatedObject(self, &NSObject.AssociatedKeys.DisposeBagKey) { 57 | return disposeBag as! DisposeBag 58 | } else { 59 | let disposeBag = DisposeBag() 60 | objc_setAssociatedObject(self, &NSObject.AssociatedKeys.DisposeBagKey, disposeBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 61 | return disposeBag 62 | } 63 | } 64 | } 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcbaselines/ECBCCDD91BEB6B9B00723476.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 15721443-CEB9-478C-919D-711F1A7CCA56 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 2700 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro12,1 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone9,2 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | FB4F14DE-1778-47BF-9AF1-A000D430FBBA 39 | 40 | localComputer 41 | 42 | busSpeedInMHz 43 | 100 44 | cpuCount 45 | 1 46 | cpuKind 47 | Intel Core i5 48 | cpuSpeedInMHz 49 | 2700 50 | logicalCPUCoresPerPackage 51 | 4 52 | modelCode 53 | MacBookPro12,1 54 | physicalCPUCoresPerPackage 55 | 2 56 | platformIdentifier 57 | com.apple.platform.macosx 58 | 59 | targetArchitecture 60 | x86_64 61 | targetDevice 62 | 63 | modelCode 64 | iPhone9,1 65 | platformIdentifier 66 | com.apple.platform.iphonesimulator 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Playground.playground/Pages/Working with UI.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import ReactiveKit 5 | import PlaygroundSupport 6 | import UIKit 7 | 8 | PlaygroundPage.current.needsIndefiniteExecution = true 9 | 10 | //: # Working with UI 11 | 12 | // Let's play with Pokemons again! 13 | 14 | struct Pokemon: Codable { 15 | let name: String 16 | let height: Int 17 | let weight: Int 18 | } 19 | 20 | // We will make a Pokemon profile view and fill it with a Pokemon details 21 | 22 | class PokeProfile: UIView { 23 | let nameLabel = UILabel() 24 | let heightLabel = UILabel() 25 | let weightLabel = UILabel() 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | let stackView = UIStackView(arrangedSubviews: [nameLabel, heightLabel, weightLabel]) 30 | stackView.distribution = .fillEqually 31 | stackView.axis = .vertical 32 | stackView.frame = frame 33 | addSubview(stackView) 34 | backgroundColor = .white 35 | } 36 | required init?(coder aDecoder: NSCoder) { fatalError() } 37 | } 38 | 39 | // Open Assistent Editor to see the view! 40 | let profileView = PokeProfile(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) 41 | PlaygroundPage.current.liveView = profileView 42 | 43 | // Load the Pokemon from PokéAPI and bind it to the view. 44 | // First we fetch the JSON response as Data 45 | Signal { try Data(contentsOf: URL(string: "https://pokeapi.co/api/v2/pokemon/chandelure")!) } 46 | // Then decode the Pokemon type from the data 47 | .map { try JSONDecoder().decode(Pokemon.self, from: $0) } 48 | // Make sure we do the fetching and parsing on a non-main thread (queue) 49 | .executeOn(.global(qos: .utility)) 50 | // We can only bind non-failable signals, so handle the potential error somehow 51 | .suppressError(logging: true) 52 | // Finally, bind the data to the view, ensuring the main thread 53 | .bind(to: profileView, context: .main) { view, pokemon in 54 | view.nameLabel.text = "Name: \(pokemon.name)" 55 | view.heightLabel.text = "Height: \(pokemon.height)" 56 | view.weightLabel.text = "Weight: \(pokemon.weight)" 57 | } 58 | 59 | // Make sure to check out [Bond](https://github.com/DeclarativeHub/Bond) framework that 60 | // provides many extensions that simplify usage of ReactiveKit in UI based apps. 61 | 62 | //: [Next](@next) 63 | -------------------------------------------------------------------------------- /Sources/ObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | #if compiler(>=5.1) 26 | 27 | import Foundation 28 | 29 | /// A type of object with a publisher that emits before the object has changed. 30 | /// 31 | /// By default an `ObservableObject` will synthesize an `objectWillChange` 32 | /// publisher that emits before any of its `@Published` properties changes: 33 | public protocol ObservableObject: AnyObject { 34 | 35 | /// The type of signal that emits before the object has changed. 36 | associatedtype ObjectWillChangeSignal: SignalProtocol = Signal where Self.ObjectWillChangeSignal.Error == Never 37 | 38 | /// A signal that emits before the object has changed. 39 | var objectWillChange: Self.ObjectWillChangeSignal { get } 40 | } 41 | 42 | extension ObservableObject where Self.ObjectWillChangeSignal == Signal { 43 | 44 | /// A publisher that emits before the object has changed. 45 | public var objectWillChange: Signal { 46 | var signals: [Signal] = [] 47 | let mirror = Mirror(reflecting: self) 48 | for child in mirror.children { 49 | if var publishedProperty = child.value as? _MutablePropertyWrapper { 50 | signals.append(publishedProperty.willChange) 51 | } 52 | } 53 | return Signal(flattening: signals, strategy: .merge) 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/Publishers/Empty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Empty.swift 3 | // ReactiveKit-iOS 4 | // 5 | // Created by Ibrahim Koteish on 06/03/2020. 6 | // Copyright © 2020 DeclarativeHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A signal that never publishes any values, and optionally finishes immediately. 12 | /// 13 | /// You can create a ”Never” signal — one which never sends values and never 14 | /// finishes or fails — with the initializer `Empty(completeImmediately: false)`. 15 | public struct Empty: SignalProtocol, Equatable { 16 | 17 | /// The kind of values published by this signal. 18 | public typealias Element = Element 19 | 20 | /// The kind of errors this signal might publish. 21 | /// 22 | /// Use `Never` if this `signal` does not publish errors. 23 | public typealias Error = Error 24 | 25 | /// Creates an empty signal. 26 | /// 27 | /// - Parameter completeImmediately: A Boolean value that indicates whether 28 | /// the signal should immediately finish. 29 | public init(completeImmediately: Bool = true) { 30 | self.completeImmediately = completeImmediately 31 | } 32 | 33 | /// Creates an empty signal with the given completion behavior and output and 34 | /// failure types. 35 | /// 36 | /// Use this initializer to connect the empty signal to observers or other 37 | /// signals that have specific output and failure types. 38 | /// - Parameters: 39 | /// - completeImmediately: A Boolean value that indicates whether the signal 40 | /// should immediately finish. 41 | /// - outputType: The output type exposed by this signal. 42 | /// - failureType: The failure type exposed by this signal. 43 | public init(completeImmediately: Bool = true, 44 | outputType: Element.Type, 45 | failureType: Error.Type) { 46 | self.init(completeImmediately: completeImmediately) 47 | } 48 | 49 | /// A Boolean value that indicates whether the signal immediately sends 50 | /// a completion. 51 | /// 52 | /// If `true`, the signal finishes immediately after sending an event 53 | /// to the observer. If `false`, it never completes. 54 | public let completeImmediately: Bool 55 | 56 | public func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 57 | 58 | if completeImmediately { 59 | return Signal.completed().observe(with: observer) 60 | } 61 | return Signal.never().observe(with: observer) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Sequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Map element into a collection, flattening the collection into next elements. 30 | /// Shorthand for `map(transform).flattenElements()`. 31 | public func flatMap(_ transform: @escaping (Element) -> [NewElement]) -> Signal { 32 | return map(transform).flattenElements() 33 | } 34 | } 35 | 36 | extension SignalProtocol where Element: Sequence { 37 | 38 | /// Map inner sequence. 39 | public func mapElement(_ transform: @escaping (Element.Iterator.Element) -> NewElement) -> Signal<[NewElement], Error> { 40 | return map { $0.map(transform) } 41 | } 42 | 43 | /// Unwrap elements from each emitted sequence into the elements of the signal. 44 | public func flattenElements() -> Signal { 45 | return Signal { observer in 46 | return self.observe { event in 47 | switch event { 48 | case .next(let sequence): 49 | sequence.forEach(observer.receive(_:)) 50 | case .completed: 51 | observer.receive(completion: .finished) 52 | case .failed(let error): 53 | observer.receive(completion: .failure(error)) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Threading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Set the scheduler on which to execute the signal (i.e. to run the signal's producer). 30 | /// 31 | /// In contrast with `receive(on:)`, which affects downstream actions, `subscribe(on:)` changes the execution context of upstream actions. 32 | public func subscribe(on scheduler: S) -> Signal { 33 | return Signal { observer in 34 | let serialDisposable = SerialDisposable(otherDisposable: nil) 35 | scheduler.schedule { 36 | if !serialDisposable.isDisposed { 37 | serialDisposable.otherDisposable = self.observe(with: observer.on) 38 | } 39 | } 40 | return serialDisposable 41 | } 42 | } 43 | 44 | /// Set the scheduler used to receive events (i.e. to run the observer / sink). 45 | /// 46 | /// In contrast with `subscribe(on:)`, which affects upstream actions, `receive(on:)` changes the execution context of downstream actions. 47 | public func receive(on scheduler: S) -> Signal { 48 | return Signal { observer in 49 | return self.observe { event in 50 | scheduler.schedule { 51 | observer.on(event) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Subscribers/Sink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension Subscribers { 28 | 29 | final public class Sink: Subscriber, Cancellable where Failure: Error { 30 | 31 | private enum State { 32 | case initialized 33 | case subscribed(Cancellable) 34 | case terminated 35 | } 36 | 37 | private var state = State.initialized 38 | 39 | final public let receiveValue: (Input) -> Void 40 | final public let receiveCompletion: (Subscribers.Completion) -> Void 41 | 42 | public init(receiveCompletion: @escaping ((Subscribers.Completion) -> Void), receiveValue: @escaping ((Input) -> Void)) { 43 | self.receiveValue = receiveValue 44 | self.receiveCompletion = receiveCompletion 45 | } 46 | 47 | final public func receive(subscription: Subscription) { 48 | switch state { 49 | case .initialized: 50 | state = .subscribed(subscription) 51 | subscription.request(.unlimited) 52 | default: 53 | subscription.cancel() 54 | } 55 | } 56 | 57 | final public func receive(_ value: Input) -> Subscribers.Demand { 58 | receiveValue(value) 59 | return .unlimited 60 | } 61 | 62 | final public func receive(completion: Subscribers.Completion) { 63 | receiveCompletion(completion) 64 | state = .terminated 65 | } 66 | 67 | final public func cancel() { 68 | switch state { 69 | case .subscribed(let subscription): 70 | subscription.cancel() 71 | state = .terminated 72 | default: 73 | break 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcschemes/ReactiveKit-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcschemes/ReactiveKit-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcschemes/ReactiveKit-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Map element into a result, propagating `.some` value as a next event or skipping an element in case of a `nil`. 30 | /// Shorthand for `map(transform).ignoreNils()`. 31 | public func compactMap(_ transform: @escaping (Element) -> NewWrapped?) -> Signal { 32 | return map(transform).ignoreNils() 33 | } 34 | } 35 | 36 | extension SignalProtocol where Element: OptionalProtocol { 37 | 38 | /// Map inner optional. 39 | /// Shorthand for `map { $0.map(transform) }`. 40 | public func mapWrapped(_ transform: @escaping (Element.Wrapped) -> NewWrapped) -> Signal { 41 | return map { $0._unbox.map(transform) } 42 | } 43 | 44 | /// Replace all `nil`-elements with the provided replacement. 45 | public func replaceNils(with replacement: Element.Wrapped) -> Signal { 46 | return compactMap { $0._unbox ?? replacement } 47 | } 48 | 49 | /// Suppress all `nil`-elements. 50 | public func ignoreNils() -> Signal { 51 | return Signal { observer in 52 | return self.observe { event in 53 | switch event { 54 | case .next(let element): 55 | if let element = element._unbox { 56 | observer.receive(element) 57 | } 58 | case .failed(let error): 59 | observer.receive(completion: .failure(error)) 60 | case .completed: 61 | observer.receive(completion: .finished) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | public protocol OptionalProtocol { 69 | associatedtype Wrapped 70 | var _unbox: Optional { get } 71 | init(nilLiteral: ()) 72 | init(_ some: Wrapped) 73 | } 74 | 75 | extension Optional: OptionalProtocol { 76 | 77 | public var _unbox: Optional { 78 | return self 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Unwrap events into elements. 30 | public func materialize() -> Signal.Event, Never> { 31 | return Signal { observer in 32 | return self.observe { event in 33 | switch event { 34 | case .next(let element): 35 | observer.receive(.next(element)) 36 | case .failed(let error): 37 | observer.receive(.failed(error)) 38 | observer.receive(completion: .finished) 39 | case .completed: 40 | observer.receive(.completed) 41 | observer.receive(completion: .finished) 42 | } 43 | } 44 | } 45 | } 46 | 47 | /// Inverse of `materialize`. 48 | public func dematerialize() -> Signal where Element == Signal.Event, E == Error { 49 | return Signal { observer in 50 | return self.observe { event in 51 | switch event { 52 | case .next(let innerEvent): 53 | switch innerEvent { 54 | case .next(let element): 55 | observer.receive(element) 56 | case .failed(let error): 57 | observer.receive(completion: .failure(error)) 58 | case .completed: 59 | observer.receive(completion: .finished) 60 | } 61 | case .failed(let error): 62 | observer.receive(completion: .failure(error)) 63 | case .completed: 64 | observer.receive(completion: .finished) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | extension SignalProtocol where Error == Never { 72 | 73 | /// Inverse of `materialize`. 74 | public func dematerialize() -> Signal where Element == Signal.Event { 75 | return (castError() as Signal).dematerialize() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Signal.Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | extension Signal { 26 | 27 | /// An event of a sequence. 28 | public enum Event { 29 | 30 | /// An event that carries next element. 31 | case next(Element) 32 | 33 | /// An event that represents failure. Carries an error. 34 | case failed(Error) 35 | 36 | /// An event that marks the completion of a sequence. 37 | case completed 38 | } 39 | } 40 | 41 | extension Signal.Event { 42 | 43 | /// Return `true` in case of `.next` event. 44 | public var isNext: Bool { 45 | switch self { 46 | case .next: 47 | return true 48 | default: 49 | return false 50 | } 51 | } 52 | 53 | /// Return `true` in case of `.failed` event. 54 | public var isFailed: Bool { 55 | switch self { 56 | case .failed: 57 | return true 58 | default: 59 | return false 60 | } 61 | } 62 | 63 | /// Return `true` in case of `.completed` event. 64 | public var isCompleted: Bool { 65 | switch self { 66 | case .completed: 67 | return true 68 | default: 69 | return false 70 | } 71 | } 72 | 73 | /// Return `true` in case of `.failure` or `.completed` event. 74 | public var isTerminal: Bool { 75 | switch self { 76 | case .next: 77 | return false 78 | default: 79 | return true 80 | } 81 | } 82 | 83 | /// Returns the next element, or nil if the event is not `.next` 84 | public var element: Element? { 85 | switch self { 86 | case .next(let element): 87 | return element 88 | default: 89 | return nil 90 | } 91 | } 92 | 93 | /// Return the failed error, or nil if the event is not `.failed` 94 | public var error: Error? { 95 | switch self { 96 | case .failed(let error): 97 | return error 98 | default: 99 | return nil 100 | } 101 | } 102 | } 103 | 104 | extension Signal.Event: Equatable where Element: Equatable, Error: Equatable {} 105 | -------------------------------------------------------------------------------- /Playground.playground/Pages/Observing Signals.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import ReactiveKit 5 | import PlaygroundSupport 6 | 7 | PlaygroundPage.current.needsIndefiniteExecution = true 8 | 9 | //: # Observing Signals 10 | 11 | // Let's make a signal that simulates doing some work like loading of a large file. 12 | 13 | let loadedFile = SafeSignal { observer in 14 | print("Now loading file...") 15 | sleep(1) 16 | observer.completed(with: "first line\nsecond line") 17 | print("File loaded.") 18 | return NonDisposable.instance 19 | } 20 | 21 | // If you run the playground up to this line, nothing will happen. 22 | // You console log will be empty. Even if we access the signal 23 | 24 | _ = loadedFile 25 | 26 | // still nothing happens. We could transform it using any of the operators 27 | 28 | let numberOfLines = loadedFile 29 | .map { $0.split(separator: "\n").count } 30 | .map { "The file has \($0) lines." } 31 | 32 | // and if you run the playground up to this line, nothing again. 33 | 34 | // While this might be a bit confusing, it's the most powerful feature of signals and 35 | // functional-reactive programming. Signals allow us to express the logic without doing side effects. 36 | 37 | // In our example, we've defined how to load a file and how to count number of lines 38 | // in the file, but we have not actually loaded the file nor counted the lines. 39 | 40 | // To make side effects, to do the work, one has to observe the signal. It's the act 41 | // of observing that starts everything! Let's try observing the signal. 42 | 43 | numberOfLines.observe { event in 44 | print(event) 45 | } 46 | 47 | // Run the playground up to this line and watch the console log. 48 | // It's only now that the file gets loaded, signal transformed and events printed. 49 | 50 | // This is very useful in real world development, but be aware of a caveat: observing the 51 | // signal again will repeat the side efects. In our example, the file will be loaded again. 52 | 53 | numberOfLines.observe { event in 54 | print(event) 55 | } 56 | 57 | // Bummer? No. Once you get more into functional-reactive parading you will see that 58 | // being able to express the logic without doing side effects outweights this inconvenience. 59 | // In order to share the sequence, all we need to do is apply `shareReplay` operator. 60 | 61 | let sharedLoadedFile = loadedFile.share() 62 | 63 | // The first time we observe the shared signal, it will load the file: 64 | 65 | sharedLoadedFile.observe { print($0) } 66 | 67 | // However, any subsequent observation will just use the shared sequence cached in the memory: 68 | 69 | sharedLoadedFile.observe { print($0) } 70 | sharedLoadedFile.observe { print($0) } 71 | 72 | // There are few convience methods for observing signals. When you are only interested into elements 73 | // of the signal, as opposed to events that contain element, you could just observe next events: 74 | 75 | sharedLoadedFile.observeNext { fileContent in 76 | print(fileContent) 77 | } 78 | 79 | // If you are interested only when the signal completes, then observe just the completed event: 80 | 81 | sharedLoadedFile.observeCompleted { 82 | print("Done") 83 | } 84 | 85 | // Some signals can fail with an error. When you are interested only in the failure, observe the failed event: 86 | 87 | sharedLoadedFile.observeFailed { error in 88 | print("Failed with error", error) // Will not happen because our signal doesn't fail. 89 | } 90 | 91 | //: [Next](@next) 92 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/project.xcworkspace/xcshareddata/ReactiveKit.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "C1F6A63EF115019142AF6E3E8A173E32E3445DB3", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "C1F6A63EF115019142AF6E3E8A173E32E3445DB3" : 0, 8 | "D78924967B5ECB0A1D20E197FAB93ACBFA47F0D1" : 9223372036854775807, 9 | "46F7DF751715C6D9E4FA7D31B7BBF03DFA4C06D4" : 9223372036854775807, 10 | "95438028B10BBB846574013D29F154A00556A9D1" : 0, 11 | "422BABDE4288A2028DC1FD12E5A5767EA1FD5926" : 0, 12 | "D0725CAC6FF2D66F2C83C2C48DC12106D42DAA64" : 0 13 | }, 14 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "DF86A0A1-D0CF-4030-B5AC-6FE303947238", 15 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 16 | "C1F6A63EF115019142AF6E3E8A173E32E3445DB3" : "ReactiveKit\/", 17 | "D78924967B5ECB0A1D20E197FAB93ACBFA47F0D1" : "..\/..", 18 | "46F7DF751715C6D9E4FA7D31B7BBF03DFA4C06D4" : "..\/..\/..\/..", 19 | "95438028B10BBB846574013D29F154A00556A9D1" : "ReactiveKit\/Carthage\/Checkouts\/Nimble\/", 20 | "422BABDE4288A2028DC1FD12E5A5767EA1FD5926" : "", 21 | "D0725CAC6FF2D66F2C83C2C48DC12106D42DAA64" : "ReactiveKit\/Carthage\/Checkouts\/Quick\/" 22 | }, 23 | "DVTSourceControlWorkspaceBlueprintNameKey" : "ReactiveKit", 24 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 25 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "ReactiveKit.xcodeproj", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 27 | { 28 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:ReactiveKit\/ReactiveFoundation.git", 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 30 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "422BABDE4288A2028DC1FD12E5A5767EA1FD5926" 31 | }, 32 | { 33 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "bitbucket.org:shapedk\/goboat-ios.git", 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 35 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "46F7DF751715C6D9E4FA7D31B7BBF03DFA4C06D4" 36 | }, 37 | { 38 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Quick\/Nimble.git", 39 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 40 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "95438028B10BBB846574013D29F154A00556A9D1" 41 | }, 42 | { 43 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:ReactiveKit\/ReactiveKit.git", 44 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 45 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "C1F6A63EF115019142AF6E3E8A173E32E3445DB3" 46 | }, 47 | { 48 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Quick\/Quick.git", 49 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 50 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "D0725CAC6FF2D66F2C83C2C48DC12106D42DAA64" 51 | }, 52 | { 53 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:ReactiveKit\/Bond.git", 54 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 55 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "D78924967B5ECB0A1D20E197FAB93ACBFA47F0D1" 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /Sources/Reactive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// A proxy protocol for reactive extensions. 28 | /// 29 | /// To provide reactive extensions on type `X`, do 30 | /// 31 | /// extension ReactiveExtensions where Base == X { 32 | /// var y: SafeSignal { ... } 33 | /// } 34 | /// 35 | /// where `X` conforms to `ReactiveExtensionsProvider`. 36 | public protocol ReactiveExtensions { 37 | associatedtype Base 38 | var base: Base { get } 39 | } 40 | 41 | public struct Reactive: ReactiveExtensions { 42 | public let base: Base 43 | 44 | public init(_ base: Base) { 45 | self.base = base 46 | } 47 | } 48 | 49 | public protocol ReactiveExtensionsProvider: class {} 50 | 51 | extension ReactiveExtensionsProvider { 52 | 53 | /// Reactive extensions of `self`. 54 | public var reactive: Reactive { 55 | return Reactive(self) 56 | } 57 | 58 | /// Reactive extensions of `Self`. 59 | public static var reactive: Reactive.Type { 60 | return Reactive.self 61 | } 62 | } 63 | 64 | extension NSObject: ReactiveExtensionsProvider {} 65 | 66 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 67 | 68 | extension ReactiveExtensions where Base: NSObject { 69 | 70 | /// A signal that fires completion event when the object is deallocated. 71 | public var deallocated: SafeSignal { 72 | return base.bag.deallocated 73 | } 74 | 75 | /// A `DisposeBag` that can be used to dispose observations and bindings. 76 | public var bag: DisposeBag { 77 | return base.bag 78 | } 79 | 80 | /// Create a Signal that establishes a key-value observation of the given key path when observed. 81 | /// 82 | /// For example: 83 | /// 84 | /// let player = AVPlayer() 85 | /// player.reactive.publisher(for: \.status).sink { print("Playback status: \($0)") } 86 | /// 87 | public func publisher(for keyPath: KeyPath, options: NSKeyValueObservingOptions = [.initial, .new]) -> Signal { 88 | return Signal { [weak base] observer in 89 | guard let base = base else { 90 | observer.receive(completion: .finished) 91 | return SimpleDisposable(isDisposed: true) 92 | } 93 | let observation = base.observe(keyPath, options: options) { (base, change) in 94 | observer.receive(base[keyPath: keyPath]) 95 | } 96 | return BlockDisposable { 97 | observation.invalidate() 98 | } 99 | } 100 | } 101 | } 102 | 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /ReactiveKit.xcodeproj/xcshareddata/xcschemes/ReactiveKit-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 65 | 66 | 72 | 73 | 74 | 75 | 81 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Sources/Subscribers/Accumulator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension Subscribers { 28 | 29 | /// A subscriber that accumulates received values into an array. This subscriber can be useful in unit testing by 30 | /// asserting the state of various properties of the subscriber. 31 | final public class Accumulator: Subscriber, Cancellable { 32 | 33 | private enum State { 34 | case initialized 35 | case subscribed(Cancellable) 36 | case terminated(Completion?) 37 | } 38 | 39 | private var state = State.initialized 40 | 41 | /// An array of values received by the subscriber. 42 | final public private(set) var values: [Input] = [] 43 | 44 | /// True if the subscriber has received a finished event. 45 | final public var isFinished: Bool { 46 | switch state { 47 | case .terminated(.some(.finished)): 48 | return true 49 | default: 50 | return false 51 | } 52 | } 53 | 54 | /// True if the subscriber has received a failure event. 55 | final public var isFailure: Bool { 56 | switch state { 57 | case .terminated(.some(.failure)): 58 | return true 59 | default: 60 | return false 61 | } 62 | } 63 | 64 | /// Non-nil if the subscriber has received a failure event. 65 | final public var error: Failure? { 66 | switch state { 67 | case .terminated(.some(.failure(let error))): 68 | return error 69 | default: 70 | return nil 71 | } 72 | } 73 | 74 | 75 | public init() { 76 | } 77 | 78 | final public func receive(subscription: Subscription) { 79 | switch state { 80 | case .initialized: 81 | state = .subscribed(subscription) 82 | subscription.request(.unlimited) 83 | default: 84 | subscription.cancel() 85 | } 86 | } 87 | 88 | final public func receive(_ value: Input) -> Subscribers.Demand { 89 | values.append(value) 90 | return .unlimited 91 | } 92 | 93 | final public func receive(completion: Subscribers.Completion) { 94 | switch state { 95 | case .terminated: 96 | break 97 | default: 98 | state = .terminated(completion) 99 | } 100 | } 101 | 102 | final public func cancel() { 103 | switch state { 104 | case .subscribed(let subscription): 105 | subscription.cancel() 106 | state = .terminated(nil) 107 | default: 108 | break 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2020 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | #if canImport(Combine) 26 | import Combine 27 | 28 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 29 | extension Combine.Publisher { 30 | 31 | /// Convert `Combine.Publisher` into `ReactiveKit.Signal` 32 | public func toSignal() -> Signal { 33 | return Signal { observer in 34 | let sink = Combine.Subscribers.Sink( 35 | receiveCompletion: { completion in 36 | switch completion { 37 | case .finished: 38 | observer.receive(completion: .finished) 39 | case .failure(let error): 40 | observer.receive(completion: .failure(error)) 41 | } 42 | }, 43 | receiveValue: observer.receive(_:)) 44 | self.subscribe(sink) 45 | return BlockDisposable(sink.cancel) 46 | } 47 | } 48 | } 49 | 50 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 51 | extension Signal { 52 | 53 | public struct CombinePublisher: Combine.Publisher { 54 | 55 | private class DisposableSubscription: Combine.Subscription { 56 | 57 | let disposable: Disposable 58 | 59 | init(disposable: Disposable) { 60 | self.disposable = disposable 61 | } 62 | 63 | func request(_ demand: Combine.Subscribers.Demand) { 64 | } 65 | 66 | func cancel() { 67 | disposable.dispose() 68 | } 69 | } 70 | 71 | public typealias Output = Element 72 | public typealias Failure = Error 73 | 74 | let signal: Signal 75 | 76 | public func receive(subscriber: S) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input { 77 | let disposable = CompositeDisposable() 78 | let subscription = DisposableSubscription(disposable: disposable) 79 | subscriber.receive(subscription: subscription) 80 | disposable += signal.observe { event in 81 | switch event { 82 | case .next(let element): 83 | _ = subscriber.receive(element) 84 | case .failed(let error): 85 | subscriber.receive(completion: .failure(error)) 86 | case .completed: 87 | subscriber.receive(completion: .finished) 88 | } 89 | } 90 | } 91 | } 92 | 93 | /// Convert `ReactiveKit.Signal` in `Combine.Publisher` 94 | public func toPublisher() -> Signal.CombinePublisher { 95 | return .init(signal: self) 96 | } 97 | } 98 | 99 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 100 | extension SignalProtocol { 101 | 102 | /// Convert `ReactiveKit.Signal` in `Combine.Publisher` 103 | public func toPublisher() -> Signal.CombinePublisher { 104 | return .init(signal: toSignal()) 105 | } 106 | } 107 | 108 | #endif 109 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 28 | extension SignalProtocol where Error == Never { 29 | 30 | public func toAsyncStream() -> AsyncStream { 31 | AsyncStream { continuation in 32 | let disposable = self.observe { event in 33 | switch event { 34 | case .next(let element): 35 | continuation.yield(element) 36 | case .completed: 37 | continuation.finish() 38 | } 39 | } 40 | continuation.onTermination = { @Sendable _ in 41 | disposable.dispose() 42 | } 43 | } 44 | } 45 | } 46 | 47 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 48 | extension SignalProtocol where Error == Swift.Error { 49 | 50 | public func toAsyncThrowingStream() -> AsyncThrowingStream { 51 | AsyncThrowingStream { continuation in 52 | let disposable = self.observe { event in 53 | switch event { 54 | case .next(let element): 55 | continuation.yield(element) 56 | case .failed(let error): 57 | continuation.finish(throwing: error) 58 | case .completed: 59 | continuation.finish() 60 | } 61 | } 62 | continuation.onTermination = { @Sendable _ in 63 | disposable.dispose() 64 | } 65 | } 66 | } 67 | } 68 | 69 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 70 | extension AsyncSequence { 71 | 72 | public func toSignal() -> Signal { 73 | Signal { observer in 74 | let task = Task { 75 | do { 76 | for try await element in self { 77 | observer.receive(element) 78 | } 79 | observer.receive(completion: .finished) 80 | } catch { 81 | observer.receive(completion: .failure(error)) 82 | } 83 | } 84 | return BlockDisposable(task.cancel) 85 | } 86 | } 87 | } 88 | 89 | #if swift(>=6.0) 90 | @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, *) 91 | extension AsyncSequence { 92 | 93 | public func toFullyTypedSignal() -> Signal { 94 | Signal { observer in 95 | let task = Task { 96 | do { 97 | for try await element in self { 98 | observer.receive(element) 99 | } 100 | observer.receive(completion: .finished) 101 | } catch let error as Failure { 102 | observer.receive(completion: .failure(error)) 103 | } 104 | } 105 | return BlockDisposable(task.cancel) 106 | } 107 | } 108 | } 109 | #endif 110 | -------------------------------------------------------------------------------- /Sources/Property.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Represents mutable state that can be observed as a signal of events. 28 | public protocol PropertyProtocol { 29 | associatedtype ProperyElement 30 | var value: ProperyElement { get } 31 | } 32 | 33 | /// Represents mutable state that can be observed as a signal of events. 34 | public final class Property: PropertyProtocol, SubjectProtocol, BindableProtocol, DisposeBagProvider { 35 | 36 | private let lock = NSRecursiveLock(name: "com.reactive_kit.property") 37 | 38 | private let subject: Subject 39 | 40 | public var bag: DisposeBag { 41 | return subject.disposeBag 42 | } 43 | 44 | /// Underlying value. Changing it emits `.next` event with new value. 45 | private var _value: Value 46 | public var value: Value { 47 | get { 48 | lock.lock(); defer { lock.unlock() } 49 | return _value 50 | } 51 | set { 52 | lock.lock(); defer { lock.unlock() } 53 | _value = newValue 54 | subject.send(newValue) 55 | } 56 | } 57 | 58 | public init(_ value: Value, subject: Subject = PassthroughSubject()) { 59 | _value = value 60 | self.subject = subject 61 | } 62 | 63 | public func on(_ event: Signal.Event) { 64 | lock.lock(); defer { lock.unlock() } 65 | if case .next(let element) = event { 66 | _value = element 67 | } 68 | subject.on(event) 69 | } 70 | 71 | public func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 72 | lock.lock(); defer { lock.unlock() } 73 | return subject.prepend(_value).observe(with: observer) 74 | } 75 | 76 | public var readOnlyView: AnyProperty { 77 | return AnyProperty(property: self) 78 | } 79 | 80 | /// Change the underlying value without notifying the observers. 81 | public func silentUpdate(value: Value) { 82 | lock.lock(); defer { lock.unlock() } 83 | _value = value 84 | } 85 | 86 | public func bind(signal: Signal) -> Disposable { 87 | return signal 88 | .prefix(untilOutputFrom: bag.deallocated) 89 | .receive(on: ExecutionContext.nonRecursive()) 90 | .observeNext { [weak self] element in 91 | self?.on(.next(element)) 92 | } 93 | } 94 | 95 | deinit { 96 | subject.send(completion: .finished) 97 | } 98 | } 99 | 100 | /// Represents mutable state that can be observed as a signal of events. 101 | public final class AnyProperty: PropertyProtocol, SignalProtocol { 102 | 103 | private let property: Property 104 | 105 | public var value: Value { 106 | return property.value 107 | } 108 | 109 | public init(property: Property) { 110 | self.property = property 111 | } 112 | 113 | public func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 114 | return property.observe(with: observer) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/ExecutionContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | import Dispatch 27 | 28 | /// Execution context is an abstraction over a thread or a dispatch queue. 29 | /// 30 | /// let context = ExecutionContext.main 31 | /// 32 | /// context.execute { 33 | /// print("Printing on main queue.") 34 | /// } 35 | /// 36 | public struct ExecutionContext { 37 | 38 | public let context: (@escaping () -> Void) -> Void 39 | 40 | /// Execution context is just a function that executes other function. 41 | public init(_ context: @escaping (@escaping () -> Void) -> Void) { 42 | self.context = context 43 | } 44 | 45 | /// Execute given block in the context. 46 | @inlinable 47 | public func execute(_ block: @escaping () -> Void) { 48 | context(block) 49 | } 50 | 51 | /// Execution context that executes immediately and synchronously on current thread or queue. 52 | public static var immediate: ExecutionContext { 53 | return ExecutionContext { block in block () } 54 | } 55 | 56 | /// Executes immediately and synchronously if current thread is main thread. Otherwise executes 57 | /// asynchronously on main dispatch queue (main thread). 58 | public static var immediateOnMain: ExecutionContext { 59 | return ExecutionContext { block in 60 | if Thread.isMainThread { 61 | block() 62 | } else { 63 | DispatchQueue.main.async(execute: block) 64 | } 65 | } 66 | } 67 | 68 | /// Execution context bound to main dispatch queue. 69 | public static var main: ExecutionContext { 70 | return DispatchQueue.main.context 71 | } 72 | 73 | /// Execution context bound to global dispatch queue. 74 | @available(macOS 10.10, *) 75 | public static func global(qos: DispatchQoS.QoSClass = .default) -> ExecutionContext { 76 | return DispatchQueue.global(qos: qos).context 77 | } 78 | 79 | /// Execution context that breaks recursive class by ingoring them. 80 | public static func nonRecursive() -> ExecutionContext { 81 | var updating: Bool = false 82 | return ExecutionContext { block in 83 | guard !updating else { return } 84 | updating = true 85 | block() 86 | updating = false 87 | } 88 | } 89 | } 90 | 91 | extension DispatchQueue { 92 | 93 | /// Creates ExecutionContext from the queue. 94 | public var context: ExecutionContext { 95 | return ExecutionContext { block in 96 | self.async(execute: block) 97 | } 98 | } 99 | 100 | /// Schedule given block for execution after given interval passes. 101 | @available(*, deprecated, message: "Please use asyncAfter(deadline:execute:)") 102 | public func after(when interval: Double, block: @escaping () -> Void) { 103 | asyncAfter(deadline: .now() + interval, execute: block) 104 | } 105 | 106 | /// Schedule given block for execution after given interval passes. 107 | /// Scheduled execution can be cancelled by disposing the returned disposable. 108 | public func disposableAfter(when interval: Double, block: @escaping () -> Void) -> Disposable { 109 | let workItem = DispatchWorkItem(block: block) 110 | asyncAfter(deadline: .now() + interval, execute: workItem) 111 | return BlockDisposable(workItem.cancel) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Maps signal elements into `Result.success` elements and signal errors into `Result.failure` elements. 30 | public func mapToResult() -> Signal, Never> { 31 | return materialize().compactMap { (event) -> Result? in 32 | switch event { 33 | case .next(let element): 34 | return .success(element) 35 | case .failed(let error): 36 | return .failure(error) 37 | case .completed: 38 | return nil 39 | } 40 | } 41 | } 42 | 43 | /// Map element into a result, propagating success value as a next event or failure as a failed event. 44 | /// Shorthand for `map(transform).getValues()`. 45 | public func tryMap(_ transform: @escaping (Element) -> Result) -> Signal { 46 | return map(transform).getValues() 47 | } 48 | } 49 | 50 | extension SignalProtocol where Error == Never { 51 | 52 | /// Map element into a result, propagating success value as a next event or failure as a failed event. 53 | /// Shorthand for `map(transform).getValues()`. 54 | public func tryMap(_ transform: @escaping (Element) -> Result) -> Signal { 55 | return castError().map(transform).getValues() 56 | } 57 | } 58 | 59 | extension SignalProtocol where Element: _ResultProtocol { 60 | 61 | /// Map inner result. 62 | /// Shorthand for `map { $0.map(transform) }`. 63 | public func mapValue(_ transform: @escaping (Element.Value) -> NewSuccess) -> Signal, Error> { 64 | return map { $0._unbox.map(transform) } 65 | } 66 | } 67 | 68 | extension SignalProtocol where Element: _ResultProtocol, Error == Element.Error { 69 | 70 | /// Unwraps values from result elements into elements of the signal. 71 | /// A failure result will trigger signal failure. 72 | public func getValues() -> Signal { 73 | return Signal { observer in 74 | return self.observe { event in 75 | switch event { 76 | case .next(let result): 77 | switch result._unbox { 78 | case .success(let element): 79 | observer.receive(element) 80 | case .failure(let error): 81 | observer.receive(completion: .failure(error)) 82 | } 83 | case .completed: 84 | observer.receive(completion: .finished) 85 | case .failed(let error): 86 | observer.receive(completion: .failure(error)) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | extension SignalProtocol where Element: _ResultProtocol, Error == Never { 94 | 95 | /// Unwraps values from result elements into elements of the signal. 96 | /// A failure result will trigger signal failure. 97 | public func getValues() -> Signal { 98 | return (castError() as Signal).getValues() 99 | } 100 | } 101 | 102 | public protocol _ResultProtocol { 103 | associatedtype Value 104 | associatedtype Error: Swift.Error 105 | var _unbox: Result { get } 106 | } 107 | 108 | extension Result: _ResultProtocol { 109 | 110 | public var _unbox: Result { 111 | return self 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/ReactiveKitTests/PropertyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyTests.swift 3 | // ReactiveKit 4 | // 5 | // Created by Srdan Rasic on 17/10/2016. 6 | // Copyright © 2016 Srdan Rasic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import ReactiveKit 11 | 12 | class PropertyTests: XCTestCase { 13 | 14 | var property: Property! 15 | 16 | override func setUp() { 17 | property = Property(0) 18 | } 19 | 20 | func testValue() { 21 | XCTAssert(property.value == 0) 22 | property.value = 1 23 | XCTAssert(property.value == 1) 24 | } 25 | 26 | func testEvents() { 27 | let subscriber = Subscribers.Accumulator() 28 | property.subscribe(subscriber) 29 | 30 | property.value = 5 31 | property.value = 10 32 | SafeSignal(sequence: [20, 30]).bind(to: property) 33 | property.value = 40 34 | 35 | weak var weakProperty = property 36 | property = nil 37 | XCTAssert(weakProperty == nil) 38 | 39 | XCTAssertEqual(subscriber.values, [0, 5, 10, 20, 30, 40]) 40 | XCTAssertTrue(subscriber.isFinished) 41 | } 42 | 43 | func testReadOnlyView() { 44 | var readOnlyView: AnyProperty! = property.readOnlyView 45 | XCTAssert(readOnlyView.value == 0) 46 | 47 | let subscriber = Subscribers.Accumulator() 48 | readOnlyView.subscribe(subscriber) 49 | 50 | property.value = 5 51 | property.value = 10 52 | SafeSignal(sequence: [20, 30]).bind(to: property) 53 | property.value = 40 54 | 55 | XCTAssert(readOnlyView.value == 40) 56 | 57 | weak var weakProperty = property 58 | weak var weakReadOnlyView = readOnlyView 59 | property = nil 60 | readOnlyView = nil 61 | XCTAssert(weakProperty == nil) 62 | XCTAssert(weakReadOnlyView == nil) 63 | 64 | XCTAssertEqual(subscriber.values, [0, 5, 10, 20, 30, 40]) 65 | XCTAssertTrue(subscriber.isFinished) 66 | } 67 | 68 | func testBidirectionalBind() { 69 | let target = Property(100) 70 | let s1 = Subscribers.Accumulator() 71 | let s2 = Subscribers.Accumulator() 72 | 73 | target.ignoreTerminal().subscribe(s1) 74 | property.ignoreTerminal().subscribe(s2) 75 | 76 | property.bidirectionalBind(to: target) 77 | property.value = 50 78 | target.value = 60 79 | 80 | XCTAssertEqual(s1.values, [100, 0, 50, 60]) 81 | XCTAssertEqual(s2.values, [0, 0, 50, 60]) 82 | } 83 | 84 | func testPropertyForThreadSafety_oneEventDispatchedOnALotOfProperties() { 85 | let exp = expectation(description: "race_condition?") 86 | exp.expectedFulfillmentCount = 10000 87 | 88 | for _ in 0.. () -> Void)] { 144 | return [ 145 | ("testValue", testValue), 146 | ("testEvents", testEvents), 147 | ("testReadOnlyView", testReadOnlyView), 148 | ("testBidirectionalBind", testBidirectionalBind), 149 | ("testPropertyForThreadSafety_oneEventDispatchedOnALotOfProperties", testPropertyForThreadSafety_oneEventDispatchedOnALotOfProperties), 150 | ("testPropertyForThreadSafety_lotsOfEventsDispatchedOnOneProperty", testPropertyForThreadSafety_lotsOfEventsDispatchedOnOneProperty), 151 | ("testPropertyForThreadSafety_someEventsDispatchedOnSomeProperties", testPropertyForThreadSafety_someEventsDispatchedOnSomeProperties), 152 | ] 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Sources/Observer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Represents a type that receives events. 28 | public typealias Observer = (Signal.Event) -> Void 29 | 30 | /// An observer of safe signals. 31 | public typealias SafeObserver = (Signal.Event) -> Void 32 | 33 | /// Represents a type that receives events. 34 | public protocol ObserverProtocol { 35 | 36 | /// Type of elements being received. 37 | associatedtype Element 38 | 39 | /// Type of error that can be received. 40 | associatedtype Error: Swift.Error 41 | 42 | /// Send the event to the observer. 43 | func on(_ event: Signal.Event) 44 | } 45 | 46 | /// Represents a type that receives events. Observer is just a convenience 47 | /// wrapper around a closure observer `Observer`. 48 | public struct AnyObserver: ObserverProtocol { 49 | 50 | public let observer: Observer 51 | 52 | /// Creates an observer that wraps a closure observer. 53 | public init(observer: @escaping Observer) { 54 | self.observer = observer 55 | } 56 | 57 | /// Calles wrapped closure with the given element. 58 | @inlinable 59 | public func on(_ event: Signal.Event) { 60 | observer(event) 61 | } 62 | } 63 | 64 | /// Observer that ensures events are sent atomically. 65 | public final class AtomicObserver: ObserverProtocol, Disposable { 66 | 67 | private var observer: Observer? 68 | private var upstreamDisposables: [Disposable] = [] 69 | private let observerLock = NSRecursiveLock(name: "com.reactive_kit.atomic_observer.observer") 70 | private let disposablesLock = NSRecursiveLock(name: "com.reactive_kit.atomic_observer.disposablesLock") 71 | 72 | public var isDisposed: Bool { 73 | observerLock.lock(); defer { observerLock.unlock() } 74 | return observer == nil 75 | } 76 | 77 | /// Creates an observer that wraps given closure. 78 | public init(_ observer: @escaping Observer) { 79 | self.observer = observer 80 | } 81 | 82 | @available(*, deprecated, message: "Will be remove in favour of `init(_:)`. AtomicObserver is a Disposable itself now.") 83 | public convenience init(disposable: Disposable, observer: @escaping Observer) { 84 | self.init(observer) 85 | upstreamDisposables.append(disposable) 86 | } 87 | 88 | /// Calles wrapped closure with the given element. 89 | public func on(_ event: Signal.Event) { 90 | observerLock.lock() 91 | if let observer = observer { 92 | if event.isTerminal { 93 | self.observer = nil 94 | observerLock.unlock() 95 | disposablesLock.lock() 96 | self.upstreamDisposables.forEach { $0.dispose() } 97 | disposablesLock.unlock() 98 | } else { 99 | observerLock.unlock() 100 | } 101 | observer(event) 102 | } else { 103 | observerLock.unlock() 104 | } 105 | } 106 | 107 | public func attach(_ producer: Signal.Producer) { 108 | let disposable = producer(self) 109 | if self.isDisposed { 110 | disposable.dispose() 111 | } else { 112 | disposablesLock.lock() 113 | self.upstreamDisposables.append(disposable) 114 | disposablesLock.unlock() 115 | } 116 | } 117 | 118 | public func dispose() { 119 | observerLock.lock() 120 | withExtendedLifetime(self.observer) { 121 | self.observer = nil 122 | } 123 | observerLock.unlock() 124 | disposablesLock.lock() 125 | self.upstreamDisposables.forEach { $0.dispose() } 126 | disposablesLock.unlock() 127 | } 128 | } 129 | 130 | // MARK: - Extensions 131 | 132 | extension ObserverProtocol { 133 | 134 | /// Convenience method to send `.next` event. 135 | public func receive(_ element: Element) { 136 | on(.next(element)) 137 | } 138 | 139 | /// Convenience method to send `.failed` or `.completed` event. 140 | public func receive(completion: Subscribers.Completion) { 141 | switch completion { 142 | case .finished: 143 | on(.completed) 144 | case .failure(let error): 145 | on(.failed(error)) 146 | } 147 | } 148 | 149 | /// Convenience method to send `.next` event followed by a `.completed` event. 150 | public func receive(lastElement element: Element) { 151 | receive(element) 152 | receive(completion: .finished) 153 | } 154 | 155 | /// Converts the receiver to the Observer closure. 156 | public func toObserver() -> Observer { 157 | return on 158 | } 159 | } 160 | 161 | extension ObserverProtocol where Element == Void { 162 | 163 | /// Convenience method to send `.next` event. 164 | public func receive() { 165 | on(.next(())) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/Connectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Represents a signal that is started by calling `connect` on it. 28 | public protocol ConnectableSignalProtocol: SignalProtocol { 29 | 30 | /// Start the signal. 31 | func connect() -> Disposable 32 | } 33 | 34 | /// Makes a signal connectable through the given subject. 35 | public final class ConnectableSignal: ConnectableSignalProtocol { 36 | 37 | private let source: Source 38 | private let subject: Subject 39 | 40 | public init(source: Source, subject: Subject) { 41 | self.source = source 42 | self.subject = subject 43 | } 44 | 45 | /// Start the signal. 46 | public func connect() -> Disposable { 47 | if !subject.isTerminated { 48 | return source.observe(with: subject) 49 | } else { 50 | return SimpleDisposable(isDisposed: true) 51 | } 52 | } 53 | 54 | /// Register an observer that will receive events from the signal. 55 | /// Note that the events will not be generated until `connect` is called. 56 | public func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 57 | return subject.observe(with: observer) 58 | } 59 | } 60 | 61 | extension ConnectableSignalProtocol { 62 | 63 | /// Convert connectable signal into the ordinary signal by calling `connect` 64 | /// on the first observation and calling dispose when number of observers goes down to zero. 65 | /// - parameter disconnectCount: Subscriptions count on which to disconnect. Defaults to `0`. 66 | public func refCount(disconnectCount: Int = 0) -> Signal { 67 | let lock = NSRecursiveLock(name: "com.reactive_kit.connectable_signal.ref_count") 68 | var _count = 0 69 | var _connectionDisposable: Disposable? = nil 70 | return Signal { observer in 71 | lock.lock(); defer { lock.unlock() } 72 | _count = _count + 1 73 | let disposable = self.observe(with: observer.on) 74 | if _connectionDisposable == nil { 75 | _connectionDisposable = self.connect() 76 | } 77 | return BlockDisposable { 78 | lock.lock(); defer { lock.unlock() } 79 | disposable.dispose() 80 | _count = _count - 1 81 | if _count == disconnectCount { 82 | _connectionDisposable?.dispose() 83 | _connectionDisposable = nil 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | extension SignalProtocol { 91 | 92 | public func multicast(_ createSubject: () -> Subject) -> ConnectableSignal { 93 | return ConnectableSignal(source: self, subject: createSubject()) 94 | } 95 | 96 | public func multicast(subject: Subject) -> ConnectableSignal { 97 | return ConnectableSignal(source: self, subject: subject) 98 | } 99 | 100 | /// Ensure that all observers see the same sequence of elements. Connectable. 101 | public func replay(limit: Int = Int.max) -> ConnectableSignal { 102 | if limit == 0 { 103 | return multicast(subject: PassthroughSubject()) 104 | } else if limit == 1 { 105 | return multicast(subject: ReplayOneSubject()) 106 | } else { 107 | return multicast(subject: ReplaySubject(bufferSize: limit)) 108 | } 109 | } 110 | 111 | /// Convert signal to a connectable signal. 112 | public func publish() -> ConnectableSignal { 113 | return multicast(subject: PassthroughSubject()) 114 | } 115 | 116 | /// Ensure that all observers see the same sequence of elements. 117 | /// Shorthand for `replay(limit).refCount()`. 118 | /// - parameter limit: Number of latest elements to buffer. 119 | /// - parameter keepAlive: Whether to keep the source signal connected even when all subscribers disconnect. 120 | public func share(limit: Int = Int.max, keepAlive: Bool = false) -> Signal { 121 | return replay(limit: limit).refCount(disconnectCount: keepAlive ? Int.min : 0) 122 | } 123 | } 124 | 125 | extension SignalProtocol where Element: LoadingStateProtocol { 126 | 127 | /// Ensure that all observers see the same sequence of elements. Connectable. 128 | public func replayValues(limit: Int = Int.max) -> ConnectableSignal, Error>> { 129 | if limit == 0 { 130 | return ConnectableSignal(source: map { $0.asLoadingState }, subject: PassthroughSubject()) 131 | } else { 132 | return ConnectableSignal(source: map { $0.asLoadingState }, subject: ReplayLoadingValueSubject(bufferSize: limit)) 133 | } 134 | } 135 | 136 | /// Ensure that all observers see the same sequence of elements. 137 | /// Shorthand for `replay(limit).refCount()`. 138 | public func shareReplayValues(limit: Int = Int.max) -> Signal, Error> { 139 | return replayValues(limit: limit).refCount() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Playground.playground/Pages/Transforming Signals.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import ReactiveKit 5 | import PlaygroundSupport 6 | 7 | PlaygroundPage.current.needsIndefiniteExecution = true 8 | 9 | //: # Transforming Signals 10 | //: Uncomment the `observe { ... }` line to explore the behaviour! 11 | 12 | // Let's play with Pokemons! We've got the following squad: 13 | 14 | let pokemons = SafeSignal(sequence: ["Ditto", "Scizor", "Pikachu", "Squirtle"]) 15 | 16 | // Let's start simple and print thier names in uppercase. 17 | // To modify elements of a signal, we can use the map operator: 18 | 19 | pokemons 20 | .map { $0.uppercased() } 21 | // .observe { print($0) } 22 | 23 | 24 | // If we are interested only in some elements of the signal, for 25 | // example in Pokemons whose name starts with "S", we can use the filter operator: 26 | 27 | pokemons 28 | .filter { $0.hasPrefix("S") } 29 | // .observe { print($0) } 30 | 31 | // Alright, we know names of our Pokemons, but we need more details about them to make this fun! 32 | // Let's define a type that will represent our Pokemons: 33 | 34 | struct Pokemon: Codable { 35 | let name: String 36 | let height: Int 37 | let weight: Int 38 | } 39 | 40 | // We will make use of an awesome API called PokéAPI to fetch the details. 41 | 42 | /// Returns a signal that fetches the details of the Pokemon with the given name. 43 | func fetchPokemonDetails(name: String) -> Signal { 44 | return Signal { // A signal that fetches data from the given URL 45 | print("Fetching Pokemon named", name) 46 | return try Data( 47 | contentsOf: URL(string: "https://pokeapi.co/api/v2/pokemon/\(name.lowercased())")! 48 | ) 49 | } 50 | .map { try JSONDecoder().decode(Pokemon.self, from: $0) } // We then map the data by decoding it into our Pokemon type 51 | } 52 | 53 | // Now that we have a way of fetching Pokemon details, let's fetch the details of our squad. 54 | // We will use one of the three `flatMap*` operators provided by ReactiveKit. 55 | 56 | pokemons 57 | .flatMapConcat(fetchPokemonDetails) 58 | // .observe { print($0) } 59 | 60 | // Awesome! What is a flat map operator? It's actually just a shorthand for two operators. 61 | // One that maps signal elements into new signals and the other the flattens the 62 | // resulting signal of signals into one signal. Huh? 63 | 64 | // No worries, let's do this step by step: 65 | 66 | pokemons 67 | // First we map names into signals that fetch Pokemons. 68 | // We map `String` elements into `Signal` elements. 69 | // That will give us a signal of type `Signal, Error>` 70 | .map(fetchPokemonDetails) 71 | // Signals whose elements are other signals are usually not what we need. 72 | // We need Pokemons and Pokemons are elements of the inner signals. Who do we get them out? 73 | // Simple, just use the `flatten` operator. It will unwrap elements from the inner signals into our signal. 74 | .flatten(.concat) 75 | // .observe { print($0) } 76 | 77 | // There are few possible strategies of flattening a signal. Ctrl + Cmd click the type bellow to see them all: 78 | FlattenStrategy.self 79 | 80 | // Nice, we now know what flat mapping is. It's pretty much the same as flat mapping a Swift collection like an Array. 81 | // As you have probably noticed by now, signals and collections have so much in common! 82 | // The reason is that both of them represent sequences of elements. Collections represent sequences in space, i.e. 83 | // in the computer memory, while signals represent sequences in time. Many functional operations that you remember 84 | // from collections will also work on signals. 85 | 86 | // How much does our squad weigh? With functional-reactive programming, something like that is simple to answer: 87 | 88 | pokemons 89 | .flatMapConcat(fetchPokemonDetails) 90 | .reduce(0) { totalWeight, pokemon in totalWeight + pokemon.weight } 91 | // .observeNext { print("Total weight of our Pokemons is \($0) hectograms!") } 92 | 93 | // Fetching Pokemons every time we need them is wasteful. We can improve that by fetching once and then sharing the results: 94 | 95 | let pokemonDetails = pokemons 96 | .flatMapConcat(fetchPokemonDetails) 97 | .shareReplay() 98 | 99 | // We can now use `pokemonDetails` signal many times without making redundant network calls. 100 | // The call will be made only the first time `pokemonDetails` is observed. 101 | 102 | pokemonDetails 103 | // .observe { print($0) } 104 | 105 | // If we now observe it one more time, we won't see "Fetching..." in the console. 106 | 107 | pokemonDetails 108 | // .observe { print($0) } 109 | 110 | // Try commenting out `.shareReplay()` line and see what happens in that case! 111 | 112 | 113 | // There are many more operators on signals. Let's go through few of them. 114 | 115 | // When we are interested only in the first few elements, we can apply `take(first:)` operator: 116 | 117 | pokemons 118 | .take(first: 2) 119 | // .observe { print($0) } 120 | 121 | // Similarly, when we are interested in last few elements, we can apply `take(last:)` operator: 122 | 123 | pokemons 124 | .take(last: 1) 125 | // .observe { print($0) } 126 | 127 | // Sometimes we need to combine element from more than one signal. To pair them into tuple, there 128 | // is a zip operator available 129 | 130 | pokemons 131 | .zip(with: SafeSignal(sequence: 0...)) 132 | // .observe { print($0) } 133 | 134 | // Let's use the zip operator again to combine our Pokemon names with a signal that emits an integer 135 | // every second. We can pass a closure to the zip operator that maps the pair into something else, 136 | // in our case into the name, ignoring the number. 137 | 138 | let aPokemonEverySecond = pokemons 139 | .zip(with: SafeSignal(sequence: 0..., interval: 1)) { name, index in name } 140 | 141 | aPokemonEverySecond 142 | // .observe { print($0) } 143 | 144 | // There are other ways to combine signals than zipping them. 145 | // For example, we could just merge elements from the two signals as the arrive. 146 | 147 | aPokemonEverySecond 148 | .merge(with: SafeSignal(sequence: ["Suicune", "Genesect", "Lugia"], interval: 0.8)) 149 | // .observe { print($0) } 150 | 151 | // There is also a combine latest operator that combines the latest emitted events 152 | // from the two signals. 153 | 154 | aPokemonEverySecond 155 | .combineLatest(with: SafeSignal(sequence: 0...6, interval: 0.5)) 156 | .observe { print($0) } 157 | 158 | //: [Next](@next) 159 | -------------------------------------------------------------------------------- /Sources/SignalProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Dispatch 26 | import Foundation 27 | 28 | /// Represents a sequence of events. 29 | public protocol SignalProtocol { 30 | 31 | /// The type of elements generated by the signal. 32 | associatedtype Element 33 | 34 | /// The type of error that can terminate the signal. 35 | associatedtype Error: Swift.Error 36 | 37 | /// Register the given observer. 38 | /// - Parameter observer: A function that will receive events. 39 | /// - Returns: A disposable that can be used to cancel the observation. 40 | func observe(with observer: @escaping Observer) -> Disposable 41 | } 42 | 43 | extension SignalProtocol { 44 | 45 | /// Register an observer that will receive events from a signal. 46 | public func observe(with observer: O) -> Disposable 47 | where O.Element == Element, O.Error == Error { 48 | return observe(with: observer.on) 49 | } 50 | 51 | /// Register an observer that will receive elements from `.next` events of the signal. 52 | public func observeNext(with observer: @escaping (Element) -> Void) -> Disposable { 53 | return observe { event in 54 | if case .next(let element) = event { 55 | observer(element) 56 | } 57 | } 58 | } 59 | 60 | /// Register an observer that will receive elements from `.failed` events of the signal. 61 | public func observeFailed(with observer: @escaping (Error) -> Void) -> Disposable { 62 | return observe { event in 63 | if case .failed(let error) = event { 64 | observer(error) 65 | } 66 | } 67 | } 68 | 69 | /// Register an observer that will be executed on `.completed` event. 70 | public func observeCompleted(with observer: @escaping () -> Void) -> Disposable { 71 | return observe { event in 72 | if case .completed = event { 73 | observer() 74 | } 75 | } 76 | } 77 | 78 | /// Convert the receiver to a concrete signal. 79 | public func toSignal() -> Signal { 80 | return (self as? Signal) ?? Signal(self.observe) 81 | } 82 | } 83 | 84 | extension SignalProtocol { 85 | 86 | /// Attaches a subscriber with closure-based behavior. 87 | /// 88 | /// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber. 89 | /// - parameter receiveComplete: The closure to execute on completion. 90 | /// - parameter receiveValue: The closure to execute on receipt of a value. 91 | /// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 92 | public func sink(receiveCompletion: @escaping ((Subscribers.Completion) -> Void), receiveValue: @escaping ((Element) -> Void)) -> AnyCancellable { 93 | let disposable = observe { event in 94 | switch event { 95 | case .next(let element): 96 | receiveValue(element) 97 | case .failed(let error): 98 | receiveCompletion(.failure(error)) 99 | case .completed: 100 | receiveCompletion(.finished) 101 | } 102 | } 103 | return AnyCancellable(disposable.dispose) 104 | } 105 | } 106 | 107 | extension SignalProtocol where Error == Never { 108 | 109 | /// Attaches a subscriber with closure-based behavior. 110 | /// 111 | /// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber. 112 | /// - parameter receiveValue: The closure to execute on receipt of a value. 113 | /// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 114 | public func sink(receiveValue: @escaping ((Element) -> Void)) -> AnyCancellable { 115 | return sink(receiveCompletion: { _ in }, receiveValue: receiveValue) 116 | } 117 | } 118 | 119 | extension SignalProtocol where Error == Never { 120 | 121 | /// Assigns each element from a signal to a property on an object. 122 | /// 123 | /// - note: The object will be retained as long as the returned cancellable is retained. 124 | public func assign(to keyPath: ReferenceWritableKeyPath, on object: Root) -> AnyCancellable { 125 | return sink(receiveValue: { object[keyPath: keyPath] = $0 }) 126 | } 127 | } 128 | 129 | extension SignalProtocol { 130 | 131 | public func subscribe(_ subscriber: Downstream) where Downstream.Input == Element, Downstream.Failure == Error { 132 | let disposable = CompositeDisposable() 133 | let subscription = DisposableSubscription(disposable: disposable) 134 | subscriber.receive(subscription: subscription) 135 | disposable += observe { event in 136 | switch event { 137 | case .next(let element): 138 | _ = subscriber.receive(element) 139 | case .failed(let error): 140 | subscriber.receive(completion: .failure(error)) 141 | case .completed: 142 | subscriber.receive(completion: .finished) 143 | } 144 | } 145 | } 146 | } 147 | 148 | private class DisposableSubscription: Subscription { 149 | 150 | let disposable: Disposable 151 | 152 | init(disposable: Disposable) { 153 | self.disposable = disposable 154 | } 155 | 156 | func request(_ demand: Subscribers.Demand) { 157 | } 158 | 159 | func cancel() { 160 | disposable.dispose() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/LoadingProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2018 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// A property that lazily loads its value using the given signal producer closure. 28 | /// The value will be loaded when the property is observed for the first time. 29 | public class LoadingProperty: PropertyProtocol, SignalProtocol, DisposeBagProvider { 30 | 31 | private let lock = NSRecursiveLock(name: "com.reactive_kit.loading_property") 32 | 33 | private let signalProducer: () -> LoadingSignal 34 | private let subject = PassthroughSubject, Never>() 35 | 36 | private var _loadingDisposable: Disposable? 37 | 38 | public var bag: DisposeBag { 39 | return subject.disposeBag 40 | } 41 | 42 | private var _loadingState: LoadingState = .loading { 43 | didSet { 44 | subject.send(_loadingState) 45 | } 46 | } 47 | 48 | /// Current state of the property. In `.loading` state until the value is loaded. 49 | /// When the property is observed for the first time, the value will be loaded and 50 | /// the state will be updated to either `.loaded` or `.failed` state. 51 | public var loadingState: LoadingState { 52 | lock.lock(); defer { lock.unlock() } 53 | return _loadingState 54 | } 55 | 56 | /// Underlying value. `nil` if not yet loaded or if the property is in error state. 57 | public var value: LoadingValue? { 58 | get { 59 | return loadingState.value 60 | } 61 | set { 62 | lock.lock(); defer { lock.unlock() } 63 | _loadingState = newValue.flatMap { .loaded($0) } ?? .loading 64 | } 65 | } 66 | 67 | /// Create a loading property with the given signal producer closure. 68 | /// The closure will be executed when the propery is observed for the first time. 69 | public init(_ signalProducer: @escaping () -> LoadingSignal) { 70 | self.signalProducer = signalProducer 71 | } 72 | 73 | /// Create a signal that when observed reloads the property. 74 | /// - parameter silently: When `true` (default), do not transition property to loading or failed states during reload. 75 | public func reload(silently: Bool = true) -> LoadingSignal { 76 | return load(silently: silently) 77 | } 78 | 79 | private func load(silently: Bool) -> LoadingSignal { 80 | return LoadingSignal { observer in 81 | self.lock.lock(); defer { self.lock.unlock() } 82 | if !silently { 83 | self._loadingState = .loading 84 | } 85 | observer.receive(.loading) 86 | self._loadingDisposable = self.signalProducer().observe { event in 87 | switch event { 88 | case .next(let anyLoadingState): 89 | let loadingSate = anyLoadingState.asLoadingState 90 | switch loadingSate { 91 | case .loading: 92 | break 93 | case .loaded: 94 | self.lock.lock(); defer { self.lock.unlock() } 95 | self._loadingState = loadingSate 96 | case .failed: 97 | if !silently { 98 | self.lock.lock(); defer { self.lock.unlock() } 99 | self._loadingState = loadingSate 100 | } 101 | } 102 | observer.receive(loadingSate) 103 | case .completed: 104 | observer.receive(completion: .finished) 105 | case .failed: 106 | break // Never 107 | } 108 | } 109 | 110 | return BlockDisposable { 111 | self.lock.lock(); defer { self.lock.unlock() } 112 | self._loadingDisposable?.dispose() 113 | self._loadingDisposable = nil 114 | } 115 | } 116 | } 117 | 118 | public func observe(with observer: @escaping (Signal, Never>.Event) -> Void) -> Disposable { 119 | lock.lock(); defer { lock.unlock() } 120 | if case .loading = _loadingState, _loadingDisposable == nil { 121 | _loadingDisposable = load(silently: false).observeCompleted { [weak self] in 122 | guard let self = self else { return } 123 | self.lock.lock(); defer { self.lock.unlock() } 124 | self._loadingDisposable = nil 125 | } 126 | } 127 | return subject.prepend(_loadingState).observe(with: observer) 128 | } 129 | } 130 | 131 | extension SignalProtocol { 132 | 133 | /// Pauses the propagation of the receiver's elements until the given property is reloaded. 134 | public func reloading(_ property: LoadingProperty) -> Signal { 135 | return flatMapLatest { (element: Element) -> Signal in 136 | return property.reload().dematerializeLoadingState().map { _ in element } 137 | } 138 | } 139 | } 140 | 141 | extension SignalProtocol where Element: LoadingStateProtocol, Error == Never { 142 | 143 | /// Pauses the propagation of the receiver's loading values until the given property is reloaded. 144 | public func reloading(_ property: LoadingProperty, strategy: FlattenStrategy = .latest) -> LoadingSignal { 145 | return flatMapValue { (value: LoadingValue) -> LoadingSignal in 146 | return property.reload().mapValue { _ in value } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Transforming.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Batch signal elements into arrays of the given size. 30 | /// 31 | /// Check out interactive example at [https://rxmarbles.com/#bufferCount](https://rxmarbles.com/#bufferCount) 32 | public func buffer(size: Int) -> Signal<[Element], Error> { 33 | return Signal { observer in 34 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.buffer") 35 | var _buffer: [Element] = [] 36 | return self.observe { event in 37 | switch event { 38 | case .next(let element): 39 | lock.lock(); defer { lock.unlock() } 40 | _buffer.append(element) 41 | if _buffer.count == size { 42 | observer.receive(_buffer) 43 | _buffer.removeAll() 44 | } 45 | case .failed(let error): 46 | observer.receive(completion: .failure(error)) 47 | case .completed: 48 | observer.receive(completion: .finished) 49 | } 50 | } 51 | } 52 | } 53 | 54 | /// Collect all elements into an array and emit the array as a single element. 55 | public func collect() -> Signal<[Element], Error> { 56 | return reduce([], { memo, new in memo + [new] }) 57 | } 58 | 59 | /// Emit default element if the signal completes without emitting any element. 60 | /// 61 | /// Check out interactive example at [https://rxmarbles.com/#defaultIfEmpty](https://rxmarbles.com/#defaultIfEmpty) 62 | public func replaceEmpty(with element: Element) -> Signal { 63 | return Signal { observer in 64 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.default_if_empty") 65 | var _didEmitNonTerminal = false 66 | return self.observe { event in 67 | switch event { 68 | case .next(let element): 69 | lock.lock(); defer { lock.unlock() } 70 | _didEmitNonTerminal = true 71 | observer.receive(element) 72 | case .failed(let error): 73 | observer.receive(completion: .failure(error)) 74 | case .completed: 75 | lock.lock(); defer { lock.unlock() } 76 | if !_didEmitNonTerminal { 77 | observer.receive(element) 78 | } 79 | observer.receive(completion: .finished) 80 | } 81 | } 82 | } 83 | } 84 | 85 | /// Map all elements to instances of Void. 86 | public func eraseType() -> Signal { 87 | return replaceElements(with: ()) 88 | } 89 | 90 | /// Par each element with its predecessor, starting from the second element. 91 | /// Similar to `zipPrevious`, but starts from the second element. 92 | /// 93 | /// Check out interactive example at [https://rxmarbles.com/#pairwise](https://rxmarbles.com/#pairwise) 94 | public func pairwise() -> Signal<(Element, Element), Error> { 95 | return zipPrevious().compactMap { a, b in a.flatMap { ($0, b) } } 96 | } 97 | 98 | /// Replace all emitted elements with the given element. 99 | public func replaceElements(with element: ReplacementElement) -> Signal { 100 | return map { _ in element } 101 | } 102 | 103 | /// Reduce all elements to a single element. Similar to `scan`, but emits only the final element. 104 | /// 105 | /// Check out interactive example at [https://rxmarbles.com/#reduce](https://rxmarbles.com/#reduce) 106 | public func reduce(_ initial: U, _ combine: @escaping (U, Element) -> U) -> Signal { 107 | return scan(initial, combine).last() 108 | } 109 | 110 | /// Apply `combine` to each element starting with `initial` and emit each 111 | /// intermediate result. This differs from `reduce` which only emits the final result. 112 | /// 113 | /// Check out interactive example at [https://rxmarbles.com/#scan](https://rxmarbles.com/#scan) 114 | public func scan(_ initial: U, _ combine: @escaping (U, Element) -> U) -> Signal { 115 | return Signal { observer in 116 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.scan") 117 | var _accumulator = initial 118 | observer.receive(_accumulator) 119 | return self.observe { event in 120 | switch event { 121 | case .next(let element): 122 | lock.lock(); defer { lock.unlock() } 123 | _accumulator = combine(_accumulator, element) 124 | observer.receive(_accumulator) 125 | case .failed(let error): 126 | observer.receive(completion: .failure(error)) 127 | case .completed: 128 | observer.receive(completion: .finished) 129 | } 130 | } 131 | } 132 | } 133 | 134 | /// Prepend the given element to the signal element sequence. 135 | /// 136 | /// Check out interactive example at [https://rxmarbles.com/#startWith](https://rxmarbles.com/#startWith) 137 | public func prepend(_ element: Element) -> Signal { 138 | return scan(element, { _, next in next }) 139 | } 140 | 141 | /// Append the given element to the signal element sequence. 142 | public func append(_ element: Element) -> Signal { 143 | return append(Signal(just: element)) 144 | } 145 | 146 | /// Batch each `size` elements into another signal. 147 | public func window(ofSize size: Int) -> Signal, Error> { 148 | return buffer(size: size).map { Signal(sequence: $0) } 149 | } 150 | 151 | /// Par each element with its predecessor. 152 | /// Similar to `parwise`, but starts from the first element which is paird with `nil`. 153 | public func zipPrevious() -> Signal<(Element?, Element), Error> { 154 | return scan(nil) { (pair, next) in (pair?.1, next) }.ignoreNils() 155 | } 156 | } 157 | 158 | extension SignalProtocol where Element == Void { 159 | 160 | public func prepend() -> Signal { 161 | prepend(()) 162 | } 163 | 164 | public func append() -> Signal { 165 | append(()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/Subjects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// A type that is both a signal and an observer. 28 | public protocol SubjectProtocol: SignalProtocol, ObserverProtocol { 29 | } 30 | 31 | extension SubjectProtocol { 32 | 33 | /// Convenience method to send `.next` event. 34 | public func send(_ element: Element) { 35 | on(.next(element)) 36 | } 37 | 38 | /// Convenience method to send `.failed` or `.completed` event. 39 | public func send(completion: Subscribers.Completion) { 40 | switch completion { 41 | case .finished: 42 | on(.completed) 43 | case .failure(let error): 44 | on(.failed(error)) 45 | } 46 | } 47 | 48 | /// Convenience method to send `.next` event followed by a `.completed` event. 49 | public func send(lastElement element: Element) { 50 | send(element) 51 | send(completion: .finished) 52 | } 53 | } 54 | 55 | extension SubjectProtocol where Element == Void { 56 | 57 | /// Convenience method to send `.next` event. 58 | public func send() { 59 | send(()) 60 | } 61 | } 62 | 63 | /// A type that is both a signal and an observer. 64 | /// Subject is a base subject class, please use one of the subclassesin your code. 65 | open class Subject: SubjectProtocol { 66 | 67 | internal let lock = NSRecursiveLock(name: "com.reactive_kit.subject.lock") 68 | 69 | private typealias Token = Int64 70 | private var nextToken: Token = 0 71 | 72 | private var observers: Atomic<[(Token, Observer)]> = Atomic([]) 73 | 74 | public private(set) var isTerminated: Bool = false 75 | 76 | public let disposeBag = DisposeBag() 77 | 78 | public init() {} 79 | 80 | open func on(_ event: Signal.Event) { 81 | guard !isTerminated else { return } 82 | isTerminated = event.isTerminal 83 | for (_, observer) in observers.value { 84 | observer(event) 85 | } 86 | } 87 | 88 | open func observe(with observer: @escaping Observer) -> Disposable { 89 | let token = nextToken 90 | nextToken += 1 91 | observers.mutate { 92 | $0.append((token, observer)) 93 | } 94 | return BlockDisposable { [weak self] in 95 | self?.observers.mutate { 96 | $0.removeAll(where: { (t, _) in t == token }) 97 | } 98 | } 99 | } 100 | } 101 | 102 | extension Subject: BindableProtocol { 103 | 104 | public func bind(signal: Signal) -> Disposable { 105 | return signal 106 | .prefix(untilOutputFrom: disposeBag.deallocated) 107 | .receive(on: ExecutionContext.nonRecursive()) 108 | .observeNext { [weak self] element in 109 | guard let s = self else { return } 110 | s.on(.next(element)) 111 | } 112 | } 113 | } 114 | 115 | /// A subject that propagates received events to the registered observes. 116 | public final class PassthroughSubject: Subject { 117 | 118 | public override init() { 119 | super.init() 120 | } 121 | 122 | public override func on(_ event: Signal.Event) { 123 | lock.lock(); defer { lock.unlock() } 124 | super.on(event) 125 | } 126 | 127 | public override func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 128 | lock.lock(); defer { lock.unlock() } 129 | return super.observe(with: observer) 130 | } 131 | } 132 | 133 | /// A subject that replies accumulated sequence of events to each observer. 134 | public final class ReplaySubject: Subject { 135 | 136 | private var _buffer: ArraySlice.Event> = [] 137 | 138 | public let bufferSize: Int 139 | 140 | public init(bufferSize: Int = Int.max) { 141 | if bufferSize < Int.max { 142 | self.bufferSize = bufferSize + 1 // plus terminal event 143 | } else { 144 | self.bufferSize = bufferSize 145 | } 146 | } 147 | 148 | public override func on(_ event: Signal.Event) { 149 | lock.lock(); defer { lock.unlock() } 150 | guard !isTerminated else { return } 151 | _buffer.append(event) 152 | _buffer = _buffer.suffix(bufferSize) 153 | super.on(event) 154 | } 155 | 156 | public override func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 157 | lock.lock(); defer { lock.unlock() } 158 | let buffer = _buffer 159 | buffer.forEach(observer) 160 | return super.observe(with: observer) 161 | } 162 | } 163 | 164 | /// A ReplaySubject compile-time guaranteed never to emit an error. 165 | public typealias SafeReplaySubject = ReplaySubject 166 | 167 | /// A subject that replies latest event to each observer. 168 | public final class ReplayOneSubject: Subject { 169 | 170 | private var _lastEvent: Signal.Event? 171 | private var _terminalEvent: Signal.Event? 172 | 173 | public override init() { 174 | super.init() 175 | } 176 | 177 | public override func on(_ event: Signal.Event) { 178 | lock.lock(); defer { lock.unlock() } 179 | guard !isTerminated else { return } 180 | if event.isTerminal { 181 | _terminalEvent = event 182 | } else { 183 | _lastEvent = event 184 | } 185 | super.on(event) 186 | } 187 | 188 | public override func observe(with observer: @escaping (Signal.Event) -> Void) -> Disposable { 189 | lock.lock(); defer { lock.unlock() } 190 | let (lastEvent, terminalEvent) = (_lastEvent, _terminalEvent) 191 | if let event = lastEvent { 192 | observer(event) 193 | } 194 | if let event = terminalEvent { 195 | observer(event) 196 | } 197 | return super.observe(with: observer) 198 | } 199 | } 200 | 201 | /// A ReplayOneSubject compile-time guaranteed never to emit an error. 202 | public typealias SafeReplayOneSubject = ReplayOneSubject 203 | 204 | /// A subject that replies accumulated sequence of loading values to each observer. 205 | public final class ReplayLoadingValueSubject: Subject, Error> { 206 | 207 | private enum State { 208 | case notStarted 209 | case loading 210 | case loadedOrFailedAtLeastOnce 211 | } 212 | 213 | private var _state: State = .notStarted 214 | private var _buffer: ArraySlice> = [] 215 | private var _terminalEvent: Signal, Error>.Event? 216 | 217 | public let bufferSize: Int 218 | 219 | public init(bufferSize: Int = Int.max) { 220 | self.bufferSize = bufferSize 221 | } 222 | 223 | public override func on(_ event: Signal, Error>.Event) { 224 | lock.lock(); defer { lock.unlock() } 225 | guard !isTerminated else { return } 226 | switch event { 227 | case .next(let loadingState): 228 | switch loadingState { 229 | case .loading: 230 | if _state == .notStarted { 231 | _state = .loading 232 | } 233 | case .loaded: 234 | _state = .loadedOrFailedAtLeastOnce 235 | _buffer.append(loadingState) 236 | _buffer = _buffer.suffix(bufferSize) 237 | case .failed: 238 | _state = .loadedOrFailedAtLeastOnce 239 | _buffer = [loadingState] 240 | } 241 | case .failed, .completed: 242 | _terminalEvent = event 243 | } 244 | super.on(event) 245 | } 246 | 247 | public override func observe(with observer: @escaping (Signal, Error>.Event) -> Void) -> Disposable { 248 | lock.lock(); defer { lock.unlock() } 249 | switch _state { 250 | case .notStarted: 251 | break 252 | case .loading: 253 | observer(.next(.loading)) 254 | case .loadedOrFailedAtLeastOnce: 255 | _buffer.forEach { observer(.next($0)) } 256 | } 257 | if let event = _terminalEvent { 258 | observer(event) 259 | } 260 | return super.observe(with: observer) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/Bindable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | /// Bindable is like an observer, but knows to manage the subscription by itself. 28 | public protocol BindableProtocol { 29 | 30 | /// Type of the received elements. 31 | associatedtype Element 32 | 33 | /// Establish a one-way binding between the signal and the receiver. 34 | /// - Warning: You are recommended to use `bind(to:)` on the signal when binding. 35 | func bind(signal: Signal) -> Disposable 36 | } 37 | 38 | extension SignalProtocol where Error == Never { 39 | 40 | /// Establish a one-way binding between the source and the bindable. 41 | /// - Parameter bindable: A binding target that will receive signal events. 42 | /// - Returns: A disposable that can cancel the binding. 43 | @discardableResult 44 | public func bind(to bindable: B) -> Disposable where B.Element == Element { 45 | return bindable.bind(signal: toSignal()) 46 | } 47 | 48 | /// Establish a one-way binding between the source and the bindable. 49 | /// - Parameter bindable: A binding target that will receive signal events. 50 | /// - Returns: A disposable that can cancel the binding. 51 | @discardableResult 52 | public func bind(to bindable: B) -> Disposable where B.Element: OptionalProtocol, B.Element.Wrapped == Element { 53 | return map { B.Element($0) }.bind(to: bindable) 54 | } 55 | } 56 | 57 | extension BindableProtocol where Self: SignalProtocol, Self.Error == Never { 58 | 59 | /// Establish a two-way binding between the source and the bindable. 60 | /// - Parameter target: A binding target that will receive events from 61 | /// the receiver and a source that will send events to the receiver. 62 | /// - Returns: A disposable that can cancel the binding. 63 | @discardableResult 64 | public func bidirectionalBind(to target: B) -> Disposable where B.Element == Element, B.Error == Error { 65 | let scheduler = ExecutionContext.nonRecursive() 66 | let d1 = receive(on: scheduler).bind(to: target) 67 | let d2 = target.receive(on: scheduler).bind(to: self) 68 | return CompositeDisposable([d1, d2]) 69 | } 70 | } 71 | 72 | extension SignalProtocol where Error == Never { 73 | 74 | /// Bind the receiver to the target using the given setter closure. Closure is 75 | /// called whenever the signal emits `next` event. 76 | /// 77 | /// Binding lives until either the signal completes or the target is deallocated. 78 | /// That means that the returned disposable can be safely ignored. 79 | /// 80 | /// - Parameters: 81 | /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding 82 | /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. 83 | /// Also conforms to `BindingExecutionContextProvider` that provides that context on which to execute the setter. 84 | /// - setter: A closure that gets called on each next signal event both with the target and the sent element. 85 | /// - Returns: A disposable that can cancel the binding. 86 | @discardableResult 87 | public func bind(to target: Target, setter: @escaping (Target, Element) -> Void) -> Disposable 88 | where Target: BindingExecutionContextProvider 89 | { 90 | return bind(to: target, context: target.bindingExecutionContext, setter: setter) 91 | } 92 | 93 | /// Bind the receiver to the target using the given setter closure. Closure is 94 | /// called whenever the signal emits `next` event. 95 | /// 96 | /// Binding lives until either the signal completes or the target is deallocated. 97 | /// That means that the returned disposable can be safely ignored. 98 | /// 99 | /// - Parameters: 100 | /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding 101 | /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. 102 | /// - context: An execution context on which to execute the setter. 103 | /// - setter: A closure that gets called on each next signal event both with the target and the sent element. 104 | /// - Returns: A disposable that can cancel the binding. 105 | @discardableResult 106 | public func bind(to target: Target, context: ExecutionContext, setter: @escaping (Target, Element) -> Void) -> Disposable { 107 | return prefix(untilOutputFrom: target.deallocated).receive(on: context).observeNext { [weak target] element in 108 | if let target = target { 109 | setter(target, element) 110 | } 111 | } 112 | } 113 | 114 | /// Bind the receiver to target's property specified by the key path. The property is 115 | /// updated whenever the signal emits `next` event. 116 | /// 117 | /// Binding lives until either the signal completes or the target is deallocated. 118 | /// That means that the returned disposable can be safely ignored. 119 | /// 120 | /// - Parameters: 121 | /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding 122 | /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. 123 | /// - keyPath: A key path to the property that will be updated with each sent element. 124 | /// - Returns: A disposable that can cancel the binding. 125 | @discardableResult 126 | public func bind(to target: Target, keyPath: ReferenceWritableKeyPath) -> Disposable where Target: BindingExecutionContextProvider 127 | { 128 | return bind(to: target) { (target, element) in 129 | target[keyPath: keyPath] = element 130 | } 131 | } 132 | 133 | /// Bind the receiver to target's property specified by the key path. The property is 134 | /// updated whenever the signal emits `next` event. 135 | /// 136 | /// Binding lives until either the signal completes or the target is deallocated. 137 | /// That means that the returned disposable can be safely ignored. 138 | /// 139 | /// - Parameters: 140 | /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding 141 | /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. 142 | /// - keyPath: A key path to the property that will be updated with each sent element. 143 | /// - context: An execution context on which to execute the setter. 144 | /// - Returns: A disposable that can cancel the binding. 145 | @discardableResult 146 | public func bind(to target: Target, keyPath: ReferenceWritableKeyPath, context: ExecutionContext) -> Disposable 147 | { 148 | return bind(to: target, context: context) { (target, element) in 149 | target[keyPath: keyPath] = element 150 | } 151 | } 152 | } 153 | 154 | extension SignalProtocol where Error == Never, Element == Void { 155 | 156 | /// Bind the receiver to the target using the given setter closure. Closure is 157 | /// called whenever the signal emits `next` event. 158 | /// 159 | /// Binding lives until either the signal completes or the target is deallocated. 160 | /// That means that the returned disposable can be safely ignored. 161 | /// 162 | /// - Parameters: 163 | /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding 164 | /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. 165 | /// Also conforms to `BindingExecutionContextProvider` that provides that context on which to execute the setter. 166 | /// - setter: A closure that gets called on each next signal event with the target. 167 | /// - Returns: A disposable that can cancel the binding. 168 | @discardableResult 169 | public func bind(to target: Target, setter: @escaping (Target) -> Void) -> Disposable 170 | where Target: BindingExecutionContextProvider 171 | { 172 | return bind(to: target, context: target.bindingExecutionContext, setter: setter) 173 | } 174 | 175 | /// Bind the receiver to the target using the given setter closure. Closure is 176 | /// called whenever the signal emits `next` event. 177 | /// 178 | /// Binding lives until either the signal completes or the target is deallocated. 179 | /// That means that the returned disposable can be safely ignored. 180 | /// 181 | /// - Parameters: 182 | /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding 183 | /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. 184 | /// - context: An execution context on which to execute the setter. 185 | /// - setter: A closure that gets called on each next signal event with the target. 186 | /// - Returns: A disposable that can cancel the binding. 187 | @discardableResult 188 | public func bind(to target: Target, context: ExecutionContext, setter: @escaping (Target) -> Void) -> Disposable { 189 | return prefix(untilOutputFrom: target.deallocated).receive(on: context).observeNext { [weak target] _ in 190 | if let target = target { 191 | setter(target) 192 | } 193 | } 194 | } 195 | } 196 | 197 | /// Provides an execution context used to deliver binding events. 198 | public protocol BindingExecutionContextProvider { 199 | 200 | /// An execution context used to deliver binding events. 201 | var bindingExecutionContext: ExecutionContext { get } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+ErrorHandling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Transform error by applying `transform` on it. 30 | public func mapError(_ transform: @escaping (Error) -> F) -> Signal { 31 | return Signal { observer in 32 | return self.observe { event in 33 | switch event { 34 | case .next(let element): 35 | observer.receive(element) 36 | case .failed(let error): 37 | observer.receive(completion: .failure(transform(error))) 38 | case .completed: 39 | observer.receive(completion: .finished) 40 | } 41 | } 42 | } 43 | } 44 | 45 | /// Branch out error into another signal. 46 | public func branchOutError() -> (Signal, Signal) { 47 | let shared = share() 48 | return (shared.suppressError(logging: false), shared.toErrorSignal()) 49 | } 50 | 51 | /// Branch out mapped error into another signal. 52 | public func branchOutError(_ mapError: @escaping (Error) -> F) -> (Signal, Signal) { 53 | let shared = share() 54 | return (shared.suppressError(logging: false), shared.toErrorSignal().map(mapError)) 55 | } 56 | 57 | /// Convert signal into a non-failable signal by suppressing the error. 58 | public func suppressError(logging: Bool, file: String = #file, line: Int = #line) -> Signal { 59 | return Signal { observer in 60 | return self.observe { event in 61 | switch event { 62 | case .next(let element): 63 | observer.receive(element) 64 | case .failed(let error): 65 | observer.receive(completion: .finished) 66 | if logging { 67 | print("Signal at \(file):\(line) encountered an error: \(error)") 68 | } 69 | case .completed: 70 | observer.receive(completion: .finished) 71 | } 72 | } 73 | } 74 | } 75 | 76 | /// Convert signal into a non-failable signal by feeding suppressed error into a subject. 77 | public func suppressAndFeedError(into listener: S, logging: Bool = true, file: String = #file, line: Int = #line) -> Signal where S.Element == Error { 78 | return feedError(into: listener).suppressError(logging: logging, file: file, line: line) 79 | } 80 | 81 | /// Recover the signal by propagating default element if an error happens. 82 | public func replaceError(with element: Element) -> Signal { 83 | return Signal { observer in 84 | return self.observe { event in 85 | switch event { 86 | case .next(let element): 87 | observer.receive(element) 88 | case .failed: 89 | observer.receive(element) 90 | observer.receive(completion: .finished) 91 | case .completed: 92 | observer.receive(completion: .finished) 93 | } 94 | } 95 | } 96 | } 97 | 98 | /// Retry the signal in case of failure at most `times` number of times. 99 | public func retry(_ times: Int, if shouldRetry: @escaping (Error) -> Bool = { _ in true }) -> Signal { 100 | guard times > 0 else { return toSignal() } 101 | return Signal { observer in 102 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.retry") 103 | var _remainingAttempts = times 104 | let serialDisposable = SerialDisposable(otherDisposable: nil) 105 | var _attempt: (() -> Void)? 106 | _attempt = { 107 | serialDisposable.otherDisposable?.dispose() 108 | serialDisposable.otherDisposable = self.observe { event in 109 | switch event { 110 | case .next(let element): 111 | observer.receive(element) 112 | case .failed(let error): 113 | lock.lock(); defer { lock.unlock() } 114 | if _remainingAttempts > 0 && shouldRetry(error) { 115 | _remainingAttempts -= 1 116 | _attempt?() 117 | } else { 118 | _attempt = nil 119 | observer.receive(completion: .failure(error)) 120 | } 121 | case .completed: 122 | lock.lock(); defer { lock.unlock() } 123 | _attempt = nil 124 | observer.receive(completion: .finished) 125 | } 126 | } 127 | } 128 | lock.lock(); defer { lock.unlock() } 129 | _attempt?() 130 | return BlockDisposable { 131 | serialDisposable.dispose() 132 | lock.lock(); defer { lock.unlock() } 133 | _attempt = nil 134 | } 135 | } 136 | } 137 | 138 | /// Retry the failed signal when other signal produces an element. 139 | /// - parameter other: Signal that triggers a retry attempt. 140 | /// - parameter shouldRetry: Retries only if this returns true for a given error. Defaults to always returning true. 141 | public func retry(when other: S, if shouldRetry: @escaping (Error) -> Bool = { _ in true }) -> Signal where S.Error == Never { 142 | return Signal { observer in 143 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.retry") 144 | let serialDisposable = SerialDisposable(otherDisposable: nil) 145 | var _attempt: (() -> Void)? 146 | _attempt = { 147 | serialDisposable.otherDisposable?.dispose() 148 | let compositeDisposable = CompositeDisposable() 149 | serialDisposable.otherDisposable = compositeDisposable 150 | compositeDisposable += self.observe { event in 151 | switch event { 152 | case .next(let element): 153 | observer.receive(element) 154 | case .completed: 155 | lock.lock(); defer { lock.unlock() } 156 | _attempt = nil 157 | serialDisposable.otherDisposable?.dispose() 158 | observer.receive(completion: .finished) 159 | case .failed(let error): 160 | if shouldRetry(error) { 161 | compositeDisposable += other.first().observe { otherEvent in 162 | lock.lock(); defer { lock.unlock() } 163 | switch otherEvent { 164 | case .next: 165 | _attempt?() 166 | default: 167 | break 168 | } 169 | } 170 | } else { 171 | lock.lock(); defer { lock.unlock() } 172 | _attempt = nil 173 | serialDisposable.otherDisposable?.dispose() 174 | observer.receive(completion: .failure(error)) 175 | } 176 | } 177 | } 178 | } 179 | 180 | lock.lock(); defer { lock.unlock() } 181 | _attempt?() 182 | 183 | return BlockDisposable { 184 | lock.lock(); defer { lock.unlock() } 185 | _attempt = nil 186 | serialDisposable.dispose() 187 | } 188 | } 189 | } 190 | 191 | /// Error out if the `interval` time passes with no emitted elements. 192 | public func timeout(after interval: Double, with error: Error, on queue: DispatchQueue = DispatchQueue(label: "com.reactive_kit.signal.timeout")) -> Signal { 193 | return Signal { observer in 194 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.timeout") 195 | var _completed = false 196 | let timeoutWhenPossible: () -> Disposable = { 197 | return queue.disposableAfter(when: interval) { 198 | lock.lock(); defer { lock.unlock() } 199 | if !_completed { 200 | _completed = true 201 | observer.receive(completion: .failure(error)) 202 | } 203 | } 204 | } 205 | var _lastSubscription = timeoutWhenPossible() 206 | return self.observe { event in 207 | lock.lock(); defer { lock.unlock() } 208 | _lastSubscription.dispose() 209 | observer.on(event) 210 | _completed = event.isTerminal 211 | _lastSubscription = timeoutWhenPossible() 212 | } 213 | } 214 | } 215 | 216 | /// Map failable signal into a non-failable signal of errors. Ignores `.next` events. 217 | public func toErrorSignal() -> Signal { 218 | return Signal { observer in 219 | return self.observe { taskEvent in 220 | switch taskEvent { 221 | case .next: 222 | break 223 | case .completed: 224 | observer.receive(completion: .finished) 225 | case .failed(let error): 226 | observer.receive(error) 227 | observer.receive(completion: .finished) 228 | } 229 | } 230 | } 231 | } 232 | } 233 | 234 | extension SignalProtocol where Error == Never { 235 | 236 | /// Safe error casting from Never to some Error type. 237 | public func castError() -> Signal { 238 | return Signal { observer in 239 | return self.observe { event in 240 | switch event { 241 | case .next(let element): 242 | observer.receive(element) 243 | case .completed: 244 | observer.receive(completion: .finished) 245 | } 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Sources/SignalProtocol+Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2016-2019 Srdan Rasic (@srdanrasic) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | extension SignalProtocol { 28 | 29 | /// Raises a debugger signal when a provided closure needs to stop the process in the debugger. 30 | /// 31 | /// When any of the provided closures returns `true`, this signal raises the `SIGTRAP` signal to stop the process in the debugger. 32 | /// Otherwise, this signal passes through values and completions as-is. 33 | /// 34 | /// - Parameters: 35 | /// - receiveSubscription: A closure that executes when when the signal receives a subscription. Return `true` from this closure to raise `SIGTRAP`, or false to continue. 36 | /// - receiveOutput: A closure that executes when when the signal receives a value. Return `true` from this closure to raise `SIGTRAP`, or false to continue. 37 | /// - receiveCompletion: A closure that executes when when the signal receives a completion. Return `true` from this closure to raise `SIGTRAP`, or false to continue. 38 | /// - Returns: A signal that raises a debugger signal when one of the provided closures returns `true`. 39 | @inlinable 40 | public func breakpoint(receiveSubscription: (() -> Bool)? = nil, receiveOutput: ((Element) -> Bool)? = nil, receiveCompletion: ((Subscribers.Completion) -> Bool)? = nil) -> Signal { 41 | return handleEvents( 42 | receiveSubscription: { 43 | if receiveSubscription?() ?? false { 44 | raise(SIGTRAP) 45 | } 46 | }, receiveOutput: { (element) in 47 | if receiveOutput?(element) ?? false { 48 | raise(SIGTRAP) 49 | } 50 | }, receiveCompletion: { (completion) in 51 | if receiveCompletion?(completion) ?? false { 52 | raise(SIGTRAP) 53 | } 54 | }) 55 | } 56 | 57 | /// Raises a debugger signal upon receiving a failure. 58 | /// 59 | /// When the upstream signal fails with an error, this signal raises the `SIGTRAP` signal, which stops the process in the debugger. 60 | /// Otherwise, this signal passes through values and completions as-is. 61 | /// - Returns: A signal that raises a debugger signal upon receiving a failure. 62 | @inlinable 63 | public func breakpointOnError() -> Signal { 64 | return breakpoint(receiveOutput: { _ in false }, receiveCompletion: { (completion) -> Bool in 65 | switch completion { 66 | case .failure: 67 | return true 68 | case .finished: 69 | return false 70 | } 71 | }) 72 | } 73 | 74 | /// Log various signal events. If title is not provided, source file and function names are printed instead. 75 | public func debug(_ title: String? = nil, file: String = #file, function: String = #function, line: Int = #line) -> Signal { 76 | let prefix: String 77 | if let title = title { 78 | prefix = "[\(title)]" 79 | } else { 80 | let filename = file.components(separatedBy: "/").last ?? file 81 | prefix = "[\(filename):\(function):\(line)]" 82 | } 83 | return handleEvents( 84 | receiveSubscription: { 85 | print("\(prefix) started") 86 | }, receiveOutput: { (element) in 87 | print("\(prefix) next(\(element))") 88 | }, receiveCompletion: { (completion) in 89 | switch completion { 90 | case .failure(let error): 91 | print("\(prefix) failed: \(error)") 92 | case .finished: 93 | print("\(prefix) finished") 94 | } 95 | }, receiveCancel: { 96 | print("\(prefix) disposed") 97 | }) 98 | } 99 | 100 | /// Delay signal elements for `interval` time. 101 | /// 102 | /// Check out interactive example at [https://rxmarbles.com/#delay](https://rxmarbles.com/#delay) 103 | public func delay(interval: Double, on queue: DispatchQueue = DispatchQueue(label: "reactive_kit.delay")) -> Signal { 104 | return Signal { observer in 105 | return self.observe { event in 106 | queue.asyncAfter(deadline: .now() + interval) { 107 | observer.on(event) 108 | } 109 | } 110 | } 111 | } 112 | 113 | /// Repeat the receiver signal whenever the signal returned from the given closure emits an element. 114 | public func `repeat`(when other: @escaping (Element) -> S) -> Signal where S.Error == Never { 115 | return Signal { observer in 116 | let lock = NSRecursiveLock(name: "com.reactive_kit.signal.repeat") 117 | var _attempt: (() -> Void)? 118 | let outerDisposable = SerialDisposable(otherDisposable: nil) 119 | let innerDisposable = SerialDisposable(otherDisposable: nil) 120 | var _completions: (me: Bool, other: Bool) = (false, false) 121 | func _completeIfPossible() { 122 | if _completions.me && _completions.other { 123 | observer.receive(completion: .finished) 124 | _attempt = nil 125 | } 126 | } 127 | _attempt = { 128 | outerDisposable.otherDisposable?.dispose() 129 | outerDisposable.otherDisposable = self.observe { event in 130 | lock.lock(); defer { lock.unlock() } 131 | switch event { 132 | case .next(let element): 133 | observer.receive(element) 134 | _completions.other = false 135 | innerDisposable.otherDisposable?.dispose() 136 | innerDisposable.otherDisposable = other(element).observe { otherEvent in 137 | lock.lock(); defer { lock.unlock() } 138 | switch otherEvent { 139 | case .next: 140 | _completions.me = false 141 | _attempt?() 142 | case .completed: 143 | _completions.other = true 144 | _completeIfPossible() 145 | } 146 | } 147 | case .completed: 148 | _completions.me = true 149 | _completeIfPossible() 150 | case .failed(let error): 151 | observer.receive(completion: .failure(error)) 152 | } 153 | } 154 | } 155 | lock.lock(); defer { lock.unlock() } 156 | _attempt?() 157 | return CompositeDisposable([outerDisposable, innerDisposable]) 158 | } 159 | } 160 | 161 | /// Performs the specified closures when signal events occur. 162 | /// 163 | /// - Parameters: 164 | /// - receiveSubscription: A closure that executes when the signal receives the subscription. Defaults to `nil`. 165 | /// - receiveOutput: A closure that executes when the signal receives a value from the upstream signal. Defaults to `nil`. 166 | /// - receiveCompletion: A closure that executes when the signal receives the completion from the upstream signal. Defaults to `nil`. 167 | /// - receiveCancel: A closure that executes when the downstream receiver is cancelled (disposed). Defaults to `nil`. 168 | /// - Returns: A publisher that performs the specified closures when publisher events occur. 169 | @inlinable 170 | public func handleEvents(receiveSubscription: (() -> Void)? = nil, receiveOutput: ((Element) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, receiveCancel: (() -> Void)? = nil) -> Signal { 171 | return Signal { observer in 172 | receiveSubscription?() 173 | let disposable = self.observe { event in 174 | switch event { 175 | case .next(let value): 176 | receiveOutput?(value) 177 | case .failed(let error): 178 | receiveCompletion?(.failure(error)) 179 | case .completed: 180 | receiveCompletion?(.finished) 181 | } 182 | observer.on(event) 183 | } 184 | return BlockDisposable { 185 | disposable.dispose() 186 | receiveCancel?() 187 | } 188 | } 189 | } 190 | 191 | /// Update the given subject with `true` when the receiver starts and with `false` when the receiver terminates. 192 | public func feedActivity(into listener: S) -> Signal where S.Element == Bool { 193 | return handleEvents(receiveSubscription: { listener.send(true) }, receiveCancel: { listener.send(false) }) 194 | } 195 | 196 | /// Update the given subject with `.next` elements. 197 | public func feedNext(into listener: S) -> Signal where S.Element == Element { 198 | return handleEvents(receiveOutput: { e in listener.send(e) }) 199 | } 200 | 201 | /// Update the given subject with mapped `.next` element whenever the element satisfies the given constraint. 202 | public func feedNext(into listener: S, when: @escaping (Element) -> Bool = { _ in true }, map: @escaping (Element) -> S.Element) -> Signal { 203 | return handleEvents(receiveOutput: { e in if when(e) { listener.send(map(e)) } }) 204 | } 205 | 206 | /// Updates the given subject with error from .failed event is such occurs. 207 | public func feedError(into listener: S) -> Signal where S.Element == Error { 208 | return handleEvents(receiveCompletion: { completion in 209 | switch completion { 210 | case .failure(let error): 211 | listener.send(error) 212 | case .finished: 213 | break 214 | } 215 | }) 216 | } 217 | 218 | /// Blocks the current thread until the signal completes and then returns all events sent by the signal collected in an array. 219 | /// 220 | /// This operator is useful for testing purposes. 221 | public func waitAndCollectEvents() -> [Signal.Event] { 222 | let semaphore = DispatchSemaphore(value: 0) 223 | var collectedEvents: [Signal.Event] = [] 224 | _ = materialize().collect().observeNext { events in 225 | collectedEvents.append(contentsOf: events) 226 | semaphore.signal() 227 | } 228 | semaphore.wait() 229 | return collectedEvents 230 | } 231 | 232 | /// Blocks the current thread until the signal completes and then returns all elements sent by the signal collected in an array. 233 | /// 234 | /// This operator is useful for testing purposes. 235 | public func waitAndCollectElements() -> [Element] { 236 | return waitAndCollectEvents().compactMap { event in 237 | switch event { 238 | case .next(let element): 239 | return element 240 | default: 241 | return nil 242 | } 243 | } 244 | } 245 | } 246 | --------------------------------------------------------------------------------