Created
November 5, 2025 23:53
-
-
Save niw/bf7a5b3068b3e000781e2225af3148c4 to your computer and use it in GitHub Desktop.
A Combine Scheduler to delay on `exit` observer on main run loop.
This file contains hidden or 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
| // | |
| // RunOnMainRunLoopExitScheduler.swift | |
| // RunOnMainRunLoopExitScheduler | |
| // | |
| // Created by Yoshimasa Niwa on 11/5/25. | |
| // | |
| import Combine | |
| import Foundation | |
| public struct RunOnMainRunLoopExitScheduler: Scheduler { | |
| public typealias SchedulerTimeType = RunLoop.SchedulerTimeType | |
| public typealias SchedulerOptions = RunLoop.SchedulerOptions | |
| public var minimumTolerance: SchedulerTimeType.Stride = .zero | |
| public var now: SchedulerTimeType | |
| public init() { | |
| self.now = RunLoop.SchedulerTimeType(Date()) | |
| } | |
| private func runOnMainRunLoopExit(_ action: @escaping () -> Void) { | |
| class Context { | |
| let action: () -> Void | |
| init(action: @escaping () -> Void) { | |
| self.action = action | |
| } | |
| } | |
| let retainedContext = Unmanaged.passRetained(Context(action: action)) | |
| var observerContext = CFRunLoopObserverContext( | |
| version: 0, | |
| info: retainedContext.toOpaque(), | |
| retain: nil, // `info` has been retained by `Unmanaged` already. | |
| release: { info in | |
| guard let info else { | |
| return | |
| } | |
| Unmanaged<Context>.fromOpaque(info).release() | |
| }, | |
| copyDescription: { _ in | |
| nil | |
| } | |
| ) | |
| guard let observer = CFRunLoopObserverCreate( | |
| kCFAllocatorDefault, | |
| CFRunLoopActivity([.exit]).rawValue, | |
| false, // repeats | |
| 0, // order | |
| { observer, _, info in | |
| if let info { | |
| // `info` is released by the `CFRunLoopObserverContext`. | |
| let retainedContext = Unmanaged<Context>.fromOpaque(info).takeUnretainedValue() | |
| retainedContext.action() | |
| } | |
| // Since this observer is not repeating, it is invalidated and removed from the run loop. | |
| // Thus, this call is not be unnecessary. | |
| //CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .commonModes) | |
| }, | |
| &observerContext, | |
| ) else { | |
| // `CFRunLoopObserverContext` may not call `release` if it's failed to create observer. | |
| retainedContext.release() | |
| return | |
| } | |
| CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes) | |
| } | |
| public func schedule( | |
| options _: SchedulerOptions?, | |
| _ action: @escaping () -> Void | |
| ) { | |
| runOnMainRunLoopExit(action) | |
| } | |
| public func schedule( | |
| after _: SchedulerTimeType, | |
| tolerance _: SchedulerTimeType.Stride, | |
| options _: SchedulerOptions?, | |
| _ action: @escaping () -> Void | |
| ) { | |
| runOnMainRunLoopExit(action) | |
| } | |
| public func schedule( | |
| after _: SchedulerTimeType, | |
| interval _: SchedulerTimeType.Stride, | |
| tolerance _: SchedulerTimeType.Stride, | |
| options _: SchedulerOptions?, | |
| _ action: @escaping () -> Void | |
| ) -> Cancellable { | |
| runOnMainRunLoopExit(action) | |
| return AnyCancellable {} | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment