skip to Main Content

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

enter image description here

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()
                }
            }
        }
    }
}

2

Answers


  1. You can wrap the closure inside a task that is isolated to the global – MainActor to avoid this warning:

    Timer.scheduledTimer(withTimeInterval: delaySeconds, repeats: true) { timer in
        Task { @MainActor in //<- here
            ...
            timer.invalidate() //⚠️
        }
    }
    

    However, there is still a warning/error here, and IMO, it’s quite impossible to resolve. Because the closure is marked with @Sendable, but the Timer itself did not conform to Sendable, unless you explicitly override the extension Sendable outside of Foudation.

    open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (Timer) -> Void) -> Timer

    @available(*, unavailable)
    extension Timer : @unchecked Sendable {
    }
    

    Since evaluateJavaScript also supports async await. I could prefer to refactor the code:

    final class ViewController: UIViewController {
        private let webView = WKWebView()
        private let maxTries = 3
        
        private var task: Task<Void, Never>?
        ...
        
        //✅ entry point here
        private func startSchedule(with url: String, and js: String) {
            executeTask(tries: 0, with: url, and: js)
        }
    
        private func executeTask(tries: Int, delaySeconds: Int = 1, with url: String, and js: String) {
            task = Task {
                if self.webView.url?.absoluteString != url || tries > self.maxTries {
                    self.task?.cancel()
                }
                
                do {
                    let result = try await evaluateWebJS(js)
                    
                    if let result {
                        if result.contains(".") || (tries==maxTries && result.count < 10) {
                            self.updateBuyProgress("AMOUNT*" + result)
                        }
                        self.task?.cancel()
                    } else {
                        guard !Task.isCancelled else {
                            return
                        }
                        
                        //delay
                        try? await Task.sleep(for: .seconds(delaySeconds))
    
                        //recursive call until the max retries have been reached
                        executeTask(tries: tries + 1, with: url, and: js)
                    }
                } catch {
                    print("cannot evaluate JS: (error)")
                    self.task?.cancel()
                }
            }
        }
    
        private func evaluateJS(_ js: String) async throws -> String? {
            let result = try await webView.evaluateJavaScript(js)
            guard let output = result as? String else { return nil }
                return output
            }
        }
    }
    
    
    Login or Signup to reply.
  2. 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”).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search