-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[APP-2869] Add Combine and SwiftUI bridges (#125)
- Loading branch information
1 parent
300e800
commit deccedd
Showing
12 changed files
with
726 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// | ||
// CancelBag.swift | ||
// Flow | ||
// | ||
// Created by Carl Ekman on 2023-02-09. | ||
// Copyright © 2023 PayPal Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
#if canImport(Combine) | ||
import Combine | ||
|
||
/// A type alias for `Set<AnyCancellable>` meant to bridge some of the patterns of `DisposeBag` | ||
/// with modern conventions, like `store(in set: inout Set<AnyCancellable>)`. | ||
@available(iOS 13.0, macOS 10.15, *) | ||
public typealias CancelBag = Set<AnyCancellable> | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
extension CancelBag: Cancellable { | ||
/// Cancel all elements in the set. | ||
public func cancel() { | ||
forEach { $0.cancel() } | ||
} | ||
|
||
/// Cancel all elements and then empty the set. | ||
public mutating func empty() { | ||
cancel() | ||
removeAll() | ||
} | ||
|
||
/// Create a new, empty set, which is itself a part of self. | ||
/// Corresponds to `innerBag()` for `DisposeBag`. | ||
public mutating func subset() -> CancelBag { | ||
let bag = CancelBag() | ||
self.insert(AnyCancellable(bag)) | ||
return bag | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
extension CancelBag { | ||
public init(disposable: Disposable) { | ||
self.init([disposable.asAnyCancellable]) | ||
} | ||
|
||
public var asAnyCancellable: AnyCancellable { | ||
AnyCancellable(self) | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
public func += (cancelBag: inout CancelBag, cancellable: AnyCancellable?) { | ||
if let cancellable = cancellable { | ||
cancelBag.insert(cancellable) | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
public func += (cancelBag: inout CancelBag, cancellation: @escaping () -> Void) { | ||
cancelBag.insert(AnyCancellable(cancellation)) | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// | ||
// Disposable+Cancellable.swift | ||
// Flow | ||
// | ||
// Created by Carl Ekman on 2023-02-09. | ||
// Copyright © 2023 PayPal Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
#if canImport(Combine) | ||
import Combine | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
extension Disposable { | ||
public var asAnyCancellable: AnyCancellable { | ||
AnyCancellable { self.dispose() } | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
extension Future { | ||
public var cancellable: AnyCancellable { | ||
AnyCancellable { self.disposable.dispose() } | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// Copyright © 2023 PayPal Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
#if canImport(Combine) | ||
import Combine | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
extension Flow.Future { | ||
/// Convert a `Flow.Future<Value>` to a `Combine.Future<Value, Error>` intended to be | ||
/// used to bridge between the `Flow` and `Combine` world | ||
public var toCombineFuture: Combine.Future<Value, Error> { | ||
Combine.Future { promise in | ||
self.onResult { promise($0) } | ||
} | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// | ||
// Callbacker+Combine.swift | ||
// Flow | ||
// | ||
// Created by Carl Ekman on 2023-02-09. | ||
// Copyright © 2023 PayPal Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
#if canImport(Combine) | ||
import Combine | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
public extension Publisher { | ||
/// Performs just link `sink(receiveValue:)`, but the cancellable produced from each received value | ||
/// will be automatically cancelled once a new value is published. Completion will cancel the last cancellable as well. | ||
/// | ||
/// - Intended to be used similarly to `onValueDisposePrevious(_:on:)`. | ||
func autosink( | ||
receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), | ||
receiveValue: @escaping ((Self.Output) -> AnyCancellable) | ||
) -> AnyCancellable { | ||
var bag = CancelBag() | ||
var subBag = bag.subset() | ||
|
||
bag += sink(receiveCompletion: { completion in | ||
subBag.cancel() | ||
receiveCompletion(completion) | ||
}, receiveValue: { value in | ||
subBag.cancel() | ||
subBag += receiveValue(value) | ||
}) | ||
|
||
return bag.asAnyCancellable | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
public extension Publisher where Self.Failure == Never { | ||
/// Performs just link `sink(receiveValue:)`, but the cancellable produced from each received value | ||
/// will be automatically cancelled once a new value is published, for publishers that never fail. | ||
/// | ||
/// - Intended to be used similarly to `onValueDisposePrevious(_:on:)`. | ||
func autosink( | ||
receiveValue: @escaping ((Self.Output) -> AnyCancellable) | ||
) -> AnyCancellable { | ||
var bag = CancelBag() | ||
var subBag = bag.subset() | ||
|
||
bag += sink { value in | ||
subBag.cancel() | ||
subBag += receiveValue(value) | ||
} | ||
|
||
return bag.asAnyCancellable | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// | ||
// Copyright © 2023 PayPal Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
#if canImport(Combine) | ||
import Combine | ||
|
||
extension CoreSignal { | ||
@available(iOS 13.0, macOS 10.15, *) | ||
final class SignalPublisher: Publisher, Cancellable { | ||
typealias Output = Value | ||
typealias Failure = Error | ||
|
||
internal var signal: CoreSignal<Kind, Value> | ||
internal var bag: CancelBag | ||
|
||
init(signal: CoreSignal<Kind, Value>) { | ||
self.signal = signal | ||
self.bag = [] | ||
} | ||
|
||
func receive<S>( | ||
subscriber: S | ||
) where S : Subscriber, Failure == S.Failure, Value == S.Input { | ||
// Creating our custom subscription instance: | ||
let subscription = EventSubscription<S>() | ||
subscription.target = subscriber | ||
|
||
// Attaching our subscription to the subscriber: | ||
subscriber.receive(subscription: subscription) | ||
|
||
// Collect cancellables when attaching to signal | ||
bag += signal | ||
.onValue { subscription.trigger(for: $0) } | ||
.asAnyCancellable | ||
|
||
if let finiteVersion = signal as? FiniteSignal<Value> { | ||
bag += finiteVersion.onEvent { event in | ||
if case let .end(error) = event { | ||
if let error = error { | ||
subscription.end(with: error) | ||
} else { | ||
subscription.end() | ||
} | ||
} | ||
}.asAnyCancellable | ||
} | ||
} | ||
|
||
func cancel() { | ||
bag.cancel() | ||
} | ||
|
||
deinit { | ||
cancel() | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
final class EventSubscription<Target: Subscriber>: Subscription | ||
where Target.Input == Value { | ||
|
||
var target: Target? | ||
|
||
func request(_ demand: Subscribers.Demand) {} | ||
|
||
func cancel() { | ||
target = nil | ||
} | ||
|
||
func end(with error: Target.Failure? = nil) { | ||
if let error = error { | ||
_ = target?.receive(completion: .failure(error)) | ||
} else { | ||
_ = target?.receive(completion: .finished) | ||
} | ||
} | ||
|
||
func trigger(for value: Value) { | ||
_ = target?.receive(value) | ||
} | ||
} | ||
|
||
@available(iOS 13.0, macOS 10.15, *) | ||
public var asAnyPublisher: AnyPublisher<Value, Error> { | ||
SignalPublisher(signal: self).eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
#endif |
Oops, something went wrong.