Last active
November 10, 2020 08:17
-
-
Save leviathan/04836c878946f970e416bb5d61cc7778 to your computer and use it in GitHub Desktop.
A Combine publisher "wrapper", that receives on a specific Scheduler.
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
import Foundation | |
import UIKit | |
import Combine | |
import PlaygroundSupport | |
/// The `ScopedReceivePublisher` modifies any returned `Publisher` property of `V` to receive | |
/// published elements on the specified `Scheduler`. | |
/// | |
/// Usage: | |
/// ``` | |
/// // When accessing `MyFunkyViewModel` through the scoped receiver... | |
/// let viewModel: ScopedReceivePublisher<MyFunkyViewModel, RunLoop> | |
/// let subscription = viewModel.publisher | |
/// .sink(receiveValue: { text in | |
/// // ... any downstream signals will be published on the Main-Thread, | |
/// // the scoped receiver ensures, that your downstream publisher will receive signals on the Main-Thread. | |
/// }) | |
/// | |
/// // ... irrespectively of the scheduler on which the `MyFunkyViewModel` might be publishing its signals. | |
/// | |
/// final class MyFunkyViewModel { | |
/// private let pipeline = PassthroughSubject<String, Never>() | |
/// | |
/// // [lots of code..] | |
/// | |
/// private func doHeavyLifting() { | |
/// DispatchQueue.global().asyncAfter(deadline: .now() + 3) { | |
/// self.pipeline.send("Yes, I'm using the background thread!") | |
/// } | |
/// } | |
/// } | |
/// ``` | |
/// | |
@dynamicMemberLookup | |
final class ScopedReceivePublisher<V, S: Scheduler> { | |
private(set) var value: V | |
private let scheduler: S | |
// MARK: Initializer | |
init(value: V, receiveOn: S) { | |
self.value = value | |
self.scheduler = receiveOn | |
} | |
// MARK: dynamicMemberLookup | |
subscript<P: Publisher>(dynamicMember keyPath: KeyPath<V, P>) -> P { | |
let publisher = value[keyPath: keyPath] | |
guard let receiveScheduledPublisher = publisher.receive(on: scheduler).eraseToAnyPublisher() as? P else { | |
return publisher | |
} | |
return receiveScheduledPublisher | |
} | |
} | |
extension ScopedReceivePublisher where S == RunLoop { | |
convenience init(value: V) { | |
self.init(value: value, receiveOn: S.main) | |
} | |
} | |
extension ScopedReceivePublisher where S == DispatchQueue { | |
convenience init(value: V) { | |
self.init(value: value, receiveOn: S.main) | |
} | |
} | |
extension ScopedReceivePublisher { | |
subscript<T>(dynamicMember keyPath: WritableKeyPath<V, T>) -> T { | |
get { value[keyPath: keyPath] } | |
set { value[keyPath: keyPath] = newValue } | |
} | |
} | |
/// ================================================================================ | |
/// Usage in Playground | |
/// ================================================================================ | |
import Foundation | |
import UIKit | |
import Combine | |
import PlaygroundSupport | |
@dynamicMemberLookup | |
final class ScopedReceivePublisher<V, S: Scheduler> { | |
private(set) var value: V | |
private let scheduler: S | |
// MARK: Initializer | |
init(value: V, receiveOn: S) { | |
self.value = value | |
self.scheduler = receiveOn | |
} | |
// MARK: dynamicMemberLookup | |
subscript<P: Publisher>(dynamicMember keyPath: KeyPath<V, P>) -> P { | |
let publisher = value[keyPath: keyPath] | |
guard let receiveScheduledPublisher = publisher.receive(on: scheduler).eraseToAnyPublisher() as? P else { | |
return publisher | |
} | |
return receiveScheduledPublisher | |
} | |
} | |
extension ScopedReceivePublisher where S == RunLoop { | |
convenience init(value: V) { | |
self.init(value: value, receiveOn: S.main) | |
} | |
} | |
extension ScopedReceivePublisher where S == DispatchQueue { | |
convenience init(value: V) { | |
self.init(value: value, receiveOn: S.main) | |
} | |
} | |
extension ScopedReceivePublisher { | |
subscript<T>(dynamicMember keyPath: WritableKeyPath<V, T>) -> T { | |
get { value[keyPath: keyPath] } | |
set { value[keyPath: keyPath] = newValue } | |
} | |
} | |
/// Define custom pre-defined types | |
typealias ViewModelScope<V> = ScopedReceivePublisher<V, RunLoop> | |
/// A View Model | |
final class ViewModel { | |
var publisher: AnyPublisher<String, Never> { | |
pipeline.eraseToAnyPublisher() | |
} | |
var text: String = "hello world!" | |
private var pipeline = PassthroughSubject<String, Never>() | |
init() { | |
// Simulating publisher update | |
DispatchQueue.global().asyncAfter(deadline: .now() + 3) { | |
print("Sending Update on Thread: \(Thread.current.isMainThread ? "[main]" : "[background]")") | |
self.pipeline.send("haha ... background !!") | |
} | |
} | |
} | |
final class MyFunkyViewController { | |
// private let viewModel: ScopedReceivePublisher<ViewModel, RunLoop> // If, you're not using custom types | |
private let viewModel: ViewModelScope<ViewModel> | |
private var subscriptions = Set<AnyCancellable>() | |
init(viewModel: ViewModel) { | |
self.viewModel = ScopedReceivePublisher(value: viewModel) | |
doWork() | |
} | |
// MARK: Private | |
private func doWork() { | |
viewModel.publisher | |
.sink(receiveValue: { text in | |
print("Received Text: \(text) on Thread: \(Thread.current.isMainThread ? "[main]" : "[background]")") | |
}).store(in: &subscriptions) | |
let text = viewModel.text | |
print("text: \(text)") | |
viewModel.text = "... reboot universe?!" | |
print("updated text: \(viewModel.text)") | |
} | |
} | |
let viewModel = ViewModel() | |
let viewController = MyFunkyViewController(viewModel: viewModel) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment