├── .gitignore ├── Package.swift ├── README.md └── Source └── MainActorPublisher ├── AnyMainActorPublisher.swift ├── MainActorCurrentValueSubject.swift ├── MainActorFuture.swift ├── MainActorPassthroughSubject.swift ├── MainActorPublisher.swift ├── MainActorTimerPublisher.swift ├── SinkSendable.swift └── UIScheduler2000.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /.build 2 | /.swiftpm 3 | /Packages 4 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MainActorPublisher", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | .library( 13 | name: "MainActorPublisher", 14 | targets: ["MainActorPublisher"] 15 | ), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "MainActorPublisher", 20 | swiftSettings: [ 21 | .enableUpcomingFeature("ExistentialAny"), 22 | .enableUpcomingFeature("IsolatedDefaultValues"), 23 | .enableUpcomingFeature("ConciseMagicFile"), 24 | .enableUpcomingFeature("DeprecateApplicationMain"), 25 | .enableUpcomingFeature("ForwardTrailingClosures"), 26 | .enableUpcomingFeature("ImplicitOpenExistentials"), 27 | .enableUpcomingFeature("ImportObjcForwardDeclarations"), 28 | .enableUpcomingFeature("InferSendableFromCaptures"), 29 | .enableUpcomingFeature("GlobalConcurrency"), 30 | .enableUpcomingFeature("DisableOutwardActorInference"), 31 | .enableUpcomingFeature("RegionBasedIsolation"), 32 | .enableExperimentalFeature("StrictConcurrency"), 33 | .enableUpcomingFeature("GlobalActorIsolatedTypesUsability") 34 | ] 35 | ) 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MainActorPublisher 2 | 3 | `MainActorPublisher` is a helper library to make working with Combine and Swift concurrency 4 | (especially strict concurrency, as used in Swift 6) less painful. 5 | 6 | ## What is it? 7 | 8 | `MainActorPublisher` is essentially just a marker protocol which inherits from `Publisher`, 9 | and adds nothing else. It is meant as a promise that this `Publisher` will only 10 | ever fire on the main actor. 11 | 12 | ## Why do I want this? 13 | 14 | Combine is somewhat woefully unequipped to work together with strict concurrency. Especially 15 | when working with UI code, which Combine is very often used for, we need to know on which 16 | thread or actor the `Publisher` fires, and we need to flag our functions with `@MainActor` to 17 | allow the compiler to enforce this. But Combine has no concept of this, and will let you 18 | fire events on any thread, and trigger effects anywhere. 19 | 20 | You can of course use operators that move execution to the main thread, but several 21 | problems remain. First, the Swift compiler knows nothing about this, forcing you to use 22 | unsafe constructs such as `MainActor.assumeIsolated()` to convince it code is safe. 23 | Second, when working with UIKit, the concept of triggering code during the same runloop 24 | becomes important in many cases, as animations can otherwise break, or you can introduce 25 | subtle race conditions. The operators that move execution to the main thread will 26 | usually delay execution to the next run loop, even in cases where execution is already 27 | happening on the main thread. 28 | 29 | Therefore, is nice to have a way to enforce that the entire chain of `Publisher` actions, 30 | from triggering to `sink()` all happens on the main thread without delays. This is what 31 | `MainActorPublisher` provides. 32 | 33 | ## How does this work? 34 | 35 | The whole setup consists of three parts: 36 | 37 | 1. A set of wrappers for existing publishers that force them to only work on the main actor, 38 | such as `MainActorPassthroughSubject`. This works exactly like `PassthroughSubject`, but 39 | the `send()` method is marked as `@MainActor`. 40 | 2. A big list of extensions for types in `Publishers`, that lets the `MainActorPublisher` 41 | propagate through the type system. For instance, if `Publishers.Map`'s `Upstream` type is a 42 | `MainActorPublisher`, then it itself will aslo now conform to `MainActorPublisher`. This lets 43 | you use most normal operators on `MainActorPublisher` while retaining the promise to only 44 | fire on the main actor. 45 | 3. A set of helper functions to take advantage of this promise. Most importantly, 46 | `MainActorPublisher` has a `.sinkOnMainActor()` whose closure is `@MainActor`. 47 | 48 | There are also some other helpers, such as a re-implementation of `AnyPublisher`, 49 | `AnyMainActorPublisher`, that is only available for `MainActorPublisher`s. There are also 50 | some helper functions to convert a normal `Publisher` to a `MainActorPublisher`, either by 51 | actually moving the execution to the main actor (`.onMainActor()`), or by promising that 52 | it already is there (`.assumeIsolatedOnMainActor()`). 53 | 54 | ## But does it actually work? 55 | 56 | Mostly. It's not quite perfect yet. The main thing missing is that `Publishers` contains 57 | an awful lot of different types, and we've only added extensions for those we actually use 58 | and the other most obvious ones. There are probably quite a few missing ones that still 59 | need to be added, so do fire off a PR if you do! 60 | 61 | ## License 62 | 63 | Copyright 2024 Wolt Enterprises Oy 64 | 65 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 66 | 67 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 68 | 69 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 70 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/AnyMainActorPublisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension MainActorPublisher { 5 | /// Wraps this publisher with a type eraser. 6 | /// 7 | /// This is similar to ``Publisher/eraseToAnyPublisher()``. 8 | /// 9 | /// - Returns: An ``AnyMainActorPublisher`` wrapping this publisher. 10 | @inlinable public func eraseToAnyMainActorPublisher() -> AnyMainActorPublisher { 11 | return .init(self) 12 | } 13 | } 14 | 15 | extension Publisher { 16 | /// Wraps this publisher with a type eraser that assumes this publisher will only ever fire on the main actor. 17 | /// 18 | /// It is important to get this right, as getting it wrong will cause a crash. 19 | /// 20 | /// - Returns: An ``AnyMainActorPublisher`` wrapping this publisher. 21 | @inlinable public func assumeIsolatedOnMainActor() -> AnyMainActorPublisher { 22 | return .init(assumeIsolatedOnMainActor: self) 23 | } 24 | } 25 | 26 | /// A publisher that performs type erasure by wrapping another ``MainActorPublisher``. 27 | /// 28 | /// This is similar to ``AnyPublisher``. 29 | /// 30 | /// You can use ``MainActorPublisher/eraseToAnyMainActorPublisher()`` operator to wrap a ``MainActorPublisher`` with ``AnyMainActorPublisher``. 31 | /// 32 | /// You can also use ``MainActorPublisher/assumeIsolatedOnMainActor()`` operator to wrap a regular ``Publisher`` with ``AnyMainActorPublisher`` if you know for sure it will only ever trigger on the main thread. Getting this wrong will cause a crash, however. 33 | public struct AnyMainActorPublisher: CustomStringConvertible, CustomPlaygroundDisplayConvertible { 34 | private let box: PublisherBoxBase 35 | 36 | public init(_ publisher: PublisherType) where Output == PublisherType.Output, Failure == PublisherType.Failure { 37 | self.init(assumeIsolatedOnMainActor: publisher) 38 | } 39 | 40 | public init(assumeIsolatedOnMainActor publisher: PublisherType) where Output == PublisherType.Output, Failure == PublisherType.Failure { 41 | if let erased = publisher as? AnyMainActorPublisher { 42 | self.box = erased.box 43 | } else { 44 | self.box = PublisherBox(base: publisher) 45 | } 46 | } 47 | 48 | public var description: String { return "AnyMainActorPublisher" } 49 | public var playgroundDescription: Any { return description } 50 | } 51 | 52 | extension AnyMainActorPublisher: MainActorPublisher { 53 | public func receive(subscriber: Downstream) where Output == Downstream.Input, Failure == Downstream.Failure { 54 | box.receive(subscriber: subscriber) 55 | } 56 | } 57 | 58 | fileprivate class PublisherBoxBase { 59 | init() {} 60 | 61 | func receive(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input { fatalError() } 62 | } 63 | 64 | fileprivate final class PublisherBox: PublisherBoxBase { 65 | let base: PublisherType 66 | 67 | init(base: PublisherType) { 68 | self.base = base 69 | super.init() 70 | } 71 | 72 | override func receive(subscriber: Downstream) where PublisherType.Failure == Downstream.Failure, PublisherType.Output == Downstream.Input { 73 | base.receive(subscriber: subscriber) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/MainActorCurrentValueSubject.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A subject that wraps a single value and publishes a new element whenever the value changes. 4 | /// 5 | /// Equivalent to `CurrentValueSubject` in Combine, but tagegd with `@MainActor` to enforce 6 | /// only using it on the main actor. 7 | public struct MainActorCurrentValueSubject: MainActorPublisher { 8 | private let currentValueSubject: CurrentValueSubject 9 | 10 | /// Creates a current value subject with the given initial value. 11 | /// 12 | /// - Parameter value: The initial value to publish. 13 | public init(_ value: Output) { 14 | self.currentValueSubject = .init(value) 15 | } 16 | 17 | /// The value wrapped by this subject, published as a new element whenever it changes. 18 | @MainActor public var value: Output { 19 | get { currentValueSubject.value } 20 | set { currentValueSubject.value = newValue } 21 | } 22 | 23 | public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { 24 | currentValueSubject.receive(subscriber: subscriber) 25 | } 26 | 27 | @MainActor public func send(_ input: Output) { 28 | currentValueSubject.send(input) 29 | } 30 | 31 | @MainActor public func send(completion: Subscribers.Completion) { 32 | currentValueSubject.send(completion: completion) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/MainActorFuture.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher that eventually produces a single value and then finishes or fails. 4 | /// 5 | /// Equivalent to `Future` in Combine, but tagegd with `@MainActor` to enforce 6 | /// only using it on the main actor. 7 | public struct MainActorFuture: MainActorPublisher { 8 | public typealias Promise = @MainActor (Result) -> Void 9 | 10 | private let future: Future 11 | 12 | /// Creates a publisher that invokes a promise closure when the publisher emits an element. 13 | @MainActor public init(_ attemptToFulfill: @MainActor (@escaping Promise) -> Void) { 14 | // Apparently the callback in Future.init will be called immediately from the 15 | // init function and not escape, even though it is marked as @escaping. 16 | // Here we assume this is happening and don't mark our version as @escaping, 17 | // and use `withoutActuallyEscaping()` to work around the incorrect labelling. 18 | self.future = withoutActuallyEscaping(attemptToFulfill) { attemptToFulfill in 19 | Future { promise in 20 | attemptToFulfill { 21 | promise($0) 22 | } 23 | } 24 | } 25 | } 26 | 27 | public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure, S: Subscriber { 28 | future.receive(subscriber: subscriber) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/MainActorPassthroughSubject.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A subject that broadcasts elements to downstream subscribers. 4 | /// 5 | /// Equivalent to `PassthroughSubject` in Combine, but tagegd with `@MainActor` to enforce 6 | /// only using it on the main actor. 7 | public struct MainActorPassthroughSubject: MainActorPublisher { 8 | private let passthroughSubject = PassthroughSubject() 9 | 10 | public init() {} 11 | 12 | public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { 13 | passthroughSubject.receive(subscriber: subscriber) 14 | } 15 | 16 | @MainActor public func send(_ input: Output) { 17 | passthroughSubject.send(input) 18 | } 19 | 20 | /// Sends a value to the subscriber. 21 | /// 22 | /// - Parameter value: The value to send. 23 | @MainActor public func send() where Output == Void { 24 | passthroughSubject.send() 25 | } 26 | 27 | @MainActor public func send(completion: Subscribers.Completion) { 28 | passthroughSubject.send(completion: completion) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/MainActorPublisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | /// A sub-protocol of ``Publisher``, which adds no new functionality, but represents a promise 5 | /// that this publisher will only ever fire on the main actor. 6 | public protocol MainActorPublisher: Publisher {} 7 | 8 | extension MainActorPublisher { 9 | /// Attaches a subscriber with closure-based behavior. 10 | /// 11 | /// Equivalent to ``Publisher/sink(receiveCompletion:receiveValue:)``, but with both 12 | /// closures marked as `@MainActor`. 13 | /// 14 | /// - parameter receiveComplete: The closure to execute on completion. 15 | /// - parameter receiveValue: The closure to execute on receipt of a value. 16 | /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 17 | @MainActor public func sinkOnMainActor( 18 | receiveCompletion: @escaping @MainActor (Subscribers.Completion) -> Void, 19 | receiveValue: @escaping @MainActor (Output) -> Void 20 | ) -> AnyCancellable { 21 | sink( 22 | receiveCompletion: { completion in 23 | MainActor.assumeIsolated { receiveCompletion(completion) } 24 | }, 25 | receiveValue: { value in 26 | MainActor.assumeIsolated { receiveValue(value) } 27 | } 28 | ) 29 | } 30 | 31 | /// Attaches a subscriber with closure-based behavior to a publisher that never fails. 32 | /// 33 | /// Equivalent to ``Publisher/sink(receiveValue:)``, but with both 34 | /// closures marked as `@MainActor`. 35 | /// 36 | /// - parameter receiveValue: The closure to execute on receipt of a value. 37 | /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 38 | @MainActor public func sinkOnMainActor( 39 | receiveValue: @escaping @MainActor (Output) -> Void 40 | ) -> AnyCancellable where Failure == Never { 41 | sink { value in MainActor.assumeIsolated { receiveValue(value) } } 42 | } 43 | } 44 | 45 | extension Publishers.ReceiveOn: MainActorPublisher where Context == MainActorScheduler {} 46 | 47 | extension Publisher { 48 | /// Create a ``MainActorPublisher`` that is guaranteed to deliver all elements on the main actor. 49 | /// 50 | /// If this publisher is already firing on the main actor, this operator is essentially 51 | /// a no-op, and elements will be delivered immediately without delay. If this publisher 52 | /// fires on a different thread, it will schedule the elements to be delivered on the 53 | /// main actor instead. 54 | /// 55 | /// - Returns: A ``MainActorPublisher`` that delivers elements on the main actor. 56 | @inlinable public func onMainActor() -> Publishers.ReceiveOn { 57 | return receive(on: MainActorScheduler.shared) 58 | } 59 | } 60 | 61 | // TODO: Finish this list. 62 | extension Publishers.Map: MainActorPublisher where Upstream: MainActorPublisher {} 63 | extension Publishers.CompactMap: MainActorPublisher where Upstream: MainActorPublisher {} 64 | extension Publishers.FlatMap: MainActorPublisher where Upstream: MainActorPublisher {} 65 | extension Publishers.Filter: MainActorPublisher where Upstream: MainActorPublisher {} 66 | extension Publishers.TryFilter: MainActorPublisher where Upstream: MainActorPublisher {} 67 | extension Publishers.PrefixWhile: MainActorPublisher where Upstream: MainActorPublisher {} 68 | extension Publishers.Scan: MainActorPublisher where Upstream: MainActorPublisher {} 69 | extension Publishers.Concatenate: MainActorPublisher where Prefix: MainActorPublisher, Suffix: MainActorPublisher {} 70 | extension Publishers.CombineLatest: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher {} 71 | extension Publishers.CombineLatest3: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher {} 72 | extension Publishers.CombineLatest4: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher {} 73 | extension Publishers.Zip: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher {} 74 | extension Publishers.Zip3: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher {} 75 | extension Publishers.Zip4: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher {} 76 | extension Publishers.Merge: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher {} 77 | extension Publishers.Merge3: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher {} 78 | extension Publishers.Merge4: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher {} 79 | extension Publishers.Merge5: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher, E: MainActorPublisher {} 80 | extension Publishers.Merge6: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher, E: MainActorPublisher, F: MainActorPublisher {} 81 | extension Publishers.Merge7: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher, E: MainActorPublisher, F: MainActorPublisher, G: MainActorPublisher {} 82 | extension Publishers.Merge8: MainActorPublisher where A: MainActorPublisher, B: MainActorPublisher, C: MainActorPublisher, D: MainActorPublisher, E: MainActorPublisher, F: MainActorPublisher, G: MainActorPublisher, H: MainActorPublisher {} 83 | extension Publishers.MergeMany: MainActorPublisher where Upstream: MainActorPublisher {} 84 | extension Publishers.RemoveDuplicates: MainActorPublisher where Upstream: MainActorPublisher {} 85 | extension Publishers.DropWhile: MainActorPublisher where Upstream: MainActorPublisher {} 86 | extension Publishers.Drop: MainActorPublisher where Upstream: MainActorPublisher {} 87 | extension Publishers.Output: MainActorPublisher where Upstream: MainActorPublisher {} 88 | extension Publishers.Throttle: MainActorPublisher where Upstream: MainActorPublisher, Context == MainActorScheduler {} 89 | extension Publishers.Debounce: MainActorPublisher where Upstream: MainActorPublisher, Context == MainActorScheduler {} 90 | extension Publishers.Delay: MainActorPublisher where Upstream: MainActorPublisher, Context == MainActorScheduler {} 91 | extension Publishers.Catch: MainActorPublisher where Upstream: MainActorPublisher {} 92 | extension Publishers.Autoconnect: MainActorPublisher where Upstream: MainActorPublisher {} 93 | extension Publishers.SetFailureType: MainActorPublisher where Upstream: MainActorPublisher {} 94 | extension Publishers.HandleEvents: MainActorPublisher where Upstream: MainActorPublisher {} 95 | extension Publishers.Timeout: MainActorPublisher where Upstream: MainActorPublisher, Context == MainActorScheduler {} 96 | extension Publishers.TryMap: MainActorPublisher where Upstream: MainActorPublisher {} 97 | extension Publishers.FirstWhere: MainActorPublisher where Upstream: MainActorPublisher {} 98 | extension Publishers.ReplaceError: MainActorPublisher where Upstream: MainActorPublisher {} 99 | 100 | extension Just: MainActorPublisher {} 101 | extension Empty: MainActorPublisher {} 102 | extension Fail: MainActorPublisher {} 103 | 104 | extension MainActorPublisher { 105 | public func throttle(for interval: MainActorScheduler.SchedulerTimeType.Stride, latest: Bool = true) -> Publishers.Throttle { 106 | return throttle(for: interval, scheduler: MainActorScheduler.shared, latest: latest) 107 | } 108 | 109 | public func debounce(for dueTime: MainActorScheduler.SchedulerTimeType.Stride) -> Publishers.Debounce { 110 | return debounce(for: dueTime, scheduler: MainActorScheduler.shared) 111 | } 112 | 113 | public func delay(for interval: MainActorScheduler.SchedulerTimeType.Stride) -> Publishers.Delay { 114 | return delay(for: interval, scheduler: MainActorScheduler.shared) 115 | } 116 | 117 | public func timeout(_ interval: MainActorScheduler.SchedulerTimeType.Stride, customError: (() -> Self.Failure)? = nil) -> Publishers.Timeout { 118 | return timeout(interval, scheduler: MainActorScheduler.shared, customError: customError) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/MainActorTimerPublisher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public struct MainActorTimerPublisher: ConnectablePublisher, MainActorPublisher { 5 | public typealias Output = Date 6 | public typealias Failure = Never 7 | 8 | private let timerPublisher: Timer.TimerPublisher 9 | 10 | public init(interval: TimeInterval, mode: RunLoop.Mode = .default) { 11 | self.timerPublisher = Timer.publish(every: interval, on: .main, in: mode) 12 | } 13 | 14 | public func connect() -> any Cancellable { 15 | return timerPublisher.connect() 16 | } 17 | 18 | public func receive(subscriber: S) where S: Subscriber, Never == S.Failure, Date == S.Input { 19 | timerPublisher.receive(subscriber: subscriber) 20 | } 21 | 22 | public func autoconnect() -> Publishers.Autoconnect { 23 | return Publishers.Autoconnect(upstream: self) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/SinkSendable.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher { 4 | /// Attaches a subscriber with closure-based behavior. 5 | /// 6 | /// Equivalent to ``Publisher/sink(receiveCompletion:receiveValue:)``, but with both 7 | /// closures correctly marked as `@Sendable` to make this safe to call without strict 8 | /// concurrency enabled. 9 | /// 10 | /// - parameter receiveComplete: The closure to execute on completion. 11 | /// - parameter receiveValue: The closure to execute on receipt of a value. 12 | /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 13 | public func sinkSendable( 14 | receiveCompletion: @escaping @Sendable (Subscribers.Completion) -> Void, 15 | receiveValue: @escaping @Sendable (Output) -> Void 16 | ) -> AnyCancellable { 17 | return sink(receiveCompletion: receiveCompletion, receiveValue: receiveValue) 18 | } 19 | } 20 | 21 | extension Publisher where Failure == Never { 22 | /// Attaches a subscriber with closure-based behavior to a publisher that never fails. 23 | /// 24 | /// Equivalent to ``Publisher/sink(receiveValue:)``, but with both 25 | /// closures correctly marked as `@Sendable` to make this safe to call without strict 26 | /// concurrency enabled. 27 | /// 28 | /// - parameter receiveValue: The closure to execute on receipt of a value. 29 | /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 30 | public func sinkSendable(receiveValue: @escaping @Sendable (Output) -> Void) -> AnyCancellable { 31 | return sink(receiveValue: receiveValue) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Source/MainActorPublisher/UIScheduler2000.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | /// A `Scheduler` implementation that always runs closures on the main actor. 5 | /// 6 | /// If the execution is already taking place on the main actor, the code will be executed 7 | /// immediately, with no delay, so this scheduler is safe to use when you need code to be 8 | /// executed in the same runloop as it was invoked in. 9 | /// 10 | /// This is similar to `ReactiveSwift`'s `UIScheduler`: https://reactivecocoa.io/reactiveswift/docs/latest/Classes/UIScheduler.html 11 | public struct MainActorScheduler: Scheduler, Sendable { 12 | public typealias SchedulerOptions = Never 13 | public typealias SchedulerTimeType = DispatchQueue.SchedulerTimeType 14 | 15 | public static let shared = Self() 16 | 17 | public var now: SchedulerTimeType { DispatchQueue.main.now } 18 | public var minimumTolerance: SchedulerTimeType.Stride { DispatchQueue.main.minimumTolerance } 19 | 20 | public func schedule(options: SchedulerOptions? = nil, _ action: @escaping () -> Void) { 21 | if Thread.isMainThread { 22 | action() 23 | } else { 24 | DispatchQueue.main.schedule(action) 25 | } 26 | } 27 | 28 | public func schedule( 29 | after date: SchedulerTimeType, 30 | tolerance: SchedulerTimeType.Stride, 31 | options: SchedulerOptions? = nil, 32 | _ action: @escaping () -> Void 33 | ) { 34 | DispatchQueue.main.schedule(after: date, tolerance: tolerance, options: nil, action) 35 | } 36 | 37 | public func schedule( 38 | after date: SchedulerTimeType, 39 | interval: SchedulerTimeType.Stride, 40 | tolerance: SchedulerTimeType.Stride, 41 | options: SchedulerOptions? = nil, 42 | _ action: @escaping () -> Void 43 | ) -> any Cancellable { 44 | DispatchQueue.main.schedule( 45 | after: date, interval: interval, tolerance: tolerance, options: nil, action 46 | ) 47 | } 48 | } 49 | --------------------------------------------------------------------------------