Skip to content

Instantly share code, notes, and snippets.

@niw
Created November 5, 2025 23:53
Show Gist options
  • Select an option

  • Save niw/bf7a5b3068b3e000781e2225af3148c4 to your computer and use it in GitHub Desktop.

Select an option

Save niw/bf7a5b3068b3e000781e2225af3148c4 to your computer and use it in GitHub Desktop.
A Combine Scheduler to delay on `exit` observer on main run loop.
//
// 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