Manually ensure thread safety for public singletons in Swift 6?

I've written a lot of UserDefaults related stuff in this class, and I'm using authToken as an example here. authToken is referenced in prepare the synchronised nonisolated protocol-method of the web library Moya.
I would like to know if this approach is really secure and reliable. I would appreciate for any help!

public final class AppDefaults: ObservableObject {
    public static var shared: AppDefaults {
        queue.sync {
            singleton
        }
    }
    nonisolated(unsafe) private static let singleton = AppDefaults()
    private init() {}
    private static let queue = DispatchQueue(label: "xxx.yyyy.zzzz.AppDefaults", qos: .userInitiated)

    @AppStorage(UserDefaults.authToken) var authToken: String = ""
}

statics are automatically backed by dispatch_once(), therefore you can simply do:

public final class AppDefaults {
    public static let shared = AppDefaults()
}

You also probably want to mark this class as Sendable.

1 Like

Thanks for your reply! Actually this class is a Non-Sendable class, which also can't be isolated to MainActor due to usage constraints, so I'd like to manually ensure thread-safety for it.

Yes you can manually ensure thread-safety, for example using a Mutex, and still mark the class Sendable. The compiler knows about Mutex and makes this possible.

Or if you are using other synchronization primitives such as dispatch queues, you can still mark the class @unchecked Sendable.

    public static var shared: AppDefaults {
        queue.sync {
            let lock = NSLock()
            lock.lock(); defer { lock.unlock() }
            return singleton
        }
    }

Thank you! NSLock can be used in my project env, under iOS 17.x.

Reread my first answer, you do not need all this.

public final class AppDefaults: ObservableObject, @unchecked Sendable {
    public static let shared = AppDefaults()
    private init() {}
    private let queue = DispatchQueue(label: "xxx.yyyy.zzzz.AppDefaults", qos: .userInitiated)

    @AppStorage(UserDefaults.wepediasKeyAuthToken) private var dq_authToken: String = ""
    var authToken: String {
        get {
            queue.sync { dq_authToken }
        }
        set {
            queue.async { [self] in
                dq_authToken = newValue
            }
        }
    }
}

I ended up with a lot more lines of code, but the effort of migrating to Swift 6 will all be worth it! Thank you for your help!