Compare commits
32 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
bd14dae265 | |
![]() |
488ff7dd71 | |
![]() |
0f5a23fcbd | |
![]() |
8584d1d6ed | |
![]() |
a09d7c8957 | |
![]() |
d71bfd8ffb | |
![]() |
5d1d7932a9 | |
![]() |
5cf1178d34 | |
![]() |
7a22d37874 | |
![]() |
ea11b7ac4f | |
![]() |
60013d90e2 | |
![]() |
35943225a0 | |
![]() |
fbc67fd179 | |
![]() |
1b1a057220 | |
![]() |
dd44190ddf | |
![]() |
be7e30ba36 | |
![]() |
b23fb7b057 | |
![]() |
176aa63666 | |
![]() |
bc1af5d872 | |
![]() |
981ccb0a01 | |
![]() |
9e65eac602 | |
![]() |
001adc694b | |
![]() |
d1e2109fc9 | |
![]() |
bab3087067 | |
![]() |
3535f3d088 | |
![]() |
119f654d44 | |
![]() |
c9198bb1d0 | |
![]() |
8c0d80e783 | |
![]() |
64010fdcc2 | |
![]() |
a6c1c064fa | |
![]() |
5b30f01e46 | |
![]() |
55f3302c3a |
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
version: 1
|
||||
builder:
|
||||
configs:
|
||||
- documentation_targets: ['Defaults']
|
|
@ -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.'
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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``
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)!
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
12
migration.md
12
migration.md
|
@ -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
254
readme.md
|
@ -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">Sindre‘s 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)
|
||||
|
|
Loading…
Reference in New Issue