Is it possible to use DispatchQueue to access MainActor isolated items

I am trying to access UIDevice.current.userInterfaceIdiom in Strict Concurrency Checks without needing to move all of my code to Async/Await.

Is it possible to use DispatchQeue.main to access @MainActor isolated variables?

var device : String {
    DispatchQueue.main.asyncAndWait {
        #if os(iOS)
        // Main actor-isolated class property 'current' can not be referenced from a non-isolated context; this is an error in Swift 6
        switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            return "iPhone"
        case .pad:
            return "iPad"
        default:
            return "Unknown"
        }
        #else
        return "Unknown"
        #endif
    }
}

This is the last piece of my puzzle.

Hi there @aliali, I think I recognize you from the server meetup just now :slight_smile: Thanks for tuning in!

Are you actually using Xcode 16 and Swift 6 mode...?

I just checked this:


import Dispatch

@MainActor
var m: Int = 123

@available(macOS 15.0, *)
func test() {
    DispatchQueue.main.async {
        m += 1
    }
    
    DispatchQueue.main.asyncAndWait {
        m += 1
    }
    
    #if os(iOS)
    switch UIDevice.current.userInterfaceIdiom {
    }
    #endif
}

in a package in swift 6 mode in Xcode 16 and this seems to build and work fine.

What's your Xcode and SDK swift versions? If you do just DispatchQueue.main.async does it work? That'd mean that your SDK does not have DispatchQueue.main.asyncAndWait annotated so I'd suggest updating SDK to the latest.

2 Likes

In my experience the checks are (still) quite shallow (although I am not running the latest Xcode which might have better checks):

Cheating 😂
struct MyDispatchQueue {
    private init() {}
    static var main: MyDispatchQueue { MyDispatchQueue() }
    func `async`<T>(execute: @MainActor () -> T) -> T {
        DispatchQueue.main.sync { @MainActor in
            execute()
        }
    }
}

func test() {
    typealias DispatchQueue = MyDispatchQueue
    var device : String {
        DispatchQueue.main.async {
            #if os(iOS)
            // Main actor-isolated class property 'current' can not be referenced from a non-isolated context; this is an error in Swift 6
            switch UIDevice.current.userInterfaceIdiom {
            case .phone:
                return "iPhone"
            case .pad:
                return "iPad"
            default:
                return "Unknown"
            }
            #else
            return "Unknown"
            #endif
        }
    }
}

BTW, what is the difference between DispatchQueue's asyncAndWait and sync?

I am using Xcode 15.3 and Swift 5.10 with StrictConcurrency check and it is giving the warning that this will be an error in swift 6.

Version 15.3 (15E204a)
// swift-tools-version:5.10

swiftSettings: [
                .enableExperimentalFeature("StrictConcurrency"),
                .unsafeFlags(["-warnings-as-errors"])
            ]

async seems to work but gives the error Cannot convert return expression of type '()' to return type 'String' since I am trying to retrieve a string.

I checked the same code on my computer running Xcode16 and Swift 6 and it worked. Thank you!

I am using asyncAndWait because I want to assign the result to a variable and that seems to be the best method to do so without creating a mutable var that is then update via DispatchQueue.main.async.

And here are the docs

This function submits work to the specified queue for execution. Unlike dispatch_async(_:_:), this function does not return until after the block finishes. Calling this function and targeting the current queue results in deadlock.

Unlike sync(execute:), this function respects all attributes of the queue when it executes the block. For example, it respects the quality-of-service level and autorelease frequency of the target queue.

If the runtime has already brought up a thread to service asynchronous work items, the system uses that same thread to execute any synchronous blocks you submitted using this function. If the runtime hasn’t brought up a thread to service asynchronous work items, the sytem executes these synchronous blocks on the current thread as an optimization. However, these optimizations apply only when queue targets a global concurrent queue. If it targets any other queue, the system executes the work on that queue’s thread. For example, if queue targets the main queue, the block always runs on the main thread.

Unlike with dispatch_async(_:_:), no retain is performed on the target queue. Because calls to this function are synchronous, it “borrows” the reference of the caller. Moreover, no Block_copy is performed on the block.

1 Like

Have you considered making device @MainActor isolated or passing it outside as dependency (so that you can avoid any kind of suspension or blocking)?

Hello,

Yes I considered it. Turning the calling functions into @MainActor.

This function called by both async/await functions and completions.

I would eventually need to make the above change to use DispatchQueue.main somewhere to get the result of the functions isolated to MainActor. Limiting that to just the variable that needs it seems like the correct way to go.

Do you have an example for

passing it outside as dependency

as I do not think I understand this part.

So I assume this device is part of some type. You then can pass it on initialization:

struct MyType {
    let device: String
}

let instance = MyType(device: …)

In that way you won’t need to access it each time through sync mechanism.

I haven’t used this blocking function, but as I understand from docs I’ve found, it is a subject to deadlocks as dispatch_sync:

Like functions of the dispatch_sync family, dispatch_async_and_wait() is
subject to dead-lock (See dispatch_sync() for details).

(from here)

1 Like

Thank you for calling out the risk of a deadlock by using that API. Can I get a check on if the following would be correct to avoid that problem?

let device: String = {
            if Thread.isMainThread {
                return MainActor.assumeIsolated {
                    switch UIDevice.current.userInterfaceIdiom {
                    case .phone:
                        return "iPhone"
                    case .pad:
                        return "iPad"
                    default:
                        return "Unknown"
                    }
                }
            } else {
                return DispatchQueue.main.asyncAndWait {
                    switch UIDevice.current.userInterfaceIdiom {
                    case .phone:
                        return "iPhone"
                    case .pad:
                        return "iPad"
                    default:
                        return "Unknown"
                    }
                }
            }
        }()