Compare commits

...

32 Commits
v6.0.0 ... main

Author SHA1 Message Date
Sindre Sorhus bd14dae265 Readme tweaks 2023-06-03 01:51:42 +03:00
Sindre Sorhus 488ff7dd71 Meta tweaks 2023-04-25 02:05:28 +07:00
Sindre Sorhus 0f5a23fcbd Tweaks 2023-04-17 20:48:39 +08:00
Sindre Sorhus 8584d1d6ed
Switch from deprecated NSSecureCoding API (#132)
Fixes #131
2023-04-07 11:49:00 +09:00
Sindre Sorhus a09d7c8957 Tweaks 2023-04-06 15:37:46 +09:00
Sindre Sorhus d71bfd8ffb Minor tweaks 2022-12-12 18:23:29 +01:00
hank121314 5d1d7932a9
Validate the default key name and emit with Xcode runtime warning (#126)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2022-12-12 18:17:27 +01:00
Sindre Sorhus 5cf1178d34
Make it possible to import the package once for a whole project (#125) 2022-11-28 14:17:10 +07:00
Sindre Sorhus 7a22d37874 Add `.updates()` method
Fixes #77
2022-11-19 14:42:48 +07:00
Sindre Sorhus ea11b7ac4f Target macOS 10.15, iOS 13, tvOS 13, watchOS 6 2022-11-18 16:50:27 +07:00
Sindre Sorhus 60013d90e2 Improve documentation 2022-11-18 15:42:31 +07:00
Sindre Sorhus 35943225a0
Remove `DefaultsBaseKey` (#123) 2022-11-18 10:53:54 +07:00
hank121314 fbc67fd179
Support dynamic default value (#121)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2022-11-17 17:36:29 +07:00
Sindre Sorhus 1b1a057220 Use Swift Package Index for docs 2022-11-13 16:29:31 +07:00
Sindre Sorhus dd44190ddf Improve docs for the defaults key
Closes #59
2022-11-09 00:48:19 +07:00
hank121314 be7e30ba36
Support serializing and deserializing nested custom types (#118) 2022-10-28 15:27:17 +07:00
Sindre Sorhus b23fb7b057 Fix tests 2022-10-27 16:48:14 +07:00
Sindre Sorhus 176aa63666 Minor tweaks 2022-10-22 00:36:48 +07:00
Sindre Sorhus bc1af5d872 Upgrade to Swit 5.7 2022-09-22 23:57:28 +07:00
Sindre Sorhus 981ccb0a01 Minor tweaks 2022-06-10 14:58:24 +07:00
hank121314 9e65eac602
Fix preserving color space for the `Color` type (#105)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2022-06-10 14:52:03 +07:00
Sindre Sorhus 001adc694b
Make `Defaults.AnyKey` conform to `Equatable` and `Hashable` (#104) 2022-06-04 19:08:34 +07:00
Sindre Sorhus d1e2109fc9 Minor tweaks 2022-05-14 13:15:43 +07:00
hank121314 bab3087067
Add support for `ClosedRange` and `Range` (#102) 2022-05-12 15:24:06 +07:00
Sindre Sorhus 3535f3d088 Add tests for #97 2022-04-20 18:16:30 +07:00
hank121314 119f654d44
Xcode 13.3 workaround (#95)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2022-03-16 00:28:40 +07:00
Sindre Sorhus c9198bb1d0
Add support for `UUID` (#92) 2022-01-27 22:14:21 +07:00
Sindre Sorhus 8c0d80e783
Remove moot conformance (#91) 2022-01-25 15:08:01 +07:00
Sindre Sorhus 64010fdcc2 Fix test warning 2022-01-25 12:34:27 +07:00
Sindre Sorhus a6c1c064fa Document benefits over `@AppStorage` 2022-01-23 13:43:12 +07:00
Sindre Sorhus 5b30f01e46 Code style tweaks 2021-11-15 02:53:40 +07:00
hank121314 55f3302c3a
Add support for SwiftUI `Color` (#84)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2021-10-16 19:01:16 +07:00
41 changed files with 1592 additions and 666 deletions

View File

@ -4,12 +4,13 @@ on:
- pull_request
jobs:
test:
runs-on: macos-11
runs-on: macos-13
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- run: sudo xcode-select -switch /Applications/Xcode_14.3.app
- run: swift test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: norio-nomura/action-swiftlint@3.2.1

4
.spi.yml Normal file
View File

@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: ['Defaults']

View File

@ -1,5 +1,4 @@
only_rules:
- anyobject_protocol
- array_init
- block_based_kvo
- class_delegate_protocol
@ -10,6 +9,7 @@ only_rules:
- collection_alignment
- colon
- comma
- comma_inheritance
- compiler_protocol_init
- computed_accessors_order
- conditional_returns_on_newline
@ -20,11 +20,14 @@ only_rules:
- control_statement
- custom_rules
- discarded_notification_center_observer
- discouraged_assert
- discouraged_direct_init
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_collection
- duplicate_enum_cases
- duplicate_imports
- duplicated_key_in_dictionary_literal
- dynamic_inline
- empty_collection_literal
- empty_count
@ -80,10 +83,12 @@ only_rules:
- operator_whitespace
- orphaned_doc_comment
- overridden_super_call
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- private_action
- private_outlet
- private_subject
- private_unit_test
- prohibited_super_call
- protocol_property_accessors_order
@ -99,6 +104,9 @@ only_rules:
- redundant_void_return
- required_enum_case
- return_arrow_whitespace
- return_value_from_void_function
- self_binding
- self_in_property_initialization
- shorthand_operator
- sorted_first_last
- statement_position
@ -115,6 +123,7 @@ only_rules:
- trailing_newline
- trailing_semicolon
- trailing_whitespace
- unavailable_condition
- unavailable_function
- unneeded_break_in_switch
- unneeded_parentheses_in_closure_argument
@ -128,16 +137,17 @@ only_rules:
- unused_setter_value
- valid_ibinspectable
- vertical_parameter_alignment
- vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
- void_function_in_ternary
- void_return
- weak_delegate
- xct_specific_matcher
- yoda_condition
analyzer_rules:
- capture_variable
- unused_declaration
- unused_import
- typesafe_array_init
number_separator:
minimum_length: 5
identifier_name:
@ -153,12 +163,14 @@ identifier_name:
excluded:
- 'x'
- 'y'
- 'z'
- 'a'
- 'b'
- 'x1'
- 'x2'
- 'y1'
- 'y2'
- 'z2'
custom_rules:
no_nsrect:
regex: '\bNSRect\b'
@ -173,8 +185,14 @@ custom_rules:
match_kinds: typeidentifier
message: 'Use CGPoint instead of NSPoint'
swiftui_state_private:
regex: '@(State|StateObject)\s+var'
message: "SwiftUI @State/@StateObject properties should be private"
regex: '@(State|StateObject|ObservedObject|EnvironmentObject)\s+var'
message: 'SwiftUI @State/@StateObject/@ObservedObject/@EnvironmentObject properties should be private'
swiftui_environment_private:
regex: '@Environment\(\\\.\w+\)\s+var'
message: 'SwiftUI @Environment properties should be private'
final_class:
regex: '^class [a-zA-Z\d]+[^{]+\{'
message: "Classes should be marked as final whenever possible. If you actually need it to be subclassable, just add `// swiftlint:disable:next final_class`."
message: 'Classes should be marked as final whenever possible. If you actually need it to be subclassable, just add `// swiftlint:disable:next final_class`.'
no_alignment_center:
regex: '\b\(alignment: .center\b'
message: 'This alignment is the default.'

View File

@ -1,13 +1,13 @@
// swift-tools-version:5.5
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "Defaults",
platforms: [
.macOS(.v10_13),
.iOS(.v12),
.tvOS(.v12),
.watchOS(.v5)
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6)
],
products: [
.library(

View File

@ -11,7 +11,7 @@ extension Defaults {
`get` will deserialize the internal value to the type that user specify in the function parameter.
```
```swift
let any = Defaults.Key<Defaults.AnySerializable>("independentAnyKey", default: 121_314)
print(Defaults[any].get(Int.self))
@ -20,7 +20,7 @@ extension Defaults {
- Note: The only way to assign a non-serializable value is using `ExpressibleByArrayLiteral` or `ExpressibleByDictionaryLiteral` to assign a type that is not a `UserDefaults` natively supported type.
```
```swift
private enum mime: String, Defaults.Serializable {
case JSON = "application/json"
}
@ -33,7 +33,7 @@ extension Defaults {
var value: Any
public static let bridge = AnyBridge()
init<T>(value: T?) {
init(value: (some Any)?) {
self.value = value ?? ()
}
@ -59,43 +59,43 @@ extension Defaults.AnySerializable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Data:
return hasher.combine(value)
hasher.combine(value)
case let value as Date:
return hasher.combine(value)
hasher.combine(value)
case let value as Bool:
return hasher.combine(value)
hasher.combine(value)
case let value as UInt8:
return hasher.combine(value)
hasher.combine(value)
case let value as Int8:
return hasher.combine(value)
hasher.combine(value)
case let value as UInt16:
return hasher.combine(value)
hasher.combine(value)
case let value as Int16:
return hasher.combine(value)
hasher.combine(value)
case let value as UInt32:
return hasher.combine(value)
hasher.combine(value)
case let value as Int32:
return hasher.combine(value)
hasher.combine(value)
case let value as UInt64:
return hasher.combine(value)
hasher.combine(value)
case let value as Int64:
return hasher.combine(value)
hasher.combine(value)
case let value as UInt:
return hasher.combine(value)
hasher.combine(value)
case let value as Int:
return hasher.combine(value)
hasher.combine(value)
case let value as Float:
return hasher.combine(value)
hasher.combine(value)
case let value as Double:
return hasher.combine(value)
hasher.combine(value)
case let value as CGFloat:
return hasher.combine(value)
hasher.combine(value)
case let value as String:
return hasher.combine(value)
hasher.combine(value)
case let value as [AnyHashable: AnyHashable]:
return hasher.combine(value)
hasher.combine(value)
case let value as [AnyHashable]:
return hasher.combine(value)
hasher.combine(value)
default:
break
}
@ -191,7 +191,7 @@ extension Defaults.AnySerializable: ExpressibleByDictionaryLiteral {
}
}
extension Defaults.AnySerializable: _DefaultsOptionalType {
extension Defaults.AnySerializable: _DefaultsOptionalProtocol {
// Since `nil` cannot be assigned to `Any`, we use `Void` instead of `nil`.
public var isNil: Bool { value is Void }
}

View File

@ -1,4 +1,4 @@
import Foundation
import SwiftUI
#if os(macOS)
import AppKit
#else
@ -7,7 +7,7 @@ import UIKit
extension Defaults.CodableBridge {
public func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -24,11 +24,11 @@ extension Defaults.CodableBridge {
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let jsonString = object else {
guard let object else {
return nil
}
return [Value].init(jsonString: "[\(jsonString)]")?.first
return [Value].init(jsonString: "[\(object)]")?.first
}
}
@ -50,7 +50,7 @@ extension Defaults {
This exists to avoid compiler ambiguity.
*/
extension Defaults {
public struct CodableNSSecureCodingBridge<Value: Codable & NSSecureCoding>: CodableBridge {}
public struct CodableNSSecureCodingBridge<Value: Codable & NSSecureCoding & NSObject>: CodableBridge {}
}
extension Defaults {
@ -69,35 +69,35 @@ extension Defaults {
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let rawValue = object else {
guard let object else {
return nil
}
return Value(rawValue: rawValue)
return Value(rawValue: object)
}
}
}
extension Defaults {
public struct NSSecureCodingBridge<Value: NSSecureCoding>: Bridge {
public struct NSSecureCodingBridge<Value: NSSecureCoding & NSObject>: Bridge {
public typealias Value = Value
public typealias Serializable = Data
public func serialize(_ value: Value?) -> Serializable? {
guard let object = value else {
guard let value else {
return nil
}
return try? NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true)
return try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let data = object else {
guard let object else {
return nil
}
do {
return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Value
return try NSKeyedUnarchiver.unarchivedObject(ofClass: Value.self, from: object)
} catch {
print(error)
return nil
@ -134,8 +134,8 @@ extension Defaults {
return array.map { Element.bridge.serialize($0) }.compact()
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let array = object else {
public func deserialize(_ array: Serializable?) -> Value? {
guard let array else {
return nil
}
@ -149,8 +149,8 @@ extension Defaults {
public typealias Value = [Key: Element.Value]
public typealias Serializable = [String: Element.Serializable]
public func serialize(_ value: Value?) -> Serializable? {
guard let dictionary = value else {
public func serialize(_ dictionary: Value?) -> Serializable? {
guard let dictionary else {
return nil
}
@ -160,8 +160,8 @@ extension Defaults {
}
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let dictionary = object else {
public func deserialize(_ dictionary: Serializable?) -> Value? {
guard let dictionary else {
return nil
}
@ -185,8 +185,8 @@ extension Defaults {
public typealias Value = Set<Element>
public typealias Serializable = Any
public func serialize(_ value: Value?) -> Serializable? {
guard let set = value else {
public func serialize(_ set: Value?) -> Serializable? {
guard let set else {
return nil
}
@ -224,8 +224,8 @@ extension Defaults {
public typealias Element = Value.Element
public typealias Serializable = Any
public func serialize(_ value: Value?) -> Serializable? {
guard let setAlgebra = value else {
public func serialize(_ setAlgebra: Value?) -> Serializable? {
guard let setAlgebra else {
return nil
}
@ -263,8 +263,8 @@ extension Defaults {
public typealias Element = Value.Element
public typealias Serializable = Any
public func serialize(_ value: Value?) -> Serializable? {
guard let collection = value else {
public func serialize(_ collection: Value?) -> Serializable? {
guard let collection else {
return nil
}
@ -296,6 +296,139 @@ extension Defaults {
}
}
extension Defaults {
public struct UUIDBridge: Bridge {
public typealias Value = UUID
public typealias Serializable = String
public func serialize(_ value: Value?) -> Serializable? {
value?.uuidString
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let object else {
return nil
}
return .init(uuidString: object)
}
}
}
extension Defaults {
public struct RangeBridge<T: RangeSerializable>: Bridge {
public typealias Value = T
public typealias Serializable = [Any]
typealias Bound = T.Bound
public func serialize(_ value: Value?) -> Serializable? {
guard let value else {
return nil
}
if Bound.isNativelySupportedType {
return [value.lowerBound, value.upperBound]
}
guard
let lowerBound = Bound.bridge.serialize(value.lowerBound as? Bound.Value),
let upperBound = Bound.bridge.serialize(value.upperBound as? Bound.Value)
else {
return nil
}
return [lowerBound, upperBound]
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let object else {
return nil
}
if Bound.isNativelySupportedType {
guard
let lowerBound = object[safe: 0] as? Bound,
let upperBound = object[safe: 1] as? Bound
else {
return nil
}
return .init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
}
guard
let lowerBound = Bound.bridge.deserialize(object[safe: 0] as? Bound.Serializable) as? Bound,
let upperBound = Bound.bridge.deserialize(object[safe: 1] as? Bound.Serializable) as? Bound
else {
return nil
}
return .init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
}
}
}
extension Defaults {
/**
The bridge which is responsible for `SwiftUI.Color` serialization and deserialization.
It is unsafe to convert `SwiftUI.Color` to `UIColor` and use `UIColor.bridge` to serialize it, because `UIColor` does not hold a color space, but `Swift.Color` does (which means color space might get lost in the conversion). The bridge will always try to preserve the color space whenever `Color#cgColor` exists. Only when `Color#cgColor` is `nil`, will it use `UIColor.bridge` to do the serialization and deserialization.
*/
@available(iOS 15.0, macOS 11.0, tvOS 15.0, watchOS 8.0, iOSApplicationExtension 15.0, macOSApplicationExtension 11.0, tvOSApplicationExtension 15.0, watchOSApplicationExtension 8.0, *)
public struct ColorBridge: Bridge {
public typealias Value = Color
public typealias Serializable = Any
#if os(macOS)
private typealias NativeColor = NSColor
#else
private typealias NativeColor = UIColor
#endif
public func serialize(_ value: Value?) -> Serializable? {
guard let value else {
return nil
}
guard
let cgColor = value.cgColor,
let colorSpace = cgColor.colorSpace?.name as? String,
let components = cgColor.components
else {
return NativeColor.bridge.serialize(NativeColor(value))
}
return [colorSpace, components] as [Any]
}
public func deserialize(_ object: Serializable?) -> Value? {
if let object = object as? NativeColor.Serializable {
guard let nativeColor = NativeColor.bridge.deserialize(object) else {
return nil
}
return Value(nativeColor)
}
guard
let object = object as? [Any],
let rawColorspace = object[0] as? String,
let colorspace = CGColorSpace(name: rawColorspace as CFString),
let components = object[1] as? [CGFloat],
let cgColor = CGColor(colorSpace: colorspace, components: components)
else {
return nil
}
if #available(macOS 12.0, macOSApplicationExtension 12.0, *) {
return Value(cgColor: cgColor)
} else {
return Value(cgColor)
}
}
}
}
extension Defaults {
public struct AnyBridge: Defaults.Bridge {
public typealias Value = Defaults.AnySerializable

View File

@ -1,5 +1,4 @@
import Foundation
import CoreGraphics
import SwiftUI
#if os(macOS)
import AppKit
#else
@ -86,11 +85,11 @@ extension Defaults.Serializable where Self: Codable {
public static var bridge: Defaults.TopLevelCodableBridge<Self> { Defaults.TopLevelCodableBridge() }
}
extension Defaults.Serializable where Self: Codable & NSSecureCoding {
extension Defaults.Serializable where Self: Codable & NSSecureCoding & NSObject {
public static var bridge: Defaults.CodableNSSecureCodingBridge<Self> { Defaults.CodableNSSecureCodingBridge() }
}
extension Defaults.Serializable where Self: Codable & NSSecureCoding & Defaults.PreferNSSecureCoding {
extension Defaults.Serializable where Self: Codable & NSSecureCoding & NSObject & Defaults.PreferNSSecureCoding {
public static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}
@ -105,7 +104,8 @@ extension Defaults.Serializable where Self: Codable & RawRepresentable & Default
extension Defaults.Serializable where Self: RawRepresentable {
public static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}
extension Defaults.Serializable where Self: NSSecureCoding {
extension Defaults.Serializable where Self: NSSecureCoding & NSObject {
public static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}
@ -136,10 +136,31 @@ extension Dictionary: Defaults.Serializable where Key: LosslessStringConvertible
public static var bridge: Defaults.DictionaryBridge<Key, Value> { Defaults.DictionaryBridge() }
}
extension UUID: Defaults.Serializable {
public static let bridge = Defaults.UUIDBridge()
}
@available(iOS 15.0, macOS 11.0, tvOS 15.0, watchOS 8.0, iOSApplicationExtension 15.0, macOSApplicationExtension 11.0, tvOSApplicationExtension 15.0, watchOSApplicationExtension 8.0, *)
extension Color: Defaults.Serializable {
public static let bridge = Defaults.ColorBridge()
}
extension Range: Defaults.RangeSerializable where Bound: Defaults.Serializable {
public static var bridge: Defaults.RangeBridge<Range> { Defaults.RangeBridge() }
}
extension ClosedRange: Defaults.RangeSerializable where Bound: Defaults.Serializable {
public static var bridge: Defaults.RangeBridge<ClosedRange> { Defaults.RangeBridge() }
}
#if os(macOS)
/// `NSColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge`.
/**
`NSColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge`.
*/
extension NSColor: Defaults.Serializable {}
#else
/// `UIColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge`.
/**
`UIColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge`.
*/
extension UIColor: Defaults.Serializable {}
#endif

View File

@ -1,86 +1,22 @@
import Foundation
/**
Types that conform to this protocol can be used with `Defaults`.
The type should have a static variable `bridge` which should reference an instance of a type that conforms to `Defaults.Bridge`.
```
struct User {
username: String
password: String
}
extension User: Defaults.Serializable {
static let bridge = UserBridge()
}
```
*/
public protocol DefaultsSerializable {
public protocol _DefaultsSerializable {
typealias Value = Bridge.Value
typealias Serializable = Bridge.Serializable
associatedtype Bridge: DefaultsBridge
associatedtype Bridge: Defaults.Bridge
/// Static bridge for the `Value` which cannot be stored natively.
/**
Static bridge for the `Value` which cannot be stored natively.
*/
static var bridge: Bridge { get }
/// A flag to determine whether `Value` can be stored natively or not.
/**
A flag to determine whether `Value` can be stored natively or not.
*/
static var isNativelySupportedType: Bool { get }
}
/**
A `Bridge` is responsible for serialization and deserialization.
It has two associated types `Value` and `Serializable`.
- `Value`: The type you want to use.
- `Serializable`: The type stored in `UserDefaults`.
- `serialize`: Executed before storing to the `UserDefaults` .
- `deserialize`: Executed after retrieving its value from the `UserDefaults`.
```
struct User {
username: String
password: String
}
extension User {
static let bridge = UserBridge()
}
struct UserBridge: Defaults.Bridge {
typealias Value = User
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
return nil
}
return [
"username": value.username,
"password": value.password
]
}
func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let username = object["username"],
let password = object["password"]
else {
return nil
}
return User(
username: username,
password: password
)
}
}
```
*/
public protocol DefaultsBridge {
public protocol _DefaultsBridge {
associatedtype Value
associatedtype Serializable
@ -88,33 +24,31 @@ public protocol DefaultsBridge {
func deserialize(_ object: Serializable?) -> Value?
}
public protocol DefaultsCollectionSerializable: Collection, Defaults.Serializable {
/// `Collection` does not have a initializer, but we need a initializer to convert an array into the `Value`.
public protocol _DefaultsCollectionSerializable: Collection, Defaults.Serializable {
/**
`Collection` does not have a initializer, but we need a initializer to convert an array into the `Value`.
*/
init(_ elements: [Element])
}
public protocol DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable {
/// Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly.
public protocol _DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable {
/**
Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly.
*/
func toArray() -> [Element]
}
/// Convenience protocol for `Codable`.
public protocol DefaultsCodableBridge: Defaults.Bridge where Serializable == String, Value: Codable {}
public protocol _DefaultsCodableBridge: Defaults.Bridge where Serializable == String, Value: Codable {}
/**
Ambiguous bridge selector protocol. This lets you select your preferred bridge when there are multiple possibilities.
public protocol _DefaultsPreferRawRepresentable: RawRepresentable {}
public protocol _DefaultsPreferNSSecureCoding: NSObject, NSSecureCoding {}
For example:
// Essential properties for serializing and deserializing `ClosedRange` and `Range`.
public protocol _DefaultsRange {
associatedtype Bound: Comparable, Defaults.Serializable
```
enum Interval: Int, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable {
case tenMinutes = 10
case halfHour = 30
case oneHour = 60
var lowerBound: Bound { get }
var upperBound: Bound { get }
init(uncheckedBounds: (lower: Bound, upper: Bound))
}
```
By default, if an `enum` conforms to `Codable` and `Defaults.Serializable`, it will use the `CodableBridge`, but by conforming to `Defaults.PreferRawRepresentable`, we can switch the bridge back to `RawRepresentableBridge`.
*/
public protocol DefaultsPreferRawRepresentable: RawRepresentable {}
public protocol DefaultsPreferNSSecureCoding: NSSecureCoding {}

View File

@ -1,52 +1,122 @@
// MIT License © Sindre Sorhus
import Foundation
public protocol DefaultsBaseKey: Defaults.AnyKey {
var name: String { get }
var suite: UserDefaults { get }
}
public enum Defaults {
/**
Access stored values.
extension DefaultsBaseKey {
/// Reset the item back to its default value.
public func reset() {
suite.removeObject(forKey: name)
```swift
import Defaults
extension Defaults.Keys {
static let quality = Key<Double>("quality", default: 0.8)
}
//
Defaults[.quality]
//=> 0.8
Defaults[.quality] = 0.5
//=> 0.5
Defaults[.quality] += 0.1
//=> 0.6
Defaults[.quality] = "🦄"
//=> [Cannot assign value of type 'String' to type 'Double']
```
*/
public static subscript<Value: Serializable>(key: Key<Value>) -> Value {
get { key.suite[key] }
set {
key.suite[key] = newValue
}
}
}
public enum Defaults {
public typealias BaseKey = DefaultsBaseKey
public typealias AnyKey = Keys
public typealias Serializable = DefaultsSerializable
public typealias CollectionSerializable = DefaultsCollectionSerializable
public typealias SetAlgebraSerializable = DefaultsSetAlgebraSerializable
public typealias PreferRawRepresentable = DefaultsPreferRawRepresentable
public typealias PreferNSSecureCoding = DefaultsPreferNSSecureCoding
public typealias Bridge = DefaultsBridge
typealias CodableBridge = DefaultsCodableBridge
public typealias _Defaults = Defaults
public typealias _Default = Default
public class Keys: BaseKey {
extension Defaults {
// We cannot use `Key` as the container for keys because of "Static stored properties not supported in generic types".
/**
Type-erased key.
*/
public class _AnyKey {
public typealias Key = Defaults.Key
public let name: String
public let suite: UserDefaults
@_alwaysEmitIntoClient
fileprivate init(name: String, suite: UserDefaults) {
runtimeWarn(
isValidKeyPath(name: name),
"The key name must be ASCII, not start with @, and cannot contain a dot (.)."
)
self.name = name
self.suite = suite
}
/**
Reset the item back to its default value.
*/
public func reset() {
suite.removeObject(forKey: name)
}
}
}
public final class Key<Value: Serializable>: AnyKey {
public let defaultValue: Value
extension Defaults {
/**
Strongly-typed key used to access values.
/// Create a defaults key.
/// The `default` parameter can be left out if the `Value` type is an optional.
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
self.defaultValue = defaultValue
You declare the defaults keys upfront with a type and default value.
super.init(name: key, suite: suite)
```swift
import Defaults
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
extension Defaults.Keys {
static let quality = Key<Double>("quality", default: 0.8)
// ^ ^ ^ ^
// Key Type UserDefaults name Default value
}
```
- Warning: The `UserDefaults` name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
*/
public final class Key<Value: Serializable>: _AnyKey {
/**
It will be executed in these situations:
- `UserDefaults.object(forKey: string)` returns `nil`
- A `bridge` cannot deserialize `Value` from `UserDefaults`
*/
@usableFromInline
internal let defaultValueGetter: () -> Value
public var defaultValue: Value { defaultValueGetter() }
/**
Create a key.
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
The `default` parameter should not be used if the `Value` type is an optional.
*/
@_alwaysEmitIntoClient
public init(
_ name: String,
default defaultValue: Value,
suite: UserDefaults = .standard
) {
self.defaultValueGetter = { defaultValue }
super.init(name: name, suite: suite)
if (defaultValue as? _DefaultsOptionalProtocol)?.isNil == true {
return
}
@ -57,13 +127,48 @@ public enum Defaults {
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
suite.register(defaults: [name: serialized])
}
}
public static subscript<Value: Serializable>(key: Key<Value>) -> Value {
get { key.suite[key] }
set {
key.suite[key] = newValue
/**
Create a key with a dynamic default value.
This can be useful in cases where you cannot define a static default value as it may change during the lifetime of the app.
```swift
extension Defaults.Keys {
static let camera = Key<AVCaptureDevice?>("camera") { .default(for: .video) }
}
```
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
- Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings.
*/
@_alwaysEmitIntoClient
public init(
_ name: String,
suite: UserDefaults = .standard,
default defaultValueGetter: @escaping () -> Value
) {
self.defaultValueGetter = defaultValueGetter
super.init(name: name, suite: suite)
}
}
}
extension Defaults.Key {
// We cannot declare this convenience initializer in class directly because of "@_transparent' attribute is not supported on declarations within classes".
/**
Create a key with an optional value.
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
*/
@_transparent
public convenience init<T>(
_ name: String,
suite: UserDefaults = .standard
) where Value == T? {
self.init(name, default: nil, suite: suite)
}
}
@ -78,8 +183,202 @@ extension Defaults {
}
}
extension Defaults.Key {
public convenience init<T: Defaults.Serializable>(_ key: String, suite: UserDefaults = .standard) where Value == T? {
self.init(key, default: nil, suite: suite)
extension Defaults._AnyKey: Equatable {
public static func == (lhs: Defaults._AnyKey, rhs: Defaults._AnyKey) -> Bool {
lhs.name == rhs.name
&& lhs.suite == rhs.suite
}
}
extension Defaults._AnyKey: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(suite)
}
}
extension Defaults {
public typealias Keys = _AnyKey
/**
Types that conform to this protocol can be used with `Defaults`.
The type should have a static variable `bridge` which should reference an instance of a type that conforms to `Defaults.Bridge`.
```swift
struct User {
username: String
password: String
}
extension User: Defaults.Serializable {
static let bridge = UserBridge()
}
```
*/
public typealias Serializable = _DefaultsSerializable
public typealias CollectionSerializable = _DefaultsCollectionSerializable
public typealias SetAlgebraSerializable = _DefaultsSetAlgebraSerializable
/**
Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities.
```swift
enum Interval: Int, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable {
case tenMinutes = 10
case halfHour = 30
case oneHour = 60
}
```
By default, if an `enum` conforms to `Codable` and `Defaults.Serializable`, it will use the `CodableBridge`, but by conforming to `Defaults.PreferRawRepresentable`, we can switch the bridge back to `RawRepresentableBridge`.
*/
public typealias PreferRawRepresentable = _DefaultsPreferRawRepresentable
/**
Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities.
*/
public typealias PreferNSSecureCoding = _DefaultsPreferNSSecureCoding
/**
A `Bridge` is responsible for serialization and deserialization.
It has two associated types `Value` and `Serializable`.
- `Value`: The type you want to use.
- `Serializable`: The type stored in `UserDefaults`.
- `serialize`: Executed before storing to the `UserDefaults` .
- `deserialize`: Executed after retrieving its value from the `UserDefaults`.
```swift
struct User {
username: String
password: String
}
extension User {
static let bridge = UserBridge()
}
struct UserBridge: Defaults.Bridge {
typealias Value = User
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value else {
return nil
}
return [
"username": value.username,
"password": value.password
]
}
func deserialize(_ object: Serializable?) -> Value? {
guard
let object,
let username = object["username"],
let password = object["password"]
else {
return nil
}
return User(
username: username,
password: password
)
}
}
```
*/
public typealias Bridge = _DefaultsBridge
public typealias RangeSerializable = _DefaultsRange & _DefaultsSerializable
/**
Convenience protocol for `Codable`.
*/
typealias CodableBridge = _DefaultsCodableBridge
}
extension Defaults {
/**
Observe updates to a stored value.
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
//
Task {
for await value in Defaults.updates(.isUnicornMode) {
print("Value:", value)
}
}
```
*/
public static func updates<Value: Serializable>(
_ key: Key<Value>,
initial: Bool = true
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
.init { continuation in
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
// TODO: Use the `.deserialize` method directly.
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
continuation.yield(value)
}
observation.start(options: initial ? [.initial] : [])
continuation.onTermination = { _ in
observation.invalidate()
}
}
}
// TODO: Make this include a tuple with the values when Swift supports variadic generics. I can then simply use `merge()` with the first `updates()` method.
/**
Observe updates to multiple stored values.
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.
```swift
Task {
for await _ in Defaults.updates([.foo, .bar]) {
print("One of the values changed")
}
}
```
- Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-9eh8`` if you need that. You could use [`merge`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md) to merge them into a single sequence.
*/
public static func updates(
_ keys: [_AnyKey],
initial: Bool = true
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
.init { continuation in
let observations = keys.indexed().map { index, key in
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { _ in
continuation.yield()
}
// Ensure we only trigger a single initial event.
observation.start(options: initial && index == 0 ? [.initial] : [])
return observation
}
continuation.onTermination = { _ in
for observation in observations {
observation.invalidate()
}
}
}
}
}

View File

@ -0,0 +1,68 @@
# ``Defaults``
Store key-value pairs persistently across launches of your app.
It uses [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults) underneath but exposes a type-safe facade with lots of nice conveniences.
## Usage
You declare the defaults keys upfront with a type and default value.
```swift
import Defaults
extension Defaults.Keys {
static let quality = Key<Double>("quality", default: 0.8)
// ^ ^ ^ ^
// Key Type UserDefaults name Default value
}
```
You can then access it as a subscript on the `Defaults` global:
```swift
Defaults[.quality]
//=> 0.8
Defaults[.quality] = 0.5
//=> 0.5
```
[Learn More](https://github.com/sindresorhus/Defaults#usage)
### Tip
If you don't want to import this package in every file you use it, add the below to a file in your app. You can then use `Defaults` and `@Default` from anywhere without an import.
```swift
import Defaults
typealias Defaults = _Defaults
typealias Default = _Default
```
## Topics
### Essentials
- ``Defaults/subscript(_:)``
- ``Defaults/Key``
- ``Defaults/Serializable``
### Methods
- ``Defaults/updates(_:initial:)-9eh8``
- ``Defaults/updates(_:initial:)-1mqkb``
- ``Defaults/reset(_:)-7jv5v``
- ``Defaults/reset(_:)-7es1e``
- ``Defaults/removeAll(suite:)``
### SwiftUI
- ``Default``
- ``Defaults/Toggle``
### Force Type Resolution
- ``Defaults/PreferRawRepresentable``
- ``Defaults/PreferNSSecureCoding``

View File

@ -8,7 +8,7 @@ extension Defaults {
/**
Migrate the given key's value from JSON string to `Value`.
```
```swift
extension Defaults.Keys {
static let array = Key<Set<String>?>("array")
}

View File

@ -2,8 +2,8 @@ import Foundation
import CoreGraphics
extension Defaults {
public typealias NativeType = DefaultsNativeType
public typealias CodableType = DefaultsCodableType
public typealias NativeType = _DefaultsNativeType
public typealias CodableType = _DefaultsCodableType
}
extension Data: Defaults.NativeType {
@ -198,7 +198,7 @@ extension Defaults.SetAlgebraSerializable where Self: Defaults.NativeType, Eleme
public typealias CodableForm = [Element.CodableForm]
}
extension Defaults.CodableType where Self: RawRepresentable, NativeForm: RawRepresentable, RawValue == NativeForm.RawValue {
extension Defaults.CodableType where Self: RawRepresentable<NativeForm.RawValue>, NativeForm: RawRepresentable {
public func toNative() -> NativeForm {
NativeForm(rawValue: rawValue)!
}

View File

@ -9,10 +9,9 @@ It should have an associated type name `CodableForm` where its protocol conform
So we can convert the JSON string into a `NativeType` like this:
```
```swift
guard
let jsonString = string,
let jsonData = jsonString.data(using: .utf8),
let jsonData = string?.data(using: .utf8),
let codable = try? JSONDecoder().decode(NativeType.CodableForm.self, from: jsonData)
else {
return nil
@ -21,7 +20,7 @@ else {
return codable.toNative()
```
*/
public protocol DefaultsNativeType: Defaults.Serializable {
public protocol _DefaultsNativeType: Defaults.Serializable {
associatedtype CodableForm: Defaults.CodableType
}
@ -32,7 +31,7 @@ Represents the type before migration an its protocol should conform to `Codable`
The main purposed of `CodableType` is trying to infer the `Codable` type to do `JSONDecoder().decode`. It should have an associated type name `NativeForm` which is the type we want it to store in `UserDefaults`. nd it also have a `toNative()` function to convert itself into `NativeForm`.
```
```swift
struct User {
username: String
password: String
@ -56,7 +55,7 @@ extension CodableUser: Defaults.CodableType {
}
```
*/
public protocol DefaultsCodableType: Codable {
public protocol _DefaultsCodableType: Codable {
associatedtype NativeForm: Defaults.NativeType
func toNative() -> NativeForm
}

View File

@ -3,8 +3,7 @@ import Foundation
extension UserDefaults {
func migrateCodableToNative<Value: Defaults.Serializable & Codable>(forKey key: String, of type: Value.Type) {
guard
let jsonString = string(forKey: key),
let jsonData = jsonString.data(using: .utf8),
let jsonData = string(forKey: key)?.data(using: .utf8),
let codable = try? JSONDecoder().decode(Value.self, from: jsonData)
else {
return
@ -20,26 +19,25 @@ extension UserDefaults {
1. If `Value` is `[String]`, `Value.CodableForm` will covert into `[String].CodableForm`.
```
```swift
JSONDecoder().decode([String].CodableForm.self, from: jsonData)
```
2. If `Array` conforms to `NativeType`, its `CodableForm` is `[Element.CodableForm]` and `Element` is `String`.
```
```swift
JSONDecoder().decode([String.CodableForm].self, from: jsonData)
```
3. `String`'s `CodableForm` is `self`, because `String` is `Codable`.
```
```swift
JSONDecoder().decode([String].self, from: jsonData)
```
*/
func migrateCodableToNative<Value: Defaults.NativeType>(forKey key: String, of type: Value.Type) {
guard
let jsonString = string(forKey: key),
let jsonData = jsonString.data(using: .utf8),
let jsonData = string(forKey: key)?.data(using: .utf8),
let codable = try? JSONDecoder().decode(Value.CodableForm.self, from: jsonData)
else {
return

View File

@ -1,4 +1,3 @@
#if canImport(Combine)
import Foundation
import Combine
@ -6,7 +5,6 @@ extension Defaults {
/**
Custom `Subscription` for `UserDefaults` key observation.
*/
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
final class DefaultsSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == BaseChange {
private var subscriber: SubscriberType?
private var observation: UserDefaultsKeyObservation?
@ -43,7 +41,6 @@ extension Defaults {
/**
Custom Publisher, which is using DefaultsSubscription.
*/
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
struct DefaultsPublisher: Publisher {
typealias Output = BaseChange
typealias Failure = Never
@ -58,7 +55,7 @@ extension Defaults {
self.options = options
}
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
func receive(subscriber: some Subscriber<Output, Failure>) {
let subscription = DefaultsSubscription(
subscriber: subscriber,
suite: suite,
@ -74,7 +71,7 @@ extension Defaults {
/**
Returns a type-erased `Publisher` that publishes changes related to the given key.
```
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
@ -86,8 +83,9 @@ extension Defaults {
//=> false
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
public static func publisher<Value: Serializable>(
_ key: Key<Value>,
options: ObservationOptions = [.initial]
@ -100,10 +98,11 @@ extension Defaults {
/**
Publisher for multiple `Key<T>` observation, but without specific information about changes.
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
public static func publisher(
keys: AnyKey...,
keys: _AnyKey...,
options: ObservationOptions = [.initial]
) -> AnyPublisher<Void, Never> {
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
@ -122,4 +121,3 @@ extension Defaults {
return combinedPublisher
}
}
#endif

View File

@ -1,12 +1,12 @@
import Foundation
public protocol DefaultsObservation: AnyObject {
public protocol _DefaultsObservation: AnyObject {
func invalidate()
/**
Keep this observation alive for as long as, and no longer than, another object exists.
```
```swift
Defaults.observe(.xyz) { [unowned self] change in
self.xyz = change.newValue
}.tieToLifetime(of: self)
@ -25,13 +25,17 @@ public protocol DefaultsObservation: AnyObject {
}
extension Defaults {
public typealias Observation = DefaultsObservation
public typealias Observation = _DefaultsObservation
public enum ObservationOption {
/// Whether a notification should be sent to the observer immediately, before the observer registration method even returns.
/**
Whether a notification should be sent to the observer immediately, before the observer registration method even returns.
*/
case initial
/// Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change.
/**
Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change.
*/
case prior
}
@ -39,7 +43,7 @@ extension Defaults {
private static func deserialize<Value: Serializable>(_ value: Any?, to type: Value.Type) -> Value? {
guard
let value = value,
let value,
!(value is NSNull)
else {
return nil
@ -91,7 +95,7 @@ extension Defaults {
- Note: This only works with `Defaults.observe()` and `Defaults.publisher()`. User-made KVO will not be affected.
```
```swift
let observer = Defaults.observe(keys: .key1, .key2) {
//
@ -136,7 +140,7 @@ extension Defaults {
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
}
public func invalidate() {
func invalidate() {
object?.removeObserver(self, forKeyPath: key, context: nil)
object = nil
lifetimeAssociation?.cancel()
@ -144,7 +148,7 @@ extension Defaults {
private var lifetimeAssociation: LifetimeAssociation?
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
// swiftlint:disable:next trailing_closure
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
@ -153,7 +157,7 @@ extension Defaults {
return self
}
public func removeLifetimeTie() {
func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}
@ -171,7 +175,7 @@ extension Defaults {
guard
selfObject == object as? NSObject,
let change = change
let change
else {
return
}
@ -212,7 +216,7 @@ extension Defaults {
invalidate()
}
public func start(options: ObservationOptions) {
func start(options: ObservationOptions) {
for observable in observables {
observable.suite?.addObserver(
self,
@ -223,7 +227,7 @@ extension Defaults {
}
}
public func invalidate() {
func invalidate() {
for observable in observables {
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
observable.suite = nil
@ -232,7 +236,7 @@ extension Defaults {
lifetimeAssociation?.cancel()
}
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
// swiftlint:disable:next trailing_closure
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
@ -241,7 +245,7 @@ extension Defaults {
return self
}
public func removeLifetimeTie() {
func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}
@ -261,7 +265,7 @@ extension Defaults {
guard
object is UserDefaults,
let change = change
let change
else {
return
}
@ -279,7 +283,7 @@ extension Defaults {
/**
Observe a defaults key.
```
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
@ -289,6 +293,8 @@ extension Defaults {
//=> false
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func observe<Value: Serializable>(
_ key: Key<Value>,
@ -308,7 +314,7 @@ extension Defaults {
/**
Observe multiple keys of any type, but without any information about the changes.
```
```swift
extension Defaults.Keys {
static let setting1 = Key<Bool>("setting1", default: false)
static let setting2 = Key<Bool>("setting2", default: true)
@ -318,9 +324,11 @@ extension Defaults {
//
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func observe(
keys: AnyKey...,
keys: _AnyKey...,
options: ObservationOptions = [.initial],
handler: @escaping () -> Void
) -> Observation {

View File

@ -9,7 +9,7 @@ extension Defaults {
- Parameter keys: String keys to reset.
- Parameter suite: `UserDefaults` suite.
```
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
@ -36,7 +36,7 @@ extension Defaults {
- Parameter keys: String keys to reset.
- Parameter suite: `UserDefaults` suite.
```
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
@ -62,7 +62,7 @@ extension Defaults {
/**
Reset the given keys back to their default values.
```
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
@ -76,14 +76,14 @@ extension Defaults {
//=> false
```
*/
public static func reset(_ keys: AnyKey...) {
public static func reset(_ keys: _AnyKey...) {
reset(keys)
}
/**
Reset the given keys back to their default values.
```
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
@ -97,7 +97,7 @@ extension Defaults {
//=> false
```
*/
public static func reset(_ keys: [AnyKey]) {
public static func reset(_ keys: [_AnyKey]) {
for key in keys {
key.reset()
}

View File

@ -1,11 +1,11 @@
#if canImport(Combine)
import SwiftUI
import Combine
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Defaults {
@MainActor
final class Observable<Value: Serializable>: ObservableObject {
private var cancellable: AnyCancellable?
private var task: Task<Void, Never>?
private let key: Defaults.Key<Value>
let objectWillChange = ObservableObjectPublisher()
@ -21,33 +21,57 @@ extension Defaults {
init(_ key: Key<Value>) {
self.key = key
self.cancellable = Defaults.publisher(key, options: [.prior])
.sink { [weak self] change in
guard change.isPrior else {
return
}
// We only use this on the latest OSes (as of adding this) since the backdeploy library has a lot of bugs.
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
// The `@MainActor` is important as the `.send()` method doesn't inherit the `@MainActor` from the class.
self.task = .detached(priority: .userInitiated) { @MainActor [weak self] in
for await _ in Defaults.updates(key) {
guard let self else {
return
}
DispatchQueue.mainSafeAsync {
self?.objectWillChange.send()
self.objectWillChange.send()
}
}
} else {
self.cancellable = Defaults.publisher(key, options: [.prior])
.sink { [weak self] change in
guard change.isPrior else {
return
}
Task { @MainActor in
self?.objectWillChange.send()
}
}
}
}
/// Reset the key back to its default value.
deinit {
task?.cancel()
}
/**
Reset the key back to its default value.
*/
func reset() {
key.reset()
}
}
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
/**
Access stored values from SwiftUI.
This is similar to `@AppStorage` but it accepts a ``Defaults/Key`` and many more types.
*/
@propertyWrapper
public struct Default<Value: Defaults.Serializable>: DynamicProperty {
public typealias Publisher = AnyPublisher<Defaults.KeyChange<Value>, Never>
private let key: Defaults.Key<Value>
// Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamicaly changed.
// Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamically changed.
@ObservedObject private var observable: Defaults.Observable<Value>
/**
@ -55,7 +79,7 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
- Important: You cannot use this in an `ObservableObject`. It's meant to be used in a `View`.
```
```swift
extension Defaults.Keys {
static let hasUnicorn = Key<Bool>("hasUnicorn", default: false)
}
@ -75,7 +99,7 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
*/
public init(_ key: Defaults.Key<Value>) {
self.key = key
self.observable = Defaults.Observable(key)
self.observable = .init(key)
}
public var wrappedValue: Value {
@ -87,10 +111,14 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
public var projectedValue: Binding<Value> { $observable.value }
/// The default value of the key.
/**
The default value of the key.
*/
public var defaultValue: Value { key.defaultValue }
/// Combine publisher that publishes values when the `Defaults` item changes.
/**
Combine publisher that publishes values when the `Defaults` item changes.
*/
public var publisher: Publisher { Defaults.publisher(key) }
public mutating func update() {
@ -100,7 +128,7 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
/**
Reset the key back to its default value.
```
```swift
extension Defaults.Keys {
static let opacity = Key<Double>("opacity", default: 1)
}
@ -121,20 +149,21 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
}
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Default where Value: Equatable {
/// Indicates whether the value is the same as the default value.
/**
Indicates whether the value is the same as the default value.
*/
public var isDefaultValue: Bool { wrappedValue == defaultValue }
}
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *)
extension Defaults {
/**
Creates a SwiftUI `Toggle` view that is connected to a `Defaults` key with a `Bool` value.
A SwiftUI `Toggle` view that is connected to a ``Defaults/Key`` with a `Bool` value.
The toggle works exactly like the SwiftUI `Toggle`.
```
```swift
extension Defaults.Keys {
static let showAllDayEvents = Key<Bool>("showAllDayEvents", default: false)
}
@ -148,7 +177,7 @@ extension Defaults {
You can also listen to changes:
```
```swift
struct ShowAllDayEventsSetting: View {
var body: some View {
Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents)
@ -160,17 +189,17 @@ extension Defaults {
}
```
*/
public struct Toggle<Label, Key>: View where Label: View, Key: Defaults.Key<Bool> {
public struct Toggle<Label: View>: View {
@ViewStorage private var onChange: ((Bool) -> Void)?
private let label: () -> Label
// Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamicaly changed.
// Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamically changed.
@ObservedObject private var observable: Defaults.Observable<Bool>
public init(key: Key, @ViewBuilder label: @escaping () -> Label) {
public init(key: Defaults.Key<Bool>, @ViewBuilder label: @escaping () -> Label) {
self.label = label
self.observable = Defaults.Observable(key)
self.observable = .init(key)
}
public var body: some View {
@ -183,23 +212,24 @@ extension Defaults {
}
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *)
extension Defaults.Toggle where Label == Text {
public init<S>(_ title: S, key: Defaults.Key<Bool>) where S: StringProtocol {
extension Defaults.Toggle<Text> {
public init(_ title: some StringProtocol, key: Defaults.Key<Bool>) {
self.label = { Text(title) }
self.observable = Defaults.Observable(key)
self.observable = .init(key)
}
}
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *)
extension Defaults.Toggle {
/// Do something when the value changes to a different value.
/**
Do something when the value changes to a different value.
*/
public func onChange(_ action: @escaping (Bool) -> Void) -> Self {
onChange = action
return self
}
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper
private struct ViewStorage<Value>: DynamicProperty {
private final class ValueBox {
@ -223,4 +253,3 @@ private struct ViewStorage<Value>: DynamicProperty {
self._valueBox = .init(wrappedValue: ValueBox(value()))
}
}
#endif

View File

@ -10,7 +10,7 @@ extension UserDefaults {
}
func _set<Value: Defaults.Serializable>(_ key: String, to value: Value) {
if (value as? _DefaultsOptionalType)?.isNil == true {
if (value as? _DefaultsOptionalProtocol)?.isNil == true {
removeObject(forKey: key)
return
}

View File

@ -1,5 +1,9 @@
import Foundation
#if DEBUG
#if canImport(OSLog)
import OSLog
#endif
#endif
extension Decodable {
init?(jsonData: Data) {
@ -20,7 +24,7 @@ extension Decodable {
}
final class ObjectAssociation<T: Any> {
final class ObjectAssociation<T> {
subscript(index: AnyObject) -> T? {
get {
objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T?
@ -59,7 +63,7 @@ final class LifetimeAssociation {
When either the owner or the new `LifetimeAssociation` is destroyed, the given deinit handler, if any, is called.
```
```swift
class Ghost {
var association: LifetimeAssociation?
@ -83,8 +87,8 @@ final class LifetimeAssociation {
init(of target: AnyObject, with owner: AnyObject, deinitHandler: @escaping () -> Void = {}) {
let wrappedObject = ObjectLifetimeTracker(for: target, deinitHandler: deinitHandler)
let associatedObjects = LifetimeAssociation.associatedObjects[owner] ?? []
LifetimeAssociation.associatedObjects[owner] = associatedObjects + [wrappedObject]
let associatedObjects = Self.associatedObjects[owner] ?? []
Self.associatedObjects[owner] = associatedObjects + [wrappedObject]
self.wrappedObject = wrappedObject
self.owner = owner
@ -104,63 +108,77 @@ final class LifetimeAssociation {
private func invalidate() {
guard
let owner = owner,
let wrappedObject = wrappedObject,
var associatedObjects = LifetimeAssociation.associatedObjects[owner],
let owner,
let wrappedObject,
var associatedObjects = Self.associatedObjects[owner],
let wrappedObjectAssociationIndex = associatedObjects.firstIndex(where: { $0 === wrappedObject })
else {
return
}
associatedObjects.remove(at: wrappedObjectAssociationIndex)
LifetimeAssociation.associatedObjects[owner] = associatedObjects
Self.associatedObjects[owner] = associatedObjects
self.owner = nil
}
}
/// A protocol for making generic type constraints of optionals.
/// - Note: It's intentionally not including `associatedtype Wrapped` as that limits a lot of the use-cases.
public protocol _DefaultsOptionalType: ExpressibleByNilLiteral {
/// This is useful as you can't compare `_OptionalType` to `nil`.
/**
A protocol for making generic type constraints of optionals.
- Note: It's intentionally not including `associatedtype Wrapped` as that limits a lot of the use-cases.
*/
public protocol _DefaultsOptionalProtocol: ExpressibleByNilLiteral {
/**
This is useful as you cannot compare `_OptionalType` to `nil`.
*/
var isNil: Bool { get }
}
extension Optional: _DefaultsOptionalType {
extension Optional: _DefaultsOptionalProtocol {
public var isNil: Bool { self == nil }
}
extension DispatchQueue {
/**
Performs the `execute` closure immediately if we're on the main thread or asynchronously puts it on the main thread otherwise.
*/
static func mainSafeAsync(execute work: @escaping () -> Void) {
if Thread.isMainThread {
work()
} else {
main.async(execute: work)
}
}
}
extension Sequence {
/// Returns an array containing the non-nil elements.
/**
Returns an array containing the non-nil elements.
*/
func compact<T>() -> [T] where Element == T? {
// TODO: Make this `compactMap(\.self)` when https://bugs.swift.org/browse/SR-12897 is fixed.
// TODO: Make this `compactMap(\.self)` when https://github.com/apple/swift/issues/55343 is fixed.
compactMap { $0 }
}
}
extension Collection {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
extension Collection {
func indexed() -> some Sequence<(Index, Element)> {
zip(indices, self)
}
}
extension Defaults {
@usableFromInline
internal static func isValidKeyPath(name: String) -> Bool {
// The key must be ASCII, not start with @, and cannot contain a dot.
!name.starts(with: "@") && name.allSatisfy { $0 != "." && $0.isASCII }
}
}
extension Defaults.Serializable {
/**
Cast a `Serializable` value to `Self`.
Converts a natively supported type from `UserDefaults` into `Self`.
```
```swift
guard let anyObject = object(forKey: key) else {
return nil
}
@ -168,18 +186,20 @@ extension Defaults.Serializable {
return Value.toValue(anyObject)
```
*/
static func toValue(_ anyObject: Any) -> Self? {
// Return directly if `anyObject` can cast to Value, since it means `Value` is a natively supported type.
static func toValue<T: Defaults.Serializable>(_ anyObject: Any, type: T.Type = Self.self) -> T? {
if
isNativelySupportedType,
let anyObject = anyObject as? Self
T.isNativelySupportedType,
let anyObject = anyObject as? T
{
return anyObject
} else if let value = bridge.deserialize(anyObject as? Serializable) {
return value as? Self
}
return nil
guard let nextType = T.Serializable.self as? any Defaults.Serializable.Type else {
// This is a special case for the types which do not conform to `Defaults.Serializable` (for example, `Any`).
return T.bridge.deserialize(anyObject as? T.Serializable) as? T
}
return T.bridge.deserialize(toValue(anyObject, type: nextType) as? T.Serializable) as? T
}
/**
@ -187,18 +207,76 @@ extension Defaults.Serializable {
Converts `Self` into `UserDefaults` native support type.
```
```swift
set(Value.toSerialize(value), forKey: key)
```
*/
static func toSerializable(_ value: Self) -> Any? {
// Return directly if `Self` is a natively supported type, since it does not need serialization.
if isNativelySupportedType {
@usableFromInline
internal static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? {
if T.isNativelySupportedType {
return value
} else if let serialized = bridge.serialize(value as? Value) {
}
guard let serialized = T.bridge.serialize(value as? T.Value) else {
return nil
}
guard let next = serialized as? any Defaults.Serializable else {
// This is a special case for the types which do not conform to `Defaults.Serializable` (for example, `Any`).
return serialized
}
return nil
return toSerializable(next)
}
}
#if DEBUG
/**
Get SwiftUI dynamic shared object.
Reference: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html
*/
@usableFromInline
internal let dynamicSharedObject: UnsafeMutableRawPointer = {
let imageCount = _dyld_image_count()
for imageIndex in 0..<imageCount {
guard
let name = _dyld_get_image_name(imageIndex),
// Use `/SwiftUI` instead of `SwiftUI` to prevent any library named `XXSwiftUI`.
String(cString: name).hasSuffix("/SwiftUI"),
let header = _dyld_get_image_header(imageIndex)
else {
continue
}
return UnsafeMutableRawPointer(mutating: header)
}
return UnsafeMutableRawPointer(mutating: #dsohandle)
}()
#endif
@_transparent
@usableFromInline
internal func runtimeWarn(
_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String
) {
#if DEBUG
#if canImport(OSLog)
let message = message()
let condition = condition()
if !condition {
os_log(
.fault,
// A token that identifies the containing executable or dylib image.
dso: dynamicSharedObject,
log: OSLog(subsystem: "com.apple.runtime-issues", category: "Defaults"),
"%@",
message
)
}
#else
assert(condition, message)
#endif
#endif
}

View File

@ -368,7 +368,6 @@ final class DefaultsAnySerializableTests: XCTestCase {
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<Defaults.AnySerializable>("observeAnyKeyCombine", default: 123)
let expect = expectation(description: "Observation closure being called")
@ -396,7 +395,6 @@ final class DefaultsAnySerializableTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<Defaults.AnySerializable?>("observeAnyOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")

View File

@ -81,7 +81,6 @@ final class DefaultsArrayTests: XCTestCase {
XCTAssertEqual(Defaults[.array][0], newName)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<[String]>("observeArrayKeyCombine", default: fixtureArray)
let newName = "Chen"
@ -108,7 +107,6 @@ final class DefaultsArrayTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<[String]?>("observeArrayOptionalKeyCombine")
let newName = ["Chen"]

View File

@ -2,7 +2,7 @@ import Foundation
import Defaults
import XCTest
private enum FixtureCodableEnum: String, Defaults.Serializable & Codable & Hashable {
private enum FixtureCodableEnum: String, Hashable, Codable, Defaults.Serializable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
@ -131,7 +131,6 @@ final class DefaultsCodableEnumTests: XCTestCase {
XCTAssertNotNil(UserDefaults.standard.integer(forKey: keyName))
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<FixtureCodableEnum>("observeCodableEnumKeyCombine", default: .tenMinutes)
let expect = expectation(description: "Observation closure being called")
@ -159,7 +158,6 @@ final class DefaultsCodableEnumTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<FixtureCodableEnum?>("observeCodableEnumOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -188,7 +186,6 @@ final class DefaultsCodableEnumTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[FixtureCodableEnum]>("observeCodableEnumArrayKeyCombine", default: [.tenMinutes])
let expect = expectation(description: "Observation closure being called")
@ -216,7 +213,6 @@ final class DefaultsCodableEnumTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: FixtureCodableEnum]>("observeCodableEnumDictionaryKeyCombine", default: ["0": .tenMinutes])
let expect = expectation(description: "Observation closure being called")

View File

@ -162,7 +162,6 @@ final class DefaultsCodableTests: XCTestCase {
XCTAssertNotNil(UserDefaults.standard.data(forKey: keyName))
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<Unicorn>("observeCodableKeyCombine", default: fixtureCodable)
let expect = expectation(description: "Observation closure being called")
@ -188,7 +187,6 @@ final class DefaultsCodableTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<Unicorn?>("observeCodableOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -216,7 +214,6 @@ final class DefaultsCodableTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[Unicorn]>("observeCodableArrayKeyCombine", default: [fixtureCodable])
let expect = expectation(description: "Observation closure being called")
@ -242,7 +239,6 @@ final class DefaultsCodableTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: Unicorn]>("observeCodableDictionaryKeyCombine", default: ["0": fixtureCodable])
let expect = expectation(description: "Observation closure being called")

View File

@ -15,7 +15,7 @@ private struct ItemBridge: Defaults.Bridge {
typealias Value = Item
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -24,7 +24,7 @@ private struct ItemBridge: Defaults.Bridge {
func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let object,
let name = object["name"],
let count = UInt(object["count"] ?? "0")
else {
@ -170,7 +170,6 @@ final class DefaultsCollectionCustomElementTests: XCTestCase {
XCTAssertEqual(Defaults[.collectionCustomElementDictionary]["1"]?[0], fixtureCustomCollection2)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<Bag<Item>>("observeCollectionCustomElementKeyCombine", default: .init(items: [fixtureCustomCollection]))
let expect = expectation(description: "Observation closure being called")
@ -196,7 +195,6 @@ final class DefaultsCollectionCustomElementTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<Bag<Item>?>("observeCollectionCustomElementOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -225,7 +223,6 @@ final class DefaultsCollectionCustomElementTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[Bag<Item>]>("observeCollectionCustomElementArrayKeyCombine", default: [.init(items: [fixtureCustomCollection])])
let expect = expectation(description: "Observation closure being called")
@ -251,7 +248,6 @@ final class DefaultsCollectionCustomElementTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: Bag<Item>]>("observeCollectionCustomElementDictionaryKeyCombine", default: ["0": .init(items: [fixtureCustomCollection])])
let expect = expectation(description: "Observation closure being called")

View File

@ -156,7 +156,6 @@ final class DefaultsCollectionTests: XCTestCase {
XCTAssertEqual(Defaults[.collectionDictionary]["1"]?[0], fixtureCollection[0])
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<Bag<String>>("observeCollectionKeyCombine", default: .init(items: fixtureCollection))
let item = "Grape"
@ -183,7 +182,6 @@ final class DefaultsCollectionTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<Bag<String>?>("observeCollectionOptionalKeyCombine")
let item = "Grape"
@ -213,7 +211,6 @@ final class DefaultsCollectionTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[Bag<String>]>("observeCollectionArrayKeyCombine", default: [.init(items: fixtureCollection)])
let item = "Grape"
@ -240,7 +237,6 @@ final class DefaultsCollectionTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: Bag<String>]>("observeCollectionArrayKeyCombine", default: ["0": .init(items: fixtureCollection)])
let item = "Grape"

View File

@ -0,0 +1,24 @@
import SwiftUI
import Defaults
import XCTest
@available(iOS 15, tvOS 15, watchOS 8, *)
final class DefaultsColorTests: XCTestCase {
override func setUp() {
super.setUp()
Defaults.removeAll()
}
override func tearDown() {
super.tearDown()
Defaults.removeAll()
}
func testPreservesColorSpace() {
let fixture = Color(.displayP3, red: 1, green: 0.3, blue: 0.7, opacity: 1)
let key = Defaults.Key<Color?>("independentColorPreservesColorSpaceKey")
Defaults[key] = fixture
XCTAssertEqual(Defaults[key]?.cgColor?.colorSpace, fixture.cgColor?.colorSpace)
XCTAssertEqual(Defaults[key]?.cgColor, fixture.cgColor)
}
}

View File

@ -16,7 +16,7 @@ public final class DefaultsUserBridge: Defaults.Bridge {
public typealias Serializable = [String: String]
public func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -25,7 +25,7 @@ public final class DefaultsUserBridge: Defaults.Bridge {
public func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let object,
let username = object["username"],
let password = object["password"]
else {
@ -38,13 +38,57 @@ public final class DefaultsUserBridge: Defaults.Bridge {
private let fixtureCustomBridge = User(username: "hank121314", password: "123456")
struct PlainHourMinuteTimeRange: Hashable, Codable {
var start: PlainHourMinuteTime
var end: PlainHourMinuteTime
}
extension PlainHourMinuteTimeRange: Defaults.Serializable {
struct Bridge: Defaults.Bridge {
typealias Value = PlainHourMinuteTimeRange
typealias Serializable = [PlainHourMinuteTime]
func serialize(_ value: Value?) -> Serializable? {
guard let value else {
return nil
}
return [value.start, value.end]
}
func deserialize(_ object: Serializable?) -> Value? {
guard
let array = object,
let start = array[safe: 0],
let end = array[safe: 1]
else {
return nil
}
return .init(start: start, end: end)
}
}
static let bridge = Bridge()
}
struct PlainHourMinuteTime: Hashable, Codable, Defaults.Serializable {
var hour: Int
var minute: Int
}
extension Collection {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
extension Defaults.Keys {
fileprivate static let customBridge = Key<User>("customBridge", default: fixtureCustomBridge)
fileprivate static let customBridgeArray = Key<[User]>("array_customBridge", default: [fixtureCustomBridge])
fileprivate static let customBridgeDictionary = Key<[String: User]>("dictionary_customBridge", default: ["0": fixtureCustomBridge])
}
final class DefaultsCustomBridge: XCTestCase {
override func setUp() {
super.setUp()
@ -148,6 +192,35 @@ final class DefaultsCustomBridge: XCTestCase {
XCTAssertEqual(Defaults[key]["0"]?[1], fixtureCustomBridge)
}
func testRecursiveKey() {
let start = PlainHourMinuteTime(hour: 1, minute: 0)
let end = PlainHourMinuteTime(hour: 2, minute: 0)
let range = PlainHourMinuteTimeRange(start: start, end: end)
let key = Defaults.Key<PlainHourMinuteTimeRange>("independentCustomBridgeRecursiveKey", default: range)
XCTAssertEqual(Defaults[key].start.hour, range.start.hour)
XCTAssertEqual(Defaults[key].start.minute, range.start.minute)
XCTAssertEqual(Defaults[key].end.hour, range.end.hour)
XCTAssertEqual(Defaults[key].end.minute, range.end.minute)
guard let rawValue = UserDefaults.standard.array(forKey: key.name) as? [String] else {
XCTFail("rawValue should not be nil")
return
}
XCTAssertEqual(rawValue, [#"{"minute":0,"hour":1}"#, #"{"minute":0,"hour":2}"#])
let next_start = PlainHourMinuteTime(hour: 3, minute: 58)
let next_end = PlainHourMinuteTime(hour: 4, minute: 59)
let next_range = PlainHourMinuteTimeRange(start: next_start, end: next_end)
Defaults[key] = next_range
XCTAssertEqual(Defaults[key].start.hour, next_range.start.hour)
XCTAssertEqual(Defaults[key].start.minute, next_range.start.minute)
XCTAssertEqual(Defaults[key].end.hour, next_range.end.hour)
XCTAssertEqual(Defaults[key].end.minute, next_range.end.minute)
guard let nextRawValue = UserDefaults.standard.array(forKey: key.name) as? [String] else {
XCTFail("nextRawValue should not be nil")
return
}
XCTAssertEqual(nextRawValue, [#"{"minute":58,"hour":3}"#, #"{"minute":59,"hour":4}"#])
}
func testType() {
XCTAssertEqual(Defaults[.customBridge], fixtureCustomBridge)
let newUser = User(username: "sindresorhus", password: "123456789")
@ -169,7 +242,6 @@ final class DefaultsCustomBridge: XCTestCase {
XCTAssertEqual(Defaults[.customBridgeDictionary]["0"], newUser)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<User>("observeCustomBridgeKeyCombine", default: fixtureCustomBridge)
let newUser = User(username: "sindresorhus", password: "123456789")
@ -196,7 +268,6 @@ final class DefaultsCustomBridge: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<User?>("observeCustomBridgeOptionalKeyCombine")
let newUser = User(username: "sindresorhus", password: "123456789")
@ -226,7 +297,6 @@ final class DefaultsCustomBridge: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[User]>("observeCustomBridgeArrayKeyCombine", default: [fixtureCustomBridge])
let newUser = User(username: "sindresorhus", password: "123456789")
@ -253,7 +323,6 @@ final class DefaultsCustomBridge: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryCombine() {
let key = Defaults.Key<[String: User]>("observeCustomBridgeDictionaryKeyCombine", default: ["0": fixtureCustomBridge])
let newUser = User(username: "sindresorhus", password: "123456789")

View File

@ -64,7 +64,6 @@ final class DefaultsDictionaryTests: XCTestCase {
XCTAssertEqual(Defaults[.dictionary]["0"], newName)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<[String: String]>("observeDictionaryKeyCombine", default: fixtureDictionary)
let expect = expectation(description: "Observation closure being called")
@ -91,7 +90,6 @@ final class DefaultsDictionaryTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<[String: String]?>("observeDictionaryOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")

View File

@ -116,7 +116,6 @@ final class DefaultsEnumTests: XCTestCase {
XCTAssertEqual(Defaults[.enumDictionary]["0"], .halfHour)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<FixtureEnum>("observeEnumKeyCombine", default: .tenMinutes)
let expect = expectation(description: "Observation closure being called")
@ -146,7 +145,6 @@ final class DefaultsEnumTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<FixtureEnum?>("observeEnumOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -176,7 +174,6 @@ final class DefaultsEnumTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[FixtureEnum]>("observeEnumArrayKeyCombine", default: [.tenMinutes])
let expect = expectation(description: "Observation closure being called")
@ -205,7 +202,6 @@ final class DefaultsEnumTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: FixtureEnum]>("observeEnumDictionaryKeyCombine", default: ["0": .tenMinutes])
let expect = expectation(description: "Observation closure being called")

View File

@ -25,7 +25,9 @@ private struct TimeZone: Hashable {
}
extension TimeZone: Defaults.NativeType {
/// Associated `CodableForm` to `CodableTimeZone`.
/**
Associated `CodableForm` to `CodableTimeZone`.
*/
typealias CodableForm = CodableTimeZone
static let bridge = TimeZoneBridge()
@ -37,7 +39,9 @@ private struct CodableTimeZone {
}
extension CodableTimeZone: Defaults.CodableType {
/// Convert from `Codable` to `Native`.
/**
Convert from `Codable` to `Native`.
*/
func toNative() -> TimeZone {
TimeZone(id: id, name: name)
}
@ -48,7 +52,7 @@ private struct TimeZoneBridge: Defaults.Bridge {
typealias Serializable = [String: Any]
func serialize(_ value: TimeZone?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -57,9 +61,9 @@ private struct TimeZoneBridge: Defaults.Bridge {
func deserialize(_ object: Serializable?) -> TimeZone? {
guard
let dictionary = object,
let id = dictionary["id"] as? String,
let name = dictionary["name"] as? String
let object,
let id = object["id"] as? String,
let name = object["name"] as? String
else {
return nil
}
@ -82,7 +86,7 @@ private struct ChosenTimeZoneBridge: Defaults.Bridge {
typealias Serializable = [String: Any]
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -91,9 +95,9 @@ private struct ChosenTimeZoneBridge: Defaults.Bridge {
func deserialize(_ object: Serializable?) -> Value? {
guard
let dictionary = object,
let id = dictionary["id"] as? String,
let name = dictionary["name"] as? String
let object,
let id = object["id"] as? String,
let name = object["name"] as? String
else {
return nil
}
@ -245,7 +249,7 @@ extension CodableEnumForm: Defaults.CodableType {
typealias NativeForm = EnumForm
}
private func setCodable<Value: Codable>(forKey keyName: String, data: Value) {
private func setCodable(forKey keyName: String, data: some Codable) {
guard
let text = try? JSONEncoder().encode(data),
let string = String(data: text, encoding: .utf8)

View File

@ -1,3 +1,4 @@
#if canImport(AppKit)
import Foundation
import Defaults
import XCTest
@ -31,6 +32,16 @@ final class DefaultsNSColorTests: XCTestCase {
XCTAssertTrue(Defaults[key].isEqual(fixtureColor1))
}
func testPreservesColorSpace() {
let fixture = NSColor(displayP3Red: 1, green: 0.3, blue: 0.7, alpha: 1)
let key = Defaults.Key<NSColor?>("independentNSColorPreservesColorSpaceKey")
Defaults[key] = fixture
XCTAssertEqual(Defaults[key]?.colorSpace, fixture.colorSpace)
XCTAssertEqual(Defaults[key]?.cgColor.colorSpace, fixture.cgColor.colorSpace)
XCTAssertEqual(Defaults[key], fixture)
XCTAssertEqual(Defaults[key]?.cgColor, fixture.cgColor)
}
func testOptionalKey() {
let key = Defaults.Key<NSColor?>("independentNSColorOptionalKey")
XCTAssertNil(Defaults[key])
@ -115,7 +126,6 @@ final class DefaultsNSColorTests: XCTestCase {
XCTAssertTrue(Defaults[.colorDictionary]["0"]?.isEqual(fixtureColor1) ?? false)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<NSColor>("observeNSColorKeyCombine", default: fixtureColor)
let expect = expectation(description: "Observation closure being called")
@ -141,7 +151,6 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<NSColor?>("observeNSColorOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -178,7 +187,6 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[NSColor]>("observeNSColorArrayKeyCombine", default: [fixtureColor])
let expect = expectation(description: "Observation closure being called")
@ -204,7 +212,6 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: NSColor]>("observeNSColorDictionaryKeyCombine", default: ["0": fixtureColor])
let expect = expectation(description: "Observation closure being called")
@ -304,3 +311,4 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
}
#endif

View File

@ -3,6 +3,7 @@ import CoreData
import Defaults
import XCTest
@objc(ExamplePersistentHistory)
private final class ExamplePersistentHistory: NSPersistentHistoryToken, Defaults.Serializable {
let value: String
@ -157,7 +158,6 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
XCTAssertEqual(Defaults[.persistentHistoryDictionary]["0"]?.value, newPersistentHistory.value)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<ExamplePersistentHistory>("observeNSSecureCodingKeyCombine", default: persistentHistoryValue)
let newPersistentHistory = ExamplePersistentHistory(value: "NewValue")
@ -184,7 +184,6 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<ExamplePersistentHistory?>("observeNSSecureCodingOptionalKeyCombine")
let newPersistentHistory = ExamplePersistentHistory(value: "NewValue")
@ -214,7 +213,6 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[ExamplePersistentHistory]>("observeNSSecureCodingArrayKeyCombine", default: [persistentHistoryValue])
let newPersistentHistory = ExamplePersistentHistory(value: "NewValue")
@ -243,7 +241,6 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: ExamplePersistentHistory]>("observeNSSecureCodingDictionaryKeyCombine", default: ["0": persistentHistoryValue])
let newPersistentHistory = ExamplePersistentHistory(value: "NewValue")
@ -272,7 +269,6 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveMultipleNSSecureKeysCombine() {
let key1 = Defaults.Key<ExamplePersistentHistory>("observeMultipleNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue"))
let key2 = Defaults.Key<ExamplePersistentHistory>("observeMultipleNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue"))
@ -291,7 +287,6 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveMultipleNSSecureOptionalKeysCombine() {
let key1 = Defaults.Key<ExamplePersistentHistory?>("observeMultipleNSSecureCodingOptionalKey1")
let key2 = Defaults.Key<ExamplePersistentHistory?>("observeMultipleNSSecureCodingOptionalKeyKey2")
@ -333,63 +328,71 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testRemoveDuplicatesObserveNSSecureCodingKeyCombine() {
let key = Defaults.Key<ExamplePersistentHistory>("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue"))
let expect = expectation(description: "Observation closure being called")
func testRemoveDuplicatesObserveNSSecureCodingKeyCombine() {
let key = Defaults.Key<ExamplePersistentHistory>("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue"))
let expect = expectation(description: "Observation closure being called")
let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"]
let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3"]
let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"]
let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3"]
let cancellable = Defaults
.publisher(key, options: [])
.removeDuplicates()
.map(\.newValue.value)
.collect(expectedArray.count)
.sink { result in
print("Result array: \(result)")
result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched")
let cancellable = Defaults
.publisher(key, options: [])
.removeDuplicates()
.map(\.newValue.value)
.collect(expectedArray.count)
.sink { result in
print("Result array: \(result)")
if result == expectedArray {
expect.fulfill()
} else {
XCTFail("Expected Array is not matched")
}
inputArray.forEach {
Defaults[key] = ExamplePersistentHistory(value: $0)
}
Defaults.reset(key)
cancellable.cancel()
waitForExpectations(timeout: 10)
inputArray.forEach {
Defaults[key] = ExamplePersistentHistory(value: $0)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testRemoveDuplicatesObserveNSSecureCodingOptionalKeyCombine() {
let key = Defaults.Key<ExamplePersistentHistory?>("observeNSSecureCodingOptionalKey")
let expect = expectation(description: "Observation closure being called")
Defaults.reset(key)
cancellable.cancel()
let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"]
let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3", nil]
waitForExpectations(timeout: 10)
}
let cancellable = Defaults
.publisher(key, options: [])
.removeDuplicates()
.map(\.newValue)
.map { $0?.value }
.collect(expectedArray.count)
.sink { result in
print("Result array: \(result)")
result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched")
func testRemoveDuplicatesObserveNSSecureCodingOptionalKeyCombine() {
let key = Defaults.Key<ExamplePersistentHistory?>("observeNSSecureCodingOptionalKey")
let expect = expectation(description: "Observation closure being called")
let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"]
let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3", nil]
let cancellable = Defaults
.publisher(key, options: [])
.removeDuplicates()
.map(\.newValue)
.map { $0?.value }
.collect(expectedArray.count)
.sink { result in
print("Result array: \(result)")
if result == expectedArray {
expect.fulfill()
} else {
XCTFail("Expected Array is not matched")
}
inputArray.forEach {
Defaults[key] = ExamplePersistentHistory(value: $0)
}
Defaults.reset(key)
cancellable.cancel()
waitForExpectations(timeout: 10)
inputArray.forEach {
Defaults[key] = ExamplePersistentHistory(value: $0)
}
Defaults.reset(key)
cancellable.cancel()
waitForExpectations(timeout: 10)
}
func testObserveKey() {
let key = Defaults.Key<ExamplePersistentHistory>("observeNSSecureCodingKey", default: persistentHistoryValue)
let newPersistentHistory = ExamplePersistentHistory(value: "NewValue")

View File

@ -0,0 +1,221 @@
import Foundation
import Defaults
import XCTest
private struct CustomDate {
let year: Int
let month: Int
let day: Int
}
extension CustomDate: Defaults.Serializable {
public struct CustomDateBridge: Defaults.Bridge {
public typealias Value = CustomDate
public typealias Serializable = [Int]
public func serialize(_ value: Value?) -> Serializable? {
guard let value else {
return nil
}
return [value.year, value.month, value.day]
}
public func deserialize(_ object: Serializable?) -> Value? {
guard let object else {
return nil
}
return .init(year: object[0], month: object[1], day: object[2])
}
}
public static let bridge = CustomDateBridge()
}
extension CustomDate: Comparable {
static func < (lhs: CustomDate, rhs: CustomDate) -> Bool {
if lhs.year != rhs.year {
return lhs.year < rhs.year
} else if lhs.month != rhs.month {
return lhs.month < rhs.month
} else {
return lhs.day < rhs.day
}
}
static func == (lhs: CustomDate, rhs: CustomDate) -> Bool {
lhs.year == rhs.year && lhs.month == rhs.month
&& lhs.day == rhs.day
}
}
// Fixtures:
private let fixtureRange = 0..<10
private let nextFixtureRange = 1..<20
private let fixtureDateRange = CustomDate(year: 2022, month: 4, day: 0)..<CustomDate(year: 2022, month: 5, day: 0)
private let nextFixtureDateRange = CustomDate(year: 2022, month: 6, day: 1)..<CustomDate(year: 2022, month: 7, day: 1)
private let fixtureClosedRange = 0...10
private let nextFixtureClosedRange = 1...20
private let fixtureDateClosedRange = CustomDate(year: 2022, month: 4, day: 0)...CustomDate(year: 2022, month: 5, day: 0)
private let nextFixtureDateClosedRange = CustomDate(year: 2022, month: 6, day: 1)...CustomDate(year: 2022, month: 7, day: 1)
final class DefaultsClosedRangeTests: XCTestCase {
override func setUp() {
super.setUp()
Defaults.removeAll()
}
override func tearDown() {
super.tearDown()
Defaults.removeAll()
}
func testKey() {
// Test native support Range type
let key = Defaults.Key<Range>("independentRangeKey", default: fixtureRange)
XCTAssertEqual(fixtureRange.upperBound, Defaults[key].upperBound)
XCTAssertEqual(fixtureRange.lowerBound, Defaults[key].lowerBound)
Defaults[key] = nextFixtureRange
XCTAssertEqual(nextFixtureRange.upperBound, Defaults[key].upperBound)
XCTAssertEqual(nextFixtureRange.lowerBound, Defaults[key].lowerBound)
// Test serializable Range type
let dateKey = Defaults.Key<Range<CustomDate>>("independentRangeDateKey", default: fixtureDateRange)
XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey].upperBound)
XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey].lowerBound)
Defaults[dateKey] = nextFixtureDateRange
XCTAssertEqual(nextFixtureDateRange.upperBound, Defaults[dateKey].upperBound)
XCTAssertEqual(nextFixtureDateRange.lowerBound, Defaults[dateKey].lowerBound)
// Test native support ClosedRange type
let closedKey = Defaults.Key<ClosedRange>("independentClosedRangeKey", default: fixtureClosedRange)
XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey].upperBound)
XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey].lowerBound)
Defaults[closedKey] = nextFixtureClosedRange
XCTAssertEqual(nextFixtureClosedRange.upperBound, Defaults[closedKey].upperBound)
XCTAssertEqual(nextFixtureClosedRange.lowerBound, Defaults[closedKey].lowerBound)
// Test serializable ClosedRange type
let closedDateKey = Defaults.Key<ClosedRange<CustomDate>>("independentClosedRangeDateKey", default: fixtureDateClosedRange)
XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey].upperBound)
XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey].lowerBound)
Defaults[closedDateKey] = nextFixtureDateClosedRange
XCTAssertEqual(nextFixtureDateClosedRange.upperBound, Defaults[closedDateKey].upperBound)
XCTAssertEqual(nextFixtureDateClosedRange.lowerBound, Defaults[closedDateKey].lowerBound)
}
func testOptionalKey() {
// Test native support Range type
let key = Defaults.Key<Range<Int>?>("independentRangeOptionalKey")
XCTAssertNil(Defaults[key])
Defaults[key] = fixtureRange
XCTAssertEqual(fixtureRange.upperBound, Defaults[key]?.upperBound)
XCTAssertEqual(fixtureRange.lowerBound, Defaults[key]?.lowerBound)
// Test serializable Range type
let dateKey = Defaults.Key<Range<CustomDate>?>("independentRangeDateOptionalKey")
XCTAssertNil(Defaults[dateKey])
Defaults[dateKey] = fixtureDateRange
XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey]?.upperBound)
XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey]?.lowerBound)
// Test native support ClosedRange type
let closedKey = Defaults.Key<ClosedRange<Int>?>("independentClosedRangeOptionalKey")
XCTAssertNil(Defaults[closedKey])
Defaults[closedKey] = fixtureClosedRange
XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey]?.upperBound)
XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey]?.lowerBound)
// Test serializable ClosedRange type
let closedDateKey = Defaults.Key<ClosedRange<CustomDate>?>("independentClosedRangeDateOptionalKey")
XCTAssertNil(Defaults[closedDateKey])
Defaults[closedDateKey] = fixtureDateClosedRange
XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey]?.upperBound)
XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey]?.lowerBound)
}
func testArrayKey() {
// Test native support Range type
let key = Defaults.Key<[Range]>("independentRangeArrayKey", default: [fixtureRange])
XCTAssertEqual(fixtureRange.upperBound, Defaults[key][0].upperBound)
XCTAssertEqual(fixtureRange.lowerBound, Defaults[key][0].lowerBound)
Defaults[key].append(nextFixtureRange)
XCTAssertEqual(fixtureRange.upperBound, Defaults[key][0].upperBound)
XCTAssertEqual(fixtureRange.lowerBound, Defaults[key][0].lowerBound)
XCTAssertEqual(nextFixtureRange.upperBound, Defaults[key][1].upperBound)
XCTAssertEqual(nextFixtureRange.lowerBound, Defaults[key][1].lowerBound)
// Test serializable Range type
let dateKey = Defaults.Key<[Range<CustomDate>]>("independentRangeDateArrayKey", default: [fixtureDateRange])
XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey][0].upperBound)
XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey][0].lowerBound)
Defaults[dateKey].append(nextFixtureDateRange)
XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey][0].upperBound)
XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey][0].lowerBound)
XCTAssertEqual(nextFixtureDateRange.upperBound, Defaults[dateKey][1].upperBound)
XCTAssertEqual(nextFixtureDateRange.lowerBound, Defaults[dateKey][1].lowerBound)
// Test native support ClosedRange type
let closedKey = Defaults.Key<[ClosedRange]>("independentClosedRangeArrayKey", default: [fixtureClosedRange])
XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey][0].upperBound)
XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey][0].lowerBound)
Defaults[closedKey].append(nextFixtureClosedRange)
XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey][0].upperBound)
XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey][0].lowerBound)
XCTAssertEqual(nextFixtureClosedRange.upperBound, Defaults[closedKey][1].upperBound)
XCTAssertEqual(nextFixtureClosedRange.lowerBound, Defaults[closedKey][1].lowerBound)
// Test serializable ClosedRange type
let closedDateKey = Defaults.Key<[ClosedRange<CustomDate>]>("independentClosedRangeDateArrayKey", default: [fixtureDateClosedRange])
XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey][0].upperBound)
XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey][0].lowerBound)
Defaults[closedDateKey].append(nextFixtureDateClosedRange)
XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey][0].upperBound)
XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey][0].lowerBound)
XCTAssertEqual(nextFixtureDateClosedRange.upperBound, Defaults[closedDateKey][1].upperBound)
XCTAssertEqual(nextFixtureDateClosedRange.lowerBound, Defaults[closedDateKey][1].lowerBound)
}
func testDictionaryKey() {
// Test native support Range type
let key = Defaults.Key<[String: Range]>("independentRangeDictionaryKey", default: ["0": fixtureRange])
XCTAssertEqual(fixtureRange.upperBound, Defaults[key]["0"]?.upperBound)
XCTAssertEqual(fixtureRange.lowerBound, Defaults[key]["0"]?.lowerBound)
Defaults[key]["1"] = nextFixtureRange
XCTAssertEqual(fixtureRange.upperBound, Defaults[key]["0"]?.upperBound)
XCTAssertEqual(fixtureRange.lowerBound, Defaults[key]["0"]?.lowerBound)
XCTAssertEqual(nextFixtureRange.upperBound, Defaults[key]["1"]?.upperBound)
XCTAssertEqual(nextFixtureRange.lowerBound, Defaults[key]["1"]?.lowerBound)
// Test serializable Range type
let dateKey = Defaults.Key<[String: Range<CustomDate>]>("independentRangeDateDictionaryKey", default: ["0": fixtureDateRange])
XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey]["0"]?.upperBound)
XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey]["0"]?.lowerBound)
Defaults[dateKey]["1"] = nextFixtureDateRange
XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey]["0"]?.upperBound)
XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey]["0"]?.lowerBound)
XCTAssertEqual(nextFixtureDateRange.upperBound, Defaults[dateKey]["1"]?.upperBound)
XCTAssertEqual(nextFixtureDateRange.lowerBound, Defaults[dateKey]["1"]?.lowerBound)
// Test native support ClosedRange type
let closedKey = Defaults.Key<[String: ClosedRange]>("independentClosedRangeDictionaryKey", default: ["0": fixtureClosedRange])
XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey]["0"]?.upperBound)
XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey]["0"]?.lowerBound)
Defaults[closedKey]["1"] = nextFixtureClosedRange
XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey]["0"]?.upperBound)
XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey]["0"]?.lowerBound)
XCTAssertEqual(nextFixtureClosedRange.upperBound, Defaults[closedKey]["1"]?.upperBound)
XCTAssertEqual(nextFixtureClosedRange.lowerBound, Defaults[closedKey]["1"]?.lowerBound)
// Test serializable ClosedRange type
let closedDateKey = Defaults.Key<[String: ClosedRange<CustomDate>]>("independentClosedRangeDateDictionaryKey", default: ["0": fixtureDateClosedRange])
XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey]["0"]?.upperBound)
XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey]["0"]?.lowerBound)
Defaults[closedDateKey]["1"] = nextFixtureDateClosedRange
XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey]["0"]?.upperBound)
XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey]["0"]?.lowerBound)
XCTAssertEqual(nextFixtureDateClosedRange.upperBound, Defaults[closedDateKey]["1"]?.upperBound)
XCTAssertEqual(nextFixtureDateClosedRange.lowerBound, Defaults[closedDateKey]["1"]?.lowerBound)
}
}

View File

@ -15,7 +15,7 @@ private struct ItemBridge: Defaults.Bridge {
typealias Value = Item
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -24,7 +24,7 @@ private struct ItemBridge: Defaults.Bridge {
func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let object,
let name = object["name"],
let count = UInt(object["count"] ?? "0")
else {
@ -174,7 +174,6 @@ final class DefaultsSetAlgebraCustomElementTests: XCTestCase {
XCTAssertEqual(Defaults[.setAlgebraCustomElementDictionary]["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3]))
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<DefaultsSetAlgebra<Item>>("observeSetAlgebraKeyCombine", default: .init([fixtureSetAlgebra]))
let expect = expectation(description: "Observation closure being called")
@ -202,7 +201,6 @@ final class DefaultsSetAlgebraCustomElementTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<DefaultsSetAlgebra<Item>?>("observeSetAlgebraOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -231,7 +229,6 @@ final class DefaultsSetAlgebraCustomElementTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[DefaultsSetAlgebra<Item>]>("observeSetAlgebraArrayKeyCombine", default: [.init([fixtureSetAlgebra])])
let expect = expectation(description: "Observation closure being called")
@ -259,7 +256,6 @@ final class DefaultsSetAlgebraCustomElementTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: DefaultsSetAlgebra<Item>]>("observeSetAlgebraDictionaryKeyCombine", default: ["0": .init([fixtureSetAlgebra])])
let expect = expectation(description: "Observation closure being called")

View File

@ -7,7 +7,7 @@ struct DefaultsSetAlgebra<Element: Defaults.Serializable & Hashable>: SetAlgebra
init() {}
init<S: Sequence>(_ sequence: __owned S) where Element == S.Element {
init(_ sequence: __owned some Sequence<Element>) {
self.store = Set(sequence)
}
@ -19,18 +19,18 @@ struct DefaultsSetAlgebra<Element: Defaults.Serializable & Hashable>: SetAlgebra
store.contains(member)
}
func union(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
DefaultsSetAlgebra(store.union(other.store))
func union(_ other: Self) -> Self {
Self(store.union(other.store))
}
func intersection(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
var defaultsSetAlgebra = DefaultsSetAlgebra()
func intersection(_ other: Self) -> Self {
var defaultsSetAlgebra = Self()
defaultsSetAlgebra.store = store.intersection(other.store)
return defaultsSetAlgebra
}
func symmetricDifference(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
var defaultedSetAlgebra = DefaultsSetAlgebra()
func symmetricDifference(_ other: Self) -> Self {
var defaultedSetAlgebra = Self()
defaultedSetAlgebra.store = store.symmetricDifference(other.store)
return defaultedSetAlgebra
}
@ -206,7 +206,6 @@ final class DefaultsSetAlgebraTests: XCTestCase {
XCTAssertEqual(Defaults[.setAlgebraDictionary]["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3]))
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<DefaultsSetAlgebra<Int>>("observeSetAlgebraKeyCombine", default: .init([fixtureSetAlgebra]))
let expect = expectation(description: "Observation closure being called")
@ -234,7 +233,6 @@ final class DefaultsSetAlgebraTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<DefaultsSetAlgebra<Int>?>("observeSetAlgebraOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -263,7 +261,6 @@ final class DefaultsSetAlgebraTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[DefaultsSetAlgebra<Int>]>("observeSetAlgebraArrayKeyCombine", default: [.init([fixtureSetAlgebra])])
let expect = expectation(description: "Observation closure being called")
@ -291,7 +288,6 @@ final class DefaultsSetAlgebraTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: DefaultsSetAlgebra<Int>]>("observeSetAlgebraDictionaryKeyCombine", default: ["0": .init([fixtureSetAlgebra])])
let expect = expectation(description: "Observation closure being called")

View File

@ -3,25 +3,35 @@ import Foundation
import SwiftUI
import Defaults
#if os(macOS)
typealias NativeColor = NSColor
#else
typealias NativeColor = UIColor
#endif
@available(macOS 11.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
extension Defaults.Keys {
fileprivate static let hasUnicorn = Key<Bool>("swiftui_hasUnicorn", default: false)
fileprivate static let user = Key<User>("swiftui_user", default: User(username: "Hank", password: "123456"))
fileprivate static let setInt = Key<Set<Int>>("swiftui_setInt", default: Set(1...3))
fileprivate static let color = Key<Color>("swiftui_color", default: .black)
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@available(macOS 11.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
struct ContentView: View {
@Default(.hasUnicorn) var hasUnicorn
@Default(.user) var user
@Default(.setInt) var setInt
@Default(.color) var color
var body: some View {
Text("User \(user.username) has Unicorn: \(String(hasUnicorn))")
.foregroundColor(color)
Toggle("Toggle Unicorn", isOn: $hasUnicorn)
}
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@available(macOS 11.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
final class DefaultsSwiftUITests: XCTestCase {
override func setUp() {
super.setUp()
@ -38,13 +48,17 @@ final class DefaultsSwiftUITests: XCTestCase {
XCTAssertFalse(view.hasUnicorn)
XCTAssertEqual(view.user.username, "Hank")
XCTAssertEqual(view.setInt.count, 3)
XCTAssertEqual(NativeColor(view.color), NativeColor(Color.black))
view.user = User(username: "Chen", password: "123456")
view.hasUnicorn.toggle()
view.setInt.insert(4)
view.color = Color(.sRGB, red: 100, green: 100, blue: 100, opacity: 1)
XCTAssertTrue(view.hasUnicorn)
XCTAssertEqual(view.user.username, "Chen")
XCTAssertEqual(view.setInt, Set(1...4))
XCTAssertFalse(Default(.hasUnicorn).defaultValue)
XCTAssertFalse(Default(.hasUnicorn).isDefaultValue)
XCTAssertNotEqual(NativeColor(view.color), NativeColor(Color.black))
XCTAssertEqual(NativeColor(view.color), NativeColor(Color(.sRGB, red: 100, green: 100, blue: 100, opacity: 1)))
}
}

View File

@ -1,7 +1,7 @@
import Foundation
import Combine
import XCTest
import Defaults
@testable import Defaults
let fixtureURL = URL(string: "https://sindresorhus.com")!
let fixtureFileURL = URL(string: "file://~/icon.png")!
@ -14,6 +14,9 @@ extension Defaults.Keys {
static let file = Key<URL>("fileURL", default: fixtureFileURL)
static let data = Key<Data>("data", default: Data([]))
static let date = Key<Date>("date", default: fixtureDate)
static let uuid = Key<UUID?>("uuid")
static let defaultDynamicDate = Key<Date>("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 0) }
static let defaultDynamicOptionalDate = Key<Date?>("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 1) }
}
final class DefaultsTests: XCTestCase {
@ -34,6 +37,15 @@ final class DefaultsTests: XCTestCase {
XCTAssertTrue(Defaults[key])
}
func testValidKeyName() {
let validKey = Defaults.Key<Bool>("test", default: false)
let containsDotKey = Defaults.Key<Bool>("test.a", default: false)
let startsWithAtKey = Defaults.Key<Bool>("@test", default: false)
XCTAssertTrue(Defaults.isValidKeyPath(name: validKey.name))
XCTAssertFalse(Defaults.isValidKeyPath(name: containsDotKey.name))
XCTAssertFalse(Defaults.isValidKeyPath(name: startsWithAtKey.name))
}
func testOptionalKey() {
let key = Defaults.Key<Bool?>("independentOptionalKey")
let url = Defaults.Key<URL?>("independentOptionalURLKey")
@ -53,6 +65,17 @@ final class DefaultsTests: XCTestCase {
XCTAssertEqual(Defaults[url], fixtureURL2)
}
func testInitializeDynamicDateKey() {
_ = Defaults.Key<Date>("independentInitializeDynamicDateKey") {
XCTFail("Init dynamic key should not trigger getter")
return Date()
}
_ = Defaults.Key<Date?>("independentInitializeDynamicOptionalDateKey") {
XCTFail("Init dynamic optional key should not trigger getter")
return Date()
}
}
func testKeyRegistersDefault() {
let keyName = "registersDefault"
XCTAssertFalse(UserDefaults.standard.bool(forKey: keyName))
@ -99,10 +122,37 @@ final class DefaultsTests: XCTestCase {
XCTAssertEqual(Defaults[.date], newDate)
}
func testDynamicDateType() {
XCTAssertEqual(Defaults[.defaultDynamicDate], Date(timeIntervalSince1970: 0))
let next = Date(timeIntervalSince1970: 1)
Defaults[.defaultDynamicDate] = next
XCTAssertEqual(Defaults[.defaultDynamicDate], next)
XCTAssertEqual(UserDefaults.standard.object(forKey: Defaults.Key<Date>.defaultDynamicDate.name) as! Date, next)
Defaults.Key<Date>.defaultDynamicDate.reset()
XCTAssertEqual(Defaults[.defaultDynamicDate], Date(timeIntervalSince1970: 0))
}
func testDynamicOptionalDateType() {
XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1))
let next = Date(timeIntervalSince1970: 2)
Defaults[.defaultDynamicOptionalDate] = next
XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], next)
XCTAssertEqual(UserDefaults.standard.object(forKey: Defaults.Key<Date>.defaultDynamicOptionalDate.name) as! Date, next)
Defaults[.defaultDynamicOptionalDate] = nil
XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1))
XCTAssertNil(UserDefaults.standard.object(forKey: Defaults.Key<Date>.defaultDynamicOptionalDate.name))
}
func testFileURLType() {
XCTAssertEqual(Defaults[.file], fixtureFileURL)
}
func testUUIDType() {
let fixture = UUID()
Defaults[.uuid] = fixture
XCTAssertEqual(Defaults[.uuid], fixture)
}
func testRemoveAll() {
let key = Defaults.Key<Bool>("removeAll", default: false)
let key2 = Defaults.Key<Bool>("removeAll2", default: false)
@ -126,7 +176,6 @@ final class DefaultsTests: XCTestCase {
Defaults.removeAll(suite: customSuite)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<Bool>("observeKey", default: false)
let expect = expectation(description: "Observation closure being called")
@ -152,7 +201,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<Bool?>("observeOptionalKey")
let expect = expectation(description: "Observation closure being called")
@ -181,7 +229,37 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testDynamicOptionalDateTypeCombine() {
let first = Date(timeIntervalSince1970: 0)
let second = Date(timeIntervalSince1970: 1)
let third = Date(timeIntervalSince1970: 2)
let key = Defaults.Key<Date?>("combineDynamicOptionalDateKey") { first }
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [])
.map { ($0.oldValue, $0.newValue) }
.collect(3)
let expectedValues: [(Date?, Date?)] = [(first, second), (second, third), (third, first)]
let cancellable = publisher.sink { actualValues in
for (expected, actual) in zip(expectedValues, actualValues) {
XCTAssertEqual(expected.0, actual.0)
XCTAssertEqual(expected.1, actual.1)
}
expect.fulfill()
}
Defaults[key] = second
Defaults[key] = third
Defaults.reset(key)
cancellable.cancel()
waitForExpectations(timeout: 10)
}
func testObserveMultipleKeysCombine() {
let key1 = Defaults.Key<String>("observeKey1", default: "x")
let key2 = Defaults.Key<Bool>("observeKey2", default: true)
@ -200,7 +278,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveMultipleOptionalKeysCombine() {
let key1 = Defaults.Key<String?>("observeOptionalKey1")
let key2 = Defaults.Key<Bool?>("observeOptionalKey2")
@ -219,7 +296,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testReceiveValueBeforeSubscriptionCombine() {
let key = Defaults.Key<String>("receiveValueBeforeSubscription", default: "hello")
let expect = expectation(description: "Observation closure being called")
@ -314,6 +390,26 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
func testObserveDynamicOptionalDateKey() {
let first = Date(timeIntervalSince1970: 0)
let second = Date(timeIntervalSince1970: 1)
let key = Defaults.Key<Date?>("observeDynamicOptionalDate") { first }
let expect = expectation(description: "Observation closure being called")
var observation: Defaults.Observation!
observation = Defaults.observe(key, options: []) { change in
XCTAssertEqual(change.oldValue, first)
XCTAssertEqual(change.newValue, second)
observation.invalidate()
expect.fulfill()
}
Defaults[key] = second
waitForExpectations(timeout: 10)
}
func testObservePreventPropagation() {
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
let expect = expectation(description: "No infinite recursion")
@ -409,7 +505,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObservePreventPropagationCombine() {
let key1 = Defaults.Key<Bool?>("preventPropagation6", default: nil)
let expect = expectation(description: "No infinite recursion")
@ -430,7 +525,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObservePreventPropagationMultipleKeysCombine() {
let key1 = Defaults.Key<Bool?>("preventPropagation7", default: nil)
let key2 = Defaults.Key<Bool?>("preventPropagation8", default: nil)
@ -452,7 +546,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObservePreventPropagationModifiersCombine() {
let key1 = Defaults.Key<Bool?>("preventPropagation9", default: nil)
let expect = expectation(description: "No infinite recursion")
@ -477,7 +570,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testRemoveDuplicatesObserveKeyCombine() {
let key = Defaults.Key<Bool>("observeKey", default: false)
let expect = expectation(description: "Observation closure being called")
@ -492,7 +584,12 @@ final class DefaultsTests: XCTestCase {
.collect(expectedArray.count)
.sink { result in
print("Result array: \(result)")
result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched")
if result == expectedArray {
expect.fulfill()
} else {
XCTFail("Expected Array is not matched")
}
}
inputArray.forEach {
@ -505,7 +602,6 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testRemoveDuplicatesOptionalObserveKeyCombine() {
let key = Defaults.Key<Bool?>("observeOptionalKey", default: nil)
let expect = expectation(description: "Observation closure being called")
@ -520,7 +616,12 @@ final class DefaultsTests: XCTestCase {
.collect(expectedArray.count)
.sink { result in
print("Result array: \(result)")
result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched")
if result == expectedArray {
expect.fulfill()
} else {
XCTFail("Expected Array is not matched")
}
}
inputArray.forEach {
@ -617,7 +718,6 @@ final class DefaultsTests: XCTestCase {
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testImmediatelyFinishingPublisherCombine() {
let key = Defaults.Key<Bool>("observeKey", default: false)
let expect = expectation(description: "Observation closure being called without crashing")
@ -632,4 +732,72 @@ final class DefaultsTests: XCTestCase {
cancellable.cancel()
waitForExpectations(timeout: 10)
}
func testKeyEquatable() {
XCTAssertEqual(Defaults.Key<Bool>("equatableKeyTest", default: false), Defaults.Key<Bool>("equatableKeyTest", default: false))
}
func testKeyHashable() {
_ = Set([Defaults.Key<Bool>("hashableKeyTest", default: false)])
}
func testUpdates() async {
let key = Defaults.Key<Bool>("updatesKey", default: false)
async let waiter = Defaults.updates(key, initial: false).first { $0 }
try? await Task.sleep(seconds: 0.1)
Defaults[key] = true
guard let result = await waiter else {
XCTFail()
return
}
XCTAssertTrue(result)
}
func testUpdatesMultipleKeys() async {
let key1 = Defaults.Key<Bool>("updatesMultipleKey1", default: false)
let key2 = Defaults.Key<Bool>("updatesMultipleKey2", default: false)
let counter = Counter()
async let waiter: Void = {
for await _ in Defaults.updates([key1, key2], initial: false) {
await counter.increment()
if await counter.count == 2 {
break
}
}
}()
try? await Task.sleep(seconds: 0.1)
Defaults[key1] = true
Defaults[key2] = true
await waiter
let count = await counter.count
XCTAssertEqual(count, 2)
}
}
actor Counter {
private var _count = 0
var count: Int { _count }
func increment() {
_count += 1
}
}
// TODO: Remove when testing on macOS 13.
extension Task<Never, Never> {
static func sleep(seconds: TimeInterval) async throws {
try await sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC)))
}
}

View File

@ -32,6 +32,15 @@ final class DefaultsNSColorTests: XCTestCase {
XCTAssertTrue(Defaults[key].isEqual(fixtureColor1))
}
func testPreservesColorSpace() {
let fixture = UIColor(displayP3Red: 1, green: 0.3, blue: 0.7, alpha: 1)
let key = Defaults.Key<UIColor?>("independentNSColorPreservesColorSpaceKey")
Defaults[key] = fixture
XCTAssertEqual(Defaults[key], fixture)
XCTAssertEqual(Defaults[key]?.cgColor.colorSpace, fixture.cgColor.colorSpace)
XCTAssertEqual(Defaults[key]?.cgColor, fixture.cgColor)
}
func testOptionalKey() {
let key = Defaults.Key<UIColor?>("independentNSColorOptionalKey")
XCTAssertNil(Defaults[key])
@ -116,7 +125,6 @@ final class DefaultsNSColorTests: XCTestCase {
XCTAssertTrue(Defaults[.colorDictionary]["0"]?.isEqual(fixtureColor1) ?? false)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<UIColor>("observeNSColorKeyCombine", default: fixtureColor)
let expect = expectation(description: "Observation closure being called")
@ -142,7 +150,6 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<UIColor?>("observeNSColorOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
@ -179,7 +186,6 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveArrayKeyCombine() {
let key = Defaults.Key<[UIColor]>("observeNSColorArrayKeyCombine", default: [fixtureColor])
let expect = expectation(description: "Observation closure being called")
@ -205,7 +211,6 @@ final class DefaultsNSColorTests: XCTestCase {
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveDictionaryKeyCombine() {
let key = Defaults.Key<[String: UIColor]>("observeNSColorDictionaryKeyCombine", default: ["0": fixtureColor])
let expect = expectation(description: "Observation closure being called")

View File

@ -331,7 +331,7 @@ private struct TimeZoneBridge: Defaults.Bridge {
typealias Serializable = [String: String]
func serialize(_ value: TimeZone?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -343,9 +343,9 @@ private struct TimeZoneBridge: Defaults.Bridge {
func deserialize(_ object: Serializable?) -> TimeZone? {
guard
let dictionary = object,
let id = dictionary["id"],
let name = dictionary["name"]
let object,
let id = object["id"],
let name = object["name"]
else {
return nil
}
@ -380,7 +380,9 @@ private struct CodableTimeZone {
}
extension CodableTimeZone: Defaults.CodableType {
/// Convert from `Codable` to native type.
/**
Convert from `Codable` to native type.
*/
func toNative() -> TimeZone {
TimeZone(id: id, name: name)
}

254
readme.md
View File

@ -4,67 +4,43 @@
Store key-value pairs persistently across launches of your app.
It uses `NSUserDefaults` underneath but exposes a type-safe facade with lots of nice conveniences.
It uses `UserDefaults` underneath but exposes a type-safe facade with lots of nice conveniences.
It's used in production by apps like [Gifski](https://github.com/sindresorhus/Gifski), [Dato](https://sindresorhus.com/dato), [Lungo](https://sindresorhus.com/lungo), [Battery Indicator](https://sindresorhus.com/battery-indicator), and [HEIC Converter](https://sindresorhus.com/heic-converter).
For a real-world example, see the [Plash app](https://github.com/sindresorhus/Plash/blob/533dbc888d8ba3bd9581e60320af282a22c53f85/Plash/Constants.swift#L9-L18).
It's used in production by [all my apps](https://sindresorhus.com/apps) (1 million+ users).
## Highlights
- **Strongly typed:** You declare the type and default value upfront.
- **SwiftUI:** Property wrapper that updates the view when the `UserDefaults` value changes.
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
- **NSSecureCoding support:** You can store any [NSSecureCoding](https://developer.apple.com/documentation/foundation/nssecurecoding) value.
- **SwiftUI:** Property wrapper that updates the view when the `UserDefaults` value changes.
- **Publishers:** Combine publishers built-in.
- **Observation:** Observe changes to keys.
- **Debuggable:** The data is stored as JSON-serialized values.
- **Customizable:** You can serialize and deserialize your own type in your own way.
## Benefits over `@AppStorage`
- You define strongly-typed identifiers in a single place and can use them everywhere.
- You also define the default values in a single place instead of having to remember what default value you used in other places.
- You can use it outside of SwiftUI.
- You can observe value updates.
- Supports many more types, even `Codable`.
- Easy to add support for your own custom types.
- Comes with a convenience SwiftUI `Toggle` component.
## Compatibility
- macOS 10.13+
- iOS 12+
- tvOS 12+
- watchOS 5+
<br>
---
<div align="center">
<p>
<p>
<sup>
<a href="https://github.com/sponsors/sindresorhus">Sindres open source work is supported by the community</a>
</sup>
</p>
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://keygen.sh">
<div>
<img src="https://sindresorhus.com/assets/thanks/keygen-logo.svg" width="210" alt="Keygen">
</div>
<b>A dead-simple software licensing and distribution API built for developers</b>
</a>
<br>
<br>
</p>
</div>
---
<br>
## Migration Guides
#### [From v4 to v5](./migration.md)
- macOS 10.15+
- iOS 13+
- tvOS 13+
- watchOS 6+
## Install
Add `https://github.com/sindresorhus/Defaults` in the [“Swift Package Manager” tab in Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
**Requires Xcode 14.1 or later**
## Support types
- `Int(8/16/32/64)`
@ -77,12 +53,15 @@ Add `https://github.com/sindresorhus/Defaults` in the [“Swift Package Manager
- `Date`
- `Data`
- `URL`
- `UUID`
- `NSColor` (macOS)
- `UIColor` (iOS)
- `Color` [^1] (SwiftUI)
- `Codable`
- `NSSecureCoding`
- `Range`, `ClosedRange`
Defaults also support the above types wrapped in `Array`, `Set`, `Dictionary`, and even wrapped in nested types. For example, `[[String: Set<[String: Int]>]]`.
Defaults also support the above types wrapped in `Array`, `Set`, `Dictionary`, `Range`, `ClosedRange`, and even wrapped in nested types. For example, `[[String: Set<[String: Int]>]]`.
For more types, see the [enum example](#enum-example), [`Codable` example](#codable-example), or [advanced Usage](#advanced-usage). For more examples, see [Tests/DefaultsTests](./Tests/DefaultsTests).
@ -90,12 +69,17 @@ You can easily add support for any custom type.
If a type conforms to both `NSSecureCoding` and `Codable`, then `Codable` will be used for the serialization.
[^1]: [You cannot use `Color.accentColor`.](https://github.com/sindresorhus/Defaults/issues/139)
## Usage
You declare the defaults keys upfront with type and default value.
[API documentation.](https://swiftpackageindex.com/sindresorhus/Defaults/documentation/defaults)
You declare the defaults keys upfront with a type and default value.
**The key name must be ASCII, not start with `@`, and cannot contain a dot (`.`).**
```swift
import Cocoa
import Defaults
extension Defaults.Keys {
@ -135,6 +119,14 @@ if let name = Defaults[.name] {
The default value is then `nil`.
You can also specify a dynamic default value. This can be useful when the default value may change during the lifetime of the app:
```swift
extension Defaults.Keys {
static let camera = Key<AVCaptureDevice?>("camera") { .default(for: .video) }
}
```
---
### Enum example
@ -250,71 +242,17 @@ extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
let observer = Defaults.observe(.isUnicornMode) { change in
// Initial event
print(change.oldValue)
//=> false
print(change.newValue)
//=> false
// …
// First actual event
print(change.oldValue)
//=> false
print(change.newValue)
//=> true
Task {
for await value in Defaults.updates(.isUnicornMode) {
print("Value:", value)
}
}
Defaults[.isUnicornMode] = true
```
In contrast to the native `UserDefaults` key observation, here you receive a strongly-typed change object.
There is also an observation API using the [Combine](https://developer.apple.com/documentation/combine) framework, exposing a [Publisher](https://developer.apple.com/documentation/combine/publisher) for key changes:
```swift
let publisher = Defaults.publisher(.isUnicornMode)
let cancellable = publisher.sink { change in
// Initial event
print(change.oldValue)
//=> false
print(change.newValue)
//=> false
// First actual event
print(change.oldValue)
//=> false
print(change.newValue)
//=> true
}
Defaults[.isUnicornMode] = true
// To invalidate the observation.
cancellable.cancel()
```
### Invalidate observations automatically
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
final class Foo {
init() {
Defaults.observe(.isUnicornMode) { change in
print(change.oldValue)
print(change.newValue)
}.tieToLifetime(of: self)
}
}
Defaults[.isUnicornMode] = true
```
The observation will be valid until `self` is deinitialized.
### Reset keys to their default values
```swift
@ -395,6 +333,9 @@ print(UserDefaults.standard.bool(forKey: Defaults.Keys.isUnicornMode.name))
//=> true
```
> **Note**
> A `Defaults.Key` with a dynamic default value will not register the default value in `UserDefaults`.
## API
### `Defaults`
@ -408,7 +349,7 @@ Stores the keys.
#### `Defaults.Key` _(alias `Defaults.Keys.Key`)_
```swift
Defaults.Key<T>(_ key: String, default: T, suite: UserDefaults = .standard)
Defaults.Key<T>(_ name: String, default: T, suite: UserDefaults = .standard)
```
Type: `class`
@ -480,53 +421,6 @@ Reset the given keys back to their default values.
You can also specify string keys, which can be useful if you need to store some keys in a collection, as it's not possible to store `Defaults.Key` in a collection because it's generic.
#### `Defaults.observe`
```swift
Defaults.observe<T: Codable>(
_ key: Defaults.Key<T>,
options: ObservationOptions = [.initial],
handler: @escaping (KeyChange<T>) -> Void
) -> Defaults.Observation
```
Type: `func`
Observe changes to a key or an optional key.
By default, it will also trigger an initial event on creation. This can be useful for setting default values on controls. You can override this behavior with the `options` argument.
#### `Defaults.observe(keys: keys..., options:)`
Type: `func`
Observe multiple keys of any type, but without any information about the changes.
Options are the same as in `.observe(…)` for a single key.
#### `Defaults.publisher(_ key:, options:)`
```swift
Defaults.publisher<T: Codable>(
_ key: Defaults.Key<T>,
options: ObservationOptions = [.initial]
) -> AnyPublisher<KeyChange<T>, Never>
```
Type: `func`
Observation API using [Publisher](https://developer.apple.com/documentation/combine/publisher) from the [Combine](https://developer.apple.com/documentation/combine) framework.
Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.
#### `Defaults.publisher(keys: keys…, options:)`
Type: `func`
[Combine](https://developer.apple.com/documentation/combine) observation API for multiple key observation, but without specific information about changes.
Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.
#### `Defaults.removeAll`
```swift
@ -537,47 +431,6 @@ Type: `func`
Remove all entries from the given `UserDefaults` suite.
### `Defaults.Observation`
Type: `protocol`
Represents an observation of a defaults key.
#### `Defaults.Observation#invalidate`
```swift
Defaults.Observation#invalidate()
```
Type: `func`
Invalidate the observation.
#### `Defaults.Observation#tieToLifetime`
```swift
@discardableResult
Defaults.Observation#tieToLifetime(of weaklyHeldObject: AnyObject) -> Self
```
Type: `func`
Keep the observation alive for as long as, and no longer than, another object exists.
When `weaklyHeldObject` is deinitialized, the observation is invalidated automatically.
#### `Defaults.Observation.removeLifetimeTie`
```swift
Defaults.Observation#removeLifetimeTie()
```
Type: `func`
Break the lifetime tie created by `tieToLifetime(of:)`, if one exists.
The effects of any call to `tieToLifetime(of:)` are reversed. Note however that if the tied-to object has already died, then the observation is already invalid and this method has no logical effect.
#### `Defaults.withoutPropagation(_ closure:)`
Execute the closure without triggering change events.
@ -652,7 +505,7 @@ struct UserBridge: Defaults.Bridge {
typealias Serializable = [String: String]
public func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
guard let value else {
return nil
}
@ -664,7 +517,7 @@ struct UserBridge: Defaults.Bridge {
public func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let object,
let name = object["name"],
let age = object["age"]
else {
@ -952,15 +805,16 @@ It's inspired by that package and other solutions. The main difference is that t
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Kacper Rączy](https://github.com/fredyshox)
- [@hank121314](https://github.com/hank121314)
**Former**
- [Kacper Rączy](https://github.com/fredyshox)
## Related
- [Preferences](https://github.com/sindresorhus/Preferences) - Add a preferences window to your macOS app
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app
- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app
- [Regex](https://github.com/sindresorhus/Regex) - Swifty regular expressions
- [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon
- [Gifski](https://github.com/sindresorhus/Gifski) - Convert videos to high-quality GIFs on your Mac
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift)
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift+archived%3Afalse&type=repositories)