UserDefaults not Sendable

Hey,

I am just about to prepare my app for Swift 6, and facing the issue that UserDefaults is not Sendable. The documentation states that its thread safe, so I am wondering, why is it not marked as Sendable? Was it just forgotten? Is it safe to mark it as nonisolated(unsafe) or @unchecked Sendable?

In my experience there are two common reasons why a Foundation type that ‘obviously’ should be sendable isn’t:

  • User info dictionaries

  • Custom subclasses

I’ve found that UserDefaults rarely needs to be sendable, because I access UserDefaults.standard at each point I need it. Can you post an example of where it’s triggering a sendability problem?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hey, thanks for your answer.

While I get the two points, I think UserDefaults does not have user info dictonaries. I get that custom subclasses can be problematic, but up to my knowledge, if an open class would be marked Sendable and someone tried to subclass it, swift would emit a warning that the subclass must also be Sendable. Also, certain other classes in Foundation (like NumberFormatter) are Sendable, despite being open.

In my particular case I cannot use the static UserDefaults.standard as I need to share the defaults within my app group. Potential code snippet could be:

public final class Defaults: Sendable {
    private let defaults: UserDefaults

    public init(
        _ defaults: UserDefaults = UserDefaults(suiteName: "foobar")
            ?? .standard
    ) {
        self.defaults = defaults
    }

I know I could in theory just pass the suite name and make UserDefaults computed (btw, is UserDefaults.standard also computed?), however I still don't get why UserDefaults is not sendable despite the docs mentioning its thread safe.

Similar question - if I wrap UserDefaults in the property wrapper (in my example to provide access to shared app group defaults).

@propertyWrapper
public struct HMASettingsDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = UserDefaults.standard

    public var wrappedValue: Value {
        get {
            return container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            if let optionalValue = newValue as? AnyOptional, optionalValue.isNil {
                container.removeObject(forKey: key)
            } 
            else {
                container.set(newValue, forKey: key)
            }
        }
    }
}

Xcode will report "is not concurrency-safe because it is non-isolated global shared mutable state" in all places where this wrapper is used e.g.

@HMASettingsDefault(key: "fixedAppTimezone", defaultValue: Calendar.current.timeZone.identifier, container: MyAppGroup.defaults)
static var userTimezoneId: String

What would be the best approach to use (promised) threadsafe objects in property wrappers in Swift6 strict concurrency world?

if an open class would be marked Sendable and someone tried to subclass it, swift would emit a warning that the subclass must also be Sendable.

Right. As long as the subclass was written in Swift. And you actually compiled it from source, rather then importing the implementation from a complied binary. Like, say, one of the frameworks built in to an Apple OS (-:

So, there’s two parts to this:

  • Why isn’t UserDefaults sendable?

  • What should you do in your code?

I’ll tackle these in order.


I usually don’t answer why questions here on DevForums [1]. However, in this case Foundation is part of the Swift project and so I have a little more latitude. Specifically, check out Sendable in Foundation on Swift Forums.

No one from the Foundation team has chimed in there, but someone posted a bug number (FB11224364). I followed that link and, again, I’m bumping into the limit of what I can talk about publicly but, it seems that subclassing is the primary concern.


As to what you can do about it, the obvious solution is to create a new UserDefaults object in every context that needs it. That’s guaranteed to work regardless of UserDefaults subclasses [2].

If you don’t like that approach you can use some alternative that opts out of strict concurrency checking. For example, the property wrapper example you posted compiles just fine if you deploy nonisolated(unsafe) on the MyAppGroup.defaults global.

enum MyAppGroup {
    nonisolated(unsafe) static let defaults: UserDefaults = { UserDefaults(suiteName: "com.example.waffle-varnish")! }()
}

ps Your exact code doesn’t compile because userTimezoneId itself is a static variable, but that’s a different problem.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] See tip 3 in Quinn’s Top Ten DevForums Tips.

[2] Because the obvious implementation is something like this:

let suiteName: String? = …
let defaults = suiteName.map { UserDefaults(suiteName: $0)! } ?? .standard

which means you can only create standard user defaults values.

UserDefaults not Sendable
 
 
Q