Skip to content
33 changes: 26 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// swift-tools-version:5.0
// swift-tools-version:6.2
// swiftlint:disable prefixed_toplevel_constant
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import class Foundation.ProcessInfo
import PackageDescription

let shouldTest = ProcessInfo.processInfo.environment["TEST"] == "1"

Expand All @@ -15,22 +16,40 @@ func resolveDependencies() -> [Package.Dependency] {
]
}

let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("InferSendableFromCaptures"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("ExistentialAny"),
]

func resolveTargets() -> [Target] {
let baseTarget = Target.target(name: "SwiftyUserDefaults", dependencies: [], path: "Sources")
let testTarget = Target.testTarget(name: "SwiftyUserDefaultsTests", dependencies: ["SwiftyUserDefaults", "Quick", "Nimble"])
let baseTarget = Target.target(
name: "SwiftyUserDefaults",
dependencies: [],
path: "Sources",
exclude: ["Info.plist"],
swiftSettings: swiftSettings
)
let testTarget = Target.testTarget(
name: "SwiftyUserDefaultsTests",
dependencies: ["SwiftyUserDefaults", "Quick", "Nimble"],
swiftSettings: swiftSettings
)

return shouldTest ? [baseTarget, testTarget] : [baseTarget]
}


let package = Package(
name: "SwiftyUserDefaults",
platforms: [
.macOS(.v10_11), .iOS(.v9), .tvOS(.v9), .watchOS(.v2)
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6),
],
products: [
.library(name: "SwiftyUserDefaults", targets: ["SwiftyUserDefaults"]),
],
dependencies: resolveDependencies(),
targets: resolveTargets()
targets: resolveTargets(),
swiftLanguageModes: [.v6]
)
34 changes: 21 additions & 13 deletions Sources/BuiltIns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,50 +24,56 @@

import Foundation

extension DefaultsSerializable {
public static var _defaultsArray: DefaultsArrayBridge<[T]> { return DefaultsArrayBridge() }
public extension DefaultsSerializable {
static var _defaultsArray: DefaultsArrayBridge<[T]> { return DefaultsArrayBridge() }
}

extension Date: DefaultsSerializable {
public static var _defaults: DefaultsObjectBridge<Date> { return DefaultsObjectBridge() }
}

extension String: DefaultsSerializable {
public static var _defaults: DefaultsStringBridge { return DefaultsStringBridge() }
}

extension Int: DefaultsSerializable {
public static var _defaults: DefaultsIntBridge { return DefaultsIntBridge() }
}

extension Double: DefaultsSerializable {
public static var _defaults: DefaultsDoubleBridge { return DefaultsDoubleBridge() }
}

extension Bool: DefaultsSerializable {
public static var _defaults: DefaultsBoolBridge { return DefaultsBoolBridge() }
}

extension Data: DefaultsSerializable {
public static var _defaults: DefaultsDataBridge { return DefaultsDataBridge() }
}

extension URL: DefaultsSerializable {
#if os(Linux)
public static var _defaults: DefaultsKeyedArchiverBridge<URL> { return DefaultsKeyedArchiverBridge() }
public static var _defaults: DefaultsKeyedArchiverBridge<URL> { return DefaultsKeyedArchiverBridge() }
#else
public static var _defaults: DefaultsUrlBridge { return DefaultsUrlBridge() }
public static var _defaults: DefaultsUrlBridge { return DefaultsUrlBridge() }
#endif
public static var _defaultsArray: DefaultsKeyedArchiverBridge<[URL]> { return DefaultsKeyedArchiverBridge() }
}

extension DefaultsSerializable where Self: Codable {
public static var _defaults: DefaultsCodableBridge<Self> { return DefaultsCodableBridge() }
public static var _defaultsArray: DefaultsCodableBridge<[Self]> { return DefaultsCodableBridge() }
public extension DefaultsSerializable where Self: Codable {
static var _defaults: DefaultsCodableBridge<Self> { return DefaultsCodableBridge() }
static var _defaultsArray: DefaultsCodableBridge<[Self]> { return DefaultsCodableBridge() }
}

extension DefaultsSerializable where Self: RawRepresentable {
public static var _defaults: DefaultsRawRepresentableBridge<Self> { return DefaultsRawRepresentableBridge() }
public static var _defaultsArray: DefaultsRawRepresentableArrayBridge<[Self]> { return DefaultsRawRepresentableArrayBridge() }
public extension DefaultsSerializable where Self: RawRepresentable {
static var _defaults: DefaultsRawRepresentableBridge<Self> { return DefaultsRawRepresentableBridge() }
static var _defaultsArray: DefaultsRawRepresentableArrayBridge<[Self]> { return DefaultsRawRepresentableArrayBridge() }
}

extension DefaultsSerializable where Self: NSCoding {
public static var _defaults: DefaultsKeyedArchiverBridge<Self> { return DefaultsKeyedArchiverBridge() }
public static var _defaultsArray: DefaultsKeyedArchiverBridge<[Self]> { return DefaultsKeyedArchiverBridge() }
public extension DefaultsSerializable where Self: NSCoding {
static var _defaults: DefaultsKeyedArchiverBridge<Self> { return DefaultsKeyedArchiverBridge() }
static var _defaultsArray: DefaultsKeyedArchiverBridge<[Self]> { return DefaultsKeyedArchiverBridge() }
}

extension Dictionary: DefaultsSerializable where Key == String {
Expand All @@ -77,13 +83,15 @@ extension Dictionary: DefaultsSerializable where Key == String {
public static var _defaults: Bridge { return Bridge() }
public static var _defaultsArray: ArrayBridge { return ArrayBridge() }
}

extension Array: DefaultsSerializable where Element: DefaultsSerializable {
public typealias T = [Element.T]
public typealias Bridge = Element.ArrayBridge
public typealias ArrayBridge = DefaultsObjectBridge<[T]>
public static var _defaults: Bridge {
return Element._defaultsArray
}

public static var _defaultsArray: ArrayBridge {
fatalError("Multidimensional arrays are not supported yet")
}
Expand Down
50 changes: 31 additions & 19 deletions Sources/Defaults+Observing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,40 @@ import Foundation

#if !os(Linux)

public extension DefaultsAdapter {
public extension DefaultsAdapter {
/// Observe changes on a `UserDefaults` key.
///
/// The `handler` is invoked on whichever thread posts the KVO notification
/// (typically the writer's thread). It is not guaranteed to run on the
/// main thread. Dispatch onto your own actor or queue inside the handler
/// if you need a specific isolation.
func observe<T: DefaultsSerializable>(_ key: DefaultsKey<T>,
options: NSKeyValueObservingOptions = [.new, .old],
handler: @escaping @Sendable (DefaultsObserver<T>.Update) -> Void) -> DefaultsDisposable
{
return defaults.observe(key, options: options, handler: handler)
}

func observe<T: DefaultsSerializable>(_ key: DefaultsKey<T>,
options: NSKeyValueObservingOptions = [.new, .old],
handler: @escaping (DefaultsObserver<T>.Update) -> Void) -> DefaultsDisposable {
return defaults.observe(key, options: options, handler: handler)
/// Observe changes on a `UserDefaults` key path.
///
/// The `handler` is invoked on whichever thread posts the KVO notification
/// (typically the writer's thread). It is not guaranteed to run on the
/// main thread. Dispatch onto your own actor or queue inside the handler
/// if you need a specific isolation.
func observe<T: DefaultsSerializable>(_ keyPath: KeyPath<KeyStore, DefaultsKey<T>>,
options: NSKeyValueObservingOptions = [.old, .new],
handler: @escaping @Sendable (DefaultsObserver<T>.Update) -> Void) -> DefaultsDisposable
{
return defaults.observe(keyStore[keyPath: keyPath],
options: options,
handler: handler)
}
}

func observe<T: DefaultsSerializable>(_ keyPath: KeyPath<KeyStore, DefaultsKey<T>>,
options: NSKeyValueObservingOptions = [.old, .new],
handler: @escaping (DefaultsObserver<T>.Update) -> Void) -> DefaultsDisposable {
return defaults.observe(keyStore[keyPath: keyPath],
options: options,
handler: handler)
public extension UserDefaults {
func observe<T: DefaultsSerializable>(_ key: DefaultsKey<T>, options: NSKeyValueObservingOptions = [.old, .new], handler: @escaping @Sendable (DefaultsObserver<T>.Update) -> Void) -> DefaultsDisposable {
return DefaultsObserver(key: key, userDefaults: self, options: options, handler: handler)
}
}
}

public extension UserDefaults {

func observe<T: DefaultsSerializable>(_ key: DefaultsKey<T>, options: NSKeyValueObservingOptions = [.old, .new], handler: @escaping (DefaultsObserver<T>.Update) -> Void) -> DefaultsDisposable {
return DefaultsObserver(key: key, userDefaults: self, options: options, handler: handler)
}
}

#endif
17 changes: 8 additions & 9 deletions Sources/Defaults+Subscripts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@

import Foundation

public protocol DefaultsProviding {
public protocol DefaultsProviding: Sendable {
associatedtype KeyStore: DefaultsKeyStore
subscript<T: DefaultsSerializable>(key key: DefaultsKey<T>) -> T.T where T: OptionalType, T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(key key: DefaultsKey<T>) -> T.T where T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(keyPath: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T: OptionalType, T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(keyPath: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(dynamicMember keyPath: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T: OptionalType, T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(dynamicMember keyPath: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T.T == T { get nonmutating set }

subscript<T: DefaultsSerializable>(key _: DefaultsKey<T>) -> T.T where T: OptionalType, T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(key _: DefaultsKey<T>) -> T.T where T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(_: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T: OptionalType, T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(_: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(dynamicMember _: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T: OptionalType, T.T == T { get nonmutating set }
subscript<T: DefaultsSerializable>(dynamicMember _: KeyPath<KeyStore, DefaultsKey<T>>) -> T.T where T.T == T { get nonmutating set }
}

extension DefaultsAdapter: DefaultsProviding {
Expand Down Expand Up @@ -94,7 +94,6 @@ extension DefaultsAdapter: DefaultsProviding {
}

public extension UserDefaults {

subscript<T: DefaultsSerializable>(key: DefaultsKey<T>) -> T.T where T: OptionalType, T.T == T {
get {
if let value = T._defaults.get(key: key._key, userDefaults: self), let _value = value as? T.T.Wrapped {
Expand Down
24 changes: 14 additions & 10 deletions Sources/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@

import Foundation

/// Global shortcut for `UserDefaults.standard`
// swiftlint:disable identifier_name prefixed_toplevel_constant
/// Global shortcut for `UserDefaults.standard`.
///
/// **Pro-Tip:** If you want to use shared user defaults, just
/// redefine this global shortcut in your app target, like so:
/// ~~~
/// var Defaults = DefaultsAdapter(defaults: UserDefaults(suiteName: "com.my.app")!, keyStore: DefaultsKeys())
/// ~~~

public var Defaults = DefaultsAdapter<DefaultsKeys>(defaults: .standard, keyStore: .init())
/// redefine this global shortcut in your app target, like so:
/// ~~~
/// var Defaults = DefaultsAdapter(defaults: UserDefaults(suiteName: "com.my.app")!, keyStore: DefaultsKeys())
/// ~~~
///
/// `nonisolated(unsafe)` makes the mutation explicit under Swift 6 strict
/// concurrency. The value type (`DefaultsAdapter`) is itself `Sendable`, so
/// the only risk is the initial assignment race, which has always been the
/// caller's responsibility to do at startup before any reads.
public nonisolated(unsafe) var Defaults = DefaultsAdapter<DefaultsKeys>(defaults: .standard, keyStore: .init())
// swiftlint:enable identifier_name prefixed_toplevel_constant

public extension UserDefaults {

/// Returns `true` if `key` exists
func hasKey<T>(_ key: DefaultsKey<T>) -> Bool {
return object(forKey: key._key) != nil
Expand All @@ -57,8 +62,7 @@ public extension UserDefaults {
}
}

internal extension UserDefaults {

extension UserDefaults {
func number(forKey key: String) -> NSNumber? {
return object(forKey: key) as? NSNumber
}
Expand Down
13 changes: 9 additions & 4 deletions Sources/DefaultsAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,23 @@ import Foundation
/// Defaults.launchCount += 1
/// ```
@dynamicMemberLookup
public struct DefaultsAdapter<KeyStore: DefaultsKeyStore> {
public struct DefaultsAdapter<KeyStore: DefaultsKeyStore>: Sendable {
private let defaultsBox: UncheckedSendable<UserDefaults>

/// The wrapped `UserDefaults` instance. Held via `UncheckedSendable` because
/// Foundation does not expose `UserDefaults` as `Sendable`, even though
/// Apple documents the type as thread-safe.
public var defaults: UserDefaults { defaultsBox.wrappedValue }

public let defaults: UserDefaults
public let keyStore: KeyStore

public init(defaults: UserDefaults, keyStore: KeyStore) {
self.defaults = defaults
self.defaultsBox = UncheckedSendable(defaults)
self.keyStore = keyStore
}

@available(*, unavailable)
public subscript(dynamicMember member: String) -> Never {
public subscript(dynamicMember _: String) -> Never {
fatalError()
}

Expand Down
Loading