Luciano Perez
Luciano Perez

Reputation: 101

@Sendable issue in Swift 6 migration

We are updating our app to Swift 6, and one of the fixes was to modify the following protocol extensions to start using @Sendable.

Code:

protocol ThreadService: AnyObject {
    func asyncAfter(
        deadline: DispatchTime,
        qos: DispatchQoS,
        flags: DispatchWorkItemFlags,
        execute work: @escaping @Sendable @convention(block) () -> Void
    )

    func sync(execute block: @Sendable () -> Void)
}

extension ThreadService {

    func async(
        group: DispatchGroup? = nil,
        qos: DispatchQoS = .unspecified,
        flags: DispatchWorkItemFlags = [],
        execute work: @escaping @Sendable @convention(block) () -> Void
    ) {
        async(group: group, qos: qos, flags: flags, execute: work)
    }

    func asyncAfter(
        deadline: DispatchTime,
        qos: DispatchQoS = .unspecified,
        flags: DispatchWorkItemFlags = [],
        execute work: @escaping @Sendable @convention(block) () -> Void
    ) {
        asyncAfter(deadline: deadline, qos: qos, flags: flags, execute: work)
    }
}

extension DispatchQueue: ThreadService {}

We are having the following issue with the code below:

The code:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
     isDisabled = false
}

The issue:

Main actor-isolated property 'isDisabled' can not be mutated from a Sendable closure

isDisabled is defined as follows:

@State private var isDisabled: Bool = false

Is there any way to fix this without using Task {} inside asyncAfter? The code looks weird, redundant, and difficult to read since asyncAfter is already running on the main thread (See the solution below).

We tried the following things:

1 - It works, but it looks strange. Why do we need to add the Task if it's already running on the main thread? Is there another way to fix this?

 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
     Task { @MainActor in
        isDisabled = false
     }
 }

2 - We added the @MainActor keyword, but we have the same issue:

@MainActor @State private var isDisabled: Bool = false

3 - I saw this approach, it looks fine, but we want to continue using asyncAfter approach, is there a way to use the asyncAfter method without using Task within it?:

Task {
    try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
    isDisabled = false
}

Upvotes: 0

Views: 179

Answers (0)

Related Questions