Skip to content

Release 7.0.0

Latest

Choose a tag to compare

@nashysolutions-bot nashysolutions-bot released this 19 Aug 06:44
fe575e1

Breaking changes. Major on API grounds.

Registering a live store

UserDefaultsLiveStore.init(suiteName:) is now failable. Foundation refuses two suite names, the current process's own bundle identifier and NSGlobalDomain, and neither can be recovered from. Previously a refused name produced a store whose every read was empty and every write was discarded, silently, for the life of the process. It now fails at the point the store is composed.

guard let store = UserDefaultsLiveStore(suiteName: "group.com.example.myapp") else {
    preconditionFailure("Check the suite name against the App Groups entitlement.")
}

static let standard is new, and is the only way to reach the app's own defaults. No suite name reaches them.

Note that a name Foundation accepts is not necessarily the container you meant. A mistyped app group identifier, or one the app has no entitlement for, still produces a working store backed by a private domain. Nothing at this layer can tell the two apart.

Endpoints are no longer isolated to the main actor

Every endpoint was @MainActor and is now a nonisolated @Sendable closure. A store may be resolved and called from any concurrency domain: a background task, a widget extension reading an app group suite, or the main actor. Nothing hops, and nothing became async, so a read still returns its value in the caller's own domain.

Thread safety is now the conformer's to arrange rather than the protocol's. UserDefaultsLiveStore leans on UserDefaults being thread-safe. UserDefaultsTestStore takes a lock around its dictionary.

Three cases, and the third is the one most likely to catch you:

  1. Calling an endpoint: no change. A nonisolated @Sendable closure converts to @MainActor, so existing main-actor call sites compile unchanged.
  2. Conforming to UserDefaultsStoreProtocol: breaks. The requirement types changed, and a @MainActor closure cannot witness a nonisolated one. Change the closure types on your conformer.
  3. Supplying endpoints by hand: breaks, and this is caller-side code. A hand-built UserDefaultsClient whose closures reach main-actor-isolated state now fails with main actor-isolated property ... can not be referenced from a Sendable closure. Supplying an endpoint is conformer-shaped even when it happens inside a caller, so a test stub that reads isolated state is affected even though it never conformed to anything. Closures that capture nothing isolated are unaffected, which is the shape shown throughout the documentation.

The test store now matches live behaviour

UserDefaultsTestStore previously diverged from UserDefaultsLiveStore in ways that let a test pass against behaviour production does not have. It now matches the measured live semantics:

  • Values coerce across types as Foundation coerces them. A stored "42" reads as 42 through int, where it previously read as 0.
  • setObject with a value UserDefaults cannot hold now ends the process, where it previously succeeded. URL is the likely case in the wild, because UserDefaults has a dedicated set(_:forKey:) overload taking one, and that overload is not this endpoint.

Assertions pinning the old direct-cast results will need revisiting. This is a behaviour change with no API change, so the compiler will not point at it.

Also in this release

Documentation examples across the README and the DocC articles were corrected and are now checked mechanically: every Swift fence is type-checked in CI, so an example that stops compiling fails the build rather than reaching a reader. The package gained continuous integration, a SwiftLint configuration with a clean baseline, and a behavioural contract suite covering both stores.

Compatibility

Building the package requires no newer toolchain than before. Running its test suite requires Swift 6.2 or later, because the trap tests use exit tests. This affects contributors only. Consumers are unaffected, since SwiftPM does not build a dependency's test targets.