In Xcode 16, I get this error, related to Swift 6.
Capture of 'timer' with non-sendable type 'Timer' in a
@Sendable
closure; this is an error in the Swift 6 language mode
How can I make this code Swift 6 compliant?
func updateBuyAmountWithJsValue(url: String, delaySeconds: Double, maxTries: Int, js: String) {
var tries = 0
Timer.scheduledTimer(withTimeInterval: delaySeconds, repeats: true) { timer in
tries += 1
if (self.webView.url?.absoluteString != url || tries > maxTries) {
timer.invalidate()
return
}
self.webView.evaluateJavaScript(js) { (result, error) in
if let error = error {
print("Error executing JavaScript: \(error)")
} else if let value = result as? String {
if value.contains(".") || (tries==maxTries && value.count < 10) {
self.updateBuyProgress("AMOUNT*" + value)
timer.invalidate()
}
}
}
}
}
The immediate problem is that the timer handler closure is not actor-isolated. We can solve that by adding MainActor.assumeIsolated {…}
(not, generally, Task { @MainActor in …}
).
There is a more subtle problem that is only evidenced when you change the “Strict concurrency checking” build setting to “Complete”, namely that the Timer
parameter of the scheduledTimer
closure is “task isolated” and this non-Sendable
type cannot be updated from an actor-isolated context.
You can solve both of these problems by moving the Timer
reference to an actor-isolated property in conjunction with assumeIsolated
:
private var timer: Timer? // main actor isolated property
func updateBuyAmountWithJsValue(url: String, delaySeconds: Double, maxTries: Int, js: String) {
var tries = 0
timer = Timer.scheduledTimer(withTimeInterval: delaySeconds, repeats: true) { _ in
MainActor.assumeIsolated { // let compiler know this is being called on main thread, and therefore can be isolated to main actor
tries += 1
if self.webView.url?.absoluteString != url || tries > maxTries {
self.timer?.invalidate()
return
}
self.webView.evaluateJavaScript(js) { result, error in
if let error {
print("Error executing JavaScript: \(error)")
} else if let value = result as? String {
if value.contains(".") || (tries==maxTries && value.count < 10) {
self.updateBuyProgress("AMOUNT*" + value)
self.timer?.invalidate()
}
}
}
}
}
}
There are other refinements I might suggest, but this illustrates the basic idea.
Alternatively, if you are willing to adopt Swift concurrency, it is simpler:
func updateBuyAmountWithJsValue(url: String, delaySeconds: TimeInterval, maxTries: Int, js: String) async throws {
for _ in 0 ..< maxTries {
try await Task.sleep(for: .seconds(delaySeconds))
if webView.url?.absoluteString != url {
return
}
if
let value = try await webView.evaluateJavaScript(js) as? String,
value.contains(".") || value.count < 10
{
updateBuyProgress("AMOUNT*" + value)
return
}
}
}
Or you could use an AsyncTimerSequence
, but the idea would be the same: Namely, retire the closure-based API and thereby eliminate all the issues those introduce in Swift concurrency (especially with Swift 6 and/or “strict concurrency checking”).