├── .github └── workflows │ ├── autoinvite.yml │ └── tests.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CombineExt.podspec ├── CombineExt.xcodeproj ├── CombineExtTests_Info.plist ├── CombineExt_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── CombineExt-Package.xcscheme ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Resources └── logo.png ├── Sources ├── Common │ ├── DemandBuffer.swift │ └── Sink.swift ├── Convenience │ └── Optional.swift ├── Models │ ├── Event.swift │ └── ObjectOwnership.swift ├── Operators │ ├── Amb.swift │ ├── AssignOwnership.swift │ ├── AssignToMany.swift │ ├── CombineLatestMany.swift │ ├── Create.swift │ ├── Dematerialize.swift │ ├── Enumerated.swift │ ├── FilterMany.swift │ ├── FlatMapBatches.swift │ ├── FlatMapFirst.swift │ ├── FlatMapLatest.swift │ ├── IgnoreFailure.swift │ ├── IgnoreOutputSetOutputType.swift │ ├── Internal │ │ ├── Lock.swift │ │ └── Timer.swift │ ├── MapMany.swift │ ├── MapToResult.swift │ ├── MapToValue.swift │ ├── Materialize.swift │ ├── MergeMany.swift │ ├── Nwise.swift │ ├── Partition.swift │ ├── PrefixDuration.swift │ ├── PrefixWhileBehavior.swift │ ├── RemoveAllDuplicates.swift │ ├── RetryWhen.swift │ ├── SetOutputType.swift │ ├── ShareReplay.swift │ ├── Toggle.swift │ ├── WithLatestFrom.swift │ └── ZipMany.swift ├── Relays │ ├── CurrentValueRelay.swift │ ├── PassthroughRelay.swift │ └── Relay.swift └── Subjects │ └── ReplaySubject.swift ├── Tests ├── AmbTests.swift ├── AssignOwnershipTests.swift ├── AssignToManyTests.swift ├── CombineLatestManyTests.swift ├── CreateTests.swift ├── CurrentValueRelayTests.swift ├── DematerializeTests.swift ├── EnumeratedTests.swift ├── FilterManyTests.swift ├── FlatMapBatchesTests.swift ├── FlatMapFirstTests.swift ├── FlatMapLatestTests.swift ├── IgnoreFailureTests.swift ├── IgnoreOutputSetOutputTypeTests.swift ├── MapManyTests.swift ├── MapToResultTests.swift ├── MapToValueTests.swift ├── MaterializeTests.swift ├── MergeManyTests.swift ├── NwiseTests.swift ├── OptionalTests.swift ├── PartitionTests.swift ├── PassthroughRelayTests.swift ├── PrefixDurationTests.swift ├── PrefixWhileBehaviorTests.swift ├── RemoveAllDuplicatesTests.swift ├── ReplaySubjectTests.swift ├── RetryWhenTests.swift ├── SetOutputTypeTests.swift ├── ShareReplayTests.swift ├── ToggleTests.swift ├── WithLatestFromTests.swift └── ZipManyTests.swift ├── codecov.yml └── scripts ├── carthage-archive.sh └── make_project.rb /.github/workflows/autoinvite.yml: -------------------------------------------------------------------------------- 1 | name: Inclusive Organization 2 | on: 3 | push: 4 | branches: main 5 | jobs: 6 | invite: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Invite contributor to the organization 10 | uses: lekterable/inclusive-organization-action@v1.1.0 11 | with: 12 | organization: CombineCommunity 13 | team: Contributors 14 | comment: | 15 | Thank you for your contribution — You Rock 🤘! 16 | 17 | I've invited you to join the [CombineCommunity](https://github.com/CombineCommunity) organization – no pressure to accept! 18 | 19 | If you'd like more information on what this means, check out our [contributor](https://github.com/CombineCommunity/contributors) guidelines and feel free to reach out with any questions. 20 | env: 21 | ACCESS_TOKEN: ${{ secrets.INVITE_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CombineExt 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | xcode-tests: 7 | name: "Test" 8 | runs-on: macOS-latest 9 | 10 | strategy: 11 | matrix: 12 | platform: [macOS, iOS, tvOS] 13 | include: 14 | - platform: macOS 15 | sdk: macosx 16 | destination: "arch=x86_64" 17 | 18 | - platform: iOS 19 | sdk: iphonesimulator 20 | destination: "name=iPhone 11" 21 | 22 | - platform: tvOS 23 | sdk: appletvsimulator 24 | destination: "name=Apple TV" 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Generate project 29 | run: make project 30 | - name: Run tests 31 | run: set -o pipefail && xcodebuild -project CombineExt.xcodeproj -scheme CombineExt-Package -enableCodeCoverage YES -sdk ${{ matrix.sdk }} -destination "${{ matrix.destination }}" test | xcpretty -c -r html --output logs/${{ matrix.platform }}.html 32 | - uses: codecov/codecov-action@v1.0.13 33 | with: 34 | token: 1519d58c-6fb9-483f-af6c-7f6f0b384345 35 | name: CombineExt 36 | - uses: actions/upload-artifact@v1 37 | with: 38 | name: build-logs-${{ github.run_id }} 39 | path: logs 40 | 41 | SPM: 42 | name: "Test (SPM)" 43 | runs-on: macOS-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Run tests 48 | run: set -o pipefail && swift test 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build/ 3 | xcuserdata 4 | DerivedData/ 5 | Carthage 6 | CombineExt.framework.zip 7 | .DS_Store -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | opt_in_rules: 4 | - overridden_super_call 5 | - prohibited_super_call 6 | - extension_access_modifier 7 | - first_where 8 | - closure_spacing 9 | - unneeded_parentheses_in_closure_argument 10 | - vertical_parameter_alignment_on_call 11 | - redundant_nil_coalescing 12 | - pattern_matching_keywords 13 | - explicit_init 14 | - fatal_error_message 15 | - contains_over_first_not_nil 16 | - vertical_whitespace_closing_braces 17 | - vertical_whitespace_opening_braces 18 | - file_header 19 | disabled_rules: 20 | - nesting 21 | - line_length 22 | - cyclomatic_complexity 23 | - function_parameter_count 24 | - large_tuple 25 | 26 | file_header: 27 | required_pattern: | 28 | \/\/ 29 | \/\/ SWIFTLINT_CURRENT_FILENAME 30 | \/\/ CombineExt 31 | \/\/ 32 | \/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{2,4}\. 33 | \/\/ Copyright © \d{4} Combine Community\. All rights reserved\. 34 | \/\/ -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CombineExt.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CombineExt" 3 | s.version = "1.8.0" 4 | s.summary = "Combine operators and helpers not provided by Apple, and inspired by other Reactive Frameworks" 5 | s.description = <<-DESC 6 | A collection of operators for Combine adding capabilities and utilities not provided by Apple, 7 | but common ones found and known from other Reactive Frameworks 8 | DESC 9 | s.homepage = "https://github.com/CombineCommunity/CombineExt" 10 | s.license = { :type => "MIT", :file => "LICENSE" } 11 | s.authors = { "Combine Community" => "https://github.com/CombineCommunity", "Shai Mishali" => "freak4pc@gmail.com" } 12 | 13 | s.ios.deployment_target = '10.0' 14 | s.osx.deployment_target = '10.12' 15 | s.watchos.deployment_target = '3.0' 16 | s.tvos.deployment_target = '10.0' 17 | 18 | s.source = { :git => "https://github.com/CombineCommunity/CombineExt.git", :tag => s.version } 19 | s.source_files = 'Sources/**/*.swift' 20 | s.frameworks = ['Combine'] 21 | s.swift_version = '5.1' 22 | end 23 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/CombineExtTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/CombineExt_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 | 1.3.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.3.0 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "xctest-dynamic-overlay", 15 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 16 | "state": { 17 | "branch": null, 18 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 19 | "version": "0.2.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /CombineExt.xcodeproj/xcshareddata/xcschemes/CombineExt-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 64 | 65 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Combine Community, and/or Shai Mishali 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | archive: 2 | make project 3 | scripts/carthage-archive.sh 4 | project: 5 | scripts/make_project.rb 6 | clean: 7 | rm -rf CombineExt.xcodeproj 8 | test: 9 | swift test -Xswiftc -suppress-warnings | xcpretty -c 10 | 11 | .PHONY: clean -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "xctest-dynamic-overlay", 15 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 16 | "state": { 17 | "branch": null, 18 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 19 | "version": "0.2.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CombineExt", 7 | platforms: [ 8 | .iOS(.v13), .tvOS(.v10), .macOS(.v10_12), .watchOS(.v3) 9 | ], 10 | products: [ 11 | .library(name: "CombineExt", targets: ["CombineExt"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.8.0"), 15 | ], 16 | targets: [ 17 | .target(name: "CombineExt", dependencies: [], path: "Sources"), 18 | .testTarget(name: "CombineExtTests", 19 | dependencies: [ 20 | "CombineExt", 21 | .product(name: "CombineSchedulers", package: "combine-schedulers") 22 | ], 23 | path: "Tests"), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CombineCommunity/CombineExt/d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56/Resources/logo.png -------------------------------------------------------------------------------- /Sources/Common/DemandBuffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemandBuffer.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 21/02/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | import class Foundation.NSRecursiveLock 12 | 13 | /// A buffer responsible for managing the demand of a downstream 14 | /// subscriber for an upstream publisher 15 | /// 16 | /// It buffers values and completion events and forwards them dynamically 17 | /// according to the demand requested by the downstream 18 | /// 19 | /// In a sense, the subscription only relays the requests for demand, as well 20 | /// the events emitted by the upstream — to this buffer, which manages 21 | /// the entire behavior and backpressure contract 22 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 23 | class DemandBuffer { 24 | private let lock = NSRecursiveLock() 25 | private var buffer = [S.Input]() 26 | private let subscriber: S 27 | private var completion: Subscribers.Completion? 28 | private var demandState = Demand() 29 | 30 | /// Initialize a new demand buffer for a provided downstream subscriber 31 | /// 32 | /// - parameter subscriber: The downstream subscriber demanding events 33 | init(subscriber: S) { 34 | self.subscriber = subscriber 35 | } 36 | 37 | /// Buffer an upstream value to later be forwarded to 38 | /// the downstream subscriber, once it demands it 39 | /// 40 | /// - parameter value: Upstream value to buffer 41 | /// 42 | /// - returns: The demand fulfilled by the bufferr 43 | func buffer(value: S.Input) -> Subscribers.Demand { 44 | precondition(self.completion == nil, 45 | "How could a completed publisher sent values?! Beats me 🤷‍♂️") 46 | lock.lock() 47 | defer { lock.unlock() } 48 | 49 | switch demandState.requested { 50 | case .unlimited: 51 | return subscriber.receive(value) 52 | default: 53 | buffer.append(value) 54 | return flush() 55 | } 56 | } 57 | 58 | /// Complete the demand buffer with an upstream completion event 59 | /// 60 | /// This method will deplete the buffer immediately, 61 | /// based on the currently accumulated demand, and relay the 62 | /// completion event down as soon as demand is fulfilled 63 | /// 64 | /// - parameter completion: Completion event 65 | func complete(completion: Subscribers.Completion) { 66 | precondition(self.completion == nil, 67 | "Completion have already occured, which is quite awkward 🥺") 68 | 69 | self.completion = completion 70 | _ = flush() 71 | } 72 | 73 | /// Signal to the buffer that the downstream requested new demand 74 | /// 75 | /// - note: The buffer will attempt to flush as many events rqeuested 76 | /// by the downstream at this point 77 | func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand { 78 | flush(adding: demand) 79 | } 80 | 81 | /// Flush buffered events to the downstream based on the current 82 | /// state of the downstream's demand 83 | /// 84 | /// - parameter newDemand: The new demand to add. If `nil`, the flush isn't the 85 | /// result of an explicit demand change 86 | /// 87 | /// - note: After fulfilling the downstream's request, if completion 88 | /// has already occured, the buffer will be cleared and the 89 | /// completion event will be sent to the downstream subscriber 90 | private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand { 91 | lock.lock() 92 | defer { lock.unlock() } 93 | 94 | if let newDemand = newDemand { 95 | demandState.requested += newDemand 96 | } 97 | 98 | // If buffer isn't ready for flushing, return immediately 99 | guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none } 100 | 101 | while !buffer.isEmpty && demandState.processed < demandState.requested { 102 | demandState.requested += subscriber.receive(buffer.remove(at: 0)) 103 | demandState.processed += 1 104 | } 105 | 106 | if let completion = completion { 107 | // Completion event was already sent 108 | buffer = [] 109 | demandState = .init() 110 | self.completion = nil 111 | subscriber.receive(completion: completion) 112 | return .none 113 | } 114 | 115 | let sentDemand = demandState.requested - demandState.sent 116 | demandState.sent += sentDemand 117 | return sentDemand 118 | } 119 | } 120 | 121 | // MARK: - Private Helpers 122 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 123 | private extension DemandBuffer { 124 | /// A model that tracks the downstream's 125 | /// accumulated demand state 126 | struct Demand { 127 | var processed: Subscribers.Demand = .none 128 | var requested: Subscribers.Demand = .none 129 | var sent: Subscribers.Demand = .none 130 | } 131 | } 132 | 133 | // MARK: - Internally-scoped helpers 134 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 135 | extension Subscription { 136 | /// Reqeust demand if it's not empty 137 | /// 138 | /// - parameter demand: Requested demand 139 | func requestIfNeeded(_ demand: Subscribers.Demand) { 140 | guard demand > .none else { return } 141 | request(demand) 142 | } 143 | } 144 | 145 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 146 | extension Optional where Wrapped == Subscription { 147 | /// Cancel the Optional subscription and nullify it 148 | mutating func kill() { 149 | self?.cancel() 150 | self = nil 151 | } 152 | } 153 | #endif 154 | -------------------------------------------------------------------------------- /Sources/Common/Sink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sink.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | /// A generic sink using an underlying demand buffer to balance 13 | /// the demand of a downstream subscriber for the events of an 14 | /// upstream publisher 15 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | class Sink: Subscriber { 17 | typealias TransformFailure = (Upstream.Failure) -> Downstream.Failure? 18 | typealias TransformOutput = (Upstream.Output) -> Downstream.Input? 19 | 20 | private(set) var buffer: DemandBuffer 21 | private var upstreamSubscription: Subscription? 22 | private let transformOutput: TransformOutput? 23 | private let transformFailure: TransformFailure? 24 | private var upstreamIsCancelled = false 25 | 26 | /// Initialize a new sink subscribing to the upstream publisher and 27 | /// fulfilling the demand of the downstream subscriber using a backpresurre 28 | /// demand-maintaining buffer. 29 | /// 30 | /// - parameter upstream: The upstream publisher 31 | /// - parameter downstream: The downstream subscriber 32 | /// - parameter transformOutput: Transform the upstream publisher's output type to the downstream's input type 33 | /// - parameter transformFailure: Transform the upstream failure type to the downstream's failure type 34 | /// 35 | /// - note: You **must** provide the two transformation functions above if you're using 36 | /// the default `Sink` implementation. Otherwise, you must subclass `Sink` with your own 37 | /// publisher's sink and manage the buffer accordingly. 38 | init(upstream: Upstream, 39 | downstream: Downstream, 40 | transformOutput: TransformOutput? = nil, 41 | transformFailure: TransformFailure? = nil) { 42 | self.buffer = DemandBuffer(subscriber: downstream) 43 | self.transformOutput = transformOutput 44 | self.transformFailure = transformFailure 45 | 46 | // A subscription can only be cancelled once. The `upstreamIsCancelled` value 47 | // is used to suppress a second call to cancel when the Sink is deallocated, 48 | // when a sink receives completion, and when a custom operator like `withLatestFrom` 49 | // calls `cancelUpstream()` manually. 50 | upstream 51 | .handleEvents( 52 | receiveCancel: { [weak self] in 53 | self?.upstreamIsCancelled = true 54 | } 55 | ) 56 | .subscribe(self) 57 | } 58 | 59 | func demand(_ demand: Subscribers.Demand) { 60 | let newDemand = buffer.demand(demand) 61 | upstreamSubscription?.requestIfNeeded(newDemand) 62 | } 63 | 64 | func receive(subscription: Subscription) { 65 | upstreamSubscription = subscription 66 | } 67 | 68 | func receive(_ input: Upstream.Output) -> Subscribers.Demand { 69 | guard let transform = transformOutput else { 70 | fatalError(""" 71 | ❌ Missing output transformation 72 | ========================= 73 | 74 | You must either: 75 | - Provide a transformation function from the upstream's output to the downstream's input; or 76 | - Subclass `Sink` with your own publisher's Sink and manage the buffer yourself 77 | """) 78 | } 79 | 80 | guard let input = transform(input) else { return .none } 81 | return buffer.buffer(value: input) 82 | } 83 | 84 | func receive(completion: Subscribers.Completion) { 85 | switch completion { 86 | case .finished: 87 | buffer.complete(completion: .finished) 88 | case .failure(let error): 89 | guard let transform = transformFailure else { 90 | fatalError(""" 91 | ❌ Missing failure transformation 92 | ========================= 93 | 94 | You must either: 95 | - Provide a transformation function from the upstream's failure to the downstream's failuer; or 96 | - Subclass `Sink` with your own publisher's Sink and manage the buffer yourself 97 | """) 98 | } 99 | 100 | guard let error = transform(error) else { return } 101 | buffer.complete(completion: .failure(error)) 102 | } 103 | 104 | cancelUpstream() 105 | } 106 | 107 | func cancelUpstream() { 108 | guard upstreamIsCancelled == false else { return } 109 | 110 | upstreamSubscription.kill() 111 | } 112 | 113 | deinit { cancelUpstream() } 114 | } 115 | #endif 116 | -------------------------------------------------------------------------------- /Sources/Convenience/Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 11/05/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Optional { 14 | /// A publisher that publishes an optional value to each subscriber exactly once, if the optional has a value. 15 | @available(OSX, obsoleted: 11.0, message: "Optional.publisher is now part of Combine.") 16 | @available(iOS, obsoleted: 14.0, message: "Optional.publisher is now part of Combine.") 17 | @available(tvOS, obsoleted: 14.0, message: "Optional.publisher is now part of Combine.") 18 | @available(watchOS, obsoleted: 7.0, message: "Optional.publisher is now part of Combine.") 19 | var publisher: Optional.Publisher { 20 | Optional.Publisher(self) 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/Models/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 13/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | /// Represents a Combine Event 11 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 12 | public enum Event { 13 | case value(Output) 14 | case failure(Failure) 15 | case finished 16 | } 17 | 18 | // MARK: - Equatable Conformance 19 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 20 | extension Event: Equatable where Output: Equatable, Failure: Equatable { 21 | static public func == (lhs: Self, rhs: Self) -> Bool { 22 | switch (lhs, rhs) { 23 | case (.finished, .finished): 24 | return true 25 | case let (.failure(err1), .failure(err2)): 26 | return err1 == err2 27 | case let (.value(val1), .value(val2)): 28 | return val1 == val2 29 | default: 30 | return false 31 | } 32 | } 33 | } 34 | 35 | // MARK: - Friendly Output 36 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 37 | extension Event: CustomStringConvertible { 38 | public var description: String { 39 | switch self { 40 | case .value(let val): 41 | return "value(\(val))" 42 | case .failure(let err): 43 | return "failure(\(err))" 44 | case .finished: 45 | return "finished" 46 | } 47 | } 48 | } 49 | 50 | // MARK: - Event Convertible 51 | 52 | /// A protocol representing `Event` convertible types 53 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 54 | public protocol EventConvertible { 55 | associatedtype Output 56 | associatedtype Failure: Swift.Error 57 | 58 | var event: Event { get } 59 | } 60 | 61 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 62 | extension Event: EventConvertible { 63 | public var event: Event { self } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/Models/ObjectOwnership.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectOwnership.swift 3 | // CombineExt 4 | // 5 | // Created by Dmitry Kuznetsov on 08/05/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The ownership of an object 12 | /// 13 | /// - seealso: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID52 14 | public enum ObjectOwnership { 15 | /// Keep a strong hold of the object, preventing ARC 16 | /// from disposing it until its released or has no references. 17 | case strong 18 | 19 | /// Weakly owned. Does not keep a strong hold of the object, 20 | /// allowing ARC to dispose it even if its referenced. 21 | case weak 22 | 23 | /// Unowned. Similar to weak, but implicitly unwrapped so may 24 | /// crash if the object is released beore being accessed. 25 | case unowned 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Operators/AssignOwnership.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssignOwnership.swift 3 | // CombineExt 4 | // 5 | // Created by Dmitry Kuznetsov on 08/05/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Self.Failure == Never { 14 | /// Assigns a publisher’s output to a property of an object. 15 | /// 16 | /// - parameter keyPath: A key path that indicates the property to assign. 17 | /// - parameter object: The object that contains the property. 18 | /// The subscriber assigns the object’s property every time 19 | /// it receives a new value. 20 | /// - parameter ownership: The retainment / ownership strategy for the object, defaults to `strong`. 21 | /// 22 | /// - returns: An AnyCancellable instance. Call cancel() on this instance when you no longer want 23 | /// the publisher to automatically assign the property. Deinitializing this instance 24 | /// will also cancel automatic assignment. 25 | func assign(to keyPath: ReferenceWritableKeyPath, 26 | on object: Root, 27 | ownership: ObjectOwnership = .strong) -> AnyCancellable { 28 | switch ownership { 29 | case .strong: 30 | return assign(to: keyPath, on: object) 31 | case .weak: 32 | return sink { [weak object] value in 33 | object?[keyPath: keyPath] = value 34 | } 35 | case .unowned: 36 | return sink { [unowned object] value in 37 | object[keyPath: keyPath] = value 38 | } 39 | } 40 | } 41 | 42 | /// Assigns each element from a Publisher to properties of the provided objects 43 | /// 44 | /// - Parameters: 45 | /// - keyPath1: The key path of the first property to assign. 46 | /// - object1: The first object on which to assign the value. 47 | /// - keyPath2: The key path of the second property to assign. 48 | /// - object2: The second object on which to assign the value. 49 | /// - ownership: The retainment / ownership strategy for the object, defaults to `strong`. 50 | /// 51 | /// - Returns: A cancellable instance; used when you end assignment of the received value. 52 | /// Deallocation of the result will tear down the subscription stream. 53 | func assign( 54 | to keyPath1: ReferenceWritableKeyPath, on object1: Root1, 55 | and keyPath2: ReferenceWritableKeyPath, on object2: Root2, 56 | ownership: ObjectOwnership = .strong 57 | ) -> AnyCancellable { 58 | switch ownership { 59 | case .strong: 60 | return assign(to: keyPath1, on: object1, and: keyPath2, on: object2) 61 | case .weak: 62 | return sink { [weak object1, weak object2] value in 63 | object1?[keyPath: keyPath1] = value 64 | object2?[keyPath: keyPath2] = value 65 | } 66 | case .unowned: 67 | return sink { [unowned object1, unowned object2] value in 68 | object1[keyPath: keyPath1] = value 69 | object2[keyPath: keyPath2] = value 70 | } 71 | } 72 | } 73 | 74 | /// Assigns each element from a Publisher to properties of the provided objects 75 | /// 76 | /// - Parameters: 77 | /// - keyPath1: The key path of the first property to assign. 78 | /// - object1: The first object on which to assign the value. 79 | /// - keyPath2: The key path of the second property to assign. 80 | /// - object2: The second object on which to assign the value. 81 | /// - keyPath3: The key path of the third property to assign. 82 | /// - object3: The third object on which to assign the value. 83 | /// - ownership: The retainment / ownership strategy for the object, defaults to `strong`. 84 | /// 85 | /// - Returns: A cancellable instance; used when you end assignment of the received value. 86 | /// Deallocation of the result will tear down the subscription stream. 87 | func assign( 88 | to keyPath1: ReferenceWritableKeyPath, on object1: Root1, 89 | and keyPath2: ReferenceWritableKeyPath, on object2: Root2, 90 | and keyPath3: ReferenceWritableKeyPath, on object3: Root3, 91 | ownership: ObjectOwnership = .strong 92 | ) -> AnyCancellable { 93 | switch ownership { 94 | case .strong: 95 | return assign(to: keyPath1, on: object1, 96 | and: keyPath2, on: object2, 97 | and: keyPath3, on: object3) 98 | case .weak: 99 | return sink { [weak object1, weak object2, weak object3] value in 100 | object1?[keyPath: keyPath1] = value 101 | object2?[keyPath: keyPath2] = value 102 | object3?[keyPath: keyPath3] = value 103 | } 104 | case .unowned: 105 | return sink { [unowned object1, unowned object2, unowned object3] value in 106 | object1[keyPath: keyPath1] = value 107 | object2[keyPath: keyPath2] = value 108 | object3[keyPath: keyPath3] = value 109 | } 110 | } 111 | } 112 | } 113 | #endif 114 | -------------------------------------------------------------------------------- /Sources/Operators/AssignToMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssignToMany.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 13/02/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Self.Failure == Never { 14 | /// Assigns each element from a Publisher to properties of the provided objects 15 | /// 16 | /// - Parameters: 17 | /// - keyPath1: The key path of the first property to assign. 18 | /// - object1: The first object on which to assign the value. 19 | /// - keyPath2: The key path of the second property to assign. 20 | /// - object2: The second object on which to assign the value. 21 | /// 22 | /// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 23 | func assign(to keyPath1: ReferenceWritableKeyPath, on object1: Root1, 24 | and keyPath2: ReferenceWritableKeyPath, on object2: Root2) -> AnyCancellable { 25 | sink(receiveValue: { value in 26 | object1[keyPath: keyPath1] = value 27 | object2[keyPath: keyPath2] = value 28 | }) 29 | } 30 | 31 | /// Assigns each element from a Publisher to properties of the provided objects 32 | /// 33 | /// - Parameters: 34 | /// - keyPath1: The key path of the first property to assign. 35 | /// - object1: The first object on which to assign the value. 36 | /// - keyPath2: The key path of the second property to assign. 37 | /// - object2: The second object on which to assign the value. 38 | /// - keyPath3: The key path of the third property to assign. 39 | /// - object3: The third object on which to assign the value. 40 | /// 41 | /// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 42 | func assign(to keyPath1: ReferenceWritableKeyPath, on object1: Root1, 43 | and keyPath2: ReferenceWritableKeyPath, on object2: Root2, 44 | and keyPath3: ReferenceWritableKeyPath, on object3: Root3) -> AnyCancellable { 45 | sink(receiveValue: { value in 46 | object1[keyPath: keyPath1] = value 47 | object2[keyPath: keyPath2] = value 48 | object3[keyPath: keyPath3] = value 49 | }) 50 | } 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /Sources/Operators/CombineLatestMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineLatestMany.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 22/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Projects `self` and a `Collection` of `Publisher`s onto a type-erased publisher that chains `combineLatest` calls on 15 | /// the inner publishers. This is a variadic overload on Combine’s variants that top out at arity three. 16 | /// 17 | /// - parameter others: A `Collection`-worth of other publishers with matching output and failure types to combine with. 18 | /// 19 | /// - returns: A type-erased publisher with value events from `self` and each of the inner publishers `combineLatest`’d 20 | /// together in an array. 21 | func combineLatest(with others: Others) 22 | -> AnyPublisher<[Output], Failure> 23 | where Others.Element: Publisher, Others.Element.Output == Output, Others.Element.Failure == Failure { 24 | ([self.eraseToAnyPublisher()] + others.map { $0.eraseToAnyPublisher() }).combineLatest() 25 | } 26 | 27 | /// Projects `self` and a `Collection` of `Publisher`s onto a type-erased publisher that chains `combineLatest` calls on 28 | /// the inner publishers. This is a variadic overload on Combine’s variants that top out at arity three. 29 | /// 30 | /// - parameter others: A `Collection`-worth of other publishers with matching output and failure types to combine with. 31 | /// 32 | /// - returns: A type-erased publisher with value events from `self` and each of the inner publishers `combineLatest`’d 33 | /// together in an array. 34 | func combineLatest(with others: Other...) 35 | -> AnyPublisher<[Output], Failure> 36 | where Other.Output == Output, Other.Failure == Failure { 37 | combineLatest(with: others) 38 | } 39 | } 40 | 41 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 42 | public extension Collection where Element: Publisher { 43 | /// Projects a `Collection` of `Publisher`s onto a type-erased publisher that chains `combineLatest` calls on 44 | /// the inner publishers. This is a variadic overload on Combine’s variants that top out at arity three. 45 | /// 46 | /// - returns: A type-erased publisher with value events from each of the inner publishers `combineLatest`’d 47 | /// together in an array. 48 | func combineLatest() -> AnyPublisher<[Element.Output], Element.Failure> { 49 | var wrapped = map { $0.map { [$0] }.eraseToAnyPublisher() } 50 | while wrapped.count > 1 { 51 | wrapped = makeCombinedQuads(input: wrapped) 52 | } 53 | return wrapped.first?.eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() 54 | } 55 | } 56 | 57 | // MARK: - Private helpers 58 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 59 | /// CombineLatest an array of input publishers in four-somes. 60 | /// 61 | /// - parameter input: An array of publishers 62 | private func makeCombinedQuads( 63 | input: [AnyPublisher<[Output], Failure>] 64 | ) -> [AnyPublisher<[Output], Failure>] { 65 | // Iterate over the array of input publishers in steps of four 66 | sequence( 67 | state: input.makeIterator(), 68 | next: { it in it.next().map { ($0, it.next(), it.next(), it.next()) } } 69 | ) 70 | .map { quad in 71 | // Only one publisher 72 | guard let second = quad.1 else { return quad.0 } 73 | 74 | // Two publishers 75 | guard let third = quad.2 else { 76 | return quad.0 77 | .combineLatest(second) 78 | .map { $0.0 + $0.1 } 79 | .eraseToAnyPublisher() 80 | } 81 | 82 | // Three publishers 83 | guard let fourth = quad.3 else { 84 | return quad.0 85 | .combineLatest(second, third) 86 | .map { $0.0 + $0.1 + $0.2 } 87 | .eraseToAnyPublisher() 88 | } 89 | 90 | // Four publishers 91 | return quad.0 92 | .combineLatest(second, third, fourth) 93 | .map { $0.0 + $0.1 + $0.2 + $0.3 } 94 | .eraseToAnyPublisher() 95 | } 96 | } 97 | #endif 98 | -------------------------------------------------------------------------------- /Sources/Operators/Create.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Create.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 13/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | import Foundation 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | public extension AnyPublisher { 15 | /// Create a publisher which accepts a closure with a subscriber argument, 16 | /// to which you can dynamically send value or completion events. 17 | /// 18 | /// You should return a `Cancelable`-conforming object from the closure in 19 | /// which you can define any cleanup actions to execute when the pubilsher 20 | /// completes or the subscription to the publisher is canceled. 21 | /// 22 | /// - parameter factory: A factory with a closure to which you can 23 | /// dynamically send value or completion events. 24 | /// You should return a `Cancelable`-conforming object 25 | /// from it to encapsulate any cleanup-logic for your work. 26 | /// 27 | /// An example usage could look as follows: 28 | /// 29 | /// ``` 30 | /// AnyPublisher.create { subscriber in 31 | /// // Values 32 | /// subscriber.send("Hello") 33 | /// subscriber.send("World!") 34 | /// 35 | /// // Complete with error 36 | /// subscriber.send(completion: .failure(MyError.someError)) 37 | /// 38 | /// // Or, complete successfully 39 | /// subscriber.send(completion: .finished) 40 | /// 41 | /// return AnyCancellable { 42 | /// // Perform clean-up 43 | /// } 44 | /// } 45 | /// 46 | init(_ factory: @escaping Publishers.Create.SubscriberHandler) { 47 | self = Publishers.Create(factory: factory).eraseToAnyPublisher() 48 | } 49 | 50 | /// Create a publisher which accepts a closure with a subscriber argument, 51 | /// to which you can dynamically send value or completion events. 52 | /// 53 | /// You should return a `Cancelable`-conforming object from the closure in 54 | /// which you can define any cleanup actions to execute when the pubilsher 55 | /// completes or the subscription to the publisher is canceled. 56 | /// 57 | /// - parameter factory: A factory with a closure to which you can 58 | /// dynamically send value or completion events. 59 | /// You should return a `Cancelable`-conforming object 60 | /// from it to encapsulate any cleanup-logic for your work. 61 | /// 62 | /// An example usage could look as follows: 63 | /// 64 | /// ``` 65 | /// AnyPublisher.create { subscriber in 66 | /// // Values 67 | /// subscriber.send("Hello") 68 | /// subscriber.send("World!") 69 | /// 70 | /// // Complete with error 71 | /// subscriber.send(completion: .failure(MyError.someError)) 72 | /// 73 | /// // Or, complete successfully 74 | /// subscriber.send(completion: .finished) 75 | /// 76 | /// return AnyCancellable { 77 | /// // Perform clean-up 78 | /// } 79 | /// } 80 | /// 81 | static func create(_ factory: @escaping Publishers.Create.SubscriberHandler) 82 | -> AnyPublisher { 83 | AnyPublisher(factory) 84 | } 85 | } 86 | 87 | // MARK: - Publisher 88 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 89 | public extension Publishers { 90 | /// A publisher which accepts a closure with a subscriber argument, 91 | /// to which you can dynamically send value or completion events. 92 | /// 93 | /// You should return a `Cancelable`-conforming object from the closure in 94 | /// which you can define any cleanup actions to execute when the pubilsher 95 | /// completes or the subscription to the publisher is canceled. 96 | struct Create: Publisher { 97 | public typealias SubscriberHandler = (Subscriber) -> Cancellable 98 | private let factory: SubscriberHandler 99 | 100 | /// Initialize the publisher with a provided factory 101 | /// 102 | /// - parameter factory: A factory with a closure to which you can 103 | /// dynamically push value or completion events 104 | public init(factory: @escaping SubscriberHandler) { 105 | self.factory = factory 106 | } 107 | 108 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 109 | subscriber.receive(subscription: Subscription(factory: factory, downstream: subscriber)) 110 | } 111 | } 112 | } 113 | 114 | // MARK: - Subscription 115 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 116 | private extension Publishers.Create { 117 | class Subscription: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure { 118 | private let buffer: DemandBuffer 119 | private var cancelable: Cancellable? 120 | 121 | init(factory: @escaping SubscriberHandler, 122 | downstream: Downstream) { 123 | self.buffer = DemandBuffer(subscriber: downstream) 124 | 125 | let subscriber = Subscriber(onValue: { [weak self] in _ = self?.buffer.buffer(value: $0) }, 126 | onCompletion: { [weak self] in self?.buffer.complete(completion: $0) }) 127 | 128 | self.cancelable = factory(subscriber) 129 | } 130 | 131 | func request(_ demand: Subscribers.Demand) { 132 | _ = self.buffer.demand(demand) 133 | } 134 | 135 | func cancel() { 136 | self.cancelable?.cancel() 137 | } 138 | } 139 | } 140 | 141 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 142 | extension Publishers.Create.Subscription: CustomStringConvertible { 143 | var description: String { 144 | return "Create.Subscription<\(Output.self), \(Failure.self)>" 145 | } 146 | } 147 | 148 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 149 | public extension Publishers.Create { 150 | struct Subscriber { 151 | private let onValue: (Output) -> Void 152 | private let onCompletion: (Subscribers.Completion) -> Void 153 | 154 | fileprivate init(onValue: @escaping (Output) -> Void, 155 | onCompletion: @escaping (Subscribers.Completion) -> Void) { 156 | self.onValue = onValue 157 | self.onCompletion = onCompletion 158 | } 159 | 160 | /// Sends a value to the subscriber. 161 | /// 162 | /// - Parameter value: The value to send. 163 | public func send(_ input: Output) { 164 | onValue(input) 165 | } 166 | 167 | /// Sends a completion event to the subscriber. 168 | /// 169 | /// - Parameter completion: A `Completion` instance which indicates whether publishing has finished normally or failed with an error. 170 | public func send(completion: Subscribers.Completion) { 171 | onCompletion(completion) 172 | } 173 | } 174 | } 175 | #endif 176 | -------------------------------------------------------------------------------- /Sources/Operators/Dematerialize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dematerialize.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Output: EventConvertible, Failure == Never { 14 | /// Converts any previously-materialized publisher into its original form 15 | /// 16 | /// - returns: A publisher dematerializing the materialized events 17 | func dematerialize() -> Publishers.Dematerialize { 18 | Publishers.Dematerialize(upstream: self) 19 | } 20 | } 21 | 22 | // MARK: - Publisher 23 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 24 | public extension Publishers { 25 | /// A publisher which takes a materialized upstream publisher and converts 26 | /// the wrapped events back into their original form 27 | struct Dematerialize: Publisher where Upstream.Output: EventConvertible { 28 | public typealias Output = Upstream.Output.Output 29 | public typealias Failure = Upstream.Output.Failure 30 | 31 | private let upstream: Upstream 32 | 33 | public init(upstream: Upstream) { 34 | self.upstream = upstream 35 | } 36 | 37 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 38 | subscriber.receive(subscription: Subscription(upstream: upstream, downstream: subscriber)) 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Subscrription 44 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 45 | private extension Publishers.Dematerialize { 46 | class Subscription: Combine.Subscription 47 | where Downstream.Input == Upstream.Output.Output, Downstream.Failure == Upstream.Output.Failure { 48 | private var sink: Sink? 49 | 50 | init(upstream: Upstream, 51 | downstream: Downstream) { 52 | self.sink = Sink(upstream: upstream, 53 | downstream: downstream) 54 | } 55 | 56 | func request(_ demand: Subscribers.Demand) { 57 | sink?.demand(demand) 58 | } 59 | 60 | func cancel() { 61 | sink = nil 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Sink 67 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 68 | private extension Publishers.Dematerialize { 69 | class Sink: CombineExt.Sink 70 | where Downstream.Input == Upstream.Output.Output, Downstream.Failure == Upstream.Output.Failure { 71 | override func receive(_ input: Upstream.Output) -> Subscribers.Demand { 72 | /// We have to override the default mechanism here to convert a 73 | /// materialized failure into an actual failure 74 | switch input.event { 75 | case .value(let value): 76 | return buffer.buffer(value: value) 77 | case .failure(let failure): 78 | buffer.complete(completion: .failure(failure)) 79 | return .none 80 | case .finished: 81 | buffer.complete(completion: .finished) 82 | return .none 83 | } 84 | } 85 | } 86 | } 87 | 88 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 89 | extension Publishers.Dematerialize.Subscription: CustomStringConvertible { 90 | var description: String { 91 | return "Dematerialize.Subscription<\(Downstream.Input.self), \(Downstream.Failure.self)>" 92 | } 93 | } 94 | #endif 95 | -------------------------------------------------------------------------------- /Sources/Operators/Enumerated.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Combine) 2 | import Combine 3 | 4 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 5 | public extension Publisher { 6 | /// Enumerates the elements of a publisher. 7 | /// - parameter initial: Initial index, default is 0. 8 | /// - returns: A publisher that contains tuples of upstream elements and their indexes. 9 | func enumerated(initial: Int = 0) -> Publishers.Enumerated { 10 | Publishers.Enumerated(upstream: self, initial: initial) 11 | } 12 | } 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | public extension Publishers { 16 | /// A publisher that enumerates the elements of another publisher by combining the index and element into a tuple. 17 | struct Enumerated: Publisher { 18 | public typealias Output = (index: Int, element: Upstream.Output) 19 | public typealias Failure = Upstream.Failure 20 | 21 | public let upstream: Upstream 22 | public let initial: Int 23 | 24 | public init(upstream: Upstream, initial: Int = 0) { 25 | self.upstream = upstream 26 | self.initial = initial 27 | } 28 | 29 | public func receive(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output { 30 | upstream.subscribe(Inner(publisher: self, downstream: subscriber)) 31 | } 32 | } 33 | } 34 | 35 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 36 | private extension Publishers.Enumerated { 37 | final class Inner: Subscriber 38 | where Downstream.Input == Output, Downstream.Failure == Upstream.Failure { 39 | private var currentIndex: Int 40 | private let downstream: Downstream 41 | 42 | fileprivate init( 43 | publisher: Publishers.Enumerated, 44 | downstream: Downstream 45 | ) { 46 | self.currentIndex = publisher.initial 47 | self.downstream = downstream 48 | } 49 | 50 | func receive(subscription: Subscription) { 51 | downstream.receive(subscription: subscription) 52 | } 53 | 54 | func receive(_ input: Upstream.Output) -> Subscribers.Demand { 55 | defer { currentIndex += 1 } 56 | return downstream.receive((index: currentIndex, element: input)) 57 | } 58 | 59 | func receive(completion: Subscribers.Completion) { 60 | downstream.receive(completion: completion) 61 | } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/Operators/FilterMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterMany.swift 3 | // CombineExt 4 | // 5 | // Created by Hugo Saynac on 29/09/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Output: Collection { 14 | /// Filters element of a publisher collection into a new publisher collection. 15 | /// 16 | /// - parameter isIncluded: A filter function which applies to each element of the source collection. 17 | /// 18 | /// - returns: A publisher collection whose elements are included by the filter function on each element of the source. 19 | /// 20 | /// An example usage could look as follows: 21 | /// 22 | /// ``` 23 | /// let intArrayPublisher = PassthroughSubject<[Int], Never>() 24 | /// 25 | /// intArrayPublisher 26 | /// .filterMany { $0.isMultiple(of: 2) } 27 | /// .sink(receiveValue: { print($0) }) 28 | /// 29 | /// intArrayPublisher.send([10, 1, 2, 4, 3, 8]) 30 | /// 31 | /// // Output: [10, 1, 2, 4, 8] 32 | /// ``` 33 | /// 34 | /// 35 | func filterMany(_ isIncluded: @escaping (Output.Element) -> Bool) -> AnyPublisher<[Output.Element], Failure> { 36 | map { $0.filter(isIncluded) } 37 | .eraseToAnyPublisher() 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /Sources/Operators/FlatMapBatches.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatMapBatches.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali, Nate Cook, and Jasdev Singh on 21/01/2021. 6 | // Copyright © 2021 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Collection where Element: Publisher { 14 | /// Subscribes to the receiver’s contained publishers `size` at a time 15 | /// and outputs their results in `size`-sized batches, while maintaining 16 | /// order within each batch — subsequent batches of publishers are only 17 | /// subscribed to when the batch before it successfully completes. Any 18 | /// one failure will be forwarded downstream. 19 | /// - Parameter size: The batch size. 20 | /// - Returns: A publisher that subscribes to `self`’s contained publishers 21 | /// `size` at a time, returning their results in-order in `size`-sized 22 | /// batches, and then repeats with subsequent batches only if the ones prior 23 | /// successfully completed. Any one failure is immediately forwarded downstream. 24 | func flatMapBatches(of size: Int) -> AnyPublisher<[Element.Output], Element.Failure> { 25 | precondition(size > 0, "Batch sizes must be positive.") 26 | 27 | let indexBreaks = sequence( 28 | first: startIndex, 29 | next: { 30 | $0 == endIndex ? 31 | nil : 32 | index($0, offsetBy: size, limitedBy: endIndex) 33 | ?? endIndex 34 | } 35 | ) 36 | 37 | return Swift.zip(indexBreaks, indexBreaks.dropFirst()) 38 | .publisher 39 | .setFailureType(to: Element.Failure.self) 40 | .flatMap(maxPublishers: .max(1)) { self[$0..<$1].zip() } 41 | .eraseToAnyPublisher() 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/Operators/FlatMapFirst.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatMapFirst.swift 3 | // CombineExt 4 | // 5 | // Created by Martin Troup on 22/03/2022. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | import Foundation 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | public extension Publisher { 15 | /// The operator is a special case of `flatMap` operator. 16 | /// 17 | /// Like `flatMapLatest`, it only allows one inner publisher at a time. Unlike `flatMapLatest`, it will not cancel an ongoing inner publisher. 18 | /// Instead it ignores events from the source until the inner publisher is done. It creates another inner publisher only when the previous one is done. 19 | /// 20 | /// - Returns: A publisher emitting the values of a single inner publisher at a time (until the inner publisher finishes). 21 | func flatMapFirst( 22 | _ transform: @escaping (Output) -> P 23 | ) -> Publishers.FlatMap, Publishers.Filter> 24 | where Self.Failure == P.Failure { 25 | var isRunning = false 26 | let lock = NSRecursiveLock() 27 | 28 | func set(isRunning newValue: Bool) { 29 | defer { lock.unlock() } 30 | lock.lock() 31 | 32 | isRunning = newValue 33 | } 34 | 35 | return filter { _ in !isRunning } 36 | .flatMap { output in 37 | transform(output) 38 | .handleEvents( 39 | receiveSubscription: { _ in 40 | set(isRunning: true) 41 | }, 42 | receiveCompletion: { _ in 43 | set(isRunning: false) 44 | }, 45 | receiveCancel: { 46 | set(isRunning: false) 47 | } 48 | ) 49 | } 50 | } 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /Sources/Operators/FlatMapLatest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatMapLatest.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 13/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Transforms an output value into a new publisher, and flattens the stream of events from these multiple upstream publishers to appear as if they were coming from a single stream of events 15 | /// 16 | /// Mapping to a new publisher will cancel the subscription to the previous one, keeping only a single 17 | /// subscription active along with its event emissions 18 | /// 19 | /// - parameter transform: A transform to apply to each emitted value, from which you can return a new Publisher 20 | /// 21 | /// - note: This operator is a combination of `map` and `switchToLatest` 22 | /// 23 | /// - returns: A publisher emitting the values of the latest inner publisher 24 | func flatMapLatest(_ transform: @escaping (Output) -> P) -> Publishers.SwitchToLatest> { 25 | map(transform).switchToLatest() 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/Operators/IgnoreFailure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IgnoreFailure.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 17/10/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// An analog to `ignoreOutput` for `Publisher`’s `Failure` generic, allowing for either no or an immediate completion on an error event. 15 | /// 16 | /// - parameter completeImmediately: Whether the returned publisher should complete on an error event. Defaults to `true`. 17 | /// 18 | /// - returns: A publisher that ignores upstream error events. 19 | func ignoreFailure(completeImmediately: Bool = true) -> AnyPublisher { 20 | `catch` { _ in Empty(completeImmediately: completeImmediately) } 21 | .eraseToAnyPublisher() 22 | } 23 | 24 | /// An `ignoreFailure` overload that also allows for setting a new failure type. 25 | /// 26 | /// - parameter setFailureType: The failure type of the returned publisher. 27 | /// - parameter completeImmediately: Whether the returned publisher should complete on an error event. Defaults to `true`. 28 | /// 29 | /// - returns: A publisher that ignores upstream error events and has its `Failure` generic pinned to the specified failure type. 30 | func ignoreFailure( 31 | setFailureType newFailureType: NewFailure.Type, 32 | completeImmediately: Bool = true) -> AnyPublisher { 33 | ignoreFailure(completeImmediately: completeImmediately) 34 | .setFailureType(to: newFailureType) 35 | .eraseToAnyPublisher() 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/Operators/IgnoreOutputSetOutputType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IgnoreOutputSetOutputType.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 02/09/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// An `ignoreOutput` overload that allows for setting a new output type. 15 | /// 16 | /// - parameter setOutputType: The new output type for downstream. 17 | /// 18 | /// - returns: A publisher that ignores upstream value events and sets its output generic to `NewOutput`. 19 | func ignoreOutput(setOutputType newOutputType: NewOutput.Type) -> Publishers.Map, NewOutput> { 20 | ignoreOutput().map { _ -> NewOutput in } 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/Operators/Internal/Lock.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | 3 | //===----------------------------------------------------------------------===// 4 | // 5 | // This source file is part of the Swift.org open source project 6 | // 7 | // Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors 8 | // Licensed under Apache License v2.0 with Runtime Library Exception 9 | // 10 | // See https://swift.org/LICENSE.txt for license information 11 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Darwin 16 | 17 | @available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) 18 | typealias Lock = os_unfair_lock_t 19 | 20 | @available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) 21 | extension UnsafeMutablePointer where Pointee == os_unfair_lock_s { 22 | internal init() { 23 | let l = UnsafeMutablePointer.allocate(capacity: 1) 24 | l.initialize(to: os_unfair_lock()) 25 | self = l 26 | } 27 | 28 | internal func cleanupLock() { 29 | deinitialize(count: 1) 30 | deallocate() 31 | } 32 | 33 | internal func lock() { 34 | os_unfair_lock_lock(self) 35 | } 36 | 37 | internal func tryLock() -> Bool { 38 | let result = os_unfair_lock_trylock(self) 39 | return result 40 | } 41 | 42 | internal func unlock() { 43 | os_unfair_lock_unlock(self) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Operators/MapMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapMany.swift 3 | // CombineExt 4 | // 5 | // Created by Joan Disho on 22/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Output: Collection { 14 | /// Projects each element of a publisher collection into a new publisher collection form. 15 | /// 16 | /// - parameter transform: A transformation function which applies to each element of the source collection. 17 | /// 18 | /// - returns: A publisher collection whose elements are the result of invoking the transformation function on each element of the source. 19 | /// 20 | /// An example usage could look as follows: 21 | /// 22 | /// ``` 23 | /// let intArrayPublisher = PassthroughSubject<[Int], Never>() 24 | /// 25 | /// intArrayPublisher 26 | /// .mapMany(String.init) 27 | /// .sink(receiveValue: { print($0) }) 28 | /// 29 | /// intArrayPublisher.send([10, 2, 2, 4, 3, 8]) 30 | /// 31 | /// // Output: ["10", "2", "2", "4", "3", "8"] 32 | /// ``` 33 | /// 34 | /// 35 | func mapMany(_ transform: @escaping (Output.Element) -> Result) -> Publishers.Map { 36 | map { $0.map(transform) } 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/Operators/MapToResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapToResult.swift 3 | // CombineExt 4 | // 5 | // Created by Yurii Zadoianchuk on 05/03/2021. 6 | // Copyright © 2021 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Transform a publisher with concrete Output and Failure types 15 | /// to a new publisher that wraps Output and Failure in Result, 16 | /// and has Never for Failure type 17 | /// - Returns: A type-erased publiser of type , Never> 18 | func mapToResult() -> AnyPublisher, Never> { 19 | map(Result.success) 20 | .catch { Just(.failure($0)) } 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/Operators/MapToValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapToValue.swift 3 | // CombineExt 4 | // 5 | // Created by Dan Halliday on 08/05/2022. 6 | // Copyright © 2022 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Replace each upstream value with a constant. 15 | /// 16 | /// - Parameter value: The constant with which to replace each upstream value. 17 | /// - Returns: A new publisher wrapping the upstream, but with output type `Value`. 18 | func mapToValue(_ value: Value) -> Publishers.Map { 19 | map { _ in value } 20 | } 21 | 22 | /// Replace each upstream value with Void. 23 | /// 24 | /// - Returns: A new publisher wrapping the upstream and replacing each element with Void. 25 | func mapToVoid() -> Publishers.Map { 26 | map { _ in () } 27 | } 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /Sources/Operators/Materialize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Materialize.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Converts any publisher to a publisher of its events 15 | /// 16 | /// - note: The returned publisher is guaranteed to never fail, 17 | /// but it will complete given any upstream completion event 18 | /// 19 | /// - returns: A publisher that wraps events in an `Event`. 20 | func materialize() -> Publishers.Materialize { 21 | return Publishers.Materialize(upstream: self) 22 | } 23 | } 24 | 25 | // MARK: - Materialized Operators 26 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 27 | public extension Publisher where Output: EventConvertible, Failure == Never { 28 | /// Given a materialized publisher, publish only the emitted 29 | /// upstream values, omitting failures 30 | /// 31 | /// - returns: A publisher emitting the `Output` of the wrapped event 32 | func values() -> AnyPublisher { 33 | compactMap { 34 | guard case .value(let value) = $0.event else { return nil } 35 | return value 36 | } 37 | .eraseToAnyPublisher() 38 | } 39 | 40 | /// Given a materialize publisher, publish only the emitted 41 | /// upstream failure, if exists, omitting values 42 | /// 43 | /// - returns: A publisher emitting the `Failure` of the wrapped event 44 | func failures() -> AnyPublisher { 45 | compactMap { 46 | guard case .failure(let error) = $0.event else { return nil } 47 | return error 48 | } 49 | .eraseToAnyPublisher() 50 | } 51 | } 52 | 53 | // MARK: - Publisher 54 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 55 | public extension Publishers { 56 | /// A publisher which takes an upstream publisher and emits its events, 57 | /// wrapped in `Event` 58 | /// 59 | /// - note: This publisher is guaranteed to never fail, but it 60 | /// will complete given any upstream completion event 61 | struct Materialize: Publisher { 62 | public typealias Output = Event 63 | public typealias Failure = Never 64 | 65 | private let upstream: Upstream 66 | 67 | public init(upstream: Upstream) { 68 | self.upstream = upstream 69 | } 70 | 71 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 72 | subscriber.receive(subscription: Subscription(upstream: upstream, downstream: subscriber)) 73 | } 74 | } 75 | } 76 | 77 | // MARK: - Subscription 78 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 79 | private extension Publishers.Materialize { 80 | class Subscription: Combine.Subscription where Downstream.Input == Event, Downstream.Failure == Never { 81 | private var sink: Sink? 82 | 83 | init(upstream: Upstream, 84 | downstream: Downstream) { 85 | self.sink = Sink(upstream: upstream, 86 | downstream: downstream, 87 | transformOutput: { .value($0) }) 88 | } 89 | 90 | func request(_ demand: Subscribers.Demand) { 91 | sink?.demand(demand) 92 | } 93 | 94 | func cancel() { 95 | sink?.cancelUpstream() 96 | sink = nil 97 | } 98 | } 99 | } 100 | 101 | // MARK: - Sink 102 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 103 | private extension Publishers.Materialize { 104 | class Sink: CombineExt.Sink 105 | where Downstream.Input == Event, Downstream.Failure == Never { 106 | override func receive(completion: Subscribers.Completion) { 107 | // We're overriding the standard completion buffering mechanism 108 | // to buffer these events as regular materialized values, and send 109 | // a regular finished event in either case 110 | switch completion { 111 | case .finished: 112 | _ = buffer.buffer(value: .finished) 113 | case .failure(let error): 114 | _ = buffer.buffer(value: .failure(error)) 115 | } 116 | 117 | buffer.complete(completion: .finished) 118 | cancelUpstream() 119 | } 120 | } 121 | } 122 | 123 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 124 | extension Publishers.Materialize.Subscription: CustomStringConvertible { 125 | var description: String { 126 | return "Materialize.Subscription<\(Downstream.Input.Output.self), \(Downstream.Input.Failure.self)>" 127 | } 128 | } 129 | #endif 130 | -------------------------------------------------------------------------------- /Sources/Operators/MergeMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergeMany.swift 3 | // CombineExt 4 | // 5 | // Created by Joe Walsh on 8/17/20. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | // MARK: - Collection Helpers 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | public extension Collection where Element: Publisher { 15 | /// Merge a collection of publishers with the same output and failure types into a single publisher. 16 | /// If any of the publishers in the collection fails, the returned publisher will also fail. 17 | /// The returned publisher will not finish until all of the merged publishers finish. 18 | /// 19 | /// - Returns: A type-erased publisher that emits all events from the publishers in the collection. 20 | func merge() -> AnyPublisher { 21 | Publishers.MergeMany(self).eraseToAnyPublisher() 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/Operators/Nwise.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nwise.swift 3 | // CombineExt 4 | // 5 | // Created by Bas van Kuijck on 14/08/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Groups the elements of the source publisher into arrays of N consecutive elements. 15 | /// The resulting publisher: 16 | /// - does not emit anything until the source publisher emits at least N elements; 17 | /// - emits an array for every element after that; 18 | /// - forwards any errors or completed events. 19 | /// 20 | /// - parameter size: size of the groups, must be greater than 1 21 | /// 22 | /// - returns: A type erased publisher that holds an array with the given size. 23 | func nwise(_ size: Int) -> AnyPublisher<[Output], Failure> { 24 | assert(size > 1, "n must be greater than 1") 25 | 26 | return scan([]) { acc, item in Array((acc + [item]).suffix(size)) } 27 | .filter { $0.count == size } 28 | .eraseToAnyPublisher() 29 | } 30 | 31 | /// Groups the elements of the source publisher into tuples of the previous and current elements 32 | /// The resulting publisher: 33 | /// - does not emit anything until the source publisher emits at least 2 elements; 34 | /// - emits a tuple for every element after that, consisting of the previous and the current item; 35 | /// - forwards any error or completed events. 36 | /// 37 | /// - returns: A type erased publisher that holds a tuple with 2 elements. 38 | func pairwise() -> AnyPublisher<(Output, Output), Failure> { 39 | nwise(2) 40 | .map { ($0[0], $0[1]) } 41 | .eraseToAnyPublisher() 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/Operators/Partition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Partition.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// A partitioned publisher 15 | typealias Partition = AnyPublisher 16 | 17 | /// Partition a publisher's values into two separate publishers of values that match, and don't match, the provided predicate. 18 | /// 19 | /// - parameter predicate: A predicate used to filter matching and non-matching values. 20 | /// 21 | /// - returns: A tuple of two publishers of values that match, and don't match, the provided predicate. 22 | /// 23 | /// - note: The source publisher is `share()`d by default so resources are shared between the partitioned publshers 24 | func partition(_ predicate: @escaping (Output) -> Bool) -> (matches: Partition, nonMatches: Partition) { 25 | let source = map { ($0, predicate($0)) }.share() 26 | 27 | let hits = source.compactMap { $0.1 ? $0.0 : nil }.eraseToAnyPublisher() 28 | let misses = source.compactMap { !$0.1 ? $0.0 : nil }.eraseToAnyPublisher() 29 | 30 | return (hits, misses) 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/Operators/PrefixDuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefixDuration.swift 3 | // CombineExt 4 | // 5 | // Created by David Ohayon and Jasdev Singh on 24/04/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !(os(iOS) && (arch(i386) || arch(arm))) && canImport(Combine) 10 | import Combine 11 | import Foundation 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | public extension Publisher { 15 | /// Republishes elements for a specified duration. 16 | /// 17 | /// - parameters: 18 | /// - duration: The time interval during which to accept value or completion events. 19 | /// - tolerance: The tolerance the underlying timer. 20 | /// - scheduler: The scheduler for the underlying timer. 21 | /// - options: The scheduler options for the underlying timer. 22 | /// 23 | /// - returns: A publisher that republishes up to the specified duration. 24 | func prefix( 25 | duration: S.SchedulerTimeType.Stride, 26 | tolerance: S.SchedulerTimeType.Stride? = nil, 27 | on scheduler: S, 28 | options: S.SchedulerOptions? = nil 29 | ) -> AnyPublisher { 30 | prefix( 31 | untilOutputFrom: Publishers.Timer( 32 | every: duration, 33 | tolerance: tolerance, 34 | scheduler: scheduler, 35 | options: options 36 | ) 37 | .autoconnect() 38 | ) 39 | .eraseToAnyPublisher() 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/Operators/PrefixWhileBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefixWhileBehavior.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 29/12/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | import Foundation 12 | 13 | /// Whether to include the first element that doesn’t pass 14 | /// the `while` predicate passed to `Combine.Publisher.prefix(while:behavior:)`. 15 | public enum PrefixWhileBehavior { 16 | /// Include the first element that doesn’t pass the `while` predicate. 17 | case inclusive 18 | 19 | /// Exclude the first element that doesn’t pass the `while` predicate. 20 | case exclusive 21 | } 22 | 23 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 24 | public extension Publisher { 25 | /// An overload on `Publisher.prefix(while:)` that allows for inclusion of the first element that doesn’t pass the `while` predicate. 26 | /// 27 | /// - parameters: 28 | /// - predicate: A closure that takes an element as its parameter and returns a Boolean value that indicates whether publishing should continue. 29 | /// - behavior: Whether or not to include the first element that doesn’t pass `predicate`. 30 | /// 31 | /// - returns: A publisher that passes through elements until the predicate indicates publishing should finish — and optionally that first `predicate`-failing element. 32 | func prefix( 33 | while predicate: @escaping (Output) -> Bool, 34 | behavior: PrefixWhileBehavior = .exclusive 35 | ) -> AnyPublisher { 36 | switch behavior { 37 | case .exclusive: 38 | return prefix(while: predicate) 39 | .eraseToAnyPublisher() 40 | case .inclusive: 41 | return flatMap { next in 42 | Just(PrefixInclusiveEvent.whileValueOrIncluded(next)) 43 | .append(!predicate(next) ? [.end] : []) 44 | .setFailureType(to: Failure.self) 45 | } 46 | .prefix(while: \.isWhileValueOrIncluded) 47 | .compactMap(\.value) 48 | .eraseToAnyPublisher() 49 | } 50 | } 51 | } 52 | #endif 53 | 54 | // MARK: - Helpers 55 | 56 | private enum PrefixInclusiveEvent { 57 | case end 58 | case whileValueOrIncluded(Output) 59 | 60 | var isWhileValueOrIncluded: Bool { 61 | switch self { 62 | case .end: 63 | return false 64 | case .whileValueOrIncluded: 65 | return true 66 | } 67 | } 68 | 69 | var value: Output? { 70 | switch self { 71 | case .end: 72 | return nil 73 | case let .whileValueOrIncluded(inner): 74 | return inner 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Operators/RemoveAllDuplicates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoveAllDuplicates.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 21/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Output: Hashable { 14 | /// De-duplicates _all_ published value events, as opposed 15 | /// to pairwise with `Publisher.removeDuplicates`. 16 | /// 17 | /// - note: It’s important to note that this operator stores all emitted values 18 | /// in an in-memory `Set`. So, use this operator with caution, when handling publishers 19 | /// that emit a large number of unique value events. 20 | /// 21 | /// - returns: A publisher that consumes duplicate values across all previous emissions from upstream. 22 | func removeAllDuplicates() -> Publishers.Filter { 23 | var seen = Set() 24 | return filter { incoming in seen.insert(incoming).inserted } 25 | } 26 | } 27 | 28 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 29 | public extension Publisher where Output: Equatable { 30 | /// `Publisher.removeAllDuplicates` de-duplicates _all_ published `Hashable`-conforming value events, as opposed to pairwise with `Publisher.removeDuplicates`. 31 | /// 32 | /// - note: It’s important to note that this operator stores all emitted values in an in-memory `Array`. So, use 33 | /// this operator with caution, when handling publishers that emit a large number of unique value events. 34 | /// 35 | /// - returns: A publisher that consumes duplicate values across all previous emissions from upstream. 36 | func removeAllDuplicates() -> Publishers.Filter { 37 | removeAllDuplicates(by: ==) 38 | } 39 | } 40 | 41 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 42 | public extension Publisher { 43 | /// De-duplicates _all_ published value events, along the provided `by` comparator, as opposed to pairwise with `Publisher.removeDuplicates(by:)`. 44 | /// 45 | /// - parameter by: A comparator to use when determining uniqueness. `Publisher.removeAllDuplicates` will iterate 46 | /// over all seen values applying each known unique value as the first argument to the comparator and the 47 | /// incoming value event as the second, i.e. `by(see, next) -> Bool`. If this comparator is `true` for any 48 | /// seen value, the next incoming value isn’t emitted downstream. 49 | /// 50 | /// - note: It’s important to note that this operator stores all emitted values 51 | /// in an in-memory `Array`. So, use this operator with caution, when handling publishers 52 | /// that emit a large number of unique value events (as per `by`). 53 | /// 54 | /// - returns: A publisher that consumes duplicate values across all previous emissions from upstream 55 | /// (signaled with `by`). 56 | func removeAllDuplicates(by comparator: @escaping (Output, Output) -> Bool) -> Publishers.Filter { 57 | var seen = [Output]() 58 | return filter { incoming in 59 | if seen.contains(where: { comparator($0, incoming) }) { 60 | return false 61 | } else { 62 | seen.append(incoming) 63 | return true 64 | } 65 | } 66 | } 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Sources/Operators/RetryWhen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetryWhen.swift 3 | // CombineExt 4 | // 5 | // Created by Daniel Tartaglia on 3/21/20. 6 | // 7 | 8 | #if canImport(Combine) 9 | import Combine 10 | 11 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 12 | public extension Publisher { 13 | /// Repeats the source publisher on error when the notifier emits a next value. If the source publisher errors and the notifier completes, it will complete the source sequence. 14 | /// 15 | /// - Parameter notificationHandler: A handler that is passed a publisher of errors raised by the source publisher and returns a publisher that either continues, completes or errors. This behavior is then applied to the source publisher. 16 | /// - Returns: A publisher producing the elements of the given sequence repeatedly until it terminates successfully or is notified to error or complete. 17 | func retryWhen(_ errorTrigger: @escaping (AnyPublisher) -> RetryTrigger) 18 | -> Publishers.RetryWhen where RetryTrigger: Publisher { 19 | .init(upstream: self, errorTrigger: errorTrigger) 20 | } 21 | } 22 | 23 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 24 | public extension Publishers { 25 | class RetryWhen: Publisher where Upstream: Publisher, Upstream.Output == Output, Upstream.Failure == Failure, RetryTrigger: Publisher { 26 | typealias ErrorTrigger = (AnyPublisher) -> RetryTrigger 27 | 28 | private let upstream: Upstream 29 | private let errorTrigger: ErrorTrigger 30 | 31 | init(upstream: Upstream, errorTrigger: @escaping ErrorTrigger) { 32 | self.upstream = upstream 33 | self.errorTrigger = errorTrigger 34 | } 35 | 36 | public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { 37 | subscriber.receive(subscription: Subscription(upstream: upstream, downstream: subscriber, errorTrigger: errorTrigger)) 38 | } 39 | } 40 | } 41 | 42 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 43 | extension Publishers.RetryWhen { 44 | class Subscription: Combine.Subscription where Downstream: Subscriber, Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure { 45 | private let upstream: Upstream 46 | private let downstream: Downstream 47 | private let errorSubject = PassthroughSubject() 48 | private var sink: Sink? 49 | private var cancellable: AnyCancellable? 50 | 51 | init( 52 | upstream: Upstream, 53 | downstream: Downstream, 54 | errorTrigger: @escaping (AnyPublisher) -> RetryTrigger 55 | ) { 56 | self.upstream = upstream 57 | self.downstream = downstream 58 | self.sink = Sink( 59 | upstream: upstream, 60 | downstream: downstream, 61 | transformOutput: { $0 }, 62 | transformFailure: { [errorSubject] in 63 | errorSubject.send($0) 64 | return nil 65 | } 66 | ) 67 | self.cancellable = errorTrigger(errorSubject.eraseToAnyPublisher()) 68 | .sink( 69 | receiveCompletion: { [sink] completion in 70 | switch completion { 71 | case .finished: 72 | sink?.buffer.complete(completion: .finished) 73 | case .failure(let error): 74 | if let error = error as? Downstream.Failure { 75 | sink?.buffer.complete(completion: .failure(error)) 76 | } 77 | } 78 | }, 79 | receiveValue: { [upstream, sink] _ in 80 | guard let sink = sink else { return } 81 | upstream.subscribe(sink) 82 | } 83 | ) 84 | upstream.subscribe(sink!) 85 | } 86 | 87 | func request(_ demand: Subscribers.Demand) { 88 | sink?.demand(demand) 89 | } 90 | 91 | func cancel() { 92 | sink = nil 93 | } 94 | } 95 | } 96 | 97 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 98 | extension Publishers.RetryWhen.Subscription: CustomStringConvertible { 99 | var description: String { 100 | return "RetryWhen.Subscription<\(Output.self), \(Failure.self)>" 101 | } 102 | } 103 | #endif 104 | -------------------------------------------------------------------------------- /Sources/Operators/SetOutputType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetOutputType.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 02/04/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Output == Never { 14 | /// An output analog to [Publisher.setFailureType(to:)](https://developer.apple.com/documentation/combine/publisher/3204753-setfailuretype) for when `Output == Never`. This is especially helpful when chained after [.ignoreOutput()](https://developer.apple.com/documentation/combine/publisher/3204714-ignoreoutput) operator calls. 15 | /// 16 | /// - parameter outputType: The new output type for downstream. 17 | /// 18 | /// - returns: A publisher with a `NewOutput` output type. 19 | func setOutputType(to outputType: NewOutput.Type) -> Publishers.Map { 20 | map { _ -> NewOutput in } 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/Operators/ShareReplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareReplay.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 13/04/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// A variation on [share()](https://developer.apple.com/documentation/combine/publisher/3204754-share) 15 | /// that allows for buffering and replaying a `replay` amount of value events to future subscribers. 16 | /// 17 | /// - Parameter count: The number of value events to buffer in a first-in-first-out manner. 18 | /// - Returns: A publisher that replays the specified number of value events to future subscribers. 19 | func share(replay count: Int) -> Publishers.Autoconnect>> { 20 | multicast { ReplaySubject(bufferSize: count) } 21 | .autoconnect() 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/Operators/Toggle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Toggle.swift 3 | // CombineExt 4 | // 5 | // Created by Keita Watanabe on 06/06/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher where Output == Bool { 14 | /// Toggles boolean values emitted by a publisher. 15 | /// 16 | /// - returns: A toggled value. 17 | func toggle() -> Publishers.Map { 18 | map(!) 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/Operators/ZipMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipMany.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 16/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 13 | public extension Publisher { 14 | /// Zips `self` with an array of publishers with the same output and failure types. 15 | /// 16 | /// Since there can be any number of `others`, arrays of `Output` values are emitted after zipping. 17 | /// 18 | /// - parameter others: The other publishers to zip with. 19 | /// 20 | /// - returns: A type-erased publisher with value events from each of the inner publishers zipped together in an array. 21 | func zip(with others: Others) 22 | -> AnyPublisher<[Output], Failure> 23 | where Others.Element: Publisher, Others.Element.Output == Output, Others.Element.Failure == Failure { 24 | ([self.eraseToAnyPublisher()] + others.map { $0.eraseToAnyPublisher() }).zip() 25 | } 26 | 27 | /// A variadic overload on `Publisher.zip(with:)`. 28 | func zip(with others: Other...) 29 | -> AnyPublisher<[Output], Failure> where Other.Output == Output, Other.Failure == Failure { 30 | zip(with: others) 31 | } 32 | } 33 | 34 | // MARK: - Collection Helpers 35 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 36 | public extension Collection where Element: Publisher { 37 | /// Zip an array of publishers with the same output and failure types. 38 | /// 39 | /// Since there can be any number of elements, arrays of `Output` values are emitted after zipping. 40 | /// 41 | /// - returns: A type-erased publisher with value events from each of the inner publishers zipped together in an array. 42 | func zip() -> AnyPublisher<[Element.Output], Element.Failure> { 43 | var wrapped = map { $0.map { [$0] }.eraseToAnyPublisher() } 44 | while wrapped.count > 1 { 45 | wrapped = makeZippedQuads(input: wrapped) 46 | } 47 | return wrapped.first?.eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() 48 | } 49 | } 50 | 51 | // MARK: - Private helper 52 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 53 | /// Zip an array of input publishers in four-somes. 54 | /// 55 | /// - parameter input: An array of publishers 56 | private func makeZippedQuads( 57 | input: [AnyPublisher<[Output], Failure>] 58 | ) -> [AnyPublisher<[Output], Failure>] { 59 | sequence( 60 | state: input.makeIterator(), 61 | next: { it in it.next().map { ($0, it.next(), it.next(), it.next()) } } 62 | ) 63 | .map { quad in 64 | // Only one publisher 65 | guard let second = quad.1 else { return quad.0 } 66 | 67 | // Two publishers 68 | guard let third = quad.2 else { 69 | return quad.0 70 | .zip(second) 71 | .map { $0.0 + $0.1 } 72 | .eraseToAnyPublisher() 73 | } 74 | 75 | // Three publishers 76 | guard let fourth = quad.3 else { 77 | return quad.0 78 | .zip(second, third) 79 | .map { $0.0 + $0.1 + $0.2 } 80 | .eraseToAnyPublisher() 81 | } 82 | 83 | // Four publishers 84 | return quad.0 85 | .zip(second, third, fourth) 86 | .map { $0.0 + $0.1 + $0.2 + $0.3 } 87 | .eraseToAnyPublisher() 88 | } 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/Relays/CurrentValueRelay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentValueRelay.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 15/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | /// A relay that wraps a single value and publishes a new element whenever the value changes. 13 | /// 14 | /// Unlike its subject-counterpart, it may only accept values, and only sends a finishing event on deallocation. 15 | /// It cannot send a failure event. 16 | /// 17 | /// - note: Unlike PassthroughRelay, CurrentValueRelay maintains a buffer of the most recently published value. 18 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 19 | public class CurrentValueRelay: Relay { 20 | public var value: Output { storage.value } 21 | private let storage: CurrentValueSubject 22 | private var subscriptions = [Subscription, 23 | AnySubscriber>]() 24 | 25 | /// Create a new relay 26 | /// 27 | /// - parameter value: Initial value for the relay 28 | public init(_ value: Output) { 29 | storage = .init(value) 30 | } 31 | 32 | /// Relay a value to downstream subscribers 33 | /// 34 | /// - parameter value: A new value 35 | public func accept(_ value: Output) { 36 | storage.send(value) 37 | } 38 | 39 | public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 40 | let subscription = Subscription(upstream: storage, downstream: AnySubscriber(subscriber)) 41 | self.subscriptions.append(subscription) 42 | subscriber.receive(subscription: subscription) 43 | } 44 | 45 | public func subscribe(_ publisher: P) -> AnyCancellable where Output == P.Output, P.Failure == Never { 46 | publisher.subscribe(storage) 47 | } 48 | 49 | public func subscribe(_ publisher: P) -> AnyCancellable where Output == P.Output { 50 | publisher.subscribe(storage) 51 | } 52 | 53 | deinit { 54 | // Send a finished event upon dealloation 55 | subscriptions.forEach { $0.forceFinish() } 56 | } 57 | } 58 | 59 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 60 | private extension CurrentValueRelay { 61 | class Subscription: Combine.Subscription where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure { 62 | private var sink: Sink? 63 | var shouldForwardCompletion: Bool { 64 | get { sink?.shouldForwardCompletion ?? false } 65 | set { sink?.shouldForwardCompletion = newValue } 66 | } 67 | 68 | init(upstream: Upstream, 69 | downstream: Downstream) { 70 | self.sink = Sink(upstream: upstream, 71 | downstream: downstream, 72 | transformOutput: { $0 }) 73 | } 74 | 75 | func forceFinish() { 76 | self.sink?.shouldForwardCompletion = true 77 | self.sink?.receive(completion: .finished) 78 | self.sink = nil 79 | } 80 | 81 | func request(_ demand: Subscribers.Demand) { 82 | sink?.demand(demand) 83 | } 84 | 85 | func cancel() { 86 | forceFinish() 87 | } 88 | } 89 | } 90 | 91 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 92 | private extension CurrentValueRelay { 93 | class Sink: CombineExt.Sink { 94 | var shouldForwardCompletion = false 95 | override func receive(completion: Subscribers.Completion) { 96 | guard shouldForwardCompletion else { return } 97 | super.receive(completion: completion) 98 | } 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/Relays/PassthroughRelay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PassthroughRelay.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 15/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | /// A relay that broadcasts values to downstream subscribers. 13 | /// 14 | /// Unlike its subject-counterpart, it may only accept values, and only sends a finishing event on deallocation. 15 | /// It cannot send a failure event. 16 | /// 17 | /// - note: Unlike CurrentValueRelay, a PassthroughRelay doesn’t have an initial value or a buffer of the most recently-published value. 18 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 19 | public class PassthroughRelay: Relay { 20 | private let storage: PassthroughSubject 21 | private var subscriptions = [Subscription, 22 | AnySubscriber>]() 23 | 24 | /// Create a new relay 25 | /// 26 | /// - parameter value: Initial value for the relay 27 | public init() { 28 | self.storage = .init() 29 | } 30 | 31 | /// Relay a value to downstream subscribers 32 | /// 33 | /// - parameter value: A new value 34 | public func accept(_ value: Output) { 35 | storage.send(value) 36 | } 37 | 38 | public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 39 | let subscription = Subscription(upstream: storage, downstream: AnySubscriber(subscriber)) 40 | self.subscriptions.append(subscription) 41 | subscriber.receive(subscription: subscription) 42 | } 43 | 44 | public func subscribe(_ publisher: P) -> AnyCancellable where Output == P.Output, P.Failure == Never { 45 | publisher.subscribe(storage) 46 | } 47 | 48 | public func subscribe(_ publisher: P) -> AnyCancellable where Output == P.Output { 49 | publisher.subscribe(storage) 50 | } 51 | 52 | deinit { 53 | // Send a finished event upon dealloation 54 | subscriptions.forEach { $0.forceFinish() } 55 | } 56 | } 57 | 58 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 59 | private extension PassthroughRelay { 60 | class Subscription: Combine.Subscription where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure { 61 | private var sink: Sink? 62 | var shouldForwardCompletion: Bool { 63 | get { sink?.shouldForwardCompletion ?? false } 64 | set { sink?.shouldForwardCompletion = newValue } 65 | } 66 | 67 | init(upstream: Upstream, 68 | downstream: Downstream) { 69 | self.sink = Sink(upstream: upstream, 70 | downstream: downstream, 71 | transformOutput: { $0 }) 72 | } 73 | 74 | func forceFinish() { 75 | self.sink?.shouldForwardCompletion = true 76 | self.sink?.receive(completion: .finished) 77 | } 78 | 79 | func request(_ demand: Subscribers.Demand) { 80 | sink?.demand(demand) 81 | } 82 | 83 | func cancel() { 84 | sink = nil 85 | } 86 | } 87 | } 88 | 89 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 90 | private extension PassthroughRelay { 91 | class Sink: CombineExt.Sink { 92 | var shouldForwardCompletion = false 93 | override func receive(completion: Subscribers.Completion) { 94 | guard shouldForwardCompletion else { return } 95 | super.receive(completion: completion) 96 | } 97 | } 98 | } 99 | #endif 100 | -------------------------------------------------------------------------------- /Sources/Relays/Relay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Relay.swift 3 | // CombineExt 4 | // 5 | // Created by Shai Mishali on 15/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | 12 | /// A publisher that exposes a method for outside callers to publish values. 13 | /// It is identical to a `Subject`, but it cannot publish a finish event (until it's deallocated). 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | public protocol Relay: Publisher where Failure == Never { 16 | associatedtype Output 17 | 18 | /// Relays a value to the subscriber. 19 | /// 20 | /// - Parameter value: The value to send. 21 | func accept(_ value: Output) 22 | 23 | /// Attaches the specified publisher to this relay. 24 | /// 25 | /// - parameter publisher: An infallible publisher with the relay's Output type 26 | /// 27 | /// - returns: `AnyCancellable` 28 | func subscribe(_ publisher: P) -> AnyCancellable where P.Failure == Failure, P.Output == Output 29 | } 30 | 31 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 32 | public extension Publisher where Failure == Never { 33 | /// Attaches the specified relay to this publisher. 34 | /// 35 | /// - parameter relay: Relay to attach to this publisher 36 | /// 37 | /// - returns: `AnyCancellable` 38 | func subscribe(_ relay: R) -> AnyCancellable where R.Output == Output { 39 | relay.subscribe(self) 40 | } 41 | } 42 | 43 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 44 | public extension Relay where Output == Void { 45 | /// Relay a void to the subscriber. 46 | func accept() { 47 | accept(()) 48 | } 49 | } 50 | #endif 51 | -------------------------------------------------------------------------------- /Sources/Subjects/ReplaySubject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplaySubject.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 13/04/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | import Foundation 12 | 13 | /// A `ReplaySubject` is a subject that can buffer one or more values. It stores value events, up to its `bufferSize` in a 14 | /// first-in-first-out manner and then replays it to 15 | /// future subscribers and also forwards completion events. 16 | /// 17 | /// The implementation borrows heavily from [Entwine’s](https://github.com/tcldr/Entwine/blob/b839c9fcc7466878d6a823677ce608da998b95b9/Sources/Entwine/Operators/ReplaySubject.swift). 18 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 19 | public final class ReplaySubject: Subject { 20 | public typealias Output = Output 21 | public typealias Failure = Failure 22 | 23 | private let bufferSize: Int 24 | private var buffer = [Output]() 25 | 26 | // Keeping track of all live subscriptions, so `send` events can be forwarded to them. 27 | private(set) var subscriptions = [Subscription>]() 28 | 29 | private var completion: Subscribers.Completion? 30 | private var isActive: Bool { completion == nil } 31 | 32 | private let lock = NSRecursiveLock() 33 | 34 | /// Create a `ReplaySubject`, buffering up to `bufferSize` values and replaying them to new subscribers 35 | /// - Parameter bufferSize: The maximum number of value events to buffer and replay to all future subscribers. 36 | public init(bufferSize: Int) { 37 | self.bufferSize = bufferSize 38 | } 39 | 40 | public func send(_ value: Output) { 41 | let subscriptions: [Subscription>] 42 | 43 | do { 44 | lock.lock() 45 | defer { lock.unlock() } 46 | 47 | guard isActive else { return } 48 | 49 | buffer.append(value) 50 | if buffer.count > bufferSize { 51 | buffer.removeFirst() 52 | } 53 | 54 | subscriptions = self.subscriptions 55 | } 56 | 57 | subscriptions.forEach { $0.forwardValueToBuffer(value) } 58 | } 59 | 60 | public func send(completion: Subscribers.Completion) { 61 | let subscriptions: [Subscription>] 62 | 63 | do { 64 | lock.lock() 65 | defer { lock.unlock() } 66 | 67 | guard isActive else { return } 68 | 69 | self.completion = completion 70 | 71 | subscriptions = self.subscriptions 72 | } 73 | 74 | subscriptions.forEach { $0.forwardCompletionToBuffer(completion) } 75 | 76 | lock.lock() 77 | defer { self.lock.unlock() } 78 | self.subscriptions.removeAll() 79 | } 80 | 81 | public func send(subscription: Combine.Subscription) { 82 | subscription.request(.unlimited) 83 | } 84 | 85 | public func receive(subscriber: Subscriber) where Failure == Subscriber.Failure, Output == Subscriber.Input { 86 | let subscriberIdentifier = subscriber.combineIdentifier 87 | 88 | let subscription = Subscription(downstream: AnySubscriber(subscriber)) { [weak self] in 89 | self?.completeSubscriber(withIdentifier: subscriberIdentifier) 90 | } 91 | 92 | do { 93 | lock.lock() 94 | defer { lock.unlock() } 95 | 96 | subscription.replay(buffer, completion: completion) 97 | subscriptions.append(subscription) 98 | } 99 | 100 | subscriber.receive(subscription: subscription) 101 | } 102 | 103 | private func completeSubscriber(withIdentifier subscriberIdentifier: CombineIdentifier) { 104 | lock.lock() 105 | defer { self.lock.unlock() } 106 | 107 | self.subscriptions.removeAll { $0.innerSubscriberIdentifier == subscriberIdentifier } 108 | } 109 | } 110 | 111 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 112 | extension ReplaySubject { 113 | final class Subscription: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure { 114 | private var demandBuffer: DemandBuffer? 115 | private var cancellationHandler: (() -> Void)? 116 | 117 | fileprivate let innerSubscriberIdentifier: CombineIdentifier 118 | 119 | init(downstream: Downstream, cancellationHandler: (() -> Void)?) { 120 | self.demandBuffer = DemandBuffer(subscriber: downstream) 121 | self.innerSubscriberIdentifier = downstream.combineIdentifier 122 | self.cancellationHandler = cancellationHandler 123 | } 124 | 125 | func replay(_ buffer: [Output], completion: Subscribers.Completion?) { 126 | buffer.forEach(forwardValueToBuffer) 127 | 128 | if let completion = completion { 129 | forwardCompletionToBuffer(completion) 130 | } 131 | } 132 | 133 | func forwardValueToBuffer(_ value: Output) { 134 | _ = demandBuffer?.buffer(value: value) 135 | } 136 | 137 | func forwardCompletionToBuffer(_ completion: Subscribers.Completion) { 138 | demandBuffer?.complete(completion: completion) 139 | } 140 | 141 | func request(_ demand: Subscribers.Demand) { 142 | _ = demandBuffer?.demand(demand) 143 | } 144 | 145 | func cancel() { 146 | cancellationHandler?() 147 | cancellationHandler = nil 148 | 149 | demandBuffer = nil 150 | } 151 | } 152 | } 153 | #endif 154 | -------------------------------------------------------------------------------- /Tests/AssignOwnershipTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssignOwnershipTests.swift 3 | // CombineExt 4 | // 5 | // Created by Dmitry Kuznetsov on 08/05/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class AssignOwnershipTests: XCTestCase { 16 | var subscription: AnyCancellable! 17 | var value1 = 0 18 | var value2 = 0 19 | var value3 = 0 20 | var subject: PassthroughSubject! 21 | 22 | override func setUp() { 23 | super.setUp() 24 | 25 | subscription = nil 26 | subject = PassthroughSubject() 27 | value1 = 0 28 | value2 = 0 29 | value3 = 0 30 | } 31 | 32 | func testWeakOwnership() { 33 | let initialRetainCount = CFGetRetainCount(self) 34 | 35 | subscription = subject 36 | .assign(to: \.value1, on: self, ownership: .weak) 37 | subject.send(10) 38 | let resultRetainCount1 = CFGetRetainCount(self) 39 | XCTAssertEqual(initialRetainCount, resultRetainCount1) 40 | 41 | subscription = subject 42 | .assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .weak) 43 | subject.send(15) 44 | let resultRetainCount2 = CFGetRetainCount(self) 45 | XCTAssertEqual(initialRetainCount, resultRetainCount2) 46 | 47 | subscription = subject 48 | .assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .weak) 49 | subject.send(20) 50 | let resultRetainCount3 = CFGetRetainCount(self) 51 | XCTAssertEqual(initialRetainCount, resultRetainCount3) 52 | } 53 | 54 | func testUnownedOwnership() { 55 | let initialRetainCount = CFGetRetainCount(self) 56 | 57 | subscription = subject 58 | .assign(to: \.value1, on: self, ownership: .unowned) 59 | subject.send(10) 60 | let resultRetainCount1 = CFGetRetainCount(self) 61 | XCTAssertEqual(initialRetainCount, resultRetainCount1) 62 | 63 | subscription = subject 64 | .assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .unowned) 65 | subject.send(15) 66 | let resultRetainCount2 = CFGetRetainCount(self) 67 | XCTAssertEqual(initialRetainCount, resultRetainCount2) 68 | 69 | subscription = subject 70 | .assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .unowned) 71 | subject.send(20) 72 | let resultRetainCount3 = CFGetRetainCount(self) 73 | XCTAssertEqual(initialRetainCount, resultRetainCount3) 74 | } 75 | 76 | func testStrongOwnership() { 77 | let initialRetainCount = CFGetRetainCount(self) 78 | 79 | subscription = subject 80 | .assign(to: \.value1, on: self, ownership: .strong) 81 | subject.send(10) 82 | let resultRetainCount1 = CFGetRetainCount(self) 83 | XCTAssertEqual(initialRetainCount + 1, resultRetainCount1) 84 | 85 | subscription = subject 86 | .assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .strong) 87 | subject.send(15) 88 | let resultRetainCount2 = CFGetRetainCount(self) 89 | XCTAssertEqual(initialRetainCount + 2, resultRetainCount2) 90 | 91 | subscription = subject 92 | .assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .strong) 93 | subject.send(20) 94 | let resultRetainCount3 = CFGetRetainCount(self) 95 | XCTAssertEqual(initialRetainCount + 3, resultRetainCount3) 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Tests/AssignToManyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssignToManyTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 13/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class AssignToManyTests: XCTestCase { 16 | var subscription: AnyCancellable! 17 | 18 | func testAssignToOne() { 19 | let source = PassthroughSubject() 20 | 21 | for ownership in [ObjectOwnership.strong, .weak, .unowned] { 22 | let dest1 = Fake1(prop: 0) 23 | 24 | XCTAssertEqual(dest1.prop, 0) 25 | 26 | subscription = source 27 | .assign(to: \.prop, on: dest1, ownership: ownership) 28 | 29 | source.send(4) 30 | XCTAssertEqual(dest1.prop, 4, "\(ownership) ownership") 31 | 32 | source.send(12) 33 | XCTAssertEqual(dest1.prop, 12, "\(ownership) ownership") 34 | 35 | source.send(-7) 36 | XCTAssertEqual(dest1.prop, -7, "\(ownership) ownership") 37 | } 38 | } 39 | 40 | func testAssignToTwo() { 41 | let source = PassthroughSubject() 42 | 43 | for ownership in [ObjectOwnership.strong, .weak, .unowned] { 44 | let dest1 = Fake1(prop: 0) 45 | let dest2 = Fake2(ivar: 0) 46 | 47 | XCTAssertEqual(dest1.prop, 0) 48 | XCTAssertEqual(dest2.ivar, 0) 49 | 50 | subscription = source 51 | .assign(to: \.prop, on: dest1, 52 | and: \.ivar, on: dest2, 53 | ownership: ownership) 54 | 55 | source.send(4) 56 | XCTAssertEqual(dest1.prop, 4, "\(ownership) ownership") 57 | XCTAssertEqual(dest2.ivar, 4, "\(ownership) ownership") 58 | 59 | source.send(12) 60 | XCTAssertEqual(dest1.prop, 12, "\(ownership) ownership") 61 | XCTAssertEqual(dest2.ivar, 12, "\(ownership) ownership") 62 | 63 | source.send(-7) 64 | XCTAssertEqual(dest1.prop, -7, "\(ownership) ownership") 65 | XCTAssertEqual(dest2.ivar, -7, "\(ownership) ownership") 66 | } 67 | } 68 | 69 | func testAssignToThree() { 70 | let source = PassthroughSubject() 71 | 72 | for ownership in [ObjectOwnership.strong, .weak, .unowned] { 73 | let dest1 = Fake1(prop: "") 74 | let dest2 = Fake2(ivar: "") 75 | let dest3 = Fake3(value: "") { String(repeating: $0, count: $0.count) } 76 | 77 | XCTAssertEqual(dest1.prop, "") 78 | XCTAssertEqual(dest2.ivar, "") 79 | XCTAssertEqual(dest3.value, "") 80 | 81 | subscription = source 82 | .assign(to: \.prop, on: dest1, 83 | and: \.ivar, on: dest2, 84 | and: \.value, on: dest3, 85 | ownership: ownership) 86 | 87 | source.send("Hello") 88 | XCTAssertEqual(dest1.prop, "Hello", "\(ownership) ownership") 89 | XCTAssertEqual(dest2.ivar, "Hello", "\(ownership) ownership") 90 | XCTAssertEqual(dest3.value, "HelloHelloHelloHelloHello", "\(ownership) ownership") 91 | 92 | source.send("Meh") 93 | XCTAssertEqual(dest1.prop, "Meh", "\(ownership) ownership") 94 | XCTAssertEqual(dest2.ivar, "Meh", "\(ownership) ownership") 95 | XCTAssertEqual(dest3.value, "MehMehMeh", "\(ownership) ownership") 96 | } 97 | } 98 | } 99 | 100 | // MARK: - Private Helpers 101 | private class Fake1 { 102 | var prop: T 103 | 104 | init(prop: T) { 105 | self.prop = prop 106 | } 107 | } 108 | 109 | private class Fake2 { 110 | var ivar: T 111 | 112 | init(ivar: T) { 113 | self.ivar = ivar 114 | } 115 | } 116 | 117 | private class Fake3 { 118 | var value: T { 119 | set { storage = transform(newValue) } 120 | get { storage } 121 | } 122 | 123 | var storage: T 124 | var transform: (T) -> T 125 | 126 | init(value: T, transform: @escaping (T) -> T) { 127 | self.storage = transform(value) 128 | self.transform = transform 129 | } 130 | } 131 | #endif 132 | -------------------------------------------------------------------------------- /Tests/CombineLatestManyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineLatestManyTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Jasdev Singh on 3/22/20. 6 | // 7 | 8 | #if !os(watchOS) 9 | import Combine 10 | import CombineExt 11 | import XCTest 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | final class CombineLatestManyTests: XCTestCase { 15 | private var subscription: AnyCancellable! 16 | 17 | private enum CombineLatestManyTestError: Error { 18 | case anError 19 | } 20 | 21 | func testCollectionCombineLatestWithFinishedEvent() { 22 | let first = PassthroughSubject() 23 | let second = PassthroughSubject() 24 | let third = PassthroughSubject() 25 | let fourth = PassthroughSubject() 26 | 27 | var completed = false 28 | var results = [[Int]]() 29 | 30 | subscription = [first, second, third, fourth] 31 | .combineLatest() 32 | .sink(receiveCompletion: { _ in completed = true }, 33 | receiveValue: { results.append($0) }) 34 | 35 | first.send(1) 36 | second.send(2) 37 | 38 | XCTAssertTrue(results.isEmpty) 39 | XCTAssertFalse(completed) 40 | 41 | third.send(3) 42 | fourth.send(4) 43 | 44 | XCTAssertEqual(results, [[1, 2, 3, 4]]) 45 | XCTAssertFalse(completed) 46 | 47 | first.send(5) 48 | 49 | XCTAssertEqual(results, [[1, 2, 3, 4], [5, 2, 3, 4]]) 50 | XCTAssertFalse(completed) 51 | 52 | fourth.send(6) 53 | 54 | XCTAssertEqual(results, [[1, 2, 3, 4], [5, 2, 3, 4], [5, 2, 3, 6]]) 55 | XCTAssertFalse(completed) 56 | 57 | first.send(completion: .finished) 58 | 59 | XCTAssertEqual(results, [[1, 2, 3, 4], [5, 2, 3, 4], [5, 2, 3, 6]]) 60 | XCTAssertFalse(completed) 61 | 62 | [second, third, fourth].forEach { 63 | $0.send(completion: .finished) 64 | } 65 | 66 | XCTAssertTrue(completed) 67 | } 68 | 69 | func testCollectionCombineLatestWithNoEvents() { 70 | let first = PassthroughSubject() 71 | let second = PassthroughSubject() 72 | 73 | var completed = false 74 | var results = [[Int]]() 75 | 76 | subscription = [first, second] 77 | .combineLatest() 78 | .sink(receiveCompletion: { _ in completed = true }, 79 | receiveValue: { results.append($0) }) 80 | 81 | XCTAssertTrue(results.isEmpty) 82 | XCTAssertFalse(completed) 83 | } 84 | 85 | func testCollectionCombineLatestWithErrorEvent() { 86 | let first = PassthroughSubject() 87 | let second = PassthroughSubject() 88 | 89 | var completion: Subscribers.Completion? 90 | var results = [[Int]]() 91 | 92 | subscription = [first, second] 93 | .combineLatest() 94 | .sink(receiveCompletion: { completion = $0 }, 95 | receiveValue: { results.append($0) }) 96 | 97 | first.send(1) 98 | second.send(2) 99 | 100 | XCTAssertEqual(results, [[1, 2]]) 101 | XCTAssertNil(completion) 102 | 103 | second.send(completion: .failure(.anError)) 104 | 105 | XCTAssertEqual(completion, .failure(.anError)) 106 | } 107 | 108 | func testCollectionCombineLatestWithASinglePublisher() { 109 | let first = PassthroughSubject() 110 | 111 | var completed = false 112 | var results = [[Int]]() 113 | 114 | subscription = [first] 115 | .combineLatest() 116 | .sink(receiveCompletion: { _ in completed = true }, 117 | receiveValue: { results.append($0) }) 118 | 119 | first.send(1) 120 | 121 | XCTAssertEqual(results, [[1]]) 122 | XCTAssertFalse(completed) 123 | 124 | first.send(completion: .finished) 125 | 126 | XCTAssertTrue(completed) 127 | } 128 | 129 | func testCollectionCombineLatestWithNoPublishers() { 130 | var completed = false 131 | var results = [[Int]]() 132 | 133 | subscription = [AnyPublisher]() 134 | .combineLatest() 135 | .sink(receiveCompletion: { _ in completed = true }, 136 | receiveValue: { results.append($0) }) 137 | 138 | XCTAssertTrue(results.isEmpty) 139 | XCTAssertTrue(completed) 140 | } 141 | 142 | func testMethodCombineLatestWithFinishedEvent() { 143 | let first = PassthroughSubject() 144 | let second = PassthroughSubject() 145 | 146 | var completed = false 147 | var results = [[Int]]() 148 | 149 | subscription = first.combineLatest(with: [second]) 150 | .sink(receiveCompletion: { _ in completed = true }, 151 | receiveValue: { results.append($0) }) 152 | 153 | first.send(1) 154 | second.send(2) 155 | 156 | XCTAssertEqual(results, [[1, 2]]) 157 | XCTAssertFalse(completed) 158 | 159 | second.send(3) 160 | second.send(3) 161 | 162 | XCTAssertEqual(results, [[1, 2], [1, 3], [1, 3]]) 163 | XCTAssertFalse(completed) 164 | 165 | first.send(completion: .finished) 166 | 167 | XCTAssertFalse(completed) 168 | 169 | second.send(completion: .finished) 170 | 171 | XCTAssertTrue(completed) 172 | } 173 | 174 | func testVariadicMethodCombineLatest() { 175 | let first = PassthroughSubject() 176 | let second = PassthroughSubject() 177 | let third = PassthroughSubject() 178 | let fourth = PassthroughSubject() 179 | let fifth = PassthroughSubject() 180 | 181 | var results = [[Int]]() 182 | 183 | subscription = first.combineLatest(with: second, third, fourth, fifth) 184 | .sink(receiveValue: { results.append($0) }) 185 | 186 | first.send(1) 187 | second.send(2) 188 | third.send(3) 189 | fourth.send(4) 190 | fifth.send(5) 191 | 192 | XCTAssertEqual(results, [[1, 2, 3, 4, 5]]) 193 | 194 | second.send(6) 195 | 196 | XCTAssertEqual(results, [[1, 2, 3, 4, 5], [1, 6, 3, 4, 5]]) 197 | } 198 | 199 | func testCombineLatestAtScale() { 200 | // Using a combineLatest implementation that combines first/the-rest triggers a stack overflow using 1e5 201 | // publishers, but the divide-and-conquer implementation gets through 1e7 just fine (though the test takes 202 | // 28s to complete on an M1 Pro). 203 | let numPublishers = Int(1e5 + 1) // +1 to minimize the odds that numPublishers%4==0 matters. 204 | 205 | let publishers = Array(repeating: 1, count: numPublishers) 206 | .map { _ in Just(2) } 207 | var results = [[Int]]() 208 | subscription = publishers.combineLatest() 209 | .sink(receiveValue: { results.append($0) }) 210 | let wantAllTwos = Array(repeating: 2, count: numPublishers) 211 | XCTAssertEqual(results, [wantAllTwos]) 212 | } 213 | } 214 | #endif 215 | -------------------------------------------------------------------------------- /Tests/CreateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class CreateTests: XCTestCase { 16 | var subscription: AnyCancellable! 17 | enum MyError: Swift.Error { 18 | case failure 19 | } 20 | 21 | private var completion: Subscribers.Completion? 22 | private var values = [String]() 23 | private var canceled = false 24 | private let allValues = ["Hello", "World", "What's", "Up?"] 25 | 26 | override func setUp() { 27 | canceled = false 28 | values = [] 29 | completion = nil 30 | } 31 | 32 | func testUnlimitedDemandFinished() { 33 | let subscriber = makeSubscriber(demand: .unlimited) 34 | let publisher = makePublisher(fail: false) 35 | 36 | publisher.subscribe(subscriber) 37 | 38 | XCTAssertEqual(completion, .finished) 39 | XCTAssertTrue(canceled) 40 | XCTAssertEqual(values, allValues) 41 | } 42 | 43 | func testLimitedDemandFinished() { 44 | let subscriber = makeSubscriber(demand: .max(2)) 45 | 46 | let publisher = AnyPublisher { subscriber in 47 | self.allValues.forEach { subscriber.send($0) } 48 | subscriber.send(completion: .finished) 49 | 50 | return AnyCancellable { [weak self] in 51 | self?.canceled = true 52 | } 53 | } 54 | 55 | publisher.subscribe(subscriber) 56 | 57 | XCTAssertEqual(completion, .finished) 58 | XCTAssertTrue(canceled) 59 | XCTAssertEqual(values, Array(allValues.prefix(2))) 60 | } 61 | 62 | func testNoDemandFinished() { 63 | let subscriber = makeSubscriber(demand: .none) 64 | let publisher = makePublisher(fail: false) 65 | 66 | publisher.subscribe(subscriber) 67 | 68 | XCTAssertEqual(completion, .finished) 69 | XCTAssertTrue(canceled) 70 | XCTAssertTrue(values.isEmpty) 71 | } 72 | 73 | func testUnlimitedDemandError() { 74 | let subscriber = makeSubscriber(demand: .unlimited) 75 | let publisher = makePublisher(fail: true) 76 | 77 | publisher.subscribe(subscriber) 78 | 79 | XCTAssertEqual(completion, .failure(MyError.failure)) 80 | XCTAssertTrue(canceled) 81 | XCTAssertEqual(values, allValues) 82 | } 83 | 84 | func testLimitedDemandError() { 85 | let subscriber = makeSubscriber(demand: .max(2)) 86 | let publisher = makePublisher(fail: true) 87 | 88 | publisher.subscribe(subscriber) 89 | 90 | XCTAssertEqual(completion, .failure(MyError.failure)) 91 | XCTAssertTrue(canceled) 92 | XCTAssertEqual(values, Array(allValues.prefix(2))) 93 | } 94 | 95 | func testNoDemandError() { 96 | let subscriber = makeSubscriber(demand: .none) 97 | let publisher = makePublisher(fail: true) 98 | 99 | publisher.subscribe(subscriber) 100 | 101 | XCTAssertEqual(completion, .failure(MyError.failure)) 102 | XCTAssertTrue(canceled) 103 | XCTAssertTrue(values.isEmpty) 104 | } 105 | 106 | var cancelable: Cancellable? 107 | func testManualCancelation() { 108 | let publisher = AnyPublisher.create { _ in 109 | return AnyCancellable { [weak self] in self?.canceled = true } 110 | } 111 | 112 | cancelable = publisher.sink { _ in } 113 | XCTAssertFalse(canceled) 114 | cancelable?.cancel() 115 | XCTAssertTrue(canceled) 116 | } 117 | } 118 | 119 | // MARK: - Private Helpers 120 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 121 | private extension CreateTests { 122 | func makePublisher(fail: Bool = false) -> AnyPublisher { 123 | AnyPublisher.create { subscriber in 124 | self.allValues.forEach { subscriber.send($0) } 125 | subscriber.send(completion: fail ? .failure(MyError.failure) : .finished) 126 | 127 | return AnyCancellable { [weak self] in 128 | self?.canceled = true 129 | } 130 | } 131 | .eraseToAnyPublisher() 132 | } 133 | 134 | func makeSubscriber(demand: Subscribers.Demand) -> AnySubscriber { 135 | return AnySubscriber( 136 | receiveSubscription: { subscription in 137 | XCTAssertEqual("\(subscription)", "Create.Subscription") 138 | subscription.request(demand) 139 | }, 140 | receiveValue: { value in 141 | self.values.append(value) 142 | return .none 143 | }, 144 | receiveCompletion: { finished in 145 | self.completion = finished 146 | }) 147 | } 148 | } 149 | #endif 150 | -------------------------------------------------------------------------------- /Tests/DematerializeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DematerializeTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class DematerializeTests: XCTestCase { 16 | var subscription: AnyCancellable? 17 | var values = [String]() 18 | var completion: Subscribers.Completion? 19 | var subject = PassthroughSubject, Never>() 20 | 21 | override func setUp() { 22 | values = [] 23 | completion = nil 24 | subject = PassthroughSubject, Never>() 25 | } 26 | 27 | override func tearDown() { 28 | subscription?.cancel() 29 | } 30 | 31 | enum MyError: Swift.Error { 32 | case someError 33 | } 34 | 35 | func testEmpty() { 36 | subscription = subject 37 | .dematerialize() 38 | .sink(receiveCompletion: { self.completion = $0 }, 39 | receiveValue: { self.values.append($0) }) 40 | 41 | subject.send(.finished) 42 | 43 | XCTAssertTrue(values.isEmpty) 44 | XCTAssertEqual(completion, .finished) 45 | } 46 | 47 | func testFail() { 48 | subscription = subject 49 | .dematerialize() 50 | .sink(receiveCompletion: { self.completion = $0 }, 51 | receiveValue: { self.values.append($0) }) 52 | 53 | subject.send(.failure(.someError)) 54 | 55 | XCTAssertTrue(values.isEmpty) 56 | XCTAssertEqual(completion, .failure(.someError)) 57 | } 58 | 59 | func testFinished() { 60 | subscription = subject 61 | .dematerialize() 62 | .sink(receiveCompletion: { self.completion = $0 }, 63 | receiveValue: { self.values.append($0) }) 64 | 65 | subject.send(.value("Hello")) 66 | subject.send(.value("There")) 67 | subject.send(.value("World!")) 68 | subject.send(.finished) 69 | 70 | XCTAssertEqual(values, ["Hello", "There", "World!"]) 71 | XCTAssertEqual(completion, .finished) 72 | } 73 | 74 | func testFinishedLimitedDemand() { 75 | let subscriber = makeSubscriber(demand: .max(2)) 76 | 77 | subject 78 | .dematerialize() 79 | .subscribe(subscriber) 80 | 81 | subject.send(.value("Hello")) 82 | subject.send(.value("There")) 83 | subject.send(.value("World!")) 84 | subject.send(.finished) 85 | 86 | XCTAssertEqual(values, ["Hello", "There"]) 87 | XCTAssertEqual(completion, nil) 88 | } 89 | 90 | func testError() { 91 | subscription = subject 92 | .dematerialize() 93 | .sink(receiveCompletion: { self.completion = $0 }, 94 | receiveValue: { self.values.append($0) }) 95 | 96 | subject.send(.value("Hello")) 97 | subject.send(.value("There")) 98 | subject.send(.value("World!")) 99 | subject.send(.failure(.someError)) 100 | 101 | XCTAssertEqual(values, ["Hello", "There", "World!"]) 102 | XCTAssertEqual(completion, .failure(.someError)) 103 | } 104 | 105 | func testErrorLimitedDemand() { 106 | let subscriber = makeSubscriber(demand: .max(2)) 107 | 108 | subject 109 | .dematerialize() 110 | .subscribe(subscriber) 111 | 112 | subject.send(.value("Hello")) 113 | subject.send(.value("There")) 114 | subject.send(.value("World!")) 115 | subject.send(.failure(.someError)) 116 | 117 | XCTAssertEqual(values, ["Hello", "There"]) 118 | XCTAssertEqual(completion, nil) 119 | } 120 | } 121 | 122 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 123 | private extension DematerializeTests { 124 | func makeSubscriber(demand: Subscribers.Demand) -> AnySubscriber { 125 | AnySubscriber( 126 | receiveSubscription: { subscription in 127 | subscription.request(demand) 128 | }, 129 | receiveValue: { value in 130 | self.values.append(value) 131 | return .none 132 | }, 133 | receiveCompletion: { finished in 134 | self.completion = finished 135 | }) 136 | } 137 | } 138 | #endif 139 | -------------------------------------------------------------------------------- /Tests/EnumeratedTests.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import Combine 3 | import CombineExt 4 | import XCTest 5 | 6 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 7 | final class EnumeratedTests: XCTestCase { 8 | private var cancellables = Set() 9 | 10 | override func tearDown() { 11 | cancellables.removeAll() 12 | 13 | super.tearDown() 14 | } 15 | 16 | func testEnumeratedWithDefaultInitialValueReturnsIndexAndElements() { 17 | let source = PassthroughSubject() 18 | var output = [(index: Int, element: String)]() 19 | var completion: Subscribers.Completion? 20 | 21 | source.enumerated().sink( 22 | receiveCompletion: { completion = $0 }, 23 | receiveValue: { output.append($0) } 24 | ).store(in: &cancellables) 25 | 26 | source.send("1") 27 | source.send("2") 28 | source.send("3") 29 | source.send(completion: .finished) 30 | 31 | XCTAssertEqual(output.map(\.index), [0, 1, 2]) 32 | XCTAssertEqual(output.map(\.element), ["1", "2", "3"]) 33 | XCTAssertEqual(completion, .finished) 34 | } 35 | 36 | func testEnumeratedWithCustomInitialValueReturnsIndexAndElements() { 37 | let initial = 10 38 | let source = PassthroughSubject() 39 | var output = [(index: Int, element: String)]() 40 | var completion: Subscribers.Completion? 41 | 42 | source.enumerated(initial: initial).sink( 43 | receiveCompletion: { completion = $0 }, 44 | receiveValue: { output.append($0) } 45 | ).store(in: &cancellables) 46 | 47 | source.send("1") 48 | source.send("2") 49 | source.send("3") 50 | source.send(completion: .finished) 51 | 52 | XCTAssertEqual(output.map(\.index), [initial, initial + 1, initial + 2]) 53 | XCTAssertEqual(output.map(\.element), ["1", "2", "3"]) 54 | XCTAssertEqual(completion, .finished) 55 | } 56 | 57 | func testEnumeratedWhenUpstreamFailsReturnsIndexAndElements() { 58 | struct MyError: Error, Equatable { 59 | let id = UUID() 60 | } 61 | 62 | let error = MyError() 63 | let source = PassthroughSubject() 64 | var output = [(index: Int, element: String)]() 65 | var completion: Subscribers.Completion? 66 | 67 | source.enumerated().sink( 68 | receiveCompletion: { completion = $0 }, 69 | receiveValue: { output.append($0) } 70 | ).store(in: &cancellables) 71 | 72 | source.send("1") 73 | source.send("2") 74 | source.send("3") 75 | source.send(completion: .failure(error)) 76 | 77 | XCTAssertEqual(output.map(\.index), [0, 1, 2]) 78 | XCTAssertEqual(output.map(\.element), ["1", "2", "3"]) 79 | XCTAssertEqual(completion, .failure(error)) 80 | } 81 | 82 | func testEnumeratedWhenUpstreamHasNoElementsReturnsNoElements() { 83 | let source = PassthroughSubject() 84 | var output = [(index: Int, element: String)]() 85 | var completion: Subscribers.Completion? 86 | 87 | source.enumerated().sink( 88 | receiveCompletion: { completion = $0 }, 89 | receiveValue: { output.append($0) } 90 | ).store(in: &cancellables) 91 | 92 | source.send(completion: .finished) 93 | 94 | XCTAssert(output.isEmpty) 95 | XCTAssertEqual(completion, .finished) 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Tests/FilterManyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterManyTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Hugo Saynac on 30/09/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | class FilterManyTests: XCTestCase { 15 | var subscription: AnyCancellable! 16 | 17 | func testFilterManyWithModelAndFinishedCompletion() { 18 | let source = PassthroughSubject<[Int], Never>() 19 | 20 | var expectedOutput = [Int]() 21 | 22 | var completion: Subscribers.Completion? 23 | 24 | subscription = source 25 | .filterMany(isPair) 26 | .sink( 27 | receiveCompletion: { completion = $0 }, 28 | receiveValue: { $0.forEach { expectedOutput.append($0) } } 29 | ) 30 | 31 | source.send([10, 1, 2, 4, 3, 8]) 32 | source.send(completion: .finished) 33 | 34 | XCTAssertEqual( 35 | expectedOutput, 36 | [10, 2, 4, 8] 37 | ) 38 | XCTAssertEqual(completion, .finished) 39 | } 40 | 41 | func testFilterManyWithModelAndFailureCompletion() { 42 | let source = PassthroughSubject<[Int], FilterManyError>() 43 | 44 | var expectedOutput = [Int]() 45 | 46 | var completion: Subscribers.Completion? 47 | 48 | subscription = source 49 | .filterMany(isPair) 50 | .sink( 51 | receiveCompletion: { completion = $0 }, 52 | receiveValue: { $0.forEach { expectedOutput.append($0) } } 53 | ) 54 | 55 | source.send([10, 1, 2, 4, 3, 8]) 56 | source.send(completion: .failure(.anErrorCase)) 57 | 58 | XCTAssertEqual( 59 | expectedOutput, 60 | [10, 2, 4, 8] 61 | ) 62 | XCTAssertEqual(completion, .failure(.anErrorCase)) 63 | } 64 | } 65 | 66 | private func isPair(_ value: Int) -> Bool { 67 | value.isMultiple(of: 2) 68 | } 69 | 70 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 71 | private extension FilterManyTests { 72 | enum FilterManyError: Error { 73 | case anErrorCase 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Tests/FlatMapBatchesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatMapBatchesTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Jasdev Singh on 23/01/2021. 6 | // Copyright © 2021 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class FlatMapBatchesTests: XCTestCase { 16 | private var subscription: AnyCancellable! 17 | 18 | private enum BatchedSubscribeError: Error, Equatable { 19 | case anError 20 | } 21 | 22 | func testEvenBatches() { 23 | let ints = (1...6).map(Just.init) 24 | 25 | var results = [[Int]]() 26 | var completed = false 27 | 28 | subscription = ints 29 | .flatMapBatches(of: 2) 30 | .sink(receiveCompletion: { _ in completed = true }, 31 | receiveValue: { results.append($0) }) 32 | 33 | XCTAssertEqual(results, [[1, 2], [3, 4], [5, 6]]) 34 | XCTAssertTrue(completed) 35 | } 36 | 37 | func testUnevenBatches() { 38 | let ints = (1...5).map(Just.init) 39 | 40 | var results = [[Int]]() 41 | var completed = false 42 | 43 | subscription = ints 44 | .flatMapBatches(of: 2) 45 | .sink(receiveCompletion: { _ in completed = true }, 46 | receiveValue: { results.append($0) }) 47 | 48 | XCTAssertEqual(results, [[1, 2], [3, 4], [5]]) 49 | XCTAssertTrue(completed) 50 | } 51 | 52 | func testForwardsError() { 53 | let publishers = [Fail(error: BatchedSubscribeError.anError).eraseToAnyPublisher()] + 54 | (1...3).map { 55 | Just($0) 56 | .setFailureType(to: BatchedSubscribeError.self) 57 | .eraseToAnyPublisher() 58 | } 59 | 60 | var results = [[Int]]() 61 | var completion: Subscribers.Completion? 62 | 63 | subscription = publishers 64 | .flatMapBatches(of: 2) 65 | .sink(receiveCompletion: { completion = $0 }, 66 | receiveValue: { results.append($0) }) 67 | 68 | XCTAssertTrue(results.isEmpty) 69 | XCTAssertEqual(completion, .failure(.anError)) 70 | } 71 | 72 | func testHangsIfEarlierBatchDoesntComplete() { 73 | let uncompleted = (1...2).map { number in 74 | AnyPublisher.create { subscriber in 75 | subscriber.send(number) 76 | return AnyCancellable { } 77 | } 78 | } 79 | 80 | let publishers = uncompleted + 81 | (3...4).map(Just.init).map(AnyPublisher.init) 82 | 83 | var results = [[Int]]() 84 | var completed = false 85 | 86 | subscription = publishers 87 | .flatMapBatches(of: 2) 88 | .sink(receiveCompletion: { _ in completed = true }, 89 | receiveValue: { results.append($0) }) 90 | 91 | XCTAssertEqual(results, [[1, 2]]) 92 | XCTAssertFalse(completed) 93 | } 94 | 95 | func testEmptyCollection() { 96 | let publishers = EmptyCollection>() 97 | 98 | var results = [[Int]]() 99 | var completed = false 100 | 101 | subscription = publishers 102 | .flatMapBatches(of: 2) 103 | .sink(receiveCompletion: { _ in completed = true }, 104 | receiveValue: { results.append($0) }) 105 | 106 | XCTAssertTrue(results.isEmpty) 107 | XCTAssertTrue(completed) 108 | } 109 | 110 | func testBatchLimitLargerThanCount() { 111 | let ints = [Just(1)] 112 | 113 | var results = [[Int]]() 114 | var completed = false 115 | 116 | subscription = ints 117 | .flatMapBatches(of: 2) 118 | .sink(receiveCompletion: { _ in completed = true }, 119 | receiveValue: { results.append($0) }) 120 | 121 | XCTAssertEqual(results, [[1]]) 122 | XCTAssertTrue(completed) 123 | } 124 | 125 | func testMultipleOutputsPerPublisher() { 126 | let publishers = (1...2).map { number in 127 | AnyPublisher.create { subscriber in 128 | subscriber.send(number) 129 | subscriber.send(number) 130 | subscriber.send(completion: .finished) 131 | 132 | return AnyCancellable { } 133 | } 134 | } + 135 | (3...4).map(Just.init).map(AnyPublisher.init) 136 | 137 | var results = [[Int]]() 138 | var completed = false 139 | 140 | subscription = publishers 141 | .flatMapBatches(of: 2) 142 | .sink(receiveCompletion: { _ in completed = true }, 143 | receiveValue: { results.append($0) }) 144 | 145 | XCTAssertEqual(results, [[1, 2], [1, 2], [3, 4]]) 146 | XCTAssertTrue(completed) 147 | } 148 | } 149 | #endif 150 | -------------------------------------------------------------------------------- /Tests/FlatMapFirstTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatMapFirstTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Martin Troup on 22/03/2022. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import Combine 11 | import CombineSchedulers 12 | import Foundation 13 | import XCTest 14 | 15 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | class FlatMapFirstTests: XCTestCase { 17 | var cancellables: Set! 18 | 19 | override func setUp() { 20 | super.setUp() 21 | 22 | cancellables = [] 23 | } 24 | 25 | struct TestError: Error, Equatable {} 26 | 27 | func testSingleUpstreamSingleFlatMap() { 28 | let testScheduler = DispatchQueue.test 29 | 30 | var innerPublisherSubscriptionCount = 0 31 | var innerPublisherCompletionCount = 0 32 | var isUpstreamCompleted = false 33 | 34 | Just("").setFailureType(to: Never.self) 35 | .delay(for: 1, scheduler: testScheduler) 36 | .flatMapFirst { _ -> AnyPublisher in 37 | return Just(Date()) 38 | .delay(for: 1, scheduler: testScheduler) 39 | .handleEvents( 40 | receiveSubscription: { _ in innerPublisherSubscriptionCount += 1 }, 41 | receiveCompletion: { _ in innerPublisherCompletionCount += 1 } 42 | ) 43 | .eraseToAnyPublisher() 44 | } 45 | .sink( 46 | receiveCompletion: { completion in 47 | if case .finished = completion { 48 | isUpstreamCompleted = true 49 | } 50 | }, 51 | receiveValue: { _ in } 52 | ) 53 | .store(in: &cancellables) 54 | 55 | testScheduler.advance(by: 2) 56 | 57 | XCTAssertEqual(innerPublisherSubscriptionCount, 1) 58 | XCTAssertEqual(innerPublisherCompletionCount, 1) 59 | XCTAssertTrue(isUpstreamCompleted) 60 | } 61 | 62 | func testErrorUpstreamSkippingFlatMap() { 63 | let testScheduler = DispatchQueue.test 64 | 65 | var innerPublisherSubscriptionCount = 0 66 | var isUpstreamCompleted = false 67 | 68 | Fail(error: TestError()).eraseToAnyPublisher() 69 | .delay(for: 1, scheduler: testScheduler) 70 | .flatMapFirst { (_: String) -> AnyPublisher in 71 | return Just(Date()).setFailureType(to: TestError.self) 72 | .handleEvents(receiveSubscription: { _ in innerPublisherSubscriptionCount += 1 }) 73 | .eraseToAnyPublisher() 74 | } 75 | .sink( 76 | receiveCompletion: { completion in 77 | if case let .failure(error) = completion { 78 | XCTAssertEqual(error, TestError()) 79 | isUpstreamCompleted = true 80 | } 81 | }, 82 | receiveValue: { _ in } 83 | ) 84 | .store(in: &cancellables) 85 | 86 | testScheduler.advance(by: 1) 87 | 88 | XCTAssertEqual(innerPublisherSubscriptionCount, 0) 89 | XCTAssertTrue(isUpstreamCompleted) 90 | } 91 | 92 | func testStandardProcessingOfFlatMapFirst() { 93 | let testScheduler = DispatchQueue.test 94 | 95 | var innerPublisherSubscriptionCount = 0 96 | var innerPublisherCompletionCount = 0 97 | var isUpstreamCompleted = false 98 | 99 | testScheduler.timerPublisher(every: 1) 100 | .autoconnect() 101 | .prefix(100) 102 | .flatMapFirst { _ -> AnyPublisher in 103 | return Just(Date()) 104 | .handleEvents( 105 | receiveSubscription: { _ in innerPublisherSubscriptionCount += 1 }, 106 | receiveCompletion: { _ in innerPublisherCompletionCount += 1 } 107 | ) 108 | .delay(for: 10, scheduler: testScheduler) 109 | .eraseToAnyPublisher() 110 | } 111 | .sink( 112 | receiveCompletion: { completion in 113 | if case .finished = completion { 114 | isUpstreamCompleted = true 115 | } 116 | }, 117 | receiveValue: { _ in } 118 | ) 119 | .store(in: &cancellables) 120 | 121 | testScheduler.advance(by: 110) 122 | 123 | XCTAssertEqual(innerPublisherSubscriptionCount, 10) 124 | XCTAssertEqual(innerPublisherCompletionCount, 10) 125 | XCTAssertTrue(isUpstreamCompleted) 126 | } 127 | } 128 | #endif 129 | -------------------------------------------------------------------------------- /Tests/FlatMapLatestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatMapLatestTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 13/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | import CombineSchedulers 14 | 15 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | class FlatMapLatestTests: XCTestCase { 17 | var subscription: AnyCancellable! 18 | 19 | func testInnerOnly() { 20 | let trigger = PassthroughSubject() 21 | var subscriptions = 0 22 | var values = 0 23 | var cancellations = 0 24 | var completed = false 25 | let scheduler = DispatchQueue.test 26 | 27 | func publish() -> AnyPublisher { 28 | Publishers.Timer(every: 0.5, scheduler: scheduler) 29 | .autoconnect() 30 | .map { _ in UUID().uuidString } 31 | .prefix(2) 32 | .eraseToAnyPublisher() 33 | } 34 | 35 | subscription = trigger 36 | .flatMapLatest { _ -> AnyPublisher in 37 | return publish() 38 | .handleEvents(receiveSubscription: { _ in subscriptions += 1 }, 39 | receiveCancel: { cancellations += 1 }) 40 | .eraseToAnyPublisher() 41 | } 42 | .sink(receiveCompletion: { _ in 43 | completed = true 44 | }, 45 | receiveValue: { _ in values += 1 }) 46 | 47 | trigger.send() 48 | trigger.send() 49 | trigger.send() 50 | trigger.send() 51 | 52 | scheduler.advance(by: 5) 53 | 54 | XCTAssertEqual(subscriptions, 4) 55 | XCTAssertEqual(cancellations, 3) 56 | XCTAssertEqual(values, 2) 57 | 58 | // There is a known bug in Xcode 11.3 and below where an inner 59 | // completion doesn't complete the outer publisher, so this test 60 | // will only work after Xcode 11.4 and iOS/tvOS 13.4 or macOS 10.15.4. 61 | // See: https://forums.swift.org/t/confused-about-behaviour-of-switchtolatest-in-combine/29914/24 62 | // XCTAssertTrue(completed) 63 | _ = completed 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /Tests/IgnoreFailureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IgnoreFailureTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Jasdev Singh on 17/10/20. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | import CombineSchedulers 14 | 15 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | final class IgnoreFailureTests: XCTestCase { 17 | private var cancellable: AnyCancellable! 18 | 19 | func testIgnoreFailure() { 20 | let publisher = Just("someString") 21 | .setFailureType(to: Error.self) 22 | .ignoreFailure() // `Never` out the above failure type. 23 | .eraseToAnyPublisher() 24 | 25 | XCTAssertTrue(type(of: publisher) == AnyPublisher.self) 26 | } 27 | 28 | private enum TestError: Error { 29 | case anError 30 | } 31 | 32 | func testIgnoreFailureErrorEventCompleteImmediately() { 33 | let subject = PassthroughSubject() 34 | 35 | var values = [Int]() 36 | var completions = [Subscribers.Completion]() 37 | 38 | cancellable = subject 39 | .ignoreFailure() 40 | .sink( 41 | receiveCompletion: { completions.append($0) }, 42 | receiveValue: { values.append($0) }) 43 | 44 | subject.send(1) 45 | subject.send(2) 46 | subject.send(3) 47 | 48 | subject.send(completion: .failure(.anError)) 49 | 50 | XCTAssertEqual([1, 2, 3], values) 51 | XCTAssertEqual([.finished], completions) 52 | } 53 | 54 | func testIgnoreFailureErrorEventNoCompletion() { 55 | let subject = PassthroughSubject() 56 | 57 | var values = [Int]() 58 | var completions = [Subscribers.Completion]() 59 | 60 | cancellable = subject 61 | .ignoreFailure(completeImmediately: false) 62 | .sink( 63 | receiveCompletion: { completions.append($0) }, 64 | receiveValue: { values.append($0) }) 65 | 66 | subject.send(1) 67 | subject.send(2) 68 | subject.send(3) 69 | 70 | subject.send(completion: .failure(.anError)) 71 | 72 | XCTAssertEqual([1, 2, 3], values) 73 | XCTAssertTrue(completions.isEmpty) 74 | } 75 | 76 | private enum AnotherTestError: Error, Equatable { 77 | case anotherError 78 | } 79 | 80 | func testIgnoreFailureSetFailureTypeCompleteImmediately() { 81 | let subject = PassthroughSubject() 82 | 83 | var values = [Int]() 84 | var completions = [Subscribers.Completion]() 85 | 86 | let newPublisher = subject 87 | .ignoreFailure(setFailureType: AnotherTestError.self) 88 | .eraseToAnyPublisher() 89 | 90 | XCTAssertTrue(type(of: newPublisher) == AnyPublisher.self) 91 | 92 | cancellable = newPublisher 93 | .sink( 94 | receiveCompletion: { completions.append($0) }, 95 | receiveValue: { values.append($0) }) 96 | 97 | subject.send(1) 98 | subject.send(2) 99 | subject.send(3) 100 | 101 | subject.send(completion: .failure(.anError)) 102 | 103 | XCTAssertEqual([1, 2, 3], values) 104 | XCTAssertEqual([.finished], completions) 105 | } 106 | 107 | func testIgnoreFailureSetFailureTypeNoCompletion() { 108 | let subject = PassthroughSubject() 109 | 110 | var values = [Int]() 111 | var completions = [Subscribers.Completion]() 112 | 113 | let newPublisher = subject 114 | .ignoreFailure(setFailureType: AnotherTestError.self, completeImmediately: false) 115 | 116 | cancellable = newPublisher 117 | .sink( 118 | receiveCompletion: { completions.append($0) }, 119 | receiveValue: { values.append($0) }) 120 | 121 | subject.send(1) 122 | subject.send(2) 123 | subject.send(3) 124 | 125 | subject.send(completion: .failure(.anError)) 126 | 127 | XCTAssertEqual([1, 2, 3], values) 128 | XCTAssertTrue(completions.isEmpty) 129 | } 130 | } 131 | #endif 132 | -------------------------------------------------------------------------------- /Tests/IgnoreOutputSetOutputTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IgnoreOutputSetOutputTypeTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Jasdev Singh on 02/09/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class IgnoreOutputSetOutputTypeTests: XCTestCase { 16 | func testIgnoreOutputSetOutputType() { 17 | let publisher = Just("someString") 18 | .ignoreOutput(setOutputType: Int.self) 19 | .eraseToAnyPublisher() 20 | 21 | XCTAssertTrue(type(of: publisher) == AnyPublisher.self) 22 | } 23 | } 24 | #endif 25 | 26 | -------------------------------------------------------------------------------- /Tests/MapManyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapManyTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Joan Disho on 22/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | class MapManyTests: XCTestCase { 15 | var subscription: AnyCancellable! 16 | 17 | func testMapManyWithModelAndFinishedCompletion() { 18 | let source = PassthroughSubject<[Int], Never>() 19 | 20 | var expectedOutput = [SomeModel]() 21 | 22 | var completion: Subscribers.Completion? 23 | 24 | subscription = source 25 | .mapMany(SomeModel.init) 26 | .sink( 27 | receiveCompletion: { completion = $0 }, 28 | receiveValue: { $0.forEach { expectedOutput.append($0) } } 29 | ) 30 | 31 | source.send([10, 2, 2, 4, 3, 8]) 32 | source.send(completion: .finished) 33 | 34 | XCTAssertEqual( 35 | expectedOutput, 36 | [ 37 | SomeModel(10), 38 | SomeModel(2), 39 | SomeModel(2), 40 | SomeModel(4), 41 | SomeModel(3), 42 | SomeModel(8) 43 | ] 44 | ) 45 | XCTAssertEqual(completion, .finished) 46 | } 47 | 48 | func testMapManyWithModelAndFailureCompletion() { 49 | let source = PassthroughSubject<[Int], MapManyError>() 50 | 51 | var expectedOutput = [SomeModel]() 52 | 53 | var completion: Subscribers.Completion? 54 | 55 | subscription = source 56 | .mapMany(SomeModel.init) 57 | .sink( 58 | receiveCompletion: { completion = $0 }, 59 | receiveValue: { $0.forEach { expectedOutput.append($0) } } 60 | ) 61 | 62 | source.send([10, 2, 2, 4, 3, 8]) 63 | source.send(completion: .failure(.anErrorCase)) 64 | 65 | XCTAssertEqual( 66 | expectedOutput, 67 | [ 68 | SomeModel(10), 69 | SomeModel(2), 70 | SomeModel(2), 71 | SomeModel(4), 72 | SomeModel(3), 73 | SomeModel(8) 74 | ] 75 | ) 76 | XCTAssertEqual(completion, .failure(.anErrorCase)) 77 | } 78 | } 79 | 80 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 81 | private extension MapManyTests { 82 | enum MapManyError: Error { 83 | case anErrorCase 84 | } 85 | 86 | struct SomeModel: Equatable, CustomStringConvertible { 87 | let number: Int 88 | var description: String { return "#\(number)" } 89 | 90 | init(_ number: Int) { 91 | self.number = number 92 | } 93 | } 94 | } 95 | #endif 96 | -------------------------------------------------------------------------------- /Tests/MapToResultTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapToResultTests.swift 3 | // CombineExt 4 | // 5 | // Created by Yurii Zadoianchuk on 05/03/2021. 6 | // Copyright © 2021 Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !os(watchOS) 12 | import XCTest 13 | import Combine 14 | import CombineExt 15 | 16 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 17 | final class MapToResultTests: XCTestCase { 18 | private var subscription: AnyCancellable! 19 | 20 | enum MapToResultError: Error { 21 | case someError 22 | } 23 | 24 | func testMapResultNoError() { 25 | let subject = PassthroughSubject() 26 | let testInt = 5 27 | var completed = false 28 | var results: [Result] = [] 29 | 30 | subscription = subject 31 | .mapToResult() 32 | .sink(receiveCompletion: { _ in completed = true }, 33 | receiveValue: { results.append($0) }) 34 | 35 | subject.send(testInt) 36 | XCTAssertFalse(completed) 37 | subject.send(testInt) 38 | subject.send(completion: .finished) 39 | XCTAssertTrue(completed) 40 | XCTAssertEqual(results.count, 2) 41 | let intsCorrect = results 42 | .compactMap { try? $0.get() } 43 | .allSatisfy { $0 == testInt } 44 | XCTAssertTrue(intsCorrect) 45 | } 46 | 47 | func testMapCustomError() { 48 | let subject = PassthroughSubject() 49 | var completed = false 50 | var gotFailure = false 51 | var gotSuccess = false 52 | var result: Result? = nil 53 | 54 | subscription = subject 55 | .tryMap { _ -> Int in throw MapToResultError.someError } 56 | .mapToResult() 57 | .eraseToAnyPublisher() 58 | .sink(receiveCompletion: { _ in completed = true }, 59 | receiveValue: { result = $0 }) 60 | 61 | subject.send(0) 62 | XCTAssertNotNil(result) 63 | 64 | do { 65 | _ = try result!.get() 66 | gotSuccess = true 67 | } catch { 68 | gotFailure = true 69 | } 70 | 71 | XCTAssertTrue(gotFailure) 72 | XCTAssertFalse(gotSuccess) 73 | XCTAssertTrue(completed) 74 | } 75 | 76 | func testCatchDecodeError() { 77 | struct ToDecode: Decodable { 78 | let foo: Int 79 | } 80 | 81 | let incorrectJson = """ 82 | { 83 | "foo": "1" 84 | } 85 | """ 86 | 87 | let subject = PassthroughSubject() 88 | var completed = false 89 | var gotFailure = false 90 | var gotSuccess = false 91 | var result: Result? = nil 92 | 93 | subscription = subject 94 | .decode(type: ToDecode.self, decoder: JSONDecoder()) 95 | .mapToResult() 96 | .eraseToAnyPublisher() 97 | .sink(receiveCompletion: { _ in completed = true }, 98 | receiveValue: { result = $0 }) 99 | 100 | subject.send(incorrectJson.data(using: .utf8)!) 101 | XCTAssertNotNil(result) 102 | 103 | do { 104 | _ = try result!.get() 105 | gotSuccess = true 106 | } catch let e { 107 | XCTAssert(e is DecodingError) 108 | gotFailure = true 109 | } 110 | 111 | XCTAssertTrue(gotFailure) 112 | XCTAssertFalse(gotSuccess) 113 | XCTAssertTrue(completed) 114 | } 115 | 116 | func testMapEncodeError() { 117 | struct ToEncode: Encodable { 118 | let foo: Int 119 | 120 | func encode(to encoder: Encoder) throws { 121 | throw EncodingError.invalidValue((), EncodingError.Context(codingPath: [], debugDescription: String())) 122 | } 123 | } 124 | 125 | let subject = PassthroughSubject() 126 | var completed = false 127 | var gotFailure = false 128 | var gotSuccess = false 129 | var result: Result? = nil 130 | 131 | subscription = subject 132 | .encode(encoder: JSONEncoder()) 133 | .mapToResult() 134 | .eraseToAnyPublisher() 135 | .sink(receiveCompletion: { _ in completed = true }, 136 | receiveValue: { result = $0 }) 137 | 138 | subject.send(ToEncode(foo: 0)) 139 | XCTAssertNotNil(result) 140 | 141 | do { 142 | _ = try result!.get() 143 | gotSuccess = true 144 | } catch let e { 145 | XCTAssert(e is EncodingError) 146 | gotFailure = true 147 | } 148 | 149 | XCTAssertTrue(gotFailure) 150 | XCTAssertFalse(gotSuccess) 151 | XCTAssertTrue(completed) 152 | } 153 | } 154 | 155 | #endif 156 | -------------------------------------------------------------------------------- /Tests/MapToValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapToValueTests.swift 3 | // CombineExt 4 | // 5 | // Created by Dan Halliday on 08/05/2022. 6 | // Copyright © 2022 Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !os(watchOS) 12 | import XCTest 13 | import Combine 14 | import CombineExt 15 | 16 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 17 | final class MapToValueTests: XCTestCase { 18 | private var subscription: AnyCancellable! 19 | 20 | func testMapToConstantValue() { 21 | let subject = PassthroughSubject() 22 | var result: Int? = nil 23 | 24 | subscription = subject 25 | .mapToValue(2) 26 | .sink(receiveValue: { result = $0 }) 27 | 28 | subject.send(1) 29 | XCTAssertEqual(result, 2) 30 | } 31 | 32 | func testMapToWithMultipleElements() { 33 | let expectation = XCTestExpectation() 34 | expectation.expectedFulfillmentCount = 3 35 | 36 | let subject = PassthroughSubject() 37 | 38 | subscription = subject 39 | .mapToValue("hello") 40 | .sink { element in 41 | XCTAssertEqual(element, "hello") 42 | expectation.fulfill() 43 | } 44 | 45 | subject.send(1) 46 | subject.send(2) 47 | subject.send(1) 48 | 49 | wait(for: [expectation], timeout: 3) 50 | } 51 | 52 | func testMapToVoidType() { 53 | let expectation = XCTestExpectation() 54 | let subject = PassthroughSubject() 55 | 56 | subscription = subject 57 | .mapToValue(Void()) 58 | .sink { element in 59 | XCTAssertTrue(type(of: element) == Void.self) 60 | 61 | expectation.fulfill() 62 | } 63 | 64 | subject.send(1) 65 | 66 | wait(for: [expectation], timeout: 3) 67 | } 68 | 69 | func testMapToOptionalType() { 70 | let subject = PassthroughSubject() 71 | let value: String? = nil 72 | 73 | var result: String? = nil 74 | 75 | subscription = subject 76 | .mapToValue(value) 77 | .sink(receiveValue: { result = $0 }) 78 | 79 | subject.send(1) 80 | XCTAssertEqual(result, nil) 81 | } 82 | 83 | /// Checks if regular map functions complies and works as expected. 84 | func testMapNameCollision() { 85 | let fooSubject = PassthroughSubject() 86 | let barSubject = PassthroughSubject() 87 | 88 | var result: String? = nil 89 | 90 | let combinedPublisher = Publishers.CombineLatest(fooSubject, barSubject) 91 | .map { fooItem, barItem in 92 | fooItem * barItem 93 | } 94 | 95 | subscription = combinedPublisher 96 | .map { 97 | "\($0)" 98 | } 99 | .sink(receiveValue: { result = $0 }) 100 | 101 | fooSubject.send(5) 102 | barSubject.send(6) 103 | XCTAssertEqual(result, "30") 104 | } 105 | 106 | func testMapToVoidWithMultipleEvents() { 107 | let expectation = XCTestExpectation() 108 | expectation.expectedFulfillmentCount = 3 109 | 110 | let subject = PassthroughSubject() 111 | subscription = subject 112 | .mapToVoid() 113 | .sink { element in 114 | XCTAssertTrue(type(of: element) == Void.self) 115 | expectation.fulfill() 116 | } 117 | 118 | subject.send("test 1") 119 | subject.send("test 2") 120 | subject.send("test 3") 121 | 122 | wait(for: [expectation], timeout: 3) 123 | } 124 | 125 | func testMapToVoidWithError() { 126 | let expectation = XCTestExpectation() 127 | expectation.expectedFulfillmentCount = 3 128 | 129 | enum TestError: Error { 130 | case example 131 | } 132 | 133 | let subject = PassthroughSubject() 134 | subscription = subject 135 | .mapToVoid() 136 | .sink(receiveCompletion: { completion in 137 | switch completion { 138 | case .finished: 139 | XCTFail() 140 | default: 141 | break 142 | } 143 | }, receiveValue: { 144 | expectation.fulfill() 145 | }) 146 | 147 | subject.send("test 1") 148 | subject.send("test 2") 149 | subject.send("test 3") 150 | subject.send(completion: .failure(TestError.example)) 151 | 152 | wait(for: [expectation], timeout: 3) 153 | } 154 | } 155 | #endif 156 | -------------------------------------------------------------------------------- /Tests/MaterializeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaterializeTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 14/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class MaterializeTests: XCTestCase { 16 | var subscription: AnyCancellable? 17 | var values = [Event]() 18 | var completed = false 19 | 20 | override func setUp() { 21 | values = [] 22 | completed = false 23 | } 24 | 25 | override func tearDown() { 26 | subscription?.cancel() 27 | } 28 | 29 | enum MyError: Swift.Error { 30 | case someError 31 | } 32 | 33 | func testEmpty() { 34 | subscription = Empty() 35 | .materialize() 36 | .sink(receiveCompletion: { _ in self.completed = true }, 37 | receiveValue: { self.values.append($0) }) 38 | 39 | XCTAssertEqual(values, [.finished]) 40 | XCTAssertTrue(completed) 41 | } 42 | 43 | func testFail() { 44 | subscription = Fail(error: .someError) 45 | .materialize() 46 | .sink(receiveCompletion: { _ in self.completed = true }, 47 | receiveValue: { self.values.append($0) }) 48 | 49 | XCTAssertEqual(values, [.failure(.someError)]) 50 | XCTAssertTrue(completed) 51 | } 52 | 53 | func testFinished() { 54 | let subject = PassthroughSubject() 55 | 56 | subscription = subject 57 | .materialize() 58 | .sink(receiveCompletion: { _ in self.completed = true }, 59 | receiveValue: { self.values.append($0) }) 60 | 61 | subject.send("Hello") 62 | subject.send("There") 63 | subject.send("World!") 64 | subject.send(completion: .finished) 65 | 66 | XCTAssertEqual(values, [ 67 | .value("Hello"), 68 | .value("There"), 69 | .value("World!"), 70 | .finished 71 | ]) 72 | 73 | XCTAssertTrue(completed) 74 | } 75 | 76 | func testValuesFinished() { 77 | let subject = PassthroughSubject() 78 | var strings = [String]() 79 | 80 | subscription = subject 81 | .materialize() 82 | .values() 83 | .sink(receiveCompletion: { _ in self.completed = true }, 84 | receiveValue: { strings.append($0) }) 85 | 86 | subject.send("Hello") 87 | subject.send("There") 88 | subject.send("World!") 89 | subject.send(completion: .finished) 90 | 91 | XCTAssertEqual(strings, ["Hello", "There", "World!"]) 92 | XCTAssertTrue(completed) 93 | } 94 | 95 | func testFailuresFinished() { 96 | let subject = PassthroughSubject() 97 | var errors = [MyError]() 98 | 99 | subscription = subject 100 | .materialize() 101 | .failures() 102 | .sink(receiveCompletion: { _ in self.completed = true }, 103 | receiveValue: { errors.append($0) }) 104 | 105 | subject.send("Hello") 106 | subject.send("There") 107 | subject.send("World!") 108 | subject.send(completion: .finished) 109 | 110 | XCTAssertTrue(errors.isEmpty) 111 | XCTAssertTrue(completed) 112 | } 113 | 114 | func testError() { 115 | let subject = PassthroughSubject() 116 | 117 | subscription = subject 118 | .materialize() 119 | .sink(receiveCompletion: { _ in self.completed = true }, 120 | receiveValue: { self.values.append($0) }) 121 | 122 | subject.send("Hello") 123 | subject.send("There") 124 | subject.send("World!") 125 | subject.send(completion: .failure(.someError)) 126 | subject.send("Meh!") 127 | 128 | XCTAssertEqual(values, [ 129 | .value("Hello"), 130 | .value("There"), 131 | .value("World!"), 132 | .failure(.someError) 133 | ]) 134 | 135 | XCTAssertTrue(completed) 136 | } 137 | 138 | func testFailureesFinished() { 139 | let subject = PassthroughSubject() 140 | var errors = [MyError]() 141 | 142 | subscription = subject 143 | .materialize() 144 | .failures() 145 | .sink(receiveCompletion: { _ in self.completed = true }, 146 | receiveValue: { errors.append($0) }) 147 | 148 | subject.send("Hello") 149 | subject.send("There") 150 | subject.send("World!") 151 | subject.send(completion: .finished) 152 | 153 | XCTAssertTrue(errors.isEmpty) 154 | XCTAssertTrue(completed) 155 | } 156 | 157 | func testFailuresFailure() { 158 | let subject = PassthroughSubject() 159 | var errors = [MyError]() 160 | 161 | subscription = subject 162 | .materialize() 163 | .failures() 164 | .sink(receiveCompletion: { _ in self.completed = true }, 165 | receiveValue: { errors.append($0) }) 166 | 167 | subject.send("Hello") 168 | subject.send("There") 169 | subject.send("World!") 170 | subject.send(completion: .failure(.someError)) 171 | 172 | XCTAssertEqual(errors, [.someError]) 173 | XCTAssertTrue(completed) 174 | } 175 | 176 | /// Test that when a stream is cancelled, the cancel is propagated upstream 177 | func testCancelled() { 178 | let subject = PassthroughSubject() 179 | var valueCount = 0 180 | var cancelCount = 0 181 | 182 | subscription = subject 183 | .handleEvents(receiveCancel: { 184 | cancelCount += 1 185 | }) 186 | .materialize() 187 | .failures() 188 | .sink(receiveCompletion: { _ in }, 189 | receiveValue: { _ in valueCount += 1 } 190 | ) 191 | 192 | subscription?.cancel() 193 | subject.send("Hello") 194 | 195 | XCTAssertEqual(valueCount, 0, "0 values should be emitted after cancel") 196 | XCTAssertEqual(cancelCount, 1, "Cancel is reported upstream") 197 | } 198 | } 199 | #endif 200 | -------------------------------------------------------------------------------- /Tests/MergeManyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergeManyTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Joe Walsh on 8/17/20. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !os(watchOS) 12 | import XCTest 13 | import Combine 14 | import CombineExt 15 | 16 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 17 | final class MergeManyTests: XCTestCase { 18 | private var subscription: AnyCancellable! 19 | 20 | private enum MergeManyError: Error { 21 | case anError 22 | } 23 | 24 | func testOneEmissionMerging() { 25 | let first = PassthroughSubject() 26 | let second = PassthroughSubject() 27 | let third = PassthroughSubject() 28 | 29 | var results = [Int]() 30 | var completed = false 31 | 32 | subscription = [first, second, third] 33 | .merge() 34 | .sink(receiveCompletion: { _ in completed = true }, 35 | receiveValue: { results.append($0) }) 36 | 37 | first.send(1) 38 | second.send(2) 39 | third.send(3) 40 | 41 | XCTAssertEqual(results, [1, 2, 3]) 42 | XCTAssertFalse(completed) 43 | 44 | first.send(completion: .finished) 45 | second.send(completion: .finished) 46 | third.send(completion: .finished) 47 | 48 | XCTAssertTrue(completed) 49 | } 50 | 51 | func testMultipleEmissionMergingEndingWithAnError() { 52 | let first = PassthroughSubject() 53 | let second = PassthroughSubject() 54 | let third = PassthroughSubject() 55 | 56 | var results = [Int]() 57 | var completed: Subscribers.Completion? 58 | 59 | subscription = [first, second, third] 60 | .merge() 61 | .sink(receiveCompletion: { completed = $0 }, 62 | receiveValue: { results.append($0) }) 63 | 64 | first.send(1) 65 | first.send(1) 66 | first.send(1) 67 | first.send(1) 68 | 69 | second.send(2) 70 | second.send(2) 71 | second.send(2) 72 | 73 | third.send(3) 74 | third.send(3) 75 | 76 | XCTAssertEqual(results, [1, 1, 1, 1, 2, 2, 2, 3, 3]) 77 | XCTAssertNil(completed) 78 | first.send(completion: .failure(.anError)) 79 | XCTAssertEqual(completed, .failure(.anError)) 80 | } 81 | 82 | func testNoEmissionMerging() { 83 | let first = PassthroughSubject() 84 | let second = PassthroughSubject() 85 | let third = PassthroughSubject() 86 | 87 | var results = [Int]() 88 | var completed = false 89 | 90 | subscription = [first, second, third] 91 | .merge() 92 | .sink(receiveCompletion: { _ in completed = true }, 93 | receiveValue: { results.append($0) }) 94 | 95 | first.send(1) 96 | second.send(2) 97 | 98 | // Gated by `third` not emitting. 99 | 100 | XCTAssertEqual(results, [1, 2]) 101 | XCTAssertFalse(completed) 102 | } 103 | 104 | func testMergingEndingWithAFinishedCompletion() { 105 | let first = PassthroughSubject() 106 | let second = PassthroughSubject() 107 | let third = PassthroughSubject() 108 | 109 | var results = [Int]() 110 | var completed: Subscribers.Completion? 111 | 112 | subscription = [first, second, third] 113 | .merge() 114 | .sink(receiveCompletion: { completed = $0 }, 115 | receiveValue: { results.append($0) }) 116 | 117 | first.send(1) 118 | 119 | second.send(2) 120 | second.send(2) 121 | 122 | third.send(3) 123 | 124 | XCTAssertEqual(results, [1, 2, 2, 3]) 125 | XCTAssertNil(completed) 126 | first.send(completion: .finished) 127 | second.send(completion: .finished) 128 | third.send(completion: .finished) 129 | XCTAssertEqual(completed, .finished) 130 | } 131 | 132 | func testMergingWithAnInnerCompletionButNotAnOuter() { 133 | let first = PassthroughSubject() 134 | let second = PassthroughSubject() 135 | let third = PassthroughSubject() 136 | 137 | var results = [Int]() 138 | var completed: Subscribers.Completion? 139 | 140 | subscription = first 141 | .merge(with: second, third) 142 | .sink(receiveCompletion: { completed = $0 }, 143 | receiveValue: { results.append($0) }) 144 | 145 | first.send(1) 146 | first.send(1) 147 | 148 | second.send(2) 149 | second.send(2) 150 | 151 | third.send(3) 152 | 153 | XCTAssertEqual(results, [1, 1, 2, 2, 3]) 154 | XCTAssertNil(completed) 155 | first.send(completion: .finished) // Doesn’t trigger a completion since only one publisher is finished 156 | XCTAssertNil(completed) 157 | } 158 | 159 | func testMergingCollection() { 160 | let first = PassthroughSubject() 161 | let second = PassthroughSubject() 162 | let third = PassthroughSubject() 163 | 164 | var results = [Int]() 165 | var completed: Subscribers.Completion? 166 | 167 | subscription = [first, second, third] 168 | .merge() 169 | .sink(receiveCompletion: { completed = $0 }, 170 | receiveValue: { results.append($0) }) 171 | 172 | first.send(1) 173 | 174 | second.send(2) 175 | second.send(2) 176 | 177 | third.send(3) 178 | 179 | XCTAssertEqual(results, [1, 2, 2, 3]) 180 | XCTAssertNil(completed) 181 | 182 | first.send(completion: .finished) 183 | second.send(completion: .finished) 184 | third.send(completion: .finished) 185 | 186 | XCTAssertEqual(completed, .finished) 187 | } 188 | 189 | func testMergingWithASinglePublisher() { 190 | let first = PassthroughSubject() 191 | 192 | var completed = false 193 | var results = [Int]() 194 | 195 | subscription = [first] 196 | .merge() 197 | .sink(receiveCompletion: { _ in completed = true }, 198 | receiveValue: { results.append($0) }) 199 | 200 | first.send(1) 201 | 202 | XCTAssertEqual(results, [1]) 203 | XCTAssertFalse(completed) 204 | 205 | first.send(completion: .finished) 206 | 207 | XCTAssertTrue(completed) 208 | } 209 | 210 | func testMergingWithNoPublishers() { 211 | var completed = false 212 | var results = [Int]() 213 | 214 | subscription = [AnyPublisher]() 215 | .merge() 216 | .sink(receiveCompletion: { _ in completed = true }, 217 | receiveValue: { results.append($0) }) 218 | 219 | XCTAssertTrue(results.isEmpty) 220 | XCTAssertTrue(completed) 221 | } 222 | } 223 | #endif 224 | -------------------------------------------------------------------------------- /Tests/NwiseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentValueRelayTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Bas van Kuijck on 14/08/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class NwiseTests: XCTestCase { 16 | private var subscriptions = Set() 17 | 18 | func testNwise() { 19 | var expectedOutput: [[Int]] = [] 20 | var completion: Subscribers.Completion? 21 | 22 | Publishers.Sequence(sequence: [1, 2, 3, 4, 5, 6]) 23 | .nwise(3) 24 | .sink( 25 | receiveCompletion: { completion = $0 }, 26 | receiveValue: { expectedOutput.append($0) } 27 | ).store(in: &subscriptions) 28 | 29 | XCTAssertEqual( 30 | expectedOutput, 31 | [ 32 | [1, 2, 3], 33 | [2, 3, 4], 34 | [3, 4, 5], 35 | [4, 5, 6] 36 | ] 37 | ) 38 | XCTAssertEqual(completion, .finished) 39 | } 40 | 41 | func testNwiseNone() { 42 | var completion: Subscribers.Completion? 43 | 44 | Publishers.Sequence(sequence: [1, 2, 3]) 45 | .nwise(4) 46 | .sink( 47 | receiveCompletion: { completion = $0 }, 48 | receiveValue: { XCTAssert(false, "Should not receive a value, got \($0)") } 49 | ).store(in: &subscriptions) 50 | 51 | XCTAssertEqual(completion, .finished) 52 | } 53 | 54 | func testPairwise() { 55 | var expectedOutput: [[Int]] = [] 56 | var completion: Subscribers.Completion? 57 | 58 | Publishers.Sequence(sequence: [1, 2, 3, 4, 5, 6]) 59 | .pairwise() 60 | .sink( 61 | receiveCompletion: { completion = $0 }, 62 | receiveValue: { expectedOutput.append([$0.0, $0.1]) } 63 | ).store(in: &subscriptions) 64 | 65 | XCTAssertEqual( 66 | expectedOutput, 67 | [ 68 | [1, 2], 69 | [2, 3], 70 | [3, 4], 71 | [4, 5], 72 | [5, 6], 73 | ] 74 | ) 75 | XCTAssertEqual(completion, .finished) 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Tests/OptionalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalTests.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 11/05/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import Combine 11 | import CombineExt 12 | import XCTest 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class OptionalTests: XCTestCase { 16 | private var subscription: AnyCancellable! 17 | 18 | func testSomeInitialization() { 19 | var results = [Int]() 20 | var completion: Subscribers.Completion? 21 | 22 | subscription = Optional(1) 23 | .publisher 24 | .sink(receiveCompletion: { completion = $0 }, 25 | receiveValue: { results.append($0) }) 26 | 27 | XCTAssertEqual([1], results) 28 | XCTAssertEqual(.finished, completion) 29 | } 30 | 31 | func testNoneInitialization() { 32 | var results = [Int]() 33 | var completion: Subscribers.Completion? 34 | 35 | subscription = Optional.none 36 | .publisher 37 | .sink(receiveCompletion: { completion = $0 }, 38 | receiveValue: { results.append($0) }) 39 | 40 | XCTAssertTrue(results.isEmpty) 41 | XCTAssertEqual(.finished, completion) 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Tests/PartitionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithLatestFrom.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 24/10/2019. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class PartitionTests: XCTestCase { 16 | var source = PassthroughRelay() 17 | var evenSub: AnyCancellable! 18 | var oddSub: AnyCancellable! 19 | 20 | override func setUp() { 21 | source = .init() 22 | evenSub = nil 23 | oddSub = nil 24 | } 25 | 26 | func testPartitionBothMatch() { 27 | let (evens, odds) = source.partition { $0 % 2 == 0 } 28 | var evenValues = [Int]() 29 | var oddValues = [Int]() 30 | 31 | evenSub = evens 32 | .sink(receiveValue: { evenValues.append($0) }) 33 | 34 | oddSub = odds 35 | .sink(receiveValue: { oddValues.append($0) }) 36 | 37 | (0...10).forEach { source.accept($0) } 38 | 39 | XCTAssertEqual([1, 3, 5, 7, 9], oddValues) 40 | XCTAssertEqual([0, 2, 4, 6, 8, 10], evenValues) 41 | } 42 | 43 | func testPartitionOneSideMatch() { 44 | let (all, none) = source.partition { $0 <= 10 } 45 | var allValues = [Int]() 46 | var noneValues = [Int]() 47 | 48 | evenSub = all 49 | .sink(receiveValue: { allValues.append($0) }) 50 | 51 | oddSub = none 52 | .sink(receiveValue: { noneValues.append($0) }) 53 | 54 | (0...10).forEach { source.accept($0) } 55 | 56 | XCTAssertEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], allValues) 57 | XCTAssertTrue(noneValues.isEmpty) 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Tests/PassthroughRelayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PassthroughRelayTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Shai Mishali on 15/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | class PassthroughRelayTests: XCTestCase { 16 | private var relay: PassthroughRelay? 17 | private var values = [String]() 18 | private var subscriptions = Set() 19 | 20 | override func setUp() { 21 | relay = PassthroughRelay() 22 | subscriptions = .init() 23 | values = [] 24 | } 25 | 26 | func testFinishesOnDeinit() { 27 | var completed = false 28 | relay? 29 | .sink(receiveCompletion: { _ in completed = true }, 30 | receiveValue: { _ in }) 31 | .store(in: &subscriptions) 32 | 33 | XCTAssertFalse(completed) 34 | relay = nil 35 | XCTAssertTrue(completed) 36 | } 37 | 38 | func testNoReplay() { 39 | relay?.accept("these") 40 | relay?.accept("values") 41 | relay?.accept("shouldnt") 42 | relay?.accept("be") 43 | relay?.accept("forwaded") 44 | 45 | relay? 46 | .sink(receiveValue: { self.values.append($0) }) 47 | .store(in: &subscriptions) 48 | 49 | XCTAssertEqual(values, []) 50 | 51 | relay?.accept("yo") 52 | XCTAssertEqual(values, ["yo"]) 53 | 54 | relay?.accept("sup") 55 | XCTAssertEqual(values, ["yo", "sup"]) 56 | 57 | var secondInitial: String? 58 | _ = relay?.sink(receiveValue: { secondInitial = $0 }) 59 | XCTAssertNil(secondInitial) 60 | } 61 | 62 | func testVoidAccept() { 63 | let voidRelay = PassthroughRelay() 64 | var count = 0 65 | 66 | voidRelay 67 | .sink(receiveValue: { count += 1 }) 68 | .store(in: &subscriptions) 69 | 70 | voidRelay.accept() 71 | voidRelay.accept() 72 | voidRelay.accept() 73 | voidRelay.accept() 74 | voidRelay.accept() 75 | 76 | XCTAssertEqual(count, 5) 77 | } 78 | 79 | func testSubscribePublisher() { 80 | var completed = false 81 | relay? 82 | .sink(receiveCompletion: { _ in completed = true }, 83 | receiveValue: { self.values.append($0) }) 84 | .store(in: &subscriptions) 85 | 86 | ["1", "2", "3"] 87 | .publisher 88 | .subscribe(relay!) 89 | .store(in: &subscriptions) 90 | 91 | XCTAssertFalse(completed) 92 | XCTAssertEqual(values, ["1", "2", "3"]) 93 | } 94 | 95 | func testSubscribeRelay_Passthroughs() { 96 | var completed = false 97 | 98 | let input = PassthroughRelay() 99 | let output = PassthroughRelay() 100 | 101 | input 102 | .subscribe(output) 103 | .store(in: &subscriptions) 104 | output 105 | .sink(receiveCompletion: { _ in completed = true }, 106 | receiveValue: { self.values.append($0) }) 107 | .store(in: &subscriptions) 108 | 109 | input.accept("1") 110 | input.accept("2") 111 | input.accept("3") 112 | 113 | XCTAssertFalse(completed) 114 | XCTAssertEqual(values, ["1", "2", "3"]) 115 | } 116 | 117 | func testSubscribeRelay_CurrentValueToPassthrough() { 118 | var completed = false 119 | 120 | let input = CurrentValueRelay("initial") 121 | let output = PassthroughRelay() 122 | 123 | input 124 | .subscribe(output) 125 | .store(in: &subscriptions) 126 | output 127 | .sink(receiveCompletion: { _ in completed = true }, 128 | receiveValue: { self.values.append($0) }) 129 | .store(in: &subscriptions) 130 | 131 | input.accept("1") 132 | input.accept("2") 133 | input.accept("3") 134 | 135 | XCTAssertFalse(completed) 136 | XCTAssertEqual(values, ["initial", "1", "2", "3"]) 137 | } 138 | } 139 | #endif 140 | -------------------------------------------------------------------------------- /Tests/PrefixDurationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefixDurationTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by David Ohayon and Jasdev Singh on 24/04/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import Combine 11 | import CombineExt 12 | import CombineSchedulers 13 | import XCTest 14 | 15 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | final class PrefixDurationTests: XCTestCase { 17 | private var cancellable: AnyCancellable! 18 | 19 | func testValueEventInWindow() { 20 | let scheduler = DispatchQueue.test 21 | 22 | let subject = PassthroughSubject() 23 | 24 | var results = [Int]() 25 | var completions = [Subscribers.Completion]() 26 | 27 | cancellable = subject 28 | .prefix(duration: 0.5, on: scheduler) 29 | .sink(receiveCompletion: { completions.append($0) }, 30 | receiveValue: { results.append($0) }) 31 | 32 | scheduler.schedule(after: scheduler.now.advanced(by: 0.25)) { 33 | subject.send(1) 34 | } 35 | 36 | scheduler.schedule(after: scheduler.now.advanced(by: 1.5)) { 37 | subject.send(2) 38 | } 39 | 40 | scheduler.advance(by: 2) 41 | 42 | XCTAssertEqual(results, [1]) 43 | XCTAssertEqual(completions, [.finished]) 44 | } 45 | 46 | func testMultipleEventsInAndOutOfWindow() { 47 | let subject = PassthroughSubject() 48 | let scheduler = DispatchQueue.test 49 | 50 | var results = [Int]() 51 | var completions = [Subscribers.Completion]() 52 | 53 | cancellable = subject 54 | .prefix(duration: 0.8, on: scheduler) 55 | .sink(receiveCompletion: { completions.append($0) }, 56 | receiveValue: { results.append($0) }) 57 | 58 | subject.send(1) 59 | 60 | scheduler.schedule(after: scheduler.now.advanced(by: 0.25)) { 61 | subject.send(2) 62 | } 63 | 64 | scheduler.schedule(after: scheduler.now.advanced(by: 0.4)) { 65 | subject.send(3) 66 | } 67 | 68 | scheduler.schedule(after: scheduler.now.advanced(by: 1)) { 69 | subject.send(4) 70 | subject.send(5) 71 | subject.send(completion: .finished) 72 | } 73 | 74 | scheduler.advance(by: 2) 75 | 76 | XCTAssertEqual(results, [1, 2, 3]) 77 | XCTAssertEqual(completions, [.finished]) 78 | } 79 | 80 | func testNoValueEventsInWindow() { 81 | let subject = PassthroughSubject() 82 | let scheduler = DispatchQueue.test 83 | 84 | var results = [Int]() 85 | var completions = [Subscribers.Completion]() 86 | 87 | cancellable = subject 88 | .prefix(duration: 0.5, on: scheduler) 89 | .sink(receiveCompletion: { completions.append($0 ) }, 90 | receiveValue: { results.append($0) }) 91 | 92 | scheduler.schedule(after: scheduler.now.advanced(by: 1.5)) { 93 | subject.send(1) 94 | } 95 | 96 | scheduler.advance(by: 2) 97 | 98 | XCTAssertTrue(results.isEmpty) 99 | } 100 | 101 | func testFinishedInWindow() { 102 | let subject = PassthroughSubject() 103 | let scheduler = DispatchQueue.test 104 | 105 | var results = [Subscribers.Completion]() 106 | 107 | cancellable = subject 108 | .prefix(duration: 0.5, on: scheduler) 109 | .sink(receiveCompletion: { results.append($0) }, 110 | receiveValue: { _ in }) 111 | 112 | scheduler.schedule(after: scheduler.now.advanced(by: 0.25)) { 113 | subject.send(completion: .finished) 114 | } 115 | 116 | scheduler.advance(by: 2) 117 | 118 | XCTAssertEqual(results, [.finished]) 119 | } 120 | 121 | private enum AnError: Error { 122 | case someError 123 | } 124 | 125 | func testErrorInWindow() { 126 | let subject = PassthroughSubject() 127 | let scheduler = DispatchQueue.test 128 | 129 | var results = [Subscribers.Completion]() 130 | 131 | cancellable = subject 132 | .prefix(duration: 0.5, on: scheduler) 133 | .sink(receiveCompletion: { results.append($0) }, 134 | receiveValue: { _ in }) 135 | 136 | scheduler.schedule(after: scheduler.now.advanced(by: 0.25)) { 137 | subject.send(completion: .failure(.someError)) 138 | } 139 | 140 | scheduler.advance(by: 2) 141 | 142 | XCTAssertEqual(results, [.failure(.someError)]) 143 | } 144 | 145 | func testErrorEventOutsideWindowDoesntAffectFinishEvent() { 146 | let subject = PassthroughSubject() 147 | let scheduler = DispatchQueue.test 148 | 149 | var results = [Subscribers.Completion]() 150 | 151 | cancellable = subject 152 | .prefix(duration: 0.5, on: scheduler) 153 | .sink(receiveCompletion: { results.append($0) }, 154 | receiveValue: { _ in }) 155 | 156 | scheduler.schedule(after: scheduler.now.advanced(by: 0.75)) { 157 | subject.send(completion: .failure(.someError)) 158 | } 159 | 160 | scheduler.advance(by: 2) 161 | 162 | XCTAssertEqual(results, [.finished]) 163 | } 164 | } 165 | #endif 166 | -------------------------------------------------------------------------------- /Tests/PrefixWhileBehaviorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefixWhileBehaviorTests.swift 3 | // CombineExt 4 | // 5 | // Created by Jasdev Singh on 29/12/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import Combine 11 | import CombineExt 12 | import XCTest 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class PrefixWhileBehaviorTests: XCTestCase { 16 | private struct SomeError: Error, Equatable {} 17 | 18 | private var cancellable: AnyCancellable! 19 | 20 | func testExclusiveValueEventsWithFinished() { 21 | let intSubject = PassthroughSubject() 22 | 23 | var values = [Int]() 24 | var completions = [Subscribers.Completion]() 25 | 26 | cancellable = intSubject 27 | .prefix( 28 | while: { $0 % 2 == 0 }, 29 | behavior: .exclusive 30 | ) 31 | .sink( 32 | receiveCompletion: { completions.append($0) }, 33 | receiveValue: { values.append($0) } 34 | ) 35 | 36 | [0, 2, 4, 5] 37 | .forEach(intSubject.send) 38 | 39 | XCTAssertEqual(values, [0, 2, 4]) 40 | XCTAssertEqual(completions, [.finished]) 41 | } 42 | 43 | func testExclusiveValueEventsWithError() { 44 | let intSubject = PassthroughSubject() 45 | 46 | var values = [Int]() 47 | var completions = [Subscribers.Completion]() 48 | 49 | cancellable = intSubject 50 | .prefix( 51 | while: { $0 % 2 == 0 }, 52 | behavior: .exclusive 53 | ) 54 | .sink( 55 | receiveCompletion: { completions.append($0) }, 56 | receiveValue: { values.append($0) } 57 | ) 58 | 59 | [0, 2, 4] 60 | .forEach(intSubject.send) 61 | 62 | intSubject.send(completion: .failure(.init())) 63 | 64 | XCTAssertEqual(values, [0, 2, 4]) 65 | XCTAssertEqual(completions, [.failure(.init())]) 66 | } 67 | 68 | func testInclusiveValueEventsWithStopElement() { 69 | let intSubject = PassthroughSubject() 70 | 71 | var values = [Int]() 72 | var completions = [Subscribers.Completion]() 73 | 74 | cancellable = intSubject 75 | .prefix( 76 | while: { $0 % 2 == 0 }, 77 | behavior: .inclusive 78 | ) 79 | .sink( 80 | receiveCompletion: { completions.append($0) }, 81 | receiveValue: { values.append($0) } 82 | ) 83 | 84 | [0, 2, 4, 5] 85 | .forEach(intSubject.send) 86 | 87 | XCTAssertEqual(values, [0, 2, 4, 5]) 88 | XCTAssertEqual(completions, [.finished]) 89 | } 90 | 91 | func testInclusiveValueEventsWithErrorAfterStopElement() { 92 | let intSubject = PassthroughSubject() 93 | 94 | var values = [Int]() 95 | var completions = [Subscribers.Completion]() 96 | 97 | cancellable = intSubject 98 | .prefix( 99 | while: { $0 % 2 == 0 }, 100 | behavior: .inclusive 101 | ) 102 | .sink( 103 | receiveCompletion: { completions.append($0) }, 104 | receiveValue: { values.append($0) } 105 | ) 106 | 107 | [0, 2, 4, 5] 108 | .forEach(intSubject.send) 109 | 110 | intSubject.send(completion: .failure(.init())) 111 | 112 | XCTAssertEqual(values, [0, 2, 4, 5]) 113 | XCTAssertEqual(completions, [.finished]) 114 | } 115 | 116 | func testInclusiveValueEventsWithErrorBeforeStop() { 117 | let intSubject = PassthroughSubject() 118 | 119 | var values = [Int]() 120 | var completions = [Subscribers.Completion]() 121 | 122 | cancellable = intSubject 123 | .prefix( 124 | while: { $0 % 2 == 0 }, 125 | behavior: .inclusive 126 | ) 127 | .sink( 128 | receiveCompletion: { completions.append($0) }, 129 | receiveValue: { values.append($0) } 130 | ) 131 | 132 | [0, 2, 4] 133 | .forEach(intSubject.send) 134 | 135 | intSubject.send(completion: .failure(.init())) 136 | 137 | XCTAssertEqual(values, [0, 2, 4]) 138 | XCTAssertEqual(completions, [.failure(.init())]) 139 | } 140 | 141 | func testInclusiveEarlyCompletion() { 142 | let intSubject = PassthroughSubject() 143 | 144 | var values = [Int]() 145 | var completions = [Subscribers.Completion]() 146 | 147 | cancellable = intSubject 148 | .prefix( 149 | while: { $0 % 2 == 0 }, 150 | behavior: .inclusive 151 | ) 152 | .sink( 153 | receiveCompletion: { completions.append($0) }, 154 | receiveValue: { values.append($0) } 155 | ) 156 | 157 | [0, 2, 4] 158 | .forEach(intSubject.send) 159 | 160 | intSubject.send(completion: .finished) 161 | 162 | XCTAssertEqual(values, [0, 2, 4]) 163 | XCTAssertEqual(completions, [.finished]) 164 | } 165 | } 166 | #endif 167 | -------------------------------------------------------------------------------- /Tests/RetryWhenTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetryWhenTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Daniel Tartaglia on 8/28/21. 6 | // 7 | 8 | #if !os(watchOS) 9 | import XCTest 10 | import Combine 11 | import CombineExt 12 | 13 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | class RetryWhenTests: XCTestCase { 15 | var subscription: AnyCancellable! 16 | 17 | func testPassthroughNextAndComplete() { 18 | let source = PassthroughSubject() 19 | 20 | var expectedOutput: Int? 21 | 22 | var completion: Subscribers.Completion? 23 | 24 | subscription = source 25 | .retryWhen { error in 26 | error.filter { _ in false } 27 | } 28 | .sink( 29 | receiveCompletion: { completion = $0 }, 30 | receiveValue: { expectedOutput = $0 } 31 | ) 32 | 33 | source.send(2) 34 | source.send(completion: .finished) 35 | 36 | XCTAssertEqual( 37 | expectedOutput, 38 | 2 39 | ) 40 | XCTAssertEqual(completion, .finished) 41 | } 42 | 43 | func testSuccessfulRetry() { 44 | var times = 0 45 | 46 | var expectedOutput: Int? 47 | 48 | var completion: Subscribers.Completion? 49 | 50 | subscription = Deferred(createPublisher: { () -> AnyPublisher in 51 | defer { times += 1 } 52 | if times == 0 { 53 | return Fail(error: MyError.someError).eraseToAnyPublisher() 54 | } 55 | else { 56 | return Just(5).setFailureType(to: MyError.self).eraseToAnyPublisher() 57 | } 58 | }) 59 | .retryWhen { error in 60 | error.map { _ in } 61 | } 62 | .sink( 63 | receiveCompletion: { completion = $0 }, 64 | receiveValue: { expectedOutput = $0 } 65 | ) 66 | 67 | XCTAssertEqual( 68 | expectedOutput, 69 | 5 70 | ) 71 | XCTAssertEqual(completion, .finished) 72 | XCTAssertEqual(times, 2) 73 | } 74 | 75 | func testRetryFailure() { 76 | var expectedOutput: Int? 77 | 78 | var completion: Subscribers.Completion? 79 | 80 | subscription = Fail(error: MyError.someError) 81 | .retryWhen { error in 82 | error.tryMap { _ in throw MyError.retryError } 83 | } 84 | .sink( 85 | receiveCompletion: { completion = $0 }, 86 | receiveValue: { expectedOutput = $0 } 87 | ) 88 | 89 | XCTAssertEqual( 90 | expectedOutput, 91 | nil 92 | ) 93 | XCTAssertEqual(completion, .failure(MyError.retryError)) 94 | } 95 | 96 | func testRetryComplete() { 97 | var expectedOutput: Int? 98 | 99 | var completion: Subscribers.Completion? 100 | 101 | subscription = Fail(error: MyError.someError) 102 | .retryWhen { error in 103 | error.prefix(1) 104 | } 105 | .sink( 106 | receiveCompletion: { completion = $0 }, 107 | receiveValue: { expectedOutput = $0 } 108 | ) 109 | 110 | XCTAssertEqual( 111 | expectedOutput, 112 | nil 113 | ) 114 | XCTAssertEqual(completion, .finished) 115 | } 116 | 117 | enum MyError: Swift.Error { 118 | case someError 119 | case retryError 120 | } 121 | } 122 | #endif 123 | -------------------------------------------------------------------------------- /Tests/SetOutputTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetOutputTypeTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Jasdev Singh on 02/04/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class SetOutputTypeTests: XCTestCase { 16 | func testSetOutputType() { 17 | let publisher = Just("someString") 18 | .ignoreOutput() 19 | .setOutputType(to: Int.self) 20 | .eraseToAnyPublisher() // Erasing so the test remains stable 21 | // across any changes to `Publisher.setOutputType(to:)`’s implementation. 22 | 23 | XCTAssertTrue(type(of: publisher) == AnyPublisher.self) 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Tests/ToggleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleTests.swift 3 | // CombineExt 4 | // 5 | // Created by Keita Watanabe on 06/06/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import Combine 11 | import CombineExt 12 | import XCTest 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class ToggleTests: XCTestCase { 16 | func testSomeInitialization() { 17 | var results = [Bool]() 18 | _ = [true, false, true, false, true].publisher 19 | .toggle() 20 | .sink { results.append($0) } 21 | 22 | XCTAssertEqual([false, true, false, true, false], results) 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Tests/ZipManyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipManyTests.swift 3 | // CombineExtTests 4 | // 5 | // Created by Jasdev Singh on 16/03/2020. 6 | // Copyright © 2020 Combine Community. All rights reserved. 7 | // 8 | 9 | #if !os(watchOS) 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | 14 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 15 | final class ZipManyTests: XCTestCase { 16 | private var subscription: AnyCancellable! 17 | 18 | private enum ZipManyError: Error { 19 | case anError 20 | } 21 | 22 | func testOneEmissionZipping() { 23 | let first = PassthroughSubject() 24 | let second = PassthroughSubject() 25 | let third = PassthroughSubject() 26 | 27 | var results = [[Int]]() 28 | var completed = false 29 | 30 | subscription = first 31 | .zip(with: second, third) 32 | .sink(receiveCompletion: { _ in completed = true }, 33 | receiveValue: { results.append($0) }) 34 | 35 | first.send(1) 36 | second.send(2) 37 | third.send(3) 38 | 39 | XCTAssertEqual(results, [[1, 2, 3]]) 40 | XCTAssertFalse(completed) 41 | first.send(completion: .finished) 42 | XCTAssertTrue(completed) 43 | } 44 | 45 | func testMultipleEmissionZippingEndingWithAnError() { 46 | let first = PassthroughSubject() 47 | let second = PassthroughSubject() 48 | let third = PassthroughSubject() 49 | 50 | var results = [[Int]]() 51 | var completed: Subscribers.Completion? 52 | 53 | subscription = first 54 | .zip(with: second, third) 55 | .sink(receiveCompletion: { completed = $0 }, 56 | receiveValue: { results.append($0) }) 57 | 58 | first.send(1) 59 | first.send(1) 60 | first.send(1) 61 | first.send(1) 62 | 63 | second.send(2) 64 | second.send(2) 65 | second.send(2) 66 | 67 | third.send(3) 68 | third.send(3) 69 | 70 | XCTAssertEqual(results, [[1, 2, 3], [1, 2, 3]]) 71 | XCTAssertNil(completed) 72 | first.send(completion: .failure(.anError)) 73 | XCTAssertEqual(completed, .failure(.anError)) 74 | } 75 | 76 | func testNoEmissionZipping() { 77 | let first = PassthroughSubject() 78 | let second = PassthroughSubject() 79 | let third = PassthroughSubject() 80 | 81 | var results = [[Int]]() 82 | var completed = false 83 | 84 | subscription = first 85 | .zip(with: second, third) 86 | .sink(receiveCompletion: { _ in completed = true }, 87 | receiveValue: { results.append($0) }) 88 | 89 | first.send(1) 90 | second.send(2) 91 | 92 | // Gated by `third` not emitting. 93 | 94 | XCTAssertTrue(results.isEmpty) 95 | XCTAssertFalse(completed) 96 | } 97 | 98 | func testZippingEndingWithAFinishedCompletion() { 99 | let first = PassthroughSubject() 100 | let second = PassthroughSubject() 101 | let third = PassthroughSubject() 102 | 103 | var results = [[Int]]() 104 | var completed: Subscribers.Completion? 105 | 106 | subscription = first 107 | .zip(with: second, third) 108 | .sink(receiveCompletion: { completed = $0 }, 109 | receiveValue: { results.append($0) }) 110 | 111 | first.send(1) 112 | 113 | second.send(2) 114 | second.send(2) 115 | 116 | third.send(3) 117 | 118 | XCTAssertEqual(results, [[1, 2, 3]]) 119 | XCTAssertNil(completed) 120 | first.send(completion: .finished) // Triggers a completion, since, there 121 | // aren’t any buffered events from `first` (or `third`) to possibly pair with. 122 | XCTAssertEqual(completed, .finished) 123 | } 124 | 125 | func testZippingWithAnInnerCompletionButNotAnOuter() { 126 | let first = PassthroughSubject() 127 | let second = PassthroughSubject() 128 | let third = PassthroughSubject() 129 | 130 | var results = [[Int]]() 131 | var completed: Subscribers.Completion? 132 | 133 | subscription = first 134 | .zip(with: second, third) 135 | .sink(receiveCompletion: { completed = $0 }, 136 | receiveValue: { results.append($0) }) 137 | 138 | first.send(1) 139 | first.send(1) 140 | 141 | second.send(2) 142 | second.send(2) 143 | 144 | third.send(3) 145 | 146 | XCTAssertEqual(results, [[1, 2, 3]]) 147 | XCTAssertNil(completed) 148 | first.send(completion: .finished) // Doesn’t trigger a completion, since `first` has an extra un-paired value event. 149 | XCTAssertNil(completed) 150 | } 151 | 152 | func testZippingCollection() { 153 | let first = PassthroughSubject() 154 | let second = PassthroughSubject() 155 | let third = PassthroughSubject() 156 | 157 | var results = [[Int]]() 158 | var completed: Subscribers.Completion? 159 | 160 | subscription = [first, second, third] 161 | .zip() 162 | .sink(receiveCompletion: { completed = $0 }, 163 | receiveValue: { results.append($0) }) 164 | 165 | first.send(1) 166 | 167 | second.send(2) 168 | second.send(2) 169 | 170 | third.send(3) 171 | 172 | XCTAssertEqual(results, [[1, 2, 3]]) 173 | XCTAssertNil(completed) 174 | first.send(completion: .finished) // Triggers a completion, since, there 175 | // aren’t any buffered events from `first` (or `third`) to possibly pair with. 176 | XCTAssertEqual(completed, .finished) 177 | } 178 | 179 | func testZippingWithASinglePublisher() { 180 | let first = PassthroughSubject() 181 | 182 | var completed = false 183 | var results = [[Int]]() 184 | 185 | subscription = [first] 186 | .zip() 187 | .sink(receiveCompletion: { _ in completed = true }, 188 | receiveValue: { results.append($0) }) 189 | 190 | first.send(1) 191 | 192 | XCTAssertEqual(results, [[1]]) 193 | XCTAssertFalse(completed) 194 | 195 | first.send(completion: .finished) 196 | 197 | XCTAssertTrue(completed) 198 | } 199 | 200 | func testZippingWithNoPublishers() { 201 | var completed = false 202 | var results = [[Int]]() 203 | 204 | subscription = [AnyPublisher]() 205 | .zip() 206 | .sink(receiveCompletion: { _ in completed = true }, 207 | receiveValue: { results.append($0) }) 208 | 209 | XCTAssertTrue(results.isEmpty) 210 | XCTAssertTrue(completed) 211 | } 212 | 213 | func testZipAtScale() { 214 | let numPublishers = Int(1e5 + 1) // +1 to minimize the odds that numPublishers%4==0 matters. 215 | 216 | let publishers = Array(repeating: 1, count: numPublishers) 217 | .map { _ in Just(2) } 218 | var results = [[Int]]() 219 | subscription = publishers.zip() 220 | .sink(receiveValue: { results.append($0) }) 221 | let wantAllTwos = Array(repeating: 2, count: numPublishers) 222 | XCTAssertEqual(results, [wantAllTwos]) 223 | } 224 | } 225 | #endif 226 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/**/*" 3 | 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | target: auto 9 | threshold: 1% 10 | base: auto -------------------------------------------------------------------------------- /scripts/carthage-archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! which carthage > /dev/null; then 4 | echo 'Error: Carthage is not installed' >&2 5 | exit 1 6 | fi 7 | 8 | if ! which swift > /dev/null; then 9 | echo 'Swift is not installed' >&2 10 | exit 1 11 | fi 12 | 13 | carthage build --no-skip-current --platform iOS 14 | carthage archive 15 | 16 | echo "Upload CombineExt.framework.zip to the latest release" -------------------------------------------------------------------------------- /scripts/make_project.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'xcodeproj' 4 | 5 | ### This script creates an Xcode project using Swift Package Manager 6 | ### and then applies every needed configurations and other changes. 7 | ### 8 | ### Written by Shai Mishali, June 1st 2019. 9 | 10 | project_name = "CombineExt" 11 | project_file = "#{project_name}.xcodeproj" 12 | podspec = "#{project_name}.podspec" 13 | plist_file = "#{project_file}/#{project_name}_Info.plist" 14 | core_targets = [project_name, "#{project_name}Tests", "#{project_name}PackageDescription", "#{project_name}PackageTests"] 15 | 16 | # Make sure SPM is Installed 17 | system("swift package > /dev/null 2>&1") 18 | abort("SPM is not installed") unless $?.exitstatus == 0 19 | 20 | # Make sure PlistBuddy is Installed 21 | abort("PlistBuddy is not installed") unless File.file?("/usr/libexec/PlistBuddy") 22 | 23 | # Make sure we have a Package.swift file 24 | abort("Can't locate Package.swift") unless File.exist?("Package.swift") 25 | 26 | # Make sure Podspec exists and we can find a version 27 | abort("Can't locate #{podspec}") unless File.exist?(podspec) 28 | podspec_version = nil 29 | File.open(podspec).each do |line| 30 | version = line[/^\s+s\.version\s+=\s+\"(.*?)\"$/, 1] 31 | unless version.nil? 32 | podspec_version = version 33 | break 34 | end 35 | end 36 | 37 | abort("Can't find podspec vesrion") if podspec_version.nil? 38 | 39 | # Attempt generating Xcode Project 40 | system("rf -rf #{project_file}") 41 | system("swift package generate-xcodeproj --enable-code-coverage") 42 | 43 | # Apply CFBundleVersion and CFBundleShortVersionString 44 | fail("Can't find project #{project_file}") unless File.directory?(project_file) 45 | fail("Can't find plist #{plist_file}") unless File.file?(plist_file) 46 | 47 | system("/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion #{podspec_version}\" #{plist_file}") 48 | system("/usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString #{podspec_version}\" #{plist_file}") 49 | 50 | # Apply SwiftLint and other configurations to targets 51 | project = Xcodeproj::Project.open(project_file) 52 | project.targets.each do |target| 53 | if core_targets.include?(target.name) 54 | swiftlint = target.new_shell_script_build_phase('SwiftLint') 55 | swiftlint.shell_script = <<-SwiftLint 56 | if which swiftlint >/dev/null; then 57 | swiftlint 58 | else 59 | echo "warning: SwiftLint not installed" 60 | fi 61 | SwiftLint 62 | 63 | index = target.build_phases.index { |phase| (defined? phase.name) && phase.name == 'SwiftLint' } 64 | target.build_phases.move_from(index, 0) 65 | else 66 | target.build_configurations.each do |config| 67 | config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = 'YES' 68 | config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -suppress-warnings' 69 | end 70 | end 71 | end 72 | 73 | project::save() --------------------------------------------------------------------------------