Reputation: 98
I'm upgrading my API manager to Swift 6, but I’m running into issues with @Sendable requirements for callbacks due to Timer usage.
In my API manager, I have methods that take completion callbacks (like onSuccess, onFailure, etc.) which are then executed within a Timer. These callbacks are triggered based on network connectivity checks, retries, and timeouts within the Timer. However, Swift 6 requires the Timer’s closure to be @Sendable, and since my callbacks capture various non-sendable types, adding @Sendable isn’t an option for me.
Is there a way to structure this so that I can still use the Timer and callbacks without needing to mark them as @Sendable? Alternatively, are there any patterns that allow keeping callbacks without modifying the whole API manager to avoid @Sendable?
Here's a simplified version of the problematic code:
func performRequest<T: Decodable>(
request: T,
onSuccess: @escaping (T) -> Void,
onFailure: @escaping (Error) -> Void,
onOffline: (() -> Void)? = nil
) {
// Network check and timer setup
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
// Callbacks are invoked here
if someCondition {
onSuccess(someResult)
} else {
onFailure(someError)
}
}
}
What’s the best approach here to avoid @Sendable requirements without rearchitecting the entire API manager?
Any help or best practices for handling this in Swift 6 would be greatly appreciated!
Upvotes: 2
Views: 302
Reputation: 438467
You can retire the Timer.scheduledTimer
and instead just loop with Task.sleep
:
@MainActor
class ApiManager {
private var task: Task<Void, any Error>?
func performRequest<T: Decodable>(
request: T,
onSuccess: @escaping (T) -> Void,
onFailure: @escaping (any Error) -> Void,
onOffline: (() -> Void)? = nil
) {
// cancel previous, if any
task?.cancel()
task = Task {
…
while !Task.isCancelled {
try await Task.sleep(for: .seconds(1))
if someCondition {
onSuccess(someResult)
} else {
onFailure(someError)
}
}
}
}
}
That requires that performRequest
is actor-isolated. I just isolated the whole type, but if you want to isolate performRequest
(and task
) individually, you can do that, too.
Alternatively, you can use AsyncTimerSequence
from Apple’s swift-async-algorithms package:
@MainActor
class ApiManager {
private var task: Task<Void, any Error>?
func performRequest<T: Decodable>(
request: T,
onSuccess: @escaping (T) -> Void,
onFailure: @escaping (any Error) -> Void,
onOffline: (() -> Void)? = nil
) {
// cancel previous, if any
task?.cancel()
task = Task {
…
for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
if someCondition {
onSuccess(someResult)
} else {
onFailure(someError)
}
}
}
}
}
Personally, I would generally stay within structured concurrency:
@MainActor
class ApiManager {
func performRequest<T: Decodable>(
request: T,
onSuccess: @escaping (T) -> Void,
onFailure: @escaping (any Error) -> Void,
onOffline: (() -> Void)? = nil
) async {
…
for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
if someCondition {
onSuccess(someResult)
} else {
onFailure(someError)
}
}
}
}
But that would likely require a more general refactoring of the call points, which I gather you are trying to minimize.
I would not advise it, but you could also use a GCD timer:
private var timer: (any DispatchSourceTimer)?
func performRequest<T: Decodable>(
request: T,
onSuccess: @escaping (T) -> Void,
onFailure: @escaping (any Error) -> Void,
onOffline: (() -> Void)? = nil
) {
…
let timer = DispatchSource.makeTimerSource(queue: .main)
self.timer = timer
timer.schedule(deadline: .now() + 1, repeating: 1)
timer.setEventHandler {
if someCondition {
onSuccess(someResult)
} else {
onFailure(someError)
}
}
timer.activate()
}
And to cancel the timer, set timer
to nil
or call timer?.cancel()
.
In terms of embracing Swift 6, this is a step backwards. But, it does avoid the compiler warning/error (currently, at least; I suspect when they audit this for Swift concurrency, this will likely produce errors similar to what you are seeing with Timer.scheduledTimer
).
Upvotes: 4
Reputation: 1125
Here is my workaround for your solution:
func performRequest<T: Decodable>(
request: T,
onSuccess: @escaping (T) -> Void,
onFailure: @escaping (Error) -> Void,
onOffline: (() -> Void)? = nil
) {
// Network check and timer setup
var comple = Completion<T>()
comple.onFailure = onFailure
comple.onSuccess = onSuccess
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [comple] timer in
// Callbacks are invoked here
if true {
comple.onSuccess?()
} else {
comple.onFailure?(NSError(domain: "", code: 0))
}
}
}
struct Completion<T: Decodable>: @unchecked Sendable {
var onFailure: ((Error) -> Void)?
var onSuccess: ((T) -> Void)?
}
Upvotes: 0