Skip to content

Instantly share code, notes, and snippets.

@leviathan
Last active November 10, 2020 08:17
Show Gist options
  • Save leviathan/04836c878946f970e416bb5d61cc7778 to your computer and use it in GitHub Desktop.
Save leviathan/04836c878946f970e416bb5d61cc7778 to your computer and use it in GitHub Desktop.
A Combine publisher "wrapper", that receives on a specific Scheduler.
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