├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .swift-version
├── .swiftformat
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── PrefsKit-CI.xcscheme
├── Images
└── prefskit-banner.png
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── PrefsKit
│ └── PrefsKit.swift
├── PrefsKitCore
│ ├── Macro Declarations.swift
│ └── PrefsKitCore.swift
├── PrefsKitMacrosImplementation
│ ├── PrefMacro
│ │ ├── AtomicPrefMacro.swift
│ │ ├── CodingPrefMacro.swift
│ │ ├── InlinePrefMacro.swift
│ │ ├── JSONDataCodablePrefMacro.swift
│ │ ├── JSONStringCodablePrefMacro.swift
│ │ ├── PrefMacro.swift
│ │ ├── PrefMacroError.swift
│ │ └── RawRepresentablePrefMacro.swift
│ ├── PrefsKitMacrosPlugin.swift
│ ├── PrefsSchemaMacro
│ │ ├── PrefsSchemaMacro.swift
│ │ └── PrefsSchemaMacroError.swift
│ ├── RawPrefMacro
│ │ └── RawPrefMacro.swift
│ └── Support
│ │ ├── Internal Types.swift
│ │ └── PrefMacroUtils.swift
├── PrefsKitTypes
│ ├── PrefsCodable Prototypes
│ │ ├── AtomicPrefsCoding.swift
│ │ ├── CodableArrayPrefsCoding.swift
│ │ ├── CodableDictionaryPrefsCoding.swift
│ │ ├── CodablePrefsCoding.swift
│ │ ├── JSONDataCodablePrefsCoding.swift
│ │ ├── JSONStringCodablePrefsCoding.swift
│ │ ├── PrefsCoding.swift
│ │ ├── PrefsCodingTuple.swift
│ │ ├── RawRepresentableArrayPrefsCoding.swift
│ │ ├── RawRepresentableDictionaryPrefsCoding.swift
│ │ └── RawRepresentablePrefsCoding.swift
│ ├── PrefsCodable Strategies
│ │ ├── Base64StringDataPrefsCoding.swift
│ │ ├── BoolIntegerPrefsCoding.swift
│ │ ├── BoolStringPrefsCoding.swift
│ │ ├── CompressedDataPrefsCoding.swift
│ │ ├── ISO8601StringDatePrefsCoding.swift
│ │ ├── IntegerPrefsCoding.swift
│ │ ├── IntegerStringPrefsCoding.swift
│ │ └── URLStringPrefsCoding.swift
│ ├── PrefsCodable
│ │ ├── Atomic
│ │ │ └── AtomicPrefsCodable.swift
│ │ ├── Codable
│ │ │ ├── CodablePrefsCodable.swift
│ │ │ └── JSON
│ │ │ │ ├── JSONDataCodablePrefsCodable.swift
│ │ │ │ └── JSONStringCodablePrefsCodable.swift
│ │ ├── PrefsCodable.swift
│ │ └── RawRepresentable
│ │ │ ├── PrefsCodable+RawRepresentable.swift
│ │ │ └── RawRepresentablePrefsCodable.swift
│ ├── PrefsKey Prototypes
│ │ └── PrefsKey Prototypes.swift
│ ├── PrefsKey
│ │ ├── DefaultedPrefsKey.swift
│ │ └── PrefsKey.swift
│ ├── PrefsSchema
│ │ ├── AnyPrefsSchema.swift
│ │ ├── PrefsSchema.swift
│ │ └── PropertyWrappers
│ │ │ ├── PrefsStorageModeWrapper.swift
│ │ │ └── PrefsStorageWrapper.swift
│ ├── PrefsStorage Prototypes
│ │ ├── AnyPrefsStorage+PrefsStorageExportable.swift
│ │ ├── AnyPrefsStorage+PrefsStorageImportable.swift
│ │ ├── AnyPrefsStorage.swift
│ │ ├── DictionaryPrefsStorage+PrefsStorageExportable.swift
│ │ ├── DictionaryPrefsStorage+PrefsStorageImportable.swift
│ │ ├── DictionaryPrefsStorage+PrefsStorageInitializable.swift
│ │ ├── DictionaryPrefsStorage.swift
│ │ ├── UserDefaultsPrefsStorage+PrefsStorageExportable.swift
│ │ ├── UserDefaultsPrefsStorage+PrefsStorageImportable.swift
│ │ ├── UserDefaultsPrefsStorage+Utilities.swift
│ │ └── UserDefaultsPrefsStorage.swift
│ ├── PrefsStorage Traits Prototypes
│ │ ├── JSON
│ │ │ ├── JSON Utilities.swift
│ │ │ ├── JSONPrefsStorageExportFormat.swift
│ │ │ └── JSONPrefsStorageImportFormat.swift
│ │ └── PList
│ │ │ ├── PList Utilities.swift
│ │ │ ├── PListPrefsStorageExportFormat.swift
│ │ │ ├── PListPrefsStorageExportStrategy.swift
│ │ │ ├── PListPrefsStorageImportFormat.swift
│ │ │ └── PListPrefsStorageImportStrategy.swift
│ ├── PrefsStorage Traits
│ │ ├── PrefsStorageExportable
│ │ │ ├── PrefsStorageExportFormat
│ │ │ │ └── PrefsStorageExportFormat.swift
│ │ │ ├── PrefsStorageExportStrategy
│ │ │ │ ├── PrefsStorageExportStrategy.swift
│ │ │ │ ├── PrefsStorageMappingExportStrategy.swift
│ │ │ │ └── PrefsStoragePassthroughExportStrategy.swift
│ │ │ └── PrefsStorageExportable.swift
│ │ ├── PrefsStorageImportable
│ │ │ ├── PrefsStorageImportFormat
│ │ │ │ └── PrefsStorageImportFormat.swift
│ │ │ ├── PrefsStorageImportStrategy
│ │ │ │ ├── PrefsStorageImportStrategy.swift
│ │ │ │ ├── PrefsStorageMappingImportStrategy.swift
│ │ │ │ ├── PrefsStoragePassthroughImportStrategy.swift
│ │ │ │ └── PrefsStorageTypedImportStrategy.swift
│ │ │ ├── PrefsStorageImportable.swift
│ │ │ └── PrefsStorageUpdateStrategy.swift
│ │ └── PrefsStorageInitializable
│ │ │ └── PrefsStorageInitializable.swift
│ ├── PrefsStorage
│ │ ├── PrefsStorage+DefaultedPrefsKey.swift
│ │ ├── PrefsStorage+PrefsCodable.swift
│ │ ├── PrefsStorage+PrefsKey.swift
│ │ ├── PrefsStorage+Static.swift
│ │ ├── PrefsStorage.swift
│ │ ├── PrefsStorageError.swift
│ │ └── PrefsStorageMode.swift
│ ├── PrefsStorageValue
│ │ ├── PrefsStorageValue Types.swift
│ │ └── PrefsStorageValue.swift
│ └── Utilities
│ │ ├── Concurrency.swift
│ │ ├── NSNumber.swift
│ │ ├── Outsourced
│ │ └── UserDefaults Outsourced.swift
│ │ └── UserDefaults.swift
└── PrefsKitUI
│ ├── MultiplatformSection.swift
│ ├── PrefsKitUI.swift
│ ├── SectionFooterView.swift
│ └── SystemSettings
│ ├── SystemSettings Panel.swift
│ └── SystemSettings.swift
└── Tests
├── PrefsKitCoreTests
├── ActorTests.swift
├── ChainingEncodingStrategiesTests.swift
├── CodableArrayTests.swift
├── CodableDictionaryTests.swift
├── CustomEncodingTests.swift
├── MacroTests.swift
├── PrefsCodable Strategies
│ ├── Base64StringDataPrefsCodingTests.swift
│ ├── BoolIntegerPrefCodingTests.swift
│ ├── BoolStringPrefsCodingTests.swift
│ ├── CompressedDataPrefsCodingTests.swift
│ ├── ISO8601StringDatePrefsCodingTests.swift
│ ├── IntegerPrefsCodingTests.swift
│ ├── IntegerStringPrefsCodingTests.swift
│ └── URLStringPrefsCodingTests.swift
├── PrefsSchemaPrefsStorageTests.swift
├── RawPrefsKeyTests.swift
├── RawRepresentableArrayTests.swift
├── RawRepresentableDictionaryTests.swift
└── UserDefaultsPrefsSchemaTests.swift
└── PrefsKitTypesTests
├── PrefsStorage Traits
├── PrefsStorageExportableTests.swift
├── PrefsStorageImportableLoadTests.swift
├── PrefsStorageImportableTests.swift
└── PrefsStorageInitializableTests.swift
├── PrefsStorage
├── PrefsStorage+Static Tests.swift
├── PrefsStorageArrayTests.swift
└── PrefsStorageDictionaryTests.swift
└── TestContent
├── TestContent Basic.swift
└── TestContent.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: orchetect
4 | # patreon: # Replace with a single Patreon username
5 | # open_collective: # Replace with a single Open Collective username
6 | # ko_fi: # Replace with a single Ko-fi username
7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | # liberapay: # Replace with a single Liberapay username
10 | # issuehunt: # Replace with a single IssueHunt username
11 | # otechie: # Replace with a single Otechie username
12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a bug report about a reproducible problem.
3 | labels: bug
4 | body:
5 | - type: textarea
6 | id: bug-description
7 | attributes:
8 | label: Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc.
9 | description: "A clear and concise description of the bug and steps to reproduce. Include system details (OS version) and build environment particulars (Xcode version, etc.)."
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Feature request
4 | url: https://github.com/orchetect/PrefsKit/discussions
5 | about: Suggest new features or improvements.
6 | - name: I need help setting up or troubleshooting
7 | url: https://github.com/orchetect/PrefsKit/discussions
8 | about: Questions not answered in the documentation, discussions forum, or example projects.
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Custom
2 | [Dd]ev/
3 |
4 | # Xcode
5 |
6 | # macOS
7 | .DS_Store
8 |
9 | ## Build generated
10 | build/
11 | DerivedData/
12 |
13 | ## Various settings
14 | *.pbxuser
15 | !default.pbxuser
16 | *.mode1v3
17 | !default.mode1v3
18 | *.mode2v3
19 | !default.mode2v3
20 | *.perspectivev3
21 | !default.perspectivev3
22 | xcuserdata/
23 |
24 | ## Other
25 | *.moved-aside
26 | *.xccheckout
27 | *.xcscmblueprint
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 | *.ipa
32 | *.dSYM.zip
33 | *.dSYM
34 |
35 | ## Playgrounds
36 | timeline.xctimeline
37 | playground.xcworkspace
38 |
39 | ## SPM support in Xcode
40 | # .swiftpm - for shared CI schemes we need these checked in:
41 | # -> .swiftpm/xcode/package.xcworkspace
42 | # -> .swiftpm/xcode/xcshareddata/xcschemes/*.*
43 |
44 | # Swift Package Manager
45 | #
46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
47 | Packages/
48 | Package.pins
49 | Package.resolved
50 | .build/
51 |
52 | # CocoaPods
53 | #
54 | # We recommend against adding the Pods directory to your .gitignore. However
55 | # you should judge for yourself, the pros and cons are mentioned at:
56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
57 |
58 | Pods/
59 |
60 | # Carthage
61 | #
62 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
63 | # Carthage/Checkouts
64 |
65 | Carthage/Build
66 |
67 | # fastlane
68 | #
69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
70 | # screenshots whenever they are needed.
71 | # For more information about the recommended setup visit:
72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
73 |
74 | fastlane/report.xml
75 | fastlane/Preview.html
76 | fastlane/screenshots/**/*.png
77 | fastlane/test_output
78 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 6.0
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --acronyms ID,URL,UUID
2 | --allman false
3 | --assetliterals visual-width
4 | --beforemarks
5 | --binarygrouping 8,8
6 | --categorymark "MARK: %c"
7 | --classthreshold 0
8 | --closingparen balanced
9 | --closurevoid remove
10 | --commas inline
11 | --conflictmarkers reject
12 | --decimalgrouping ignore
13 | --elseposition same-line
14 | --emptybraces spaced
15 | --enumthreshold 0
16 | --exponentcase lowercase
17 | --exponentgrouping disabled
18 | --extensionacl on-declarations
19 | --extensionlength 0
20 | --extensionmark "MARK: - %t + %c"
21 | --fractiongrouping disabled
22 | --fragment false
23 | --funcattributes prev-line
24 | --groupedextension "MARK: %c"
25 | --guardelse auto
26 | --header "\n {file}\n PrefsKit • https://github.com/orchetect/PrefsKit\n © {year} Steffan Andrews • Licensed under MIT License\n"
27 | --hexgrouping 8,8
28 | --hexliteralcase uppercase
29 | --ifdef no-indent
30 | --importgrouping alpha
31 | --indent 4
32 | --indentcase false
33 | --indentstrings true
34 | --lifecycle
35 | --lineaftermarks true
36 | --linebreaks lf
37 | --markcategories true
38 | --markextensions always
39 | --marktypes always
40 | --maxwidth 140
41 | --modifierorder
42 | --nevertrailing
43 | --nospaceoperators
44 | --nowrapoperators
45 | --octalgrouping 4,8
46 | --someany false
47 | --operatorfunc spaced
48 | --organizetypes actor,class,enum,struct
49 | --patternlet hoist
50 | --ranges spaced
51 | --redundanttype infer-locals-only
52 | --self remove
53 | --selfrequired
54 | --semicolons inline
55 | --shortoptionals always
56 | --smarttabs enabled
57 | --stripunusedargs always
58 | --structthreshold 0
59 | --tabwidth unspecified
60 | --trailingclosures
61 | --trimwhitespace nonblank-lines
62 | --typeattributes preserve
63 | --typemark "MARK: - %t"
64 | --varattributes preserve
65 | --voidtype void
66 | --wraparguments before-first
67 | --wrapcollections before-first
68 | --wrapconditions after-first
69 | --wrapparameters before-first
70 | --wrapreturntype preserve
71 | --wrapternary before-operators
72 | --wraptypealiases before-first
73 | --xcodeindentation enabled
74 | --yodaswap always
75 | --disable blankLinesAroundMark,consecutiveSpaces,preferKeyPath,redundantParens,sortDeclarations,sortedImports,unusedArguments
76 | --enable blankLinesBetweenImports,blockComments,isEmpty,wrapEnumCases
77 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/PrefsKit-CI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
44 |
50 |
51 |
52 |
53 |
54 |
60 |
61 |
63 |
69 |
70 |
71 |
73 |
79 |
80 |
81 |
82 |
83 |
94 |
95 |
99 |
100 |
101 |
102 |
108 |
109 |
115 |
116 |
117 |
118 |
120 |
121 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/Images/prefskit-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orchetect/PrefsKit/c496e0c82e186db2450eb33a9f65343023001224/Images/prefskit-banner.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Steffan Andrews - https://github.com/orchetect
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 |
3 | import CompilerPluginSupport
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "PrefsKit",
8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
9 | products: [
10 | .library(name: "PrefsKit", targets: ["PrefsKit"]),
11 | .library(name: "PrefsKitCore", targets: ["PrefsKitCore"]),
12 | .library(name: "PrefsKitUI", targets: ["PrefsKitUI"])
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest")
16 | ],
17 | targets: [
18 | .target(
19 | name: "PrefsKit",
20 | dependencies: ["PrefsKitCore", "PrefsKitUI"]
21 | ),
22 | .target(
23 | name: "PrefsKitCore",
24 | dependencies: ["PrefsKitTypes", "PrefsKitMacrosImplementation"]
25 | ),
26 | .target(
27 | name: "PrefsKitTypes",
28 | dependencies: []
29 | ),
30 | .macro(
31 | name: "PrefsKitMacrosImplementation",
32 | dependencies: [
33 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
34 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
35 | ]
36 | ),
37 | .target(
38 | name: "PrefsKitUI",
39 | dependencies: ["PrefsKitCore"]
40 | ),
41 | .testTarget(
42 | name: "PrefsKitCoreTests",
43 | dependencies: [
44 | "PrefsKitCore",
45 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
46 | ]
47 | ),
48 | .testTarget(
49 | name: "PrefsKitTypesTests",
50 | dependencies: ["PrefsKitTypes"]
51 | )
52 | ]
53 | )
54 |
--------------------------------------------------------------------------------
/Sources/PrefsKit/PrefsKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsKit.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | @_exported import PrefsKitCore
8 | @_exported import PrefsKitTypes
9 | @_exported import PrefsKitUI
10 |
--------------------------------------------------------------------------------
/Sources/PrefsKitCore/Macro Declarations.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macro Declarations.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitTypes
9 |
10 | // MARK: - PrefsSchema (Class)
11 |
12 | @attached(member, names: named(_$observationRegistrar))
13 | @attached(extension, names: named(access), named(withMutation), conformances: Observable & PrefsSchema)
14 | public macro PrefsSchema()
15 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "PrefsSchemaMacro")
16 |
17 | // MARK: - Pref
18 |
19 | @attached(accessor, names: named(get), named(set), named(_modify))
20 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_))
21 | public macro Pref(key: String? = nil)
22 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "AtomicPrefMacro")
23 |
24 | @attached(accessor, names: named(get), named(set), named(_modify))
25 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_))
26 | public macro Pref(key: String? = nil, coding: Coding)
27 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "CodingPrefMacro")
28 |
29 | @attached(accessor, names: named(get), named(set), named(_modify))
30 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_))
31 | public macro Pref(
32 | key: String? = nil,
33 | encode: (Value) -> StorageValue?,
34 | decode: (StorageValue) -> Value?
35 | )
36 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "InlinePrefMacro")
37 |
38 | // MARK: - RawRepresentablePref
39 |
40 | @attached(accessor, names: named(get), named(set), named(_modify))
41 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_))
42 | public macro RawRepresentablePref(key: String? = nil)
43 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "RawRepresentablePrefMacro")
44 |
45 | // MARK: - JSONDataCodablePref
46 |
47 | @attached(accessor, names: named(get), named(set), named(_modify))
48 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_))
49 | public macro JSONDataCodablePref(key: String? = nil)
50 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "JSONDataCodablePrefMacro")
51 |
52 | // MARK: - JSONStringCodablePref
53 |
54 | @attached(accessor, names: named(get), named(set), named(_modify))
55 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_))
56 | public macro JSONStringCodablePref(key: String? = nil)
57 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "JSONStringCodablePrefMacro")
58 |
59 | // MARK: - RawPref
60 |
61 | @attached(accessor, names: named(get), named(set), named(_modify))
62 | @attached(peer, names: /* arbitrary */ prefixed(__PrefValue_))
63 | public macro RawPref(key: String? = nil)
64 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "RawPrefMacro")
65 |
--------------------------------------------------------------------------------
/Sources/PrefsKitCore/PrefsKitCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsKitCore.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | @_exported import PrefsKitTypes
8 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/AtomicPrefMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AtomicPrefMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public struct AtomicPrefMacro: PrefMacro {
8 | public static let keyStructName: String = "AnyAtomicPrefsKey"
9 | public static let defaultedKeyStructName: String = "AnyDefaultedAtomicPrefsKey"
10 | public static let hasCustomCoding: Bool = false
11 | public static let hasInlineCoding: Bool = false
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/CodingPrefMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodingPrefMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public struct CodingPrefMacro: PrefMacro {
8 | public static let keyStructName: String = "AnyPrefsKey"
9 | public static let defaultedKeyStructName: String = "AnyDefaultedPrefsKey"
10 | public static let hasCustomCoding: Bool = true
11 | public static let hasInlineCoding: Bool = false
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/InlinePrefMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InlinePrefMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public struct InlinePrefMacro: PrefMacro {
8 | public static let keyStructName: String = "AnyPrefsKey"
9 | public static let defaultedKeyStructName: String = "AnyDefaultedPrefsKey"
10 | public static let hasCustomCoding: Bool = false
11 | public static let hasInlineCoding: Bool = true
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/JSONDataCodablePrefMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONDataCodablePrefMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public struct JSONDataCodablePrefMacro: PrefMacro {
8 | public static let keyStructName: String = "AnyJSONDataCodablePrefsKey"
9 | public static let defaultedKeyStructName: String = "AnyDefaultedJSONDataCodablePrefsKey"
10 | public static let hasCustomCoding: Bool = false
11 | public static let hasInlineCoding: Bool = false
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/JSONStringCodablePrefMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONStringCodablePrefMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public struct JSONStringCodablePrefMacro: PrefMacro {
8 | public static let keyStructName: String = "AnyJSONStringCodablePrefsKey"
9 | public static let defaultedKeyStructName: String = "AnyDefaultedJSONStringCodablePrefsKey"
10 | public static let hasCustomCoding: Bool = false
11 | public static let hasInlineCoding: Bool = false
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/PrefMacroError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefMacroError.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | public enum PrefMacroError: LocalizedError {
10 | case missingKeyArgument
11 | case missingCodingArgument
12 | case missingDecodeArgument
13 | case incorrectSyntax
14 | case invalidArgumentLabel
15 | case invalidKeyArgumentType
16 | case invalidVariableName
17 | case notVarDeclaration
18 | case missingDefaultValue
19 | case missingOrInvalidTypeAnnotation
20 | case modifiersNotAllowed
21 | case noDefaultValueAllowed
22 | case tooManyArguments
23 |
24 | public var errorDescription: String? {
25 | switch self {
26 | case .missingKeyArgument:
27 | "Missing value for key argument."
28 | case .missingCodingArgument:
29 | "Missing value for coding argument."
30 | case .missingDecodeArgument:
31 | "Missing value for decode argument."
32 | case .incorrectSyntax:
33 | "Incorrect syntax."
34 | case .invalidArgumentLabel:
35 | "Invalid argument label."
36 | case .invalidKeyArgumentType:
37 | "Invalid key argument type."
38 | case .invalidVariableName:
39 | "Invalid variable name."
40 | case .notVarDeclaration:
41 | "Must be a var declaration."
42 | case .missingDefaultValue:
43 | "Missing default value."
44 | case .missingOrInvalidTypeAnnotation:
45 | "Missing or invalid type annotation."
46 | case .modifiersNotAllowed:
47 | "Modifiers are not allowed."
48 | case .noDefaultValueAllowed:
49 | "No default value allowed."
50 | case .tooManyArguments:
51 | "Too many arguments."
52 | }
53 | }
54 | }
55 |
56 | extension PrefMacroError: CustomStringConvertible {
57 | public var description: String { errorDescription ?? localizedDescription }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefMacro/RawRepresentablePrefMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentablePrefMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public struct RawRepresentablePrefMacro: PrefMacro {
8 | public static let keyStructName: String = "AnyRawRepresentablePrefsKey"
9 | public static let defaultedKeyStructName: String = "AnyDefaultedRawRepresentablePrefsKey"
10 | public static let hasCustomCoding: Bool = false
11 | public static let hasInlineCoding: Bool = false
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefsKitMacrosPlugin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsKitMacrosPlugin.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import SwiftCompilerPlugin
8 | import SwiftSyntax
9 | import SwiftSyntaxBuilder
10 | import SwiftSyntaxMacros
11 |
12 | @main
13 | struct PrefsKitMacrosPlugin: CompilerPlugin {
14 | let providingMacros: [Macro.Type] = [
15 | PrefsSchemaMacro.self,
16 | AtomicPrefMacro.self,
17 | CodingPrefMacro.self,
18 | InlinePrefMacro.self,
19 | RawRepresentablePrefMacro.self,
20 | JSONDataCodablePrefMacro.self,
21 | JSONStringCodablePrefMacro.self,
22 | RawPrefMacro.self
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefsSchemaMacro/PrefsSchemaMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsSchemaMacro.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import SwiftCompilerPlugin
9 | import SwiftSyntax
10 | import SwiftSyntaxBuilder
11 | import SwiftSyntaxMacros
12 |
13 | public struct PrefsSchemaMacro { }
14 |
15 | extension PrefsSchemaMacro: MemberMacro {
16 | public static func expansion(
17 | of node: AttributeSyntax,
18 | providingMembersOf declaration: some DeclGroupSyntax,
19 | conformingTo protocols: [TypeSyntax],
20 | in context: some MacroExpansionContext
21 | ) throws -> [DeclSyntax] {
22 | [
23 | """
24 | @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
25 | """
26 | ]
27 | }
28 | }
29 |
30 | extension PrefsSchemaMacro: ExtensionMacro {
31 | public static func expansion(
32 | of node: SwiftSyntax.AttributeSyntax,
33 | attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
34 | providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
35 | conformingTo protocols: [SwiftSyntax.TypeSyntax],
36 | in context: some SwiftSyntaxMacros.MacroExpansionContext
37 | ) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
38 | let prefsSchemaExtension = try ExtensionDeclSyntax(
39 | "extension \(type.trimmed): PrefsSchema { }"
40 | )
41 | let observableExtension = try ExtensionDeclSyntax(
42 | """
43 | extension \(type.trimmed): Observable {
44 | internal nonisolated func access(
45 | keyPath: KeyPath<\(type.trimmed), Member>
46 | ) {
47 | _$observationRegistrar.access(self, keyPath: keyPath)
48 | }
49 |
50 | internal nonisolated func withMutation(
51 | keyPath: KeyPath<\(type.trimmed), Member>,
52 | _ mutation: () throws -> MutationResult
53 | ) rethrows -> MutationResult {
54 | try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
55 | }
56 | }
57 | """
58 | )
59 |
60 | return [prefsSchemaExtension, observableExtension]
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/PrefsSchemaMacro/PrefsSchemaMacroError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsSchemaMacroError.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension PrefsSchemaMacro {
10 | public enum PrefsSchemaMacroError: LocalizedError {
11 | case incorrectSyntax
12 |
13 | public var errorDescription: String? {
14 | switch self {
15 | case .incorrectSyntax:
16 | "Incorrect syntax."
17 | }
18 | }
19 | }
20 | }
21 |
22 | extension PrefsSchemaMacro.PrefsSchemaMacroError: CustomStringConvertible {
23 | public var description: String { errorDescription ?? localizedDescription }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PrefsKitMacrosImplementation/Support/Internal Types.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Internal Types.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import SwiftCompilerPlugin
9 | import SwiftSyntax
10 | import SwiftSyntaxBuilder
11 | import SwiftSyntaxMacros
12 |
13 | enum TypeBinding {
14 | case nonOptional(TypeSyntax)
15 | case optional(TypeSyntax)
16 |
17 | var isOptional: Bool {
18 | switch self {
19 | case .nonOptional: false
20 | case .optional: true
21 | }
22 | }
23 |
24 | var description: String {
25 | switch self {
26 | case let .nonOptional(typeSyntax):
27 | typeSyntax.trimmedDescription
28 | case let .optional(typeSyntax):
29 | typeSyntax.trimmedDescription
30 | }
31 | }
32 | }
33 |
34 | struct TypeBindingInfo {
35 | let isOptional: Bool
36 | let typeName: String
37 | let keyAndCodingStructName: String
38 | let keyAndCodingStructDeclaration: String
39 | let privateKeyVarDeclaration: String
40 | let privateValueVarDeclaration: String
41 |
42 | init(
43 | for macro: PrefMacro.Type,
44 | from declaration: some DeclSyntaxProtocol,
45 | keyName: String,
46 | privateKeyVarName: String,
47 | privateValueVarName: String
48 | ) throws {
49 | guard let varDec = declaration.as(VariableDeclSyntax.self)
50 | else {
51 | throw PrefMacroError.incorrectSyntax
52 | }
53 | try self.init(
54 | for: macro,
55 | from: varDec,
56 | keyName: keyName,
57 | privateKeyVarName: privateKeyVarName,
58 | privateValueVarName: privateValueVarName
59 | )
60 | }
61 |
62 | init(
63 | for macro: PrefMacro.Type,
64 | from varDec: VariableDeclSyntax,
65 | keyName: String,
66 | privateKeyVarName: String,
67 | privateValueVarName: String,
68 | customCodingDecl: String?
69 | ) throws {
70 | let typeBinding = try PrefMacroUtils.typeBinding(from: varDec)
71 | typeName = typeBinding.description
72 |
73 | let useCustomCodingDecl = macro.hasCustomCoding || macro.hasInlineCoding
74 |
75 | isOptional = typeBinding.isOptional
76 | if isOptional {
77 | // must not have a default value
78 | guard (try? PrefMacroUtils.defaultValue(from: varDec)) == nil else {
79 | throw PrefMacroError.noDefaultValueAllowed
80 | }
81 | keyAndCodingStructName = "\(PrefMacroUtils.moduleNamePrefix)\(macro.keyStructName)\(useCustomCodingDecl ? "" : "<\(typeName)>")"
82 | keyAndCodingStructDeclaration = keyAndCodingStructName +
83 | "(key: \(keyName)\(useCustomCodingDecl ? ", coding: \(customCodingDecl ?? "nil")" : ""))"
84 | privateKeyVarDeclaration = "private let \(privateKeyVarName) = \(keyAndCodingStructDeclaration)"
85 | privateValueVarDeclaration = "private var \(privateValueVarName): \(typeName)?"
86 | } else {
87 | // must have a default value
88 | let defaultValue = try PrefMacroUtils.defaultValue(from: varDec)
89 | keyAndCodingStructName =
90 | "\(PrefMacroUtils.moduleNamePrefix)\(macro.defaultedKeyStructName)\(useCustomCodingDecl ? "" : "<\(typeName)>")"
91 | keyAndCodingStructDeclaration = keyAndCodingStructName +
92 | "(key: \(keyName), defaultValue: \(defaultValue)\(useCustomCodingDecl ? ", coding: \(customCodingDecl ?? "nil")" : ""))"
93 | privateKeyVarDeclaration = "private let \(privateKeyVarName) = \(keyAndCodingStructDeclaration)"
94 | privateValueVarDeclaration = "private var \(privateValueVarName): \(typeName)?"
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/AtomicPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AtomicPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// A basic prefs value coding strategy that stores a standard atomic value type directly without any additional
8 | /// processing.
9 | public struct AtomicPrefsCoding: AtomicPrefsCodable where Value: PrefsStorageValue {
10 | public typealias Value = Value
11 |
12 | public init() { }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/CodableArrayPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodableArrayPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes an array of a `Codable` type to/from raw storage.
11 | public struct CodableArrayPrefsCoding: PrefsCodable where Element: CodablePrefsCodable {
12 | public typealias Value = [Element.Value]
13 | public typealias StorageValue = [Element.StorageValue]
14 | public let elementCoding: Element
15 |
16 | public init(element: Element) {
17 | self.elementCoding = element
18 | }
19 |
20 | public func decode(prefsValue: StorageValue) -> Value? {
21 | // TODO: should assert or throw on elements that return nil?
22 | prefsValue.compactMap { elementCoding.decode(prefsValue: $0) }
23 | }
24 |
25 | public func encode(prefsValue: Value) -> StorageValue? {
26 | // TODO: should assert or throw on elements that return nil?
27 | prefsValue.compactMap { elementCoding.encode(prefsValue: $0) }
28 | }
29 | }
30 |
31 | // MARK: - Static Constructor
32 |
33 | extension Array where Element: Codable, Element: Sendable {
34 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON
35 | /// `Data` element storage with default options.
36 | public static var jsonDataArrayPrefsCoding: CodableArrayPrefsCoding> {
37 | .init(element: JSONDataCodablePrefsCoding())
38 | }
39 |
40 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON
41 | /// `String` (UTF-8) element storage with default options.
42 | public static var jsonStringArrayPrefsCoding: CodableArrayPrefsCoding> {
43 | .init(element: JSONStringCodablePrefsCoding())
44 | }
45 | }
46 |
47 | // MARK: - Chaining Constructor
48 |
49 | extension PrefsCodable where StorageValue == [JSONStringCodablePrefsCoding.Value] {
50 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON
51 | /// `Data` element storage with default options.
52 | public var jsonDataArrayPrefsCoding: PrefsCodingTuple<
53 | Self,
54 | CodableArrayPrefsCoding>
55 | > {
56 | PrefsCodingTuple(
57 | self,
58 | .init(element: JSONDataCodablePrefsCoding())
59 | )
60 | }
61 |
62 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON
63 | /// `String` (UTF-8) element storage with default options.
64 | public var jsonStringArrayPrefsCoding: PrefsCodingTuple<
65 | Self,
66 | CodableArrayPrefsCoding>
67 | > {
68 | PrefsCodingTuple(
69 | self,
70 | .init(element: JSONStringCodablePrefsCoding())
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/CodableDictionaryPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodableDictionaryPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes a dictionary keyed by `String` and containing values of a `Codable` type
11 | /// to/from raw storage.
12 | public struct CodableDictionaryPrefsCoding: PrefsCodable where Element: CodablePrefsCodable {
13 | public typealias Value = [String: Element.Value]
14 | public typealias StorageValue = [String: Element.StorageValue]
15 | public let elementCoding: Element
16 |
17 | public init(element: Element) {
18 | self.elementCoding = element
19 | }
20 |
21 | public func decode(prefsValue: StorageValue) -> Value? {
22 | // TODO: should assert or throw on elements that return nil?
23 | prefsValue.compactMapValues { elementCoding.decode(prefsValue: $0) }
24 | }
25 |
26 | public func encode(prefsValue: Value) -> StorageValue? {
27 | // TODO: should assert or throw on elements that return nil?
28 | prefsValue.compactMapValues { elementCoding.encode(prefsValue: $0) }
29 | }
30 | }
31 |
32 | // MARK: - Static Constructor
33 |
34 | extension Dictionary where Key == String, Value: Codable, Value: Sendable {
35 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable`
36 | /// value type to/from a dictionary of raw JSON `Data` value storage storage with default options.
37 | public static var jsonDataDictionaryPrefsCoding: CodableDictionaryPrefsCoding> {
38 | .init(element: JSONDataCodablePrefsCoding())
39 | }
40 |
41 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable`
42 | /// value type to/from a dictionary of raw JSON `String` (UTF-8) value storage storage with default options.
43 | public static var jsonStringDictionaryPrefsCoding: CodableDictionaryPrefsCoding<
44 | JSONStringCodablePrefsCoding
45 | > {
46 | .init(element: JSONStringCodablePrefsCoding())
47 | }
48 | }
49 |
50 | // MARK: - Chaining Constructor
51 |
52 | extension PrefsCodable where StorageValue == [String: JSONStringCodablePrefsCoding.Value] {
53 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable`
54 | /// value type to/from a dictionary of raw JSON `Data` value storage storage with default options.
55 | public var jsonDataDictionaryPrefsCoding: PrefsCodingTuple<
56 | Self,
57 | CodableDictionaryPrefsCoding>
58 | > {
59 | PrefsCodingTuple(
60 | self,
61 | .init(element: JSONDataCodablePrefsCoding())
62 | )
63 | }
64 |
65 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable`
66 | /// value type to/from a dictionary of raw JSON `String` (UTF-8) value storage storage with default options.
67 | public var jsonStringDictionaryPrefsCoding: PrefsCodingTuple<
68 | Self,
69 | CodableDictionaryPrefsCoding>
70 | > {
71 | PrefsCodingTuple(
72 | self,
73 | .init(element: JSONStringCodablePrefsCoding())
74 | )
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/CodablePrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A generic prefs value coding strategy that allows the encoding and decoding logic for a `Codable` type to be
11 | /// conveniently supplied as closures, alleviating the need to create a new ``CodablePrefsCodable``-conforming type for
12 | /// basic coding logic.
13 | public struct CodablePrefsCoding: CodablePrefsCodable
14 | where Value: Codable, Value: Sendable,
15 | StorageValue: PrefsStorageValue, StorageValue == Encoder.Output,
16 | Encoder: TopLevelEncoder, Encoder: Sendable, Encoder.Output: PrefsStorageValue,
17 | Decoder: TopLevelDecoder, Decoder: Sendable, Decoder.Input: PrefsStorageValue,
18 | Encoder.Output == Decoder.Input
19 | {
20 | public typealias Value = Value
21 | public typealias StorageValue = StorageValue
22 | public typealias Encoder = Encoder
23 | public typealias Decoder = Decoder
24 | let encoder: Encoder
25 | let decoder: Decoder
26 |
27 | public init(
28 | value: Value.Type,
29 | storageValue: StorageValue.Type,
30 | encoder: @escaping @Sendable @autoclosure () -> Encoder,
31 | decoder: @escaping @Sendable @autoclosure () -> Decoder
32 | ) {
33 | self.encoder = encoder()
34 | self.decoder = decoder()
35 | }
36 |
37 | public func prefsEncoder() -> Encoder { encoder }
38 | public func prefsDecoder() -> Decoder { decoder }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/JSONDataCodablePrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONDataCodablePrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with default
10 | /// options.
11 | ///
12 | /// > Note:
13 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of
14 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured.
15 | public struct JSONDataCodablePrefsCoding: JSONDataCodablePrefsCodable
16 | where Value: Codable, Value: Sendable
17 | {
18 | public typealias Value = Value
19 |
20 | public init() { }
21 | }
22 |
23 | // MARK: - Static Constructor
24 |
25 | extension /* Codable */ Encodable where Self: Decodable, Self: Sendable {
26 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with
27 | /// default options.
28 | public static var jsonDataPrefsCoding: JSONDataCodablePrefsCoding {
29 | JSONDataCodablePrefsCoding()
30 | }
31 | }
32 |
33 | // MARK: - Chaining Constructor
34 |
35 | extension PrefsCodable where StorageValue == JSONDataCodablePrefsCoding.Value {
36 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with
37 | /// default options.
38 | public var jsonDataPrefsCoding: PrefsCodingTuple> {
39 | PrefsCodingTuple(
40 | self,
41 | JSONDataCodablePrefsCoding()
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/JSONStringCodablePrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONStringCodablePrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8) storage
10 | /// with default options.
11 | ///
12 | /// > Note:
13 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of
14 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured.
15 | public struct JSONStringCodablePrefsCoding: JSONStringCodablePrefsCodable
16 | where Value: Codable, Value: Sendable
17 | {
18 | public typealias Value = Value
19 |
20 | public init() { }
21 | }
22 |
23 | // MARK: - Static Constructor
24 |
25 | extension /* Codable */ Encodable where Self: Decodable, Self: Sendable {
26 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8)
27 | /// storage with default options.
28 | public static var jsonStringPrefsCoding: JSONStringCodablePrefsCoding {
29 | JSONStringCodablePrefsCoding()
30 | }
31 | }
32 |
33 | // MARK: - Chaining Constructor
34 |
35 | extension PrefsCodable where StorageValue == JSONStringCodablePrefsCoding.Value {
36 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8)
37 | /// storage with default options.
38 | public var jsonStringPrefsCoding: PrefsCodingTuple> {
39 | PrefsCodingTuple(
40 | self,
41 | JSONStringCodablePrefsCoding()
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/PrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// A generic prefs value coding strategy that allows the encoding and decoding logic to be conveniently supplied as
8 | /// closures, alleviating the need to create a new ``PrefsCodable``-conforming type for basic coding logic.
9 | public struct PrefsCoding: PrefsCodable
10 | where Value: Sendable, StorageValue: PrefsStorageValue
11 | {
12 | let encodeBlock: @Sendable (Value) -> StorageValue?
13 | let decodeBlock: @Sendable (StorageValue) -> Value?
14 |
15 | public init(
16 | encode: @escaping @Sendable (Value) -> StorageValue?,
17 | decode: @escaping @Sendable (StorageValue) -> Value?
18 | ) {
19 | encodeBlock = encode
20 | decodeBlock = decode
21 | }
22 |
23 | public func decode(prefsValue: StorageValue) -> Value? {
24 | decodeBlock(prefsValue)
25 | }
26 |
27 | public func encode(prefsValue: Value) -> StorageValue? {
28 | encodeBlock(prefsValue)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/PrefsCodingTuple.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsCodingTuple.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// A container for two ``PrefsCodable`` coding strategies in series.
8 | public struct PrefsCodingTuple: PrefsCodable
9 | where First: PrefsCodable, Second: PrefsCodable, First.StorageValue == Second.Value
10 | {
11 | public typealias Value = First.Value
12 | public typealias StorageValue = Second.StorageValue
13 |
14 | public let first: First
15 | public let second: Second
16 |
17 | public init(_ first: First, _ second: Second) {
18 | self.first = first
19 | self.second = second
20 | }
21 |
22 | public func decode(prefsValue: Second.StorageValue) -> First.Value? {
23 | guard let secondDecoded = second.decode(prefsValue: prefsValue) else { return nil }
24 | let firstDecoded = first.decode(prefsValue: secondDecoded)
25 | return firstDecoded
26 | }
27 |
28 | public func encode(prefsValue: First.Value) -> Second.StorageValue? {
29 | guard let firstEncoded = first.encode(prefsValue: prefsValue) else { return nil }
30 | let secondEncoded = second.encode(prefsValue: firstEncoded)
31 | return secondEncoded
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/RawRepresentableArrayPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentableArrayPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes an array of a `RawRepresentable` type to/from raw storage using the element's
11 | /// `RawValue` as its storage value.
12 | public struct RawRepresentableArrayPrefsCoding: PrefsCodable where Element: RawRepresentablePrefsCodable {
13 | public typealias Value = [Element.Value]
14 | public typealias StorageValue = [Element.StorageValue]
15 | public let elementCoding: Element
16 |
17 | public init(element: Element) {
18 | self.elementCoding = element
19 | }
20 |
21 | public func decode(prefsValue: StorageValue) -> Value? {
22 | // TODO: should assert or throw on elements that return nil?
23 | prefsValue.compactMap { elementCoding.decode(prefsValue: $0) }
24 | }
25 |
26 | public func encode(prefsValue: Value) -> StorageValue? {
27 | // TODO: should assert or throw on elements that return nil?
28 | prefsValue.compactMap { elementCoding.encode(prefsValue: $0) }
29 | }
30 | }
31 |
32 | // MARK: - Static Constructor
33 |
34 | extension Array where Element: RawRepresentable, Element.RawValue: PrefsStorageValue, Element: Sendable {
35 | /// A prefs key that encodes and decodes an array of a `RawRepresentable` type to/from raw storage using the
36 | /// element's `RawValue` as its storage value.
37 | public static var rawRepresentableArrayPrefsCoding: RawRepresentableArrayPrefsCoding> {
38 | .init(element: RawRepresentablePrefsCoding())
39 | }
40 | }
41 |
42 | // MARK: - Chaining Constructor
43 |
44 | // there does not seem to be a reasonable way to implement a chaining constructor
45 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/RawRepresentableDictionaryPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentableDictionaryPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes a dictionary of a `RawRepresentable` type to/from raw storage using the element's
11 | /// `RawValue` as its storage value.
12 | public struct RawRepresentableDictionaryPrefsCoding: PrefsCodable where Element: RawRepresentablePrefsCodable {
13 | public typealias Value = [String: Element.Value]
14 | public typealias StorageValue = [String: Element.StorageValue]
15 | public let elementCoding: Element
16 |
17 | public init(element: Element) {
18 | self.elementCoding = element
19 | }
20 |
21 | public func decode(prefsValue: StorageValue) -> Value? {
22 | // TODO: should assert or throw on elements that return nil?
23 | prefsValue.compactMapValues { elementCoding.decode(prefsValue: $0) }
24 | }
25 |
26 | public func encode(prefsValue: Value) -> StorageValue? {
27 | // TODO: should assert or throw on elements that return nil?
28 | prefsValue.compactMapValues { elementCoding.encode(prefsValue: $0) }
29 | }
30 | }
31 |
32 | // MARK: - Static Constructor
33 |
34 | extension Dictionary where Key == String, Value: RawRepresentable, Value.RawValue: PrefsStorageValue, Value: Sendable {
35 | /// A prefs key that encodes and decodes a dictionary of a `RawRepresentable` type to/from raw storage using the
36 | /// element's `RawValue` as its storage value.
37 | public static var rawRepresentableDictionaryPrefsCoding: RawRepresentableDictionaryPrefsCoding> {
38 | .init(element: RawRepresentablePrefsCoding())
39 | }
40 | }
41 |
42 | // MARK: - Chaining Constructor
43 |
44 | // there does not seem to be a reasonable way to implement a chaining constructor
45 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Prototypes/RawRepresentablePrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentablePrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// A prefs value coding strategy which uses a `RawRepresentable` type's `RawValue` as its storage value.
10 | public struct RawRepresentablePrefsCoding: RawRepresentablePrefsCodable
11 | where Value: Sendable, Value: RawRepresentable, Value.RawValue: PrefsStorageValue
12 | {
13 | public typealias Value = Value
14 | public typealias StorageValue = Value.RawValue
15 |
16 | public init() { }
17 | }
18 |
19 | // MARK: - Static Constructor
20 |
21 | extension RawRepresentable where Self: Sendable, Self.RawValue: PrefsStorageValue {
22 | /// A prefs value coding strategy which uses a `RawRepresentable` type's `RawValue` as its storage value.
23 | public static var rawRepresentablePrefsCoding: RawRepresentablePrefsCoding {
24 | RawRepresentablePrefsCoding()
25 | }
26 | }
27 |
28 | // MARK: - Chaining Constructor
29 |
30 | extension PrefsCodable where StorageValue: RawRepresentable, StorageValue.RawValue: PrefsStorageValue {
31 | // (Note that the availability of this chaining property is very rare, but still technically possible)
32 |
33 | /// A prefs value coding strategy which uses a `RawRepresentable` type's `RawValue` as its storage value.
34 | public var rawRepresentablePrefsCoding: RawRepresentablePrefsCoding {
35 | RawRepresentablePrefsCoding()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/Base64StringDataPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Base64StringDataPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for `Data` using base-64 encoded `String` as the encoded storage value.
10 | public struct Base64StringDataPrefsCoding: PrefsCodable {
11 | public let encodingOptions: Data.Base64EncodingOptions
12 | public let decodingOptions: Data.Base64DecodingOptions
13 |
14 | public init(encodingOptions: Data.Base64EncodingOptions, decodingOptions: Data.Base64DecodingOptions) {
15 | self.encodingOptions = encodingOptions
16 | self.decodingOptions = decodingOptions
17 | }
18 |
19 | public func encode(prefsValue: Data) -> String? {
20 | prefsValue.base64EncodedString(options: encodingOptions)
21 | }
22 |
23 | public func decode(prefsValue: String) -> Data? {
24 | Data(base64Encoded: prefsValue, options: decodingOptions)
25 | }
26 | }
27 |
28 | // MARK: - Static Constructor
29 |
30 | extension PrefsCodable where Self == Base64StringDataPrefsCoding {
31 | /// Coding strategy for `Data` using base-64 encoded `String` as the encoded storage value.
32 | public static func base64DataString(
33 | encodingOptions: Data.Base64EncodingOptions = [],
34 | decodingOptions: Data.Base64DecodingOptions = []
35 | ) -> Base64StringDataPrefsCoding {
36 | Base64StringDataPrefsCoding(encodingOptions: encodingOptions, decodingOptions: decodingOptions)
37 | }
38 | }
39 |
40 | // MARK: - Chaining Constructor
41 |
42 | extension PrefsCodable where StorageValue == Base64StringDataPrefsCoding.Value {
43 | /// Coding strategy for `Data` using base-64 encoded `String` as the encoded storage value.
44 | public func base64DataString(
45 | encodingOptions: Data.Base64EncodingOptions = [],
46 | decodingOptions: Data.Base64DecodingOptions = []
47 | ) -> PrefsCodingTuple {
48 | PrefsCodingTuple(
49 | self,
50 | Base64StringDataPrefsCoding(encodingOptions: encodingOptions, decodingOptions: decodingOptions)
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/BoolIntegerPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BoolIntegerPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for `Bool` using `Int` as the encoded storage value (`1` or `0`).
10 | public struct BoolIntegerPrefsCoding: PrefsCodable {
11 | public let decodingStrategy: DecodingStrategy
12 |
13 | public init(decodingStrategy: DecodingStrategy) {
14 | self.decodingStrategy = decodingStrategy
15 | }
16 |
17 | public func encode(prefsValue: Bool) -> Int? {
18 | prefsValue ? 1 : 0
19 | }
20 |
21 | public func decode(prefsValue: Int) -> Bool? {
22 | switch decodingStrategy {
23 | case .strict:
24 | switch prefsValue {
25 | case 1: true
26 | case 0: false
27 | default: nil
28 | }
29 | case .nearest:
30 | prefsValue >= 1
31 | }
32 | }
33 | }
34 |
35 | extension BoolIntegerPrefsCoding {
36 | /// Integer decoding strategy for ``BoolIntegerPrefsCoding``.
37 | public enum DecodingStrategy: Equatable, Hashable, Sendable {
38 | /// Strict decoding of a stored integer.
39 | /// Only `1` will be interpreted as `true` and `0` as `false`.
40 | /// Any other value will return `nil`.
41 | case strict
42 |
43 | /// Use the value nearest to `0` or `1` when reading a stored integer.
44 | /// `1` or greater will be interpreted as `true`.
45 | /// `0` or less will be interpreted as `false`.
46 | case nearest
47 | }
48 | }
49 |
50 | // MARK: - Static Constructor
51 |
52 | extension PrefsCodable where Self == BoolIntegerPrefsCoding {
53 | /// Coding strategy for `Bool` using `Int` as the encoded storage value (`1` or `0`).
54 | public static func boolAsInteger(
55 | decodingStrategy: BoolIntegerPrefsCoding.DecodingStrategy = .nearest
56 | ) -> Self {
57 | BoolIntegerPrefsCoding(decodingStrategy: decodingStrategy)
58 | }
59 | }
60 |
61 | // MARK: - Chaining Constructor
62 |
63 | extension PrefsCodable where StorageValue == Bool {
64 | /// Coding strategy for `Bool` using `Int` as the encoded storage value (`1` or `0`).
65 | public func boolAsInteger(
66 | decodingStrategy: BoolIntegerPrefsCoding.DecodingStrategy = .nearest
67 | ) -> PrefsCodingTuple {
68 | PrefsCodingTuple(self, BoolIntegerPrefsCoding(decodingStrategy: decodingStrategy))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/BoolStringPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BoolStringPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for `Bool` using `String` as the encoded storage value (`true`/`false` or `yes`/`no`).
10 | public struct BoolStringPrefsCoding: PrefsCodable {
11 | public let encodingStrategy: EncodingStrategy
12 |
13 | public init(encodingStrategy: EncodingStrategy) {
14 | self.encodingStrategy = encodingStrategy
15 | }
16 |
17 | public func encode(prefsValue: Bool) -> String? {
18 | switch encodingStrategy {
19 | case let .trueFalse(textCase):
20 | let text = prefsValue ? "true" : "false"
21 | return textCase.process(text)
22 |
23 | case let .yesNo(textCase):
24 | let text = prefsValue ? "yes" : "no"
25 | return textCase.process(text)
26 |
27 | case let .custom(true: trueValue, false: falseValue, caseInsensitive: _):
28 | return prefsValue ? trueValue : falseValue
29 | }
30 | }
31 |
32 | public func decode(prefsValue: String) -> Bool? {
33 | switch encodingStrategy {
34 | case .trueFalse, .yesNo:
35 | switch prefsValue.trimmingCharacters(in: .whitespacesAndNewlines) {
36 | case let v where v.caseInsensitiveCompare("true") == .orderedSame: true
37 | case let v where v.caseInsensitiveCompare("false") == .orderedSame: false
38 | case let v where v.caseInsensitiveCompare("yes") == .orderedSame: true
39 | case let v where v.caseInsensitiveCompare("no") == .orderedSame: false
40 | default: nil
41 | }
42 | case let .custom(true: trueValue, false: falseValue, caseInsensitive: isCaseInsensitive):
43 | if isCaseInsensitive {
44 | switch prefsValue.trimmingCharacters(in: .whitespacesAndNewlines) {
45 | case let v where v.caseInsensitiveCompare(trueValue) == .orderedSame: true
46 | case let v where v.caseInsensitiveCompare(falseValue) == .orderedSame: false
47 | default: nil
48 | }
49 | } else {
50 | switch prefsValue.trimmingCharacters(in: .whitespacesAndNewlines) {
51 | case trueValue: true
52 | case falseValue: false
53 | default: nil
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | extension BoolStringPrefsCoding {
61 | /// String encoding strategy for ``BoolStringPrefsCoding``.
62 | public enum EncodingStrategy: Equatable, Hashable, Sendable {
63 | /// True or False.
64 | case trueFalse(_ textCase: TextCase = .lowercase)
65 |
66 | /// Yes or No.
67 | case yesNo(_ textCase: TextCase = .lowercase)
68 |
69 | /// Custom string values for `true` and `false` states.
70 | case custom(true: String, false: String, caseInsensitive: Bool = true)
71 | }
72 | }
73 |
74 | extension BoolStringPrefsCoding.EncodingStrategy {
75 | public enum TextCase: Equatable, Hashable, Sendable {
76 | /// Capitalized text.
77 | case capitalized
78 |
79 | /// Lowercase text.
80 | case lowercase
81 |
82 | /// Uppercase text.
83 | case uppercase
84 |
85 | /// Process an input string based on the enumeration case.
86 | public func process(_ string: String) -> String {
87 | switch self {
88 | case .capitalized: string.capitalized
89 | case .lowercase: string.lowercased()
90 | case .uppercase: string.uppercased()
91 | }
92 | }
93 | }
94 | }
95 |
96 | // MARK: - Static Constructor
97 |
98 | extension PrefsCodable where Self == BoolStringPrefsCoding {
99 | /// Coding strategy for `Bool` using `String` as the encoded storage value (`true`/`false` or `yes`/`no`).
100 | public static func boolAsString(
101 | _ encodingStrategy: BoolStringPrefsCoding.EncodingStrategy = .trueFalse(.lowercase)
102 | ) -> Self {
103 | BoolStringPrefsCoding(encodingStrategy: encodingStrategy)
104 | }
105 | }
106 |
107 | // MARK: - Chaining Constructor
108 |
109 | extension PrefsCodable where StorageValue == Bool {
110 | /// Coding strategy for `Bool` using `String` as the encoded storage value (`true`/`false` or `yes`/`no`).
111 | public func boolAsString(
112 | _ encodingStrategy: BoolStringPrefsCoding.EncodingStrategy = .trueFalse(.lowercase)
113 | ) -> PrefsCodingTuple {
114 | PrefsCodingTuple(self, BoolStringPrefsCoding(encodingStrategy: encodingStrategy))
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/CompressedDataPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompressedDataPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for `Data` using data compression. Compresses when storing and decompresses when reading.
10 | ///
11 | /// > Note:
12 | /// >
13 | /// > Due to inherent computational overhead with compression and decompression, this strategy is not recommended
14 | /// > for use with data that has frequent access or requires low-latency access times.
15 | public struct CompressedDataPrefsCoding: PrefsCodable {
16 | let algorithm: NSData.CompressionAlgorithm
17 |
18 | public init(algorithm: NSData.CompressionAlgorithm) {
19 | self.algorithm = algorithm
20 | }
21 |
22 | public func encode(prefsValue: Data) -> Data? {
23 | try? (prefsValue as NSData)
24 | .compressed(using: algorithm) as Data
25 | }
26 |
27 | public func decode(prefsValue: Data) -> Data? {
28 | try? (prefsValue as NSData)
29 | .decompressed(using: algorithm) as Data
30 | }
31 | }
32 |
33 | // MARK: - Static Constructor
34 |
35 | extension PrefsCodable where Self == CompressedDataPrefsCoding {
36 | /// Coding strategy for `Data` using data compression. Compresses when storing and decompresses when reading.
37 | ///
38 | /// > Note:
39 | /// >
40 | /// > Due to inherent computational overhead with compression and decompression, this strategy is not recommended
41 | /// > for use with data that has frequent access or requires low-latency access times.
42 | public static func compressedData(
43 | algorithm: NSData.CompressionAlgorithm
44 | ) -> CompressedDataPrefsCoding {
45 | CompressedDataPrefsCoding(algorithm: algorithm)
46 | }
47 | }
48 |
49 | // MARK: - Chaining Constructor
50 |
51 | extension PrefsCodable where StorageValue == CompressedDataPrefsCoding.Value {
52 | /// Coding strategy for `Data` using data compression. Compresses when storing and decompresses when reading.
53 | ///
54 | /// > Note:
55 | /// >
56 | /// > Due to inherent computational overhead with compression and decompression, this strategy is not recommended
57 | /// > for use with data that has frequent access or requires low-latency access times.
58 | public func compressedData(
59 | algorithm: NSData.CompressionAlgorithm
60 | ) -> PrefsCodingTuple {
61 | PrefsCodingTuple(
62 | self,
63 | CompressedDataPrefsCoding(algorithm: algorithm)
64 | )
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/ISO8601StringDatePrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ISO8601StringDatePrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for `Date` using standard ISO-8601 format `String` as the encoded storage value.
10 | ///
11 | /// For example:
12 | ///
13 | /// ```
14 | /// 2024-12-31T21:30:35Z
15 | /// ```
16 | ///
17 | /// > Important:
18 | /// >
19 | /// > This format includes date and time with a resolution of 1 second. Any sub-second time information is truncated
20 | /// > and discarded.
21 | ///
22 | /// > Tip:
23 | /// >
24 | /// > `Date` has native `Codable` conformance, which means it may also be used directly with
25 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`.
26 | public struct ISO8601DateStringPrefsCoding: PrefsCodable {
27 | public init() { }
28 |
29 | public func encode(prefsValue: Date) -> String? {
30 | ISO8601DateFormatter().string(from: prefsValue)
31 | }
32 |
33 | public func decode(prefsValue: String) -> Date? {
34 | ISO8601DateFormatter().date(from: prefsValue)
35 | }
36 | }
37 |
38 | // MARK: - Static Constructor
39 |
40 | extension PrefsCodable where Self == ISO8601DateStringPrefsCoding {
41 | /// Coding strategy for `Date` using standard ISO-8601 format `String` as the encoded storage value.
42 | ///
43 | /// For example:
44 | ///
45 | /// ```
46 | /// 2024-12-31T21:30:35Z
47 | /// ```
48 | ///
49 | /// > Important:
50 | /// >
51 | /// > This format includes date and time with a resolution of 1 second. Any sub-second time information is truncated
52 | /// > and discarded.
53 | ///
54 | /// > Tip:
55 | /// >
56 | /// > `Date` has native `Codable` conformance, which means it may also be used directly with
57 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`.
58 | public static var iso8601DateString: ISO8601DateStringPrefsCoding {
59 | ISO8601DateStringPrefsCoding()
60 | }
61 | }
62 |
63 | // MARK: - Chaining Constructor
64 |
65 | // note: `Date` does not conform to PrefsStorageValue so we can't offer a coding strategy chaining method.
66 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/IntegerPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntegerPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for a concrete type conforming to `BinaryInteger` using `Int` as the encoded storage value.
10 | public struct IntegerPrefsCoding: PrefsCodable where Value: BinaryInteger, Value: Sendable {
11 | public init() { }
12 |
13 | public func encode(prefsValue: Value) -> Int? {
14 | Int(exactly: prefsValue)
15 | }
16 |
17 | public func decode(prefsValue: Int) -> Value? {
18 | Value(exactly: prefsValue)
19 | }
20 | }
21 |
22 | // MARK: - Static Constructor
23 |
24 | // Note: do not offer `Int` since it is already an atomic prefs storage value type
25 |
26 | extension PrefsCodable where Self == IntegerPrefsCoding {
27 | /// Coding strategy for `UInt` using `Int` as the encoded storage value.
28 | ///
29 | /// > Important:
30 | /// >
31 | /// > Values above `Int.max` will silently fail to be stored.
32 | /// > Consider encoding as `String` or raw `Data` to ensure lossless storage of very large `UInt` values.
33 | public static var uIntAsInt: Self { .init() }
34 | }
35 |
36 | extension PrefsCodable where Self == IntegerPrefsCoding {
37 | /// Coding strategy for `Int8` using `Int` as the encoded storage value.
38 | public static var int8AsInt: Self { .init() }
39 | }
40 |
41 | extension PrefsCodable where Self == IntegerPrefsCoding {
42 | /// Coding strategy for `UInt8` using `Int` as the encoded storage value.
43 | public static var uInt8AsInt: Self { .init() }
44 | }
45 |
46 | extension PrefsCodable where Self == IntegerPrefsCoding {
47 | /// Coding strategy for `Int16` using `Int` as the encoded storage value.
48 | public static var int16AsInt: Self { .init() }
49 | }
50 |
51 | extension PrefsCodable where Self == IntegerPrefsCoding {
52 | /// Coding strategy for `UInt16` using `Int` as the encoded storage value.
53 | public static var uInt16AsInt: Self { .init() }
54 | }
55 |
56 | extension PrefsCodable where Self == IntegerPrefsCoding {
57 | /// Coding strategy for `Int32` using `Int` as the encoded storage value.
58 | public static var int32AsInt: Self { .init() }
59 | }
60 |
61 | extension PrefsCodable where Self == IntegerPrefsCoding {
62 | /// Coding strategy for `UInt32` using `Int` as the encoded storage value.
63 | public static var uInt32AsInt: Self { .init() }
64 | }
65 |
66 | extension PrefsCodable where Self == IntegerPrefsCoding {
67 | /// Coding strategy for `Int64` using `Int` as the encoded storage value.
68 | public static var int64AsInt: Self { .init() }
69 | }
70 |
71 | extension PrefsCodable where Self == IntegerPrefsCoding {
72 | /// Coding strategy for `UInt64` using `Int` as the encoded storage value.
73 | ///
74 | /// > Important:
75 | /// >
76 | /// > Values above `Int.max` will silently fail to be stored.
77 | /// > Consider encoding as `String` or raw `Data` to ensure lossless storage of very large `UInt64` values.
78 | public static var uInt64AsInt: Self { .init() }
79 | }
80 |
81 | // MARK: - Chaining Constructor
82 |
83 | // note: non-Int integers do not conform to PrefsStorageValue so we can't offer coding strategy chaining methods.
84 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/IntegerStringPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntegerStringPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for a concrete type conforming to `FixedWidthInteger` using `String` as the encoded storage value.
10 | public struct IntegerStringPrefsCoding: PrefsCodable where Value: FixedWidthInteger, Value: Sendable {
11 | public init() { }
12 |
13 | public func encode(prefsValue: Value) -> String? {
14 | String(prefsValue)
15 | }
16 |
17 | public func decode(prefsValue: String) -> Value? {
18 | Value(prefsValue)
19 | }
20 | }
21 |
22 | // MARK: - Static Constructors
23 |
24 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
25 | /// Coding strategy for `Int` using `String` as the encoded storage value.
26 | public static var intAsString: Self { .init() }
27 | }
28 |
29 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
30 | /// Coding strategy for `UInt` using `String` as the encoded storage value.
31 | public static var uIntAsString: Self { .init() }
32 | }
33 |
34 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
35 | /// Coding strategy for `Int8` using `String` as the encoded storage value.
36 | public static var int8AsString: Self { .init() }
37 | }
38 |
39 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
40 | /// Coding strategy for `UInt8` using `String` as the encoded storage value.
41 | public static var uInt8AsString: Self { .init() }
42 | }
43 |
44 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
45 | /// Coding strategy for `Int16` using `String` as the encoded storage value.
46 | public static var int16AsString: Self { .init() }
47 | }
48 |
49 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
50 | /// Coding strategy for `UInt16` using `String` as the encoded storage value.
51 | public static var uInt16AsString: Self { .init() }
52 | }
53 |
54 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
55 | /// Coding strategy for `Int32` using `String` as the encoded storage value.
56 | public static var int32AsString: Self { .init() }
57 | }
58 |
59 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
60 | /// Coding strategy for `UInt32` using `String` as the encoded storage value.
61 | public static var uInt32AsString: Self { .init() }
62 | }
63 |
64 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
65 | /// Coding strategy for `Int64` using `String` as the encoded storage value.
66 | public static var int64AsString: Self { .init() }
67 | }
68 |
69 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
70 | /// Coding strategy for `UInt64` using `String` as the encoded storage value.
71 | public static var uInt64AsString: Self { .init() }
72 | }
73 |
74 | @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
75 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
76 | /// Coding strategy for `Int128` using `String` as the encoded storage value.
77 | public static var int128AsString: Self { .init() }
78 | }
79 |
80 | @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
81 | extension PrefsCodable where Self == IntegerStringPrefsCoding {
82 | /// Coding strategy for `UInt128` using `String` as the encoded storage value.
83 | public static var uInt128AsString: Self { .init() }
84 | }
85 |
86 | // MARK: - Chaining Constructor
87 |
88 | extension PrefsCodable where StorageValue == Int {
89 | /// Coding strategy for `Int` using `String` as the encoded storage value.
90 | public var intAsString: PrefsCodingTuple> {
91 | PrefsCodingTuple(self, IntegerStringPrefsCoding())
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable Strategies/URLStringPrefsCoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLStringPrefsCoding.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Coding strategy for `URL` using absolute `String` as the encoded storage value type.
10 | ///
11 | /// > Tip:
12 | /// >
13 | /// > `URL` has native `Codable` conformance, which means it may also be used directly with
14 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`.
15 | public struct URLStringPrefsCoding: PrefsCodable {
16 | public init() { }
17 |
18 | public func encode(prefsValue: URL) -> String? {
19 | prefsValue.absoluteString
20 | }
21 |
22 | public func decode(prefsValue: String) -> URL? {
23 | URL(string: prefsValue)
24 | }
25 | }
26 |
27 | // MARK: - Static Constructor
28 |
29 | extension PrefsCodable where Self == URLStringPrefsCoding {
30 | /// Coding strategy for `URL` using absolute `String` as the encoded storage value type.
31 | ///
32 | /// > Tip:
33 | /// >
34 | /// > `URL` has native `Codable` conformance, which means it may also be used directly with
35 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`.
36 | public static var urlString: URLStringPrefsCoding { .init() }
37 | }
38 |
39 | // MARK: - Chaining Constructor
40 |
41 | // note: `URL` does not conform to PrefsStorageValue so we can't offer a coding strategy chaining method.
42 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/Atomic/AtomicPrefsCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AtomicPrefsCodable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// A basic prefs value coding protocol for storing a standard atomic value type.
8 | public protocol AtomicPrefsCodable: PrefsCodable where Value == StorageValue { }
9 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/Codable/CodablePrefsCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePrefsCodable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes a `Codable` type to/from raw storage.
11 | ///
12 | /// > Tip:
13 | /// >
14 | /// > It is suggested that if multiple `Codable` types that stored in prefs storage use the same
15 | /// > underlying encoder/decoder, that you create a protocol that inherits from ``CodablePrefsCodable``
16 | /// > for all non-defaulted `Codable` prefs and then implement `prefEncoder()` and `prefDecoder()` to return
17 | /// > the same instances. These types can then adopt this new protocol.
18 | public protocol CodablePrefsCodable: PrefsCodable
19 | where Value: Codable
20 | {
21 | associatedtype Encoder: TopLevelEncoder
22 | associatedtype Decoder: TopLevelDecoder
23 |
24 | /// Return a new instance of the encoder used to encode the type for prefs storage.
25 | func prefsEncoder() -> Encoder
26 |
27 | /// Return a new instance of the decoder used to decode the type from prefs storage.
28 | func prefsDecoder() -> Decoder
29 | }
30 |
31 | extension CodablePrefsCodable where StorageValue == Encoder.Output, Encoder.Output == Decoder.Input {
32 | public func decode(prefsValue: StorageValue) -> Value? {
33 | let decoder = prefsDecoder()
34 | guard let value = try? decoder.decode(Value.self, from: prefsValue) else { return nil }
35 | return value
36 | }
37 |
38 | public func encode(prefsValue: Value) -> StorageValue? {
39 | let encoder = prefsEncoder()
40 | guard let encoded = try? encoder.encode(prefsValue) else { return nil }
41 | return encoded
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/Codable/JSON/JSONDataCodablePrefsCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONDataCodablePrefsCodable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with default options.
11 | ///
12 | /// > Note:
13 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of
14 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured.
15 | public protocol JSONDataCodablePrefsCodable: CodablePrefsCodable
16 | where Encoder == JSONEncoder, Decoder == JSONDecoder, StorageValue == Data { }
17 |
18 | extension JSONDataCodablePrefsCodable {
19 | public func prefsEncoder() -> JSONEncoder {
20 | JSONEncoder()
21 | }
22 |
23 | public func prefsDecoder() -> JSONDecoder {
24 | JSONDecoder()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/Codable/JSON/JSONStringCodablePrefsCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONStringCodablePrefsCodable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// A prefs key that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8) storage with default
11 | /// options.
12 | ///
13 | /// > Note:
14 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of
15 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured.
16 | public protocol JSONStringCodablePrefsCodable: CodablePrefsCodable
17 | where Encoder == JSONEncoder, Decoder == JSONDecoder, StorageValue == String { }
18 |
19 | extension JSONStringCodablePrefsCodable {
20 | public func prefsEncoder() -> JSONEncoder {
21 | JSONEncoder()
22 | }
23 |
24 | public func prefsDecoder() -> JSONDecoder {
25 | JSONDecoder()
26 | }
27 | }
28 |
29 | extension JSONStringCodablePrefsCodable {
30 | public func decode(prefsValue: StorageValue) -> Value? {
31 | let decoder = prefsDecoder()
32 | guard let data = prefsValue.data(using: .utf8),
33 | let value = try? decoder.decode(Value.self, from: data)
34 | else { return nil }
35 | return value
36 | }
37 |
38 | public func encode(prefsValue: Value) -> StorageValue? {
39 | let encoder = prefsEncoder()
40 | guard let data = try? encoder.encode(prefsValue),
41 | let string = String(data: data, encoding: .utf8)
42 | else { return nil }
43 | return string
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/PrefsCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsCodable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Defines value encoding and decoding for reading and writing a value to prefs storage.
10 | public protocol PrefsCodable: Sendable
11 | where Value: Sendable, StorageValue: PrefsStorageValue {
12 | associatedtype Value
13 | associatedtype StorageValue
14 |
15 | /// Decodes a raw atomic prefs storage value to a value.
16 | func decode(prefsValue: StorageValue) -> Value?
17 |
18 | /// Encodes a value to a raw atomic prefs storage value.
19 | func encode(prefsValue: Value) -> StorageValue?
20 | }
21 |
22 | extension PrefsCodable where Value == StorageValue {
23 | public func decode(prefsValue: StorageValue) -> Value? {
24 | prefsValue
25 | }
26 |
27 | public func encode(prefsValue: Value) -> StorageValue? {
28 | prefsValue
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/RawRepresentable/PrefsCodable+RawRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsCodable+RawRepresentable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension PrefsCodable where Value: RawRepresentable, Value.RawValue == StorageValue {
10 | public func decode(prefsValue: StorageValue) -> Value? {
11 | Value(rawValue: prefsValue)
12 | }
13 |
14 | public func encode(prefsValue: Value) -> StorageValue? {
15 | prefsValue.rawValue
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsCodable/RawRepresentable/RawRepresentablePrefsCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentablePrefsCodable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Defines value encoding and decoding for reading and writing a `RawRepresentable` value to prefs storage.
8 | public protocol RawRepresentablePrefsCodable: PrefsCodable where Value: RawRepresentable, Value.RawValue == StorageValue { }
9 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsKey/DefaultedPrefsKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultedPrefsKey.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public protocol DefaultedPrefsKey: PrefsKey {
8 | var defaultValue: Value { get }
9 | }
10 |
11 | extension DefaultedPrefsKey {
12 | public func decodeDefaulted(_ storageValue: StorageValue?) -> Value {
13 | decode(storageValue) ?? defaultValue
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsKey/PrefsKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsKey.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | public protocol PrefsKey where Self: Sendable, Coding.Value == Value, Coding.StorageValue == StorageValue {
8 | associatedtype Value: Sendable
9 | associatedtype StorageValue: PrefsStorageValue
10 | associatedtype Coding: PrefsCodable
11 |
12 | var key: String { get }
13 | var coding: Coding { get }
14 | func encode(_ value: Value) -> StorageValue?
15 | func decode(_ storageValue: StorageValue) -> Value?
16 | }
17 |
18 | extension PrefsKey {
19 | public func encode(_ value: Value?) -> StorageValue? {
20 | guard let value else { return nil }
21 | return encode(value)
22 | }
23 |
24 | public func decode(_ storageValue: StorageValue?) -> Value? {
25 | guard let storageValue else { return nil }
26 | return decode(storageValue)
27 | }
28 | }
29 |
30 | extension PrefsKey where Value == StorageValue {
31 | public func encode(_ value: Value) -> StorageValue? { value }
32 | public func decode(_ storageValue: StorageValue) -> Value? { storageValue }
33 | }
34 |
35 | extension PrefsKey {
36 | public func encode(_ value: Value) -> StorageValue? {
37 | coding.encode(prefsValue: value)
38 | }
39 |
40 | public func decode(_ storageValue: StorageValue) -> Value? {
41 | coding.decode(prefsValue: storageValue)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsSchema/AnyPrefsSchema.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyPrefsSchema.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Protocol for prefs schema containing type-erased prefs storage.
10 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
11 | public protocol AnyPrefsSchema: PrefsSchema where SchemaStorage == AnyPrefsStorage { }
12 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsSchema/PrefsSchema.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsSchema.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Combine
8 | import Foundation
9 |
10 | /// Protocol for prefs schema.
11 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
12 | public protocol PrefsSchema /* where Self: Sendable */ {
13 | /// Storage provider type for prefs.
14 | associatedtype SchemaStorage: PrefsStorage
15 |
16 | /// Storage provider for prefs.
17 | var storage: SchemaStorage { get }
18 |
19 | /// Storage mode for prefs.
20 | var storageMode: PrefsStorageMode { get }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsSchema/PropertyWrappers/PrefsStorageModeWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageModeWrapper.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Pref schema property access storage mode.
8 | @propertyWrapper
9 | public struct PrefsStorageModeWrapper {
10 | public var wrappedValue: PrefsStorageMode
11 |
12 | public init(wrappedValue: PrefsStorageMode) {
13 | self.wrappedValue = wrappedValue
14 | }
15 |
16 | public init(_ mode: PrefsStorageMode) {
17 | wrappedValue = mode
18 | }
19 | }
20 |
21 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
22 | extension PrefsSchema {
23 | /// Pref schema property access storage mode.
24 | public typealias StorageMode = PrefsStorageModeWrapper
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsSchema/PropertyWrappers/PrefsStorageWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageWrapper.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Prefs schema storage.
8 | @propertyWrapper
9 | public struct PrefsStorageWrapper where S: PrefsStorage {
10 | public var wrappedValue: S
11 |
12 | public init(wrappedValue: S) {
13 | self.wrappedValue = wrappedValue
14 | }
15 |
16 | public init(_ storage: S) {
17 | wrappedValue = storage
18 | }
19 | }
20 |
21 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
22 | extension PrefsSchema {
23 | /// Prefs schema storage.
24 | public typealias Storage = PrefsStorageWrapper
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/AnyPrefsStorage+PrefsStorageExportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyPrefsStorage+PrefsStorageExportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension AnyPrefsStorage: PrefsStorageExportable {
10 | public func dictionaryRepresentation() throws -> [String: Any] {
11 | guard let wrapped = wrapped as? PrefsStorageExportable else {
12 | throw PrefsStorageError.contentExportingNotSupported
13 | }
14 | return try wrapped.dictionaryRepresentation()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/AnyPrefsStorage+PrefsStorageImportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyPrefsStorage+PrefsStorageImportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension AnyPrefsStorage: PrefsStorageImportable {
10 | @discardableResult
11 | public func load(
12 | from contents: [String: any PrefsStorageValue],
13 | by behavior: PrefsStorageUpdateStrategy
14 | ) throws -> Set {
15 | guard let wrapped = wrapped as? PrefsStorageImportable else {
16 | throw PrefsStorageError.contentLoadingNotSupported
17 | }
18 | return try wrapped.load(from: contents, by: behavior)
19 | }
20 |
21 | @discardableResult
22 | public func load(
23 | unsafe contents: [String: Any],
24 | by behavior: PrefsStorageUpdateStrategy
25 | ) throws -> Set {
26 | guard let wrapped = wrapped as? PrefsStorageImportable else {
27 | throw PrefsStorageError.contentLoadingNotSupported
28 | }
29 | return try wrapped.load(unsafe: contents, by: behavior)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/AnyPrefsStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyPrefsStorage.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Type-erased box containing an instance of a concrete class conforming to ``PrefsStorage``.
10 | public final class AnyPrefsStorage: PrefsStorage {
11 | public let wrapped: any PrefsStorage
12 |
13 | public init(_ wrapped: any PrefsStorage) {
14 | self.wrapped = wrapped
15 | }
16 | }
17 |
18 | extension AnyPrefsStorage {
19 | // MARK: - Set
20 |
21 | public func setStorageValue(forKey key: String, to value: StorageValue?) {
22 | wrapped.setStorageValue(forKey: key, to: value)
23 | }
24 |
25 | public func setUnsafeStorageValue(forKey key: String, to value: Any?) {
26 | wrapped.setUnsafeStorageValue(forKey: key, to: value)
27 | }
28 |
29 | // MARK: - Get
30 |
31 | public func storageValue(forKey key: String) -> Int? {
32 | wrapped.storageValue(forKey: key)
33 | }
34 |
35 | public func storageValue(forKey key: String) -> String? {
36 | wrapped.storageValue(forKey: key)
37 | }
38 |
39 | public func storageValue(forKey key: String) -> Bool? {
40 | wrapped.storageValue(forKey: key)
41 | }
42 |
43 | public func storageValue(forKey key: String) -> Double? {
44 | wrapped.storageValue(forKey: key)
45 | }
46 |
47 | public func storageValue(forKey key: String) -> Float? {
48 | wrapped.storageValue(forKey: key)
49 | }
50 |
51 | public func storageValue(forKey key: String) -> Data? {
52 | wrapped.storageValue(forKey: key)
53 | }
54 |
55 | public func storageValue(forKey key: String) -> Date? {
56 | wrapped.storageValue(forKey: key)
57 | }
58 |
59 | public func storageValue(forKey key: String) -> [Element]? {
60 | wrapped.storageValue(forKey: key)
61 | }
62 |
63 | public func storageValue(forKey key: String) -> [String: Element]? {
64 | wrapped.storageValue(forKey: key)
65 | }
66 |
67 | public func unsafeStorageValue(forKey key: String) -> Any? {
68 | wrapped.unsafeStorageValue(forKey: key)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage+PrefsStorageExportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DictionaryPrefsStorage+PrefsStorageExportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension DictionaryPrefsStorage: PrefsStorageExportable {
10 | public func dictionaryRepresentation() throws -> [String: Any] {
11 | storage
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage+PrefsStorageImportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DictionaryPrefsStorage+PrefsStorageImportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension DictionaryPrefsStorage: PrefsStorageImportable {
10 | @discardableResult
11 | public func load(
12 | from contents: [String: any PrefsStorageValue],
13 | by behavior: PrefsStorageUpdateStrategy
14 | ) throws -> Set {
15 | switch behavior {
16 | case .reinitializing:
17 | storage = contents
18 | return Set(contents.keys)
19 | case .updating:
20 | storage.merge(contents) { old, new in new }
21 | return Set(contents.keys)
22 | case let .updatingWithPredicate(predicate):
23 | return try load(contents: contents, updatingWithPredicate: predicate)
24 | }
25 | }
26 |
27 | @discardableResult
28 | public func load(
29 | unsafe contents: [String: Any],
30 | by behavior: PrefsStorageUpdateStrategy
31 | ) throws -> Set {
32 | switch behavior {
33 | case .reinitializing:
34 | storage = contents
35 | return Set(contents.keys)
36 | case .updating:
37 | storage.merge(contents) { old, new in new }
38 | return Set(contents.keys)
39 | case let .updatingWithPredicate(predicate):
40 | return try load(contents: contents, updatingWithPredicate: predicate)
41 | }
42 | }
43 | }
44 |
45 | // MARK: - Utilities
46 |
47 | extension DictionaryPrefsStorage {
48 | func load(
49 | contents: [String: Any],
50 | updatingWithPredicate predicate: PrefsStorageUpdateStrategy.UpdatePredicate
51 | ) throws -> Set {
52 | var updatedKeys: Set = []
53 |
54 | for (key, newValue) in contents {
55 | if let existingValue = storage[key] {
56 | let result = try predicate(key, existingValue, newValue)
57 | switch result {
58 | case .preserveOldValue:
59 | break
60 | case .takeNewValue:
61 | storage[key] = newValue
62 | updatedKeys.insert(key)
63 | }
64 | } else {
65 | storage[key] = newValue
66 | updatedKeys.insert(key)
67 | }
68 | }
69 | return updatedKeys
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage+PrefsStorageInitializable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DictionaryPrefsStorage+PrefsStorageInitializable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // Note:
10 | //
11 | // `PrefsStorageInitializable` conformance is in class definition, as
12 | // `open class` requires protocol-required inits to be defined there and not in an extension.
13 | //
14 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DictionaryPrefsStorage.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Dictionary-backed ``PrefsStorage`` for use in ``PrefsSchema`` with internally-synchronized local access.
10 | ///
11 | /// This class may be used as-is, or subclassed to add additional functionality to dictionary-backed storage as an
12 | /// alternative to implementing a custom ``PrefsStorage`` type.
13 | open class DictionaryPrefsStorage: PrefsStorageInitializable {
14 | @SynchronizedLock
15 | var storage: [String: Any]
16 |
17 | /// Initialize from type-safe dictionary content.
18 | public init(root: [String: any PrefsStorageValue] = [:]) {
19 | storage = root
20 | }
21 |
22 | /// Initialize from raw untyped dictionary content.
23 | /// You are responsible for ensuring value types are compatible with related methods such as plist conversion.
24 | public required init(unsafe storage: [String: Any]) {
25 | self.storage = storage
26 | }
27 |
28 | // MARK: PrefsStorageInitializable inits
29 |
30 | // Note:
31 | //
32 | // `PrefsStorageInitializable` conformance is in class definition, as
33 | // `open class` requires protocol-required inits to be defined there and not in an extension.
34 | //
35 |
36 | required public convenience init(
37 | from url: URL,
38 | format: Format
39 | ) throws where Format: PrefsStorageImportFormatFileImportable {
40 | self.init()
41 | try load(from: url, format: format, by: .reinitializing)
42 | }
43 |
44 | required public convenience init(
45 | from data: Data,
46 | format: Format
47 | ) throws where Format: PrefsStorageImportFormatDataImportable {
48 | self.init()
49 | try load(from: data, format: format, by: .reinitializing)
50 | }
51 |
52 | required public convenience init(
53 | from string: String,
54 | format: Format
55 | ) throws where Format: PrefsStorageImportFormatStringImportable {
56 | self.init()
57 | try load(from: string, format: format, by: .reinitializing)
58 | }
59 | }
60 |
61 | extension DictionaryPrefsStorage: @unchecked Sendable { }
62 |
63 | extension DictionaryPrefsStorage: PrefsStorage {
64 | // MARK: - Set
65 |
66 | public func setStorageValue(forKey key: String, to value: StorageValue?) {
67 | storage[key] = value
68 | }
69 |
70 | public func setUnsafeStorageValue(forKey key: String, to value: Any?) {
71 | storage[key] = value
72 | }
73 |
74 | // MARK: - Get
75 |
76 | public func storageValue(forKey key: String) -> Int? {
77 | storage[key] as? Int
78 | }
79 |
80 | public func storageValue(forKey key: String) -> String? {
81 | storage[key] as? String
82 | }
83 |
84 | public func storageValue(forKey key: String) -> Bool? {
85 | storage[key] as? Bool
86 | }
87 |
88 | public func storageValue(forKey key: String) -> Double? {
89 | storage[key] as? Double
90 | }
91 |
92 | public func storageValue(forKey key: String) -> Float? {
93 | storage[key] as? Float
94 | }
95 |
96 | public func storageValue(forKey key: String) -> Data? {
97 | storage[key] as? Data
98 | }
99 |
100 | public func storageValue(forKey key: String) -> Date? {
101 | storage[key] as? Date
102 | }
103 |
104 | public func storageValue(forKey key: String) -> [Element]? {
105 | storage[key] as? [Element]
106 | }
107 |
108 | public func storageValue(forKey key: String) -> [String: Element]? {
109 | storage[key] as? [String: Element]
110 | }
111 |
112 | public func unsafeStorageValue(forKey key: String) -> Any? {
113 | storage[key]
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage+PrefsStorageExportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsPrefsStorage+PrefsStorageExportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension UserDefaultsPrefsStorage: PrefsStorageExportable {
10 | public func dictionaryRepresentation() throws -> [String: Any] {
11 | // UserDefaults suite includes all search lists when requesting its `dictionaryRepresentation()`,
12 | // which means a lot more keys than expected may be included.
13 | suite.dictionaryRepresentation()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage+PrefsStorageImportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsPrefsStorage+PrefsStorageImportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension UserDefaultsPrefsStorage: PrefsStorageImportable {
10 | @discardableResult
11 | public func load(
12 | from contents: [String: any PrefsStorageValue],
13 | by behavior: PrefsStorageUpdateStrategy
14 | ) throws -> Set {
15 | switch behavior {
16 | case .reinitializing:
17 | suite.removeAllKeys()
18 | suite.merge(contents)
19 | return Set(contents.keys)
20 | case .updating:
21 | suite.merge(contents)
22 | return Set(contents.keys)
23 | case let .updatingWithPredicate(predicate):
24 | return try load(contents: contents, updatingWithPredicate: predicate)
25 | }
26 | }
27 |
28 | @discardableResult
29 | public func load(
30 | unsafe contents: [String: Any],
31 | by behavior: PrefsStorageUpdateStrategy
32 | ) throws -> Set {
33 | switch behavior {
34 | case .reinitializing:
35 | suite.removeAllKeys()
36 | suite.merge(contents)
37 | return Set(contents.keys)
38 | case .updating:
39 | suite.merge(contents)
40 | return Set(contents.keys)
41 | case let .updatingWithPredicate(predicate):
42 | return try load(contents: contents, updatingWithPredicate: predicate)
43 | }
44 | }
45 | }
46 |
47 | // MARK: - Utilities
48 |
49 | extension UserDefaultsPrefsStorage {
50 | func load(
51 | contents: [String: Any],
52 | updatingWithPredicate predicate: PrefsStorageUpdateStrategy.UpdatePredicate
53 | ) throws -> Set {
54 | var updatedKeys: Set = []
55 |
56 | for (key, newValue) in contents {
57 | if let existingValue = suite.object(forKey: key) {
58 | let result = try predicate(key, existingValue, newValue)
59 | switch result {
60 | case .preserveOldValue:
61 | break
62 | case .takeNewValue:
63 | suite.set(newValue, forKey: key)
64 | updatedKeys.insert(key)
65 | }
66 | } else {
67 | suite.set(newValue, forKey: key)
68 | updatedKeys.insert(key)
69 | }
70 | }
71 | return updatedKeys
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage+Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsPrefsStorage+Utilities.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension UserDefaults {
10 | @inlinable
11 | package static func castAsPrefsStorageCompatible(value: Any) -> Any {
12 | // Note that underlying number format of NSNumber can't easily be determined
13 | // so the cleanest solution is to make NSNumber `PrefsStorageValue` and allow
14 | // the user to conditionally cast it as the number type they desire.
15 |
16 | switch value {
17 | // MARK: Atomic
18 | case let value as NSString:
19 | return value as String
20 | case let value as Bool where "\(type(of: value))" == "__NSCFBoolean":
21 | return value
22 | case let value as NSNumber:
23 | return value
24 | case let value as NSData:
25 | return value as Data
26 | case let value as NSDate:
27 | return value as Date
28 | // MARK: Arrays
29 | case let value as [NSString]:
30 | return value as [String]
31 | case let value as [Bool] where value.allSatisfy { "\(type(of: $0))" == "__NSCFBoolean" }:
32 | return value
33 | case let value as [NSNumber]:
34 | return value
35 | case let value as [NSData]:
36 | return value as [Data]
37 | case let value as [NSDate]:
38 | return value as [Date]
39 | case let value as [Any]:
40 | return value.map(castAsPrefsStorageCompatible(value:))
41 | // MARK: Dictionaries
42 | case let value as [NSString: NSString]:
43 | return value as [String: String]
44 | case let value as [NSString: Bool] where value.values.allSatisfy { "\(type(of: $0))" == "__NSCFBoolean" }:
45 | return value as [String: Bool]
46 | case let value as [NSString: NSNumber]:
47 | return value as [String: NSNumber]
48 | case let value as [NSString: NSData]:
49 | return value as [String: Data]
50 | case let value as [NSString: NSDate]:
51 | return value as [String: Date]
52 | case let value as [String: Any]:
53 | return value.mapValues(castAsPrefsStorageCompatible(value:))
54 | // MARK: Default
55 | default:
56 | assertionFailure("Unhandled UserDefaults pref storage value type: \(type(of: value))")
57 | return value
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsPrefsStorage.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// UserDefaults-backed ``PrefsStorage`` for use in ``PrefsSchema``.
10 | open class UserDefaultsPrefsStorage {
11 | public let suite: UserDefaults
12 |
13 | public init(suite: UserDefaults = .standard) {
14 | self.suite = suite
15 | }
16 | }
17 |
18 | extension UserDefaultsPrefsStorage: @unchecked Sendable { }
19 |
20 | extension UserDefaultsPrefsStorage: PrefsStorage {
21 | // MARK: - Set
22 |
23 | public func setStorageValue(forKey key: String, to value: StorageValue?) {
24 | suite.set(value, forKey: key)
25 | }
26 |
27 | public func setUnsafeStorageValue(forKey key: String, to value: Any?) {
28 | suite.set(value, forKey: key)
29 | }
30 |
31 | // MARK: - Get
32 |
33 | public func storageValue(forKey key: String) -> Int? {
34 | suite.integerOptional(forKey: key)
35 | }
36 |
37 | public func storageValue(forKey key: String) -> String? {
38 | suite.string(forKey: key)
39 | }
40 |
41 | public func storageValue(forKey key: String) -> Bool? {
42 | suite.boolOptional(forKey: key)
43 | }
44 |
45 | public func storageValue(forKey key: String) -> Double? {
46 | suite.doubleOptional(forKey: key)
47 | }
48 |
49 | public func storageValue(forKey key: String) -> Float? {
50 | suite.floatOptional(forKey: key)
51 | }
52 |
53 | public func storageValue(forKey key: String) -> Data? {
54 | suite.data(forKey: key)
55 | }
56 |
57 | public func storageValue(forKey key: String) -> Date? {
58 | (suite.object(forKey: key) as? NSDate) as Date?
59 | }
60 |
61 | public func storageValue(forKey key: String) -> [Element]? {
62 | guard let rawArray = suite.array(forKey: key) else { return nil }
63 | if let typedArray = rawArray as? [Element] {
64 | return typedArray
65 | } else if let typedArray = rawArray.map(UserDefaults.castAsPrefsStorageCompatible(value:)) as? [Element] {
66 | return typedArray
67 | } else {
68 | return nil
69 | }
70 | }
71 |
72 | public func storageValue(forKey key: String) -> [String: Element]? {
73 | guard let rawDict = suite.dictionary(forKey: key) else { return nil }
74 | if let typedDict = rawDict as? [String: Element] {
75 | return typedDict
76 | } else if let typedDict = rawDict.mapValues(UserDefaults.castAsPrefsStorageCompatible(value:)) as? [String: Element] {
77 | return typedDict
78 | } else {
79 | return nil
80 | }
81 | }
82 |
83 | public func unsafeStorageValue(forKey key: String) -> Any? {
84 | suite.object(forKey: key)
85 | }
86 | }
87 |
88 | // MARK: - Additional Storage Access Methods
89 |
90 | extension UserDefaultsPrefsStorage {
91 | public func unsafeStorageValue(forKey key: String) -> [Any]? {
92 | guard let rawArray = suite.array(forKey: key) else { return nil }
93 | let typedArray = rawArray
94 | .map(UserDefaults.castAsPrefsStorageCompatible(value:)) // TODO: may not be necessary
95 | return typedArray
96 | }
97 |
98 | public func unsafeStorageValue(forKey key: String) -> [String: Any]? {
99 | guard let rawDict = suite.dictionary(forKey: key) else { return nil }
100 | let typedDict = rawDict
101 | .mapValues(UserDefaults.castAsPrefsStorageCompatible(value:)) // TODO: may not be necessary
102 | return typedDict
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/JSON/JSON Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSON Utilities.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // MARK: - Import
10 |
11 | extension [String: Any] {
12 | package init(json url: URL, options: JSONSerialization.ReadingOptions = []) throws {
13 | let fileData = try Data(contentsOf: url)
14 | try self.init(json: fileData, options: options)
15 | }
16 |
17 | package init(json data: Data, options: JSONSerialization.ReadingOptions = []) throws {
18 | let object = try JSONSerialization.jsonObject(with: data, options: options)
19 | guard let dictionary = object as? [String: Any] else {
20 | throw PrefsStorageError.jsonFormatNotSupported
21 | }
22 | self = dictionary
23 | }
24 |
25 | package init(json string: String, options: JSONSerialization.ReadingOptions = []) throws {
26 | guard let data = string.data(using: .utf8) else {
27 | throw PrefsStorageError.jsonFormatNotSupported
28 | }
29 | try self.init(json: data, options: options)
30 | }
31 | }
32 |
33 | // MARK: - Export
34 |
35 | extension [String: Any] {
36 | package func jsonData(options: JSONSerialization.WritingOptions = []) throws -> Data {
37 | try JSONSerialization
38 | .data(withJSONObject: self, options: options)
39 | }
40 |
41 | package func jsonString(
42 | options: JSONSerialization.WritingOptions = [],
43 | encoding: String.Encoding = .utf8
44 | ) throws -> String {
45 | let data = try jsonData(options: options)
46 | guard let string = String(data: data, encoding: encoding) else {
47 | throw PrefsStorageError.jsonExportError
48 | }
49 | return string
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/JSON/JSONPrefsStorageExportFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPrefsStorageExportFormat.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Prefs storage export format to export storage as JSON.
10 | ///
11 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create and
12 | /// supply your own export strategy.
13 | public struct JSONPrefsStorageExportFormat: PrefsStorageExportFormat {
14 | public var options: JSONSerialization.WritingOptions
15 | public var strategy: any PrefsStorageExportStrategy
16 |
17 | public init(
18 | options: JSONSerialization.WritingOptions = [],
19 | strategy: any PrefsStorageExportStrategy
20 | ) {
21 | self.options = options
22 | self.strategy = strategy
23 | }
24 | }
25 |
26 | // MARK: - Static Constructor
27 |
28 | extension PrefsStorageExportFormat where Self == JSONPrefsStorageExportFormat {
29 | /// Prefs storage export format to export storage as JSON.
30 | ///
31 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create
32 | /// and supply your own export strategy.
33 | public static func json(
34 | options: JSONSerialization.WritingOptions = [],
35 | strategy: any PrefsStorageExportStrategy
36 | ) -> JSONPrefsStorageExportFormat {
37 | JSONPrefsStorageExportFormat(options: options, strategy: strategy)
38 | }
39 | }
40 |
41 | // MARK: - Format Traits
42 |
43 | extension JSONPrefsStorageExportFormat: PrefsStorageExportFormatFileExportable {
44 | // Note:
45 | // default implementation is provided when we conform to both
46 | // PrefsStorageExportFormatFileExportable & PrefsStorageExportFormatDataExportable
47 | }
48 |
49 | extension JSONPrefsStorageExportFormat: PrefsStorageExportFormatDataExportable {
50 | public func exportData(storage: [String: Any]) throws -> Data {
51 | let prepared = try strategy.prepareForExport(storage: storage)
52 | let data = try prepared.jsonData(options: options)
53 | return data
54 | }
55 | }
56 |
57 | extension JSONPrefsStorageExportFormat: PrefsStorageExportFormatStringExportable {
58 | public func exportString(storage: [String: Any]) throws -> String {
59 | let prepared = try strategy.prepareForExport(storage: storage)
60 | let string = try prepared.jsonString(options: options, encoding: .utf8)
61 | return string
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/JSON/JSONPrefsStorageImportFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPrefsStorageImportFormat.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Prefs storage import format to import JSON contents.
10 | ///
11 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create and
12 | /// supply your own import strategy.
13 | public struct JSONPrefsStorageImportFormat: PrefsStorageImportFormat {
14 | public var options: JSONSerialization.ReadingOptions
15 |
16 | public var strategy: any PrefsStorageImportStrategy
17 |
18 | public init(
19 | options: JSONSerialization.ReadingOptions = [],
20 | strategy: any PrefsStorageImportStrategy
21 | ) {
22 | self.options = options
23 | self.strategy = strategy
24 | }
25 | }
26 |
27 | // MARK: - Static Constructor
28 |
29 | extension PrefsStorageImportFormat where Self == JSONPrefsStorageImportFormat {
30 | /// Prefs storage import format to import JSON contents.
31 | ///
32 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create
33 | /// and supply your own import strategy.
34 | public static func json(
35 | options: JSONSerialization.ReadingOptions = [],
36 | strategy: any PrefsStorageImportStrategy
37 | ) -> JSONPrefsStorageImportFormat {
38 | JSONPrefsStorageImportFormat(options: options, strategy: strategy)
39 | }
40 | }
41 |
42 | // MARK: - Format Traits
43 |
44 | extension JSONPrefsStorageImportFormat: PrefsStorageImportFormatFileImportable {
45 | // Note:
46 | // default implementation is provided when we conform to both
47 | // PrefsStorageImportFormatFileImportable & PrefsStorageImportFormatDataImportable
48 | }
49 |
50 | extension JSONPrefsStorageImportFormat: PrefsStorageImportFormatDataImportable {
51 | public func load(from data: Data) throws -> [String: Any] {
52 | let loaded: [String: Any] = try .init(json: data, options: options)
53 | let prepared = try strategy.prepareForImport(storage: loaded)
54 | return prepared
55 | }
56 | }
57 |
58 | extension JSONPrefsStorageImportFormat: PrefsStorageImportFormatStringImportable {
59 | public func load(from string: String) throws -> [String: Any] {
60 | let loaded: [String: Any] = try .init(json: string, options: options)
61 | let prepared = try strategy.prepareForImport(storage: loaded)
62 | return prepared
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PList Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PList Utilities.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // MARK: - Import
10 |
11 | extension [String: Any] {
12 | package init(plist url: URL) throws {
13 | let fileData = try Data(contentsOf: url)
14 | try self.init(plist: fileData)
15 | }
16 |
17 | package init(plist data: Data) throws {
18 | var fmt: PropertyListSerialization.PropertyListFormat = .xml // will be overwritten
19 | let dict = try PropertyListSerialization.propertyList(from: data, format: &fmt)
20 | guard let nsDict = dict as? NSDictionary else {
21 | throw CocoaError(.coderReadCorrupt)
22 | }
23 | try self.init(plist: nsDict)
24 | }
25 |
26 | package init(plist string: String) throws {
27 | guard let data = string.data(using: .utf8) else {
28 | throw CocoaError(.coderReadCorrupt)
29 | }
30 | try self.init(plist: data)
31 | }
32 |
33 | package init(plist dictionary: NSDictionary) throws {
34 | // let mappedDict = try convertToPrefDict(plist: dictionary)
35 | guard let mappedDict = dictionary as? [String: Any] else {
36 | throw CocoaError(.coderReadCorrupt)
37 | }
38 | self = mappedDict
39 | }
40 | }
41 |
42 | // MARK: - Export
43 |
44 | extension [String: Any] {
45 | package func plistData(format: PropertyListSerialization.PropertyListFormat = .xml) throws -> Data {
46 | try PropertyListSerialization
47 | .data(fromPropertyList: self, format: format, options: .init())
48 | }
49 |
50 | package func plistString(encoding: String.Encoding = .utf8) throws -> String {
51 | let data = try plistData(format: .xml)
52 | guard let string = String(data: data, encoding: encoding) else {
53 | throw PrefsStorageError.plistExportError
54 | }
55 | return string
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageExportFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PListPrefsStorageExportFormat.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Prefs storage export format to export storage as plist (property list).
10 | public struct PListPrefsStorageExportFormat: PrefsStorageExportFormat {
11 | public var format: PropertyListSerialization.PropertyListFormat
12 | public var strategy: any PrefsStorageExportStrategy
13 |
14 | public init(
15 | format: PropertyListSerialization.PropertyListFormat,
16 | strategy: some PrefsStorageExportStrategy
17 | ) {
18 | self.format = format
19 | self.strategy = strategy
20 | }
21 | }
22 |
23 | // MARK: - Static Constructor
24 |
25 | extension PrefsStorageExportFormat where Self == PListPrefsStorageExportFormat {
26 | /// Prefs storage export format to export storage as plist (property list).
27 | public static func plist(
28 | format: PropertyListSerialization.PropertyListFormat = .xml,
29 | strategy: some PrefsStorageExportStrategy = .plist
30 | ) -> PListPrefsStorageExportFormat {
31 | PListPrefsStorageExportFormat(format: format, strategy: strategy)
32 | }
33 | }
34 |
35 | // MARK: - Format Traits
36 |
37 | extension PListPrefsStorageExportFormat: PrefsStorageExportFormatFileExportable {
38 | // Note:
39 | // default implementation is provided when we conform to both
40 | // PrefsStorageExportFormatFileExportable & PrefsStorageExportFormatDataExportable
41 | }
42 |
43 | extension PListPrefsStorageExportFormat: PrefsStorageExportFormatDataExportable {
44 | public func exportData(storage: [String: Any]) throws -> Data {
45 | let prepared = try strategy.prepareForExport(storage: storage)
46 | let data = try prepared.plistData(format: format)
47 | return data
48 | }
49 | }
50 |
51 | extension PListPrefsStorageExportFormat: PrefsStorageExportFormatStringExportable {
52 | public func exportString(storage: [String: Any]) throws -> String {
53 | let prepared = try strategy.prepareForExport(storage: storage)
54 | let string = try prepared.plistString(encoding: .utf8)
55 | return string
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageExportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PListPrefsStorageExportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Prefs storage export strategy to export storage as plist (property list).
10 | public struct PListPrefsStorageExportStrategy {
11 | public init() { }
12 | }
13 |
14 | extension PListPrefsStorageExportStrategy: PrefsStorageExportStrategy {
15 | public func prepareForExport(storage: [String: Any]) throws -> [String: Any] {
16 | // pass storage through as-is, no casting or conversions necessary
17 | storage
18 | }
19 | }
20 |
21 | // MARK: - Static Constructor
22 |
23 | extension PrefsStorageExportStrategy where Self == PListPrefsStorageExportStrategy {
24 | /// Prefs storage export strategy to export storage as plist (property list).
25 | public static var plist: PListPrefsStorageExportStrategy {
26 | PListPrefsStorageExportStrategy()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageImportFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PListPrefsStorageImportFormat.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Prefs storage import format to import plist (property list) contents.
10 | public struct PListPrefsStorageImportFormat: PrefsStorageImportFormat {
11 | public var strategy: any PrefsStorageImportStrategy
12 |
13 | public init(
14 | strategy: any PrefsStorageImportStrategy
15 | ) {
16 | self.strategy = strategy
17 | }
18 | }
19 |
20 | // MARK: - Static Constructor
21 |
22 | extension PrefsStorageImportFormat where Self == PListPrefsStorageImportFormat {
23 | /// Prefs storage import format to import plist (property list) contents.
24 | public static func plist(
25 | strategy: any PrefsStorageImportStrategy = .plist
26 | ) -> PListPrefsStorageImportFormat {
27 | PListPrefsStorageImportFormat(strategy: strategy)
28 | }
29 | }
30 |
31 | // MARK: - Format Traits
32 |
33 | extension PListPrefsStorageImportFormat: PrefsStorageImportFormatFileImportable {
34 | // Note:
35 | // default implementation is provided when we conform to both
36 | // PrefsStorageImportFormatFileImportable & PrefsStorageImportFormatDataImportable
37 | }
38 |
39 | extension PListPrefsStorageImportFormat: PrefsStorageImportFormatDataImportable {
40 | public func load(from data: Data) throws -> [String: Any] {
41 | let loaded: [String: Any] = try .init(plist: data)
42 | let prepared = try strategy.prepareForImport(storage: loaded)
43 | return prepared
44 | }
45 | }
46 |
47 | extension PListPrefsStorageImportFormat: PrefsStorageImportFormatStringImportable {
48 | public func load(from string: String) throws -> [String: Any] {
49 | let loaded: [String: Any] = try .init(plist: string)
50 | let prepared = try strategy.prepareForImport(storage: loaded)
51 | return prepared
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageImportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PListPrefsStorageImportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Prefs storage import strategy to import storage as plist (property list).
10 | public typealias PListPrefsStorageImportStrategy = PrefsStoragePassthroughImportStrategy
11 |
12 | // MARK: - Static Constructor
13 |
14 | extension PrefsStorageImportStrategy where Self == PListPrefsStorageImportStrategy {
15 | /// Prefs storage import strategy to import storage as plist (property list).
16 | public static var plist: PListPrefsStorageImportStrategy {
17 | PListPrefsStorageImportStrategy()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportFormat/PrefsStorageExportFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageExportFormat.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Protocol that allows creating a type that implements storage content exporting to a particular data serialization
10 | /// format.
11 | public protocol PrefsStorageExportFormat { }
12 |
13 | // MARK: - Format Traits
14 |
15 | /// Trait for ``PrefsStorageExportFormat`` that enables exporting storage to a file on disk.
16 | public protocol PrefsStorageExportFormatFileExportable where Self: PrefsStorageExportFormat {
17 | func export(storage: [String: Any], to file: URL) throws
18 | }
19 |
20 | /// Trait for ``PrefsStorageExportFormat`` that enables exporting storage as raw data.
21 | public protocol PrefsStorageExportFormatDataExportable where Self: PrefsStorageExportFormat {
22 | func exportData(storage: [String: Any]) throws -> Data
23 | }
24 |
25 | /// Trait for ``PrefsStorageExportFormat`` that enables exporting storage as string encoding/markup.
26 | public protocol PrefsStorageExportFormatStringExportable where Self: PrefsStorageExportFormat {
27 | func exportString(storage: [String: Any]) throws -> String
28 | }
29 |
30 | // MARK: - Default Implementation
31 |
32 | extension PrefsStorageExportFormat where Self: PrefsStorageExportFormatDataExportable, Self: PrefsStorageExportFormatFileExportable {
33 | public func export(storage: [String: Any], to file: URL) throws {
34 | let data = try exportData(storage: storage)
35 | try data.write(to: file)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportStrategy/PrefsStorageExportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageExportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Export strategy used by prefs export formats.
8 | public protocol PrefsStorageExportStrategy {
9 | /// The main method used to prepare local storage for export.
10 | func prepareForExport(storage: [String: Any]) throws -> [String: Any]
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportStrategy/PrefsStorageMappingExportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageMappingExportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | public protocol PrefsStorageMappingExportStrategy: PrefsStorageExportStrategy {
10 | func exportValue(forKeyPath keyPath: [String], value: Int) throws -> Any
11 | func exportValue(forKeyPath keyPath: [String], value: String) throws -> Any
12 | func exportValue(forKeyPath keyPath: [String], value: Bool) throws -> Any
13 | func exportValue(forKeyPath keyPath: [String], value: Double) throws -> Any
14 | func exportValue(forKeyPath keyPath: [String], value: Float) throws -> Any
15 | func exportValue(forKeyPath keyPath: [String], value: NSNumber) throws -> Any
16 | func exportValue(forKeyPath keyPath: [String], value: Data) throws -> Any
17 | func exportValue(forKeyPath keyPath: [String], value: Date) throws -> Any
18 | }
19 |
20 | // MARK: - Default Implementation
21 |
22 | extension PrefsStorageExportStrategy where Self: PrefsStorageMappingExportStrategy {
23 | public func prepareForExport(storage: [String: Any]) throws -> [String: Any] {
24 | // start recursive call at root
25 | try prepareForExport(keyPath: [], dict: storage)
26 | }
27 |
28 | func prepareForExport(keyPath: [String], dict: [String: Any]) throws -> [String: Any] {
29 | var copy = dict
30 |
31 | for (key, value) in copy {
32 | var keyPath = keyPath
33 | keyPath.append(key)
34 | copy[key] = try prepareForExport(keyPath: keyPath, element: value)
35 | }
36 |
37 | return copy
38 | }
39 |
40 | func prepareForExport(keyPath: [String], array: [Any]) throws -> Any {
41 | try array.map { try prepareForExport(keyPath: keyPath, element: $0) }
42 | }
43 |
44 | func prepareForExport(keyPath: [String], element: Any) throws -> Any {
45 | switch element {
46 | case let v as String:
47 | try exportValue(forKeyPath: keyPath, value: v)
48 | case let v as NSNumber where ["__NSCFNumber", "__NSCFBoolean"].contains("\(type(of: element))"):
49 | try prepareForExport(keyPath: keyPath, number: v)
50 | case let v as Int:
51 | try exportValue(forKeyPath: keyPath, value: v)
52 | case let v as Bool:
53 | try exportValue(forKeyPath: keyPath, value: v)
54 | case let v as Double:
55 | try exportValue(forKeyPath: keyPath, value: v)
56 | case let v as Float:
57 | try exportValue(forKeyPath: keyPath, value: v)
58 | case let v as Bool:
59 | try exportValue(forKeyPath: keyPath, value: v)
60 | case let v as Data:
61 | try exportValue(forKeyPath: keyPath, value: v)
62 | case let v as Date:
63 | try exportValue(forKeyPath: keyPath, value: v)
64 | case let v as [Any]:
65 | try prepareForExport(keyPath: keyPath, array: v)
66 | case let v as [String: Any]:
67 | try prepareForExport(keyPath: keyPath, dict: v)
68 | default:
69 | element
70 | }
71 | }
72 |
73 | func prepareForExport(
74 | keyPath: [String],
75 | number: NSNumber,
76 | typeEraseFloatingPoint: Bool = false
77 | ) throws -> Any {
78 | switch number {
79 | case let v as Bool where number.potentialNumberType == .int8_bool
80 | && "\(type(of: number))" == "__NSCFBoolean":
81 | try exportValue(forKeyPath: keyPath, value: v)
82 | case let v as Int where number.potentialNumberType == .int_uInt_uInt32_uInt64_uInt16:
83 | try exportValue(forKeyPath: keyPath, value: v)
84 | case let v as Double where number.potentialNumberType == .double:
85 | typeEraseFloatingPoint
86 | ? try exportValue(forKeyPath: keyPath, value: number)
87 | : try exportValue(forKeyPath: keyPath, value: v)
88 | case let v as Float where number.potentialNumberType == .float:
89 | typeEraseFloatingPoint
90 | ? try exportValue(forKeyPath: keyPath, value: number)
91 | : try exportValue(forKeyPath: keyPath, value: v)
92 | default:
93 | number
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportStrategy/PrefsStoragePassthroughExportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStoragePassthroughExportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// A prefs storage export strategy that passes local storage data through without any modification, conversion, or
8 | /// casting.
9 | public struct PrefsStoragePassthroughExportStrategy {
10 | public init() { }
11 | }
12 |
13 | extension PrefsStoragePassthroughExportStrategy: PrefsStorageExportStrategy {
14 | public func prepareForExport(storage: [String: Any]) throws -> [String: Any] {
15 | // pass storage through as-is, no casting or conversions necessary
16 | storage
17 | }
18 | }
19 |
20 | // MARK: - Static Constructor
21 |
22 | extension PrefsStorageExportStrategy where Self == PrefsStoragePassthroughExportStrategy {
23 | /// A prefs storage export strategy that passes local storage data through without any modification, conversion,
24 | /// or casting.
25 | public static var passthrough: PrefsStoragePassthroughExportStrategy {
26 | PrefsStoragePassthroughExportStrategy()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageExportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Trait for ``PrefsStorage`` that enables exporting storage contents.
10 | public protocol PrefsStorageExportable where Self: PrefsStorage {
11 | /// Returns the storage contents as a dictionary.
12 | /// This method is a required implementation detail for storage export methods provided by
13 | /// ``PrefsStorageExportable`` and is not meant to be used directly.
14 | func dictionaryRepresentation() throws -> [String: Any]
15 |
16 | /// Export the storage contents to a file on disk.
17 | func export(
18 | format: Format,
19 | to file: URL
20 | ) throws where Format: PrefsStorageExportFormatFileExportable
21 |
22 | /// Export the storage contents as raw data.
23 | func exportData(
24 | format: Format
25 | ) throws -> Data where Format: PrefsStorageExportFormatDataExportable
26 |
27 | /// Export the storage contents encoded in a format that supports string encoding/markup.
28 | func exportString(
29 | format: Format
30 | ) throws -> String where Format: PrefsStorageExportFormatStringExportable
31 | }
32 |
33 | // MARK: - Default Implementation
34 |
35 | extension PrefsStorage where Self: PrefsStorageExportable {
36 | public func export(
37 | format: Format,
38 | to file: URL
39 | ) throws where Format: PrefsStorageExportFormatFileExportable {
40 | try format.export(storage: dictionaryRepresentation(), to: file)
41 | }
42 |
43 | public func exportData(
44 | format: Format
45 | ) throws -> Data where Format: PrefsStorageExportFormatDataExportable {
46 | try format.exportData(storage: dictionaryRepresentation())
47 | }
48 |
49 | public func exportString(
50 | format: Format
51 | ) throws -> String where Format: PrefsStorageExportFormatStringExportable {
52 | try format.exportString(storage: dictionaryRepresentation())
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportFormat/PrefsStorageImportFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageImportFormat.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Protocol that allows creating a type that implements storage content importing from a particular data serialization
10 | /// format.
11 | public protocol PrefsStorageImportFormat { }
12 |
13 | // MARK: - Format Traits
14 |
15 | /// Trait for ``PrefsStorageImportFormat`` that enables importing serialized storage content from a file on disk.
16 | public protocol PrefsStorageImportFormatFileImportable where Self: PrefsStorageImportFormat {
17 | /// Read data into a raw dictionary, ready to be processed by the import strategy.
18 | func load(from file: URL) throws -> [String: Any]
19 | }
20 |
21 | /// Trait for ``PrefsStorageImportFormat`` that enables importing serialized storage content from raw data.
22 | public protocol PrefsStorageImportFormatDataImportable where Self: PrefsStorageImportFormat {
23 | /// Read data into a raw dictionary, ready to be processed by the import strategy.
24 | func load(from data: Data) throws -> [String: Any]
25 | }
26 |
27 | /// Trait for ``PrefsStorageImportFormat`` that enables importing serialized storage content from string encoding/markup.
28 | public protocol PrefsStorageImportFormatStringImportable where Self: PrefsStorageImportFormat {
29 | /// Read data into a raw dictionary, ready to be processed by the import strategy.
30 | func load(from string: String) throws -> [String: Any]
31 | }
32 |
33 | // MARK: - Default Implementation
34 |
35 | extension PrefsStorageImportFormat where Self: PrefsStorageImportFormatDataImportable, Self: PrefsStorageImportFormatFileImportable {
36 | public func load(from file: URL) throws -> [String: Any] {
37 | let data = try Data(contentsOf: file)
38 | let dict = try load(from: data)
39 | return dict
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportStrategy/PrefsStorageImportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageImportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Import strategy used by prefs import formats.
8 | public protocol PrefsStorageImportStrategy {
9 | /// The main method used to prepare imported data for merging into local storage.
10 | func prepareForImport(storage: [String: Any]) throws -> [String: Any]
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportStrategy/PrefsStoragePassthroughImportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStoragePassthroughImportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// A prefs storage import strategy that passes imported data through without any modification, conversion, or
10 | /// casting.
11 | public struct PrefsStoragePassthroughImportStrategy {
12 | public init() { }
13 | }
14 |
15 | extension PrefsStoragePassthroughImportStrategy: PrefsStorageImportStrategy {
16 | public func prepareForImport(storage: [String: Any]) throws -> [String: Any] {
17 | // pass storage through as-is, no casting or conversions necessary
18 | storage
19 | }
20 | }
21 |
22 | // MARK: - Static Constructor
23 |
24 | extension PrefsStorageImportStrategy where Self == PrefsStoragePassthroughImportStrategy {
25 | /// A prefs storage import strategy that passes imported data through without any modification, conversion, or
26 | /// casting.
27 | public static var passthrough: PrefsStoragePassthroughImportStrategy {
28 | PrefsStoragePassthroughImportStrategy()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportStrategy/PrefsStorageTypedImportStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageTypedImportStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Offers default ``PrefsStorageMappingImportStrategy`` implementation functionality.
10 | public struct PrefsStorageTypedImportStrategy {
11 | public var typeEraseAmbiguousFloatingPoint: Bool
12 |
13 | public init(typeEraseAmbiguousFloatingPoint: Bool = false) {
14 | self.typeEraseAmbiguousFloatingPoint = typeEraseAmbiguousFloatingPoint
15 | }
16 | }
17 |
18 | extension PrefsStorageTypedImportStrategy: PrefsStorageMappingImportStrategy { }
19 |
20 | // MARK: - Static Constructor
21 |
22 | extension PrefsStorageImportStrategy where Self == PrefsStorageTypedImportStrategy {
23 | /// Offers default ``PrefsStorageMappingImportStrategy`` implementation functionality.
24 | public static func typed(typeEraseAmbiguousFloatingPoint: Bool = false) -> PrefsStorageTypedImportStrategy {
25 | PrefsStorageTypedImportStrategy(
26 | typeEraseAmbiguousFloatingPoint: typeEraseAmbiguousFloatingPoint
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageImportable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Trait for ``PrefsStorage`` that enables loading storage contents.
10 | ///
11 | /// > Note:
12 | /// >
13 | /// > Loading storage contents will not update local cache properties for any `@Pref` keys defined in a `@PrefsSchema`
14 | /// > whose `storageMode` is set to `cachedReadStorageWrite`. The storage mode must be set to `storageOnly` to ensure
15 | /// > data loads correctly.
16 | public protocol PrefsStorageImportable where Self: PrefsStorage {
17 | /// Load key/values into storage.
18 | ///
19 | /// - Returns: Key names for key/value pairs that were imported.
20 | @discardableResult
21 | func load(
22 | from contents: [String: any PrefsStorageValue],
23 | by behavior: PrefsStorageUpdateStrategy
24 | ) throws -> Set
25 |
26 | /// Load key/values into storage.
27 | ///
28 | /// - Returns: Key names for key/value pairs that were imported.
29 | @discardableResult
30 | func load(
31 | unsafe contents: [String: Any],
32 | by behavior: PrefsStorageUpdateStrategy
33 | ) throws -> Set
34 |
35 | /// Import storage contents from a file on disk.
36 | ///
37 | /// - Returns: Key names for key/value pairs that were imported.
38 | @discardableResult
39 | func load(
40 | from file: URL,
41 | format: Format,
42 | by behavior: PrefsStorageUpdateStrategy
43 | ) throws -> Set where Format: PrefsStorageImportFormatFileImportable
44 |
45 | /// Import storage contents from a format's raw data.
46 | ///
47 | /// - Returns: Key names for key/value pairs that were imported.
48 | @discardableResult
49 | func load(
50 | from data: Data,
51 | format: Format,
52 | by behavior: PrefsStorageUpdateStrategy
53 | ) throws -> Set where Format: PrefsStorageImportFormatDataImportable
54 |
55 | /// Import storage contents from a format that supports string encoding/markup.
56 | ///
57 | /// - Returns: Key names for key/value pairs that were imported.
58 | @discardableResult
59 | func load(
60 | from string: String,
61 | format: Format,
62 | by behavior: PrefsStorageUpdateStrategy
63 | ) throws -> Set where Format: PrefsStorageImportFormatStringImportable
64 | }
65 |
66 | // MARK: - Default Implementation
67 |
68 | extension PrefsStorage where Self: PrefsStorageImportable {
69 | @discardableResult
70 | public func load(
71 | from file: URL,
72 | format: Format,
73 | by behavior: PrefsStorageUpdateStrategy
74 | ) throws -> Set where Format: PrefsStorageImportFormatFileImportable {
75 | let loaded = try format.load(from: file)
76 | return try load(unsafe: loaded, by: behavior)
77 | }
78 |
79 | @discardableResult
80 | public func load(
81 | from data: Data,
82 | format: Format,
83 | by behavior: PrefsStorageUpdateStrategy
84 | ) throws -> Set where Format: PrefsStorageImportFormatDataImportable {
85 | let loaded = try format.load(from: data)
86 | return try load(unsafe: loaded, by: behavior)
87 | }
88 |
89 | @discardableResult
90 | public func load(
91 | from string: String,
92 | format: Format,
93 | by behavior: PrefsStorageUpdateStrategy
94 | ) throws -> Set where Format: PrefsStorageImportFormatStringImportable {
95 | let loaded = try format.load(from: string)
96 | return try load(unsafe: loaded, by: behavior)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageUpdateStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageUpdateStrategy.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Contents loading behavior for ``PrefsStorage`` load methods.
8 | public enum PrefsStorageUpdateStrategy {
9 | /// Replaces existing storage contents with new contents, removing all existing keys first.
10 | case reinitializing
11 |
12 | /// Merges new content with existing storage contents, overwriting a key's value in the event of a key name collision.
13 | case updating
14 |
15 | /// Merges new content with existing storage contents, using a user-defined predicate in the event of key name collisions.
16 | case updatingWithPredicate(_ predicate: UpdatePredicate)
17 | }
18 |
19 | extension PrefsStorageUpdateStrategy: Sendable { }
20 |
21 | extension PrefsStorageUpdateStrategy {
22 | public typealias UpdatePredicate = @Sendable (_ key: String, _ oldValue: Any, _ newValue: Any) throws -> ValueUpdateResult
23 | }
24 |
25 | extension PrefsStorageUpdateStrategy {
26 | public enum ValueUpdateResult {
27 | case preserveOldValue
28 | case takeNewValue
29 | }
30 | }
31 |
32 | extension PrefsStorageUpdateStrategy.ValueUpdateResult: Sendable { }
33 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageInitializable/PrefsStorageInitializable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageInitializable.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Trait for ``PrefsStorage`` that enables initializing storage contents.
10 | public protocol PrefsStorageInitializable where Self: PrefsStorageImportable {
11 | /// Initialize with storage contents by importing a file on disk.
12 | init(
13 | from url: URL,
14 | format: Format
15 | ) throws where Format: PrefsStorageImportFormatFileImportable
16 |
17 | /// Initialize with storage contents by importing raw file contents.
18 | init(
19 | from data: Data,
20 | format: Format
21 | ) throws where Format: PrefsStorageImportFormatDataImportable
22 |
23 | /// Initialize with storage contents by importing raw file contents for a format that supports string
24 | /// encoding/markup.
25 | init(
26 | from string: String,
27 | format: Format
28 | ) throws where Format: PrefsStorageImportFormatStringImportable
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage/PrefsStorage+DefaultedPrefsKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorage+DefaultedPrefsKey.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // swiftformat:disable wrap
10 |
11 | // MARK: - Get Value
12 |
13 | extension PrefsStorage {
14 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Int {
15 | key.decodeDefaulted(storageValue(forKey: key))
16 | }
17 |
18 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == String {
19 | key.decodeDefaulted(storageValue(forKey: key))
20 | }
21 |
22 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Bool {
23 | key.decodeDefaulted(storageValue(forKey: key))
24 | }
25 |
26 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Double {
27 | key.decodeDefaulted(storageValue(forKey: key))
28 | }
29 |
30 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Float {
31 | key.decodeDefaulted(storageValue(forKey: key))
32 | }
33 |
34 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Data {
35 | key.decodeDefaulted(storageValue(forKey: key))
36 | }
37 |
38 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Date {
39 | key.decodeDefaulted(storageValue(forKey: key))
40 | }
41 |
42 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == [Element] {
43 | key.decodeDefaulted(storageValue(forKey: key))
44 | }
45 |
46 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == [String: Element] {
47 | key.decodeDefaulted(storageValue(forKey: key))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage/PrefsStorage+PrefsKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorage+PrefsKey.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // swiftformat:disable wrap
10 |
11 | // MARK: - Set Value
12 |
13 | extension PrefsStorage {
14 | public func setValue(forKey key: Key, to value: Key.Value?) {
15 | setStorageValue(forKey: key.key, to: key.encode(value))
16 | }
17 | }
18 |
19 | // MARK: - Get Storage Value
20 |
21 | extension PrefsStorage {
22 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Int {
23 | storageValue(forKey: key.key)
24 | }
25 |
26 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == String {
27 | storageValue(forKey: key.key)
28 | }
29 |
30 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Bool {
31 | storageValue(forKey: key.key)
32 | }
33 |
34 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Double {
35 | storageValue(forKey: key.key)
36 | }
37 |
38 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Float {
39 | storageValue(forKey: key.key)
40 | }
41 |
42 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Data {
43 | storageValue(forKey: key.key)
44 | }
45 |
46 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Date {
47 | storageValue(forKey: key.key)
48 | }
49 |
50 | public func storageValue(forKey key: Key) -> [Element]? where Key.StorageValue == [Element] {
51 | storageValue(forKey: key.key)
52 | }
53 |
54 | public func storageValue(forKey key: Key) -> [String: Element]? where Key.StorageValue == [String: Element] {
55 | storageValue(forKey: key.key)
56 | }
57 | }
58 |
59 | // MARK: - Get Value
60 |
61 | extension PrefsStorage {
62 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Int {
63 | guard let storageValue = storageValue(forKey: key) else { return nil }
64 | return key.decode(storageValue)
65 | }
66 |
67 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == String {
68 | guard let storageValue = storageValue(forKey: key) else { return nil }
69 | return key.decode(storageValue)
70 | }
71 |
72 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Bool {
73 | guard let storageValue = storageValue(forKey: key) else { return nil }
74 | return key.decode(storageValue)
75 | }
76 |
77 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Double {
78 | guard let storageValue = storageValue(forKey: key) else { return nil }
79 | return key.decode(storageValue)
80 | }
81 |
82 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Float {
83 | guard let storageValue = storageValue(forKey: key) else { return nil }
84 | return key.decode(storageValue)
85 | }
86 |
87 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Data {
88 | guard let storageValue = storageValue(forKey: key) else { return nil }
89 | return key.decode(storageValue)
90 | }
91 |
92 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Date {
93 | guard let storageValue = storageValue(forKey: key) else { return nil }
94 | return key.decode(storageValue)
95 | }
96 |
97 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == [Element] {
98 | guard let storageValue = storageValue(forKey: key) else { return nil }
99 | return key.decode(storageValue)
100 | }
101 |
102 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == [String: Element] {
103 | guard let storageValue = storageValue(forKey: key) else { return nil }
104 | return key.decode(storageValue)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage/PrefsStorage+Static.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorage+Static.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension PrefsStorage where Self == DictionaryPrefsStorage {
10 | /// Dictionary prefs storage.
11 | public static var dictionary: DictionaryPrefsStorage {
12 | DictionaryPrefsStorage()
13 | }
14 |
15 | /// Dictionary prefs storage with initial root content.
16 | public static func dictionary(root: [String: any PrefsStorageValue]) -> DictionaryPrefsStorage {
17 | DictionaryPrefsStorage(root: root)
18 | }
19 |
20 | /// Dictionary prefs storage with initial raw untyped root content.
21 | /// You are responsible for ensuring value types are compatible with related methods such as plist conversion.
22 | public static func dictionary(unsafe storage: [String: Any]) -> DictionaryPrefsStorage {
23 | DictionaryPrefsStorage(unsafe: storage)
24 | }
25 |
26 | /// Dictionary prefs storage with initial root content from a JSON file.
27 | public static func dictionary(
28 | from url: URL,
29 | format: Format
30 | ) throws -> DictionaryPrefsStorage where Format: PrefsStorageImportFormatFileImportable {
31 | try DictionaryPrefsStorage(from: url, format: format)
32 | }
33 |
34 | /// Dictionary prefs storage with initial root content from a JSON file.
35 | public static func dictionary(
36 | from data: Data,
37 | format: Format
38 | ) throws -> DictionaryPrefsStorage where Format: PrefsStorageImportFormatDataImportable {
39 | try DictionaryPrefsStorage(from: data, format: format)
40 | }
41 |
42 | /// Dictionary prefs storage with initial root content from a JSON file.
43 | public static func dictionary(
44 | from string: String,
45 | format: Format
46 | ) throws -> DictionaryPrefsStorage where Format: PrefsStorageImportFormatStringImportable {
47 | try DictionaryPrefsStorage(from: string, format: format)
48 | }
49 | }
50 |
51 | extension PrefsStorage where Self == UserDefaultsPrefsStorage {
52 | /// Standard `UserDefaults` suite prefs storage.
53 | public static var userDefaults: UserDefaultsPrefsStorage {
54 | UserDefaultsPrefsStorage()
55 | }
56 |
57 | /// Custom `UserDefaults` suite prefs storage.
58 | public static func userDefaults(suite: UserDefaults) -> UserDefaultsPrefsStorage {
59 | UserDefaultsPrefsStorage(suite: suite)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage/PrefsStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorage.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Conform a type to enable it to be used for prefs storage.
10 | /// The type must be a reference type (class).
11 | public protocol PrefsStorage: AnyObject where Self: Sendable {
12 | // MARK: - Set
13 |
14 | func setStorageValue(forKey key: String, to value: StorageValue?)
15 | func setUnsafeStorageValue(forKey key: String, to value: Any?)
16 |
17 | // MARK: - Get
18 |
19 | func storageValue(forKey key: String) -> Int?
20 | func storageValue(forKey key: String) -> String?
21 | func storageValue(forKey key: String) -> Bool?
22 | func storageValue(forKey key: String) -> Double?
23 | func storageValue(forKey key: String) -> Float?
24 | func storageValue(forKey key: String) -> Data?
25 | func storageValue(forKey key: String) -> Date?
26 | func storageValue(forKey key: String) -> [Element]?
27 | func storageValue(forKey key: String) -> [String: Element]?
28 | func unsafeStorageValue(forKey key: String) -> Any?
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage/PrefsStorageError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageError.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | public enum PrefsStorageError: LocalizedError {
10 | case contentExportingNotSupported
11 | case contentLoadingNotSupported
12 | case jsonExportError
13 | case jsonFormatNotSupported
14 | case jsonLoadingNotSupported
15 | case jsonWritingNotSupported
16 | case plistExportError
17 | case plistLoadingNotSupported
18 | case plistWritingNotSupported
19 |
20 | public var errorDescription: String? {
21 | switch self {
22 | case .contentExportingNotSupported:
23 | "Exporting content is not supported for this prefs storage implementation."
24 | case .contentLoadingNotSupported:
25 | "Loading content is not supported for this prefs storage implementation."
26 | case .jsonExportError:
27 | "JSON export failed."
28 | case .jsonFormatNotSupported:
29 | "JSON format is not supported or not recognized."
30 | case .jsonLoadingNotSupported:
31 | "Conversion from JSON is not supported for this prefs storage implementation."
32 | case .jsonWritingNotSupported:
33 | "Conversion to JSON format is not supported for this prefs storage implementation."
34 | case .plistExportError:
35 | "PList export failed."
36 | case .plistLoadingNotSupported:
37 | "Conversion from plist format is not supported for this prefs storage implementation."
38 | case .plistWritingNotSupported:
39 | "Conversion to plist format is not supported for this prefs storage implementation."
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorage/PrefsStorageMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageMode.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// Pref schema property access storage mode for ``PrefsSchema`` implementations.
8 | public enum PrefsStorageMode {
9 | /// Storage-backed only with no intermediate cache.
10 | ///
11 | /// Directly read and write from prefs schema `storage` on every access to pref properties without cacheing.
12 | /// This may have performance impacts on frequent accesses or for data types with expensive decoding operations.
13 | case storageOnly
14 |
15 | /// Cache-backed read, storage-backed write.
16 | ///
17 | /// Reads property values from storage on initialization, then utilizes an internal local cache for improved read
18 | /// performance thereafter. All writes are always written to storage immediately.
19 | ///
20 | /// This mode is recommended in most use cases for improved performance.
21 | ///
22 | /// > Note:
23 | /// > This mode is suitable for storage that cannot or will not change externally. Changes made externally
24 | /// > will not be reflected within the schema during its lifespan and will be overwritten with cached values.
25 | case cachedReadStorageWrite
26 |
27 | // TODO: could implement this feature in future if there is a way to enumerate all prefs in a PrefsSchema
28 | // /// Cache only.
29 | // /// Reads property values from storage on initialization, then operates exclusively from cache for reads and
30 | // /// writes.
31 | // ///
32 | // /// Writes are not automatically written to storage. Changes to pref values must be manually committed by calling
33 | // /// `commit()` on the prefs schema which writes all cached pref values to storage. It is recommended to do this
34 | // /// only periodically or upon context switches (such as when the user invokes a Save Settings command, or your
35 | // /// application quits).
36 | // ///
37 | // /// This storage mode is preferable for rare use cases where performance is critical, allowing storage commits
38 | // /// to be invoked electively only during optimal conditions.
39 | // ///
40 | // /// > Note:
41 | // /// > This mode is suitable for storage that cannot or will not change externally. Changes made externally
42 | // /// > will not be reflected within the schema and will be overwritten with cached values.
43 | // case cacheOnly
44 | }
45 |
46 | extension PrefsStorageMode: Equatable { }
47 |
48 | extension PrefsStorageMode: Hashable { }
49 |
50 | extension PrefsStorageMode: Sendable { }
51 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorageValue/PrefsStorageValue Types.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageValue Types.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // MARK: - Atomic Types
10 |
11 | extension Int: PrefsStorageValue { }
12 |
13 | extension String: PrefsStorageValue { }
14 |
15 | extension Bool: PrefsStorageValue { }
16 |
17 | extension Double: PrefsStorageValue { }
18 |
19 | extension Float: PrefsStorageValue { }
20 |
21 | extension Data: PrefsStorageValue { }
22 |
23 | extension Date: PrefsStorageValue { }
24 |
25 | extension Array: PrefsStorageValue where Element: PrefsStorageValue { }
26 |
27 | extension Dictionary: PrefsStorageValue where Key == String, Value: PrefsStorageValue { }
28 |
29 | // MARK: - Additional Types
30 |
31 | extension NSNumber: PrefsStorageValue { }
32 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/PrefsStorageValue/PrefsStorageValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageValue.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | /// Protocol adopted by format-agnostic atomic value types that are valid for storage in prefs storage.
10 | public protocol PrefsStorageValue where Self: Equatable, Self: Sendable { }
11 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/Utilities/Concurrency.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Concurrency.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | @propertyWrapper
10 | public struct SynchronizedLock: @unchecked Sendable {
11 | private var _value: Value
12 | private var lock = NSLock()
13 |
14 | public init(wrappedValue: Value) {
15 | _value = wrappedValue
16 | }
17 |
18 | public var wrappedValue: Value {
19 | get { lock.synchronized { _value } }
20 | set { lock.synchronized { _value = newValue } }
21 | }
22 |
23 | private mutating func synchronized(block: (inout Value) throws -> T) rethrows -> T {
24 | try lock.synchronized {
25 | try block(&_value)
26 | }
27 | }
28 | }
29 |
30 | extension NSLocking {
31 | fileprivate func synchronized(block: () throws -> T) rethrows -> T {
32 | lock()
33 | defer { unlock() }
34 | return try block()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/Utilities/NSNumber.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSNumber.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | // MARK: - objCTypeString
10 |
11 | extension NSNumber {
12 | /// Returns `objCType` (Objective-C type encoding) as `String`.
13 | ///
14 | /// Possible strings are a subset of `NSValue`'s Objective-C type encodings.
15 | ///
16 | /// ```
17 | /// “c”, “C”, “s”, “S”, “i”, “I”, “l”, “L”, “q”, “Q”, “f”, and “d”.
18 | /// ```
19 | package var objCTypeString: String {
20 | String(cString: objCType)
21 | }
22 | }
23 |
24 | // MARK: - TypeEncoding
25 |
26 | extension NSNumber {
27 | /// Objective-C type encoding for `NSNumber`.
28 | ///
29 | /// See: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
30 | package enum TypeEncoding: String, Equatable, Hashable {
31 | case char = "c" // A char
32 | case int = "i" // An int
33 | case short = "s" // A short
34 | case long = "l" // A long, treated as a 32-bit quantity on 64-bit programs.
35 | case longLong = "q" // A long long
36 | case unsignedChar = "C" // An unsigned char
37 | case unsignedInt = "I" // An unsigned int
38 | case unsignedShort = "S" // An unsigned short
39 | case unsignedLong = "L" // An unsigned long
40 | case unsignedLongLong = "Q" // An unsigned long long
41 | case float = "f" // A float
42 | case double = "d" // A double
43 | case cBool = "B" // A C++ bool or a C99 _Bool
44 | case void = "v" // A void
45 | case characterString = "*" // A character string (char *)
46 | case object = "@" // An object (whether statically typed or typed id)
47 | case classObject = "#" // A class object (Class)
48 | case methodSelector = ":" // A method selector (SEL)
49 | case unknown
50 |
51 | init(value: NSNumber) {
52 | let objCTypeString = value.objCTypeString
53 | self = Self(rawValue: objCTypeString) ?? .unknown
54 | }
55 | }
56 |
57 | package var typeEncoding: TypeEncoding {
58 | TypeEncoding(value: self)
59 | }
60 | }
61 |
62 | // MARK: - SwiftNumberType
63 |
64 | extension NSNumber {
65 | /// Swift concrete number type detection heuristic for `NSNumber`.
66 | package enum SwiftNumberType: Equatable, Hashable {
67 | case int_uInt_uInt32_uInt64_uInt16
68 | case int8_bool
69 | case uInt8_int16
70 | case double
71 | case float
72 | case unknown
73 |
74 | init(value: NSNumber) {
75 | let objCTypeString = value.objCTypeString
76 | self.init(objCType: objCTypeString)
77 | }
78 |
79 | init(objCType: String) {
80 | let typeEncoding = TypeEncoding(rawValue: objCType) ?? .unknown
81 | self.init(typeEncoding: typeEncoding)
82 | }
83 |
84 | init(typeEncoding: TypeEncoding) {
85 | // Swift Type Encoding When Cast to NSNumber
86 | // ---------- ------------------------------
87 | // c s i q d f
88 | // - - - - - -
89 | // Int q
90 | // UInt q
91 | // Int8 c
92 | // UInt8 s
93 | // Int16 s
94 | // UInt16 i
95 | // Int32 i
96 | // UInt32 q
97 | // Int64 q
98 | // UInt64 q
99 | //
100 | // Double d
101 | // Float f
102 | //
103 | // Bool c
104 |
105 | self = switch typeEncoding {
106 | case .char: // "c"
107 | .int8_bool
108 | case .short: // "s"
109 | .uInt8_int16
110 | case .longLong: // "q"
111 | .int_uInt_uInt32_uInt64_uInt16
112 | case .double: // "d"
113 | .double
114 | case .float: // "f"
115 | .float
116 | default:
117 | .unknown
118 | }
119 | }
120 | }
121 |
122 | package var potentialNumberType: SwiftNumberType {
123 | SwiftNumberType(value: self)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/Utilities/Outsourced/UserDefaults Outsourced.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults Outsourced.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | /// ----------------------------------------------
8 | /// ----------------------------------------------
9 | /// OTCore/Extensions/Foundation/CharacterSet.swift
10 | ///
11 | /// Borrowed from OTCore 1.6.0 under MIT license.
12 | /// https://github.com/orchetect/OTCore
13 | /// Methods herein are unit tested at their source
14 | /// so no unit tests are necessary.
15 | /// ----------------------------------------------
16 | /// ----------------------------------------------
17 | ///
18 |
19 | import Foundation
20 |
21 | extension UserDefaults {
22 | // custom optional methods for core data types that don't intrinsically support optionals yet
23 |
24 | /// Convenience method to wrap the built-in `.integer(forKey:)` method in an optional returning nil if the key doesn't exist.
25 | @_disfavoredOverload
26 | func integerOptional(forKey key: String) -> Int? {
27 | guard object(forKey: key) != nil else { return nil }
28 | return integer(forKey: key)
29 | }
30 |
31 | /// Convenience method to wrap the built-in `.double(forKey:)` method in an optional returning nil if the key doesn't exist.
32 | @_disfavoredOverload
33 | func doubleOptional(forKey key: String) -> Double? {
34 | guard object(forKey: key) != nil else { return nil }
35 | return double(forKey: key)
36 | }
37 |
38 | /// Convenience method to wrap the built-in `.float(forKey:)` method in an optional returning nil if the key doesn't exist.
39 | @_disfavoredOverload
40 | func floatOptional(forKey key: String) -> Float? {
41 | guard object(forKey: key) != nil else { return nil }
42 | return float(forKey: key)
43 | }
44 |
45 | /// Convenience method to wrap the built-in `.bool(forKey:)` method in an optional returning nil if the key doesn't exist.
46 | @_disfavoredOverload
47 | func boolOptional(forKey key: String) -> Bool? {
48 | guard object(forKey: key) != nil else { return nil }
49 | return bool(forKey: key)
50 | }
51 |
52 | /// Returns `true` if the key exists.
53 | ///
54 | /// This method is only useful when you don't care about extracting a value from the key and merely want to check for the key's
55 | /// existence.
56 | @_disfavoredOverload
57 | func exists(key: String) -> Bool {
58 | object(forKey: key) != nil
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/PrefsKitTypes/Utilities/UserDefaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension UserDefaults {
10 | func removeAllKeys() {
11 | let keys = dictionaryRepresentation().keys
12 | for key in keys {
13 | removeObject(forKey: key)
14 | }
15 | }
16 |
17 | func merge(_ contents: [String: Any]) {
18 | for element in contents {
19 | set(element.value, forKey: element.key)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PrefsKitUI/MultiplatformSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiplatformSection.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import SwiftUI
8 |
9 | /// `Section` SwiftUI view wrapper that incorporates footer content idiomatically for each platform.
10 | ///
11 | /// - On macOS, the footer content is combined into the form content.
12 | /// - On iOS, the footer content is attached below the form content.
13 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
14 | public struct MultiplatformSection: View {
15 | public let header: LocalizedStringKey?
16 | public let content: () -> Content
17 | public let footer: () -> Footer
18 |
19 | public init(
20 | _ header: LocalizedStringKey? = nil,
21 | @ViewBuilder content: @escaping () -> Content,
22 | @ViewBuilder footer: @escaping () -> Footer
23 | ) {
24 | self.header = header
25 | self.content = content
26 | self.footer = footer
27 | }
28 |
29 | public var body: some View {
30 | #if os(macOS)
31 | Section {
32 | VStack {
33 | content()
34 | SectionFooterView {
35 | footer()
36 | }
37 | .foregroundColor(.secondary)
38 | }
39 | } header: {
40 | if let header {
41 | Text(header)
42 | } else {
43 | EmptyView()
44 | }
45 | }
46 | #else
47 | Section {
48 | content()
49 | } footer: {
50 | SectionFooterView {
51 | footer()
52 | }
53 | }
54 | #endif
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/PrefsKitUI/PrefsKitUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsKitUI.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | @_exported import PrefsKitCore
8 |
--------------------------------------------------------------------------------
/Sources/PrefsKitUI/SectionFooterView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SectionFooterView.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import SwiftUI
8 |
9 | /// Within a SwiftUI `Form` view, use this to wrap `Section` `footer` contents to give it a standardized layout.
10 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
11 | public struct SectionFooterView: View {
12 | public let alignment: Alignment
13 | public let innerAlignment: HorizontalAlignment
14 | public let textAlignment: TextAlignment
15 | public let content: () -> Content
16 |
17 | public init(
18 | alignment: Alignment = .leading,
19 | innerAlignment: HorizontalAlignment = .leading,
20 | textAlignment: TextAlignment = .leading,
21 | @ViewBuilder content: @escaping () -> Content
22 | ) {
23 | self.alignment = alignment
24 | self.innerAlignment = innerAlignment
25 | self.textAlignment = textAlignment
26 | self.content = content
27 | }
28 |
29 | public var body: some View {
30 | ZStack(alignment: alignment) {
31 | Color.clear
32 | VStack(alignment: innerAlignment) {
33 | content()
34 | .multilineTextAlignment(textAlignment)
35 | }
36 | }
37 | .frame(maxWidth: .infinity)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/PrefsKitUI/SystemSettings/SystemSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemSettings.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | #if !os(macOS)
10 | import UIKit
11 | #endif
12 |
13 | public enum SystemSettings {
14 | #if os(macOS)
15 | /// Launches System Settings, optionally opening the specified panel.
16 | public static func launch(panel: Panel? = nil) {
17 | let command = launchCommand(panel: panel)
18 |
19 | // launch
20 | let p = Process()
21 | let shellExecutablePath = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
22 | p.executableURL = URL(fileURLWithPath: shellExecutablePath)
23 | p.arguments = ["-cl", command]
24 | do { try p.run() }
25 | catch { print(error.localizedDescription) }
26 | }
27 |
28 | static func launchCommand(panel: Panel? = nil) -> String {
29 | // see: https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751
30 | // two methods to launch System Settings:
31 | // - open -b com.apple.systempreferences /System/Library/PreferencePanes/Security.prefPane
32 | // - open "x-apple.systempreferences:com.apple.preference.security"
33 |
34 | // prefer bundle ID over bundle name where possible, since bundle ID allows sub-panel selection
35 | if let panel {
36 | if let bundleID = panel.bundleID {
37 | "open \"x-apple.systempreferences:\(bundleID)\""
38 | } else {
39 | "open -b com.apple.systempreferences \(panel.bundleName)"
40 | }
41 | } else {
42 | #"open "x-apple.systempreferences""#
43 | }
44 | }
45 |
46 | #elseif !os(watchOS)
47 |
48 | @MainActor
49 | public static func launch() {
50 | guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
51 | UIApplication.shared.open(url)
52 | }
53 |
54 | #endif
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/ActorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActorTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct ActorTests {
13 | // Note:
14 | //
15 | // Can't attach @MainActor to the class because there are
16 | // protocol requirements that can't be satisfied in that case
17 | //
18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
19 | @PrefsSchema final class TestSchema: @unchecked Sendable {
20 | @Storage var storage = .dictionary
21 | @StorageMode var storageMode = .cachedReadStorageWrite
22 |
23 | @MainActor @Pref var foo: Int? // <-- can attach to individual properties
24 | @Pref var bar: String?
25 | }
26 |
27 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
28 | @Test
29 | func baseline() {
30 | let prefs = TestSchema()
31 |
32 | Task { @MainActor in prefs.foo = 1 } // <-- needs MainActor context
33 | prefs.bar = "a string"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/ChainingEncodingStrategiesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChainingEncodingStrategiesTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct ChainingEncodingStrategiesTests {
13 | enum MyType: Int {
14 | case foo = 0x12345678
15 | case bar = 0x98765432
16 | }
17 |
18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
19 | @PrefsSchema final class TestSchema: @unchecked Sendable {
20 | @Storage var storage = .dictionary
21 | @StorageMode var storageMode = .cachedReadStorageWrite
22 |
23 | @Pref(coding: .compressedData(algorithm: .lzfse).base64DataString()) public var foo: Data?
24 |
25 | // not logic-tested, just to check if the compiler can infer the protocol extensions
26 | @Pref(
27 | coding: URL.jsonDataPrefsCoding
28 | .compressedData(algorithm: .lzfse)
29 | .compressedData(algorithm: .zlib)
30 | .base64DataString()
31 | ) public var bar: URL?
32 |
33 | @Pref(coding: FloatingPointSign.rawRepresentablePrefsCoding)
34 | public var fpsA: FloatingPointSign?
35 |
36 | @Pref(coding: FloatingPointSign.rawRepresentablePrefsCoding.intAsString)
37 | public var fpsB: FloatingPointSign?
38 |
39 | @Pref(coding: .uInt32AsInt) var binaryInt: UInt32?
40 | }
41 |
42 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
43 | @Test
44 | func chainedEncodingA() async throws {
45 | let schema = TestSchema()
46 |
47 | let testData = Data((1 ... 100).map(\.self))
48 |
49 | schema.foo = testData
50 |
51 | let encoded: String = try #require(schema.storage.storageValue(forKey: "foo"))
52 |
53 | // check that the storage value is the base-64 string of the compressed data
54 | #expect(
55 | encoded ==
56 | "YnZ4LWQAAAABAgMEBQYHCAkKCwwNDg8QERITFBUW"
57 | + "FxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0"
58 | + "NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFS"
59 | + "U1RVVldYWVpbXF1eX2BhYmNkYnZ4JA=="
60 | )
61 |
62 | let decoded = try #require(schema.foo)
63 |
64 | #expect(decoded == testData)
65 | }
66 |
67 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
68 | @Test
69 | func chainedEncoding_fpsA() async throws {
70 | let schema = TestSchema()
71 |
72 | schema.fpsA = .plus
73 |
74 | #expect(schema.storage.storageValue(forKey: "fpsA") as Int? == FloatingPointSign.plus.rawValue)
75 | #expect(schema.fpsA == .plus)
76 | }
77 |
78 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
79 | @Test
80 | func chainedEncoding_fpsB() async throws {
81 | let schema = TestSchema()
82 |
83 | schema.fpsB = .plus
84 |
85 | #expect(schema.storage.storageValue(forKey: "fpsB") as String? == String(FloatingPointSign.plus.rawValue))
86 | #expect(schema.fpsB == .plus)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/CustomEncodingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomEncodingTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitCore
9 | import Testing
10 |
11 | /// Test `PrefsCodable` implementation that uses custom `getValue(in:)` and `setValue(to:in:)` method overrides to convert
12 | /// to/from raw prefs storage data without using any of the included abstraction protocols such as
13 | /// `RawRepresentablePrefsCodable` or `CodablePrefsCodable`.
14 | @Suite
15 | struct CustomEncodingTests {
16 | struct NonCodableNonRawRepresentable: Equatable {
17 | var value: Int
18 |
19 | init(value: Int) {
20 | self.value = value
21 | }
22 |
23 | init?(encoded: String) {
24 | guard encoded.hasPrefix("VALUE:"),
25 | let val = Int(encoded.dropFirst(6))
26 | else { return nil }
27 | value = val
28 | }
29 |
30 | func encoded() -> String {
31 | "VALUE:(\(value))"
32 | }
33 | }
34 |
35 | struct CustomPrefCoding: PrefsCodable {
36 | typealias Value = NonCodableNonRawRepresentable
37 | typealias StorageValue = String
38 |
39 | func decode(prefsValue: StorageValue) -> Value? {
40 | NonCodableNonRawRepresentable(encoded: prefsValue)
41 | }
42 |
43 | func encode(prefsValue: Value) -> StorageValue? {
44 | prefsValue.encoded()
45 | }
46 | }
47 |
48 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
49 | @PrefsSchema final class TestSchema: @unchecked Sendable {
50 | @Storage var storage = .dictionary
51 | @StorageMode var storageMode = .cachedReadStorageWrite
52 |
53 | enum Key {
54 | static let custom = "custom"
55 | static let customDefaulted = "customDefaulted"
56 | }
57 |
58 | @Pref(key: Key.custom, coding: CustomPrefCoding()) var custom: NonCodableNonRawRepresentable?
59 | @Pref(key: Key.customDefaulted, coding: CustomPrefCoding()) var customDefaulted: NonCodableNonRawRepresentable = .init(value: 1)
60 | }
61 |
62 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
63 | @Test
64 | func customValueEncoding() {
65 | let schema = TestSchema()
66 |
67 | #expect(schema.custom?.value == nil)
68 |
69 | schema.custom = NonCodableNonRawRepresentable(value: 42)
70 | #expect(schema.custom?.value == 42)
71 |
72 | schema.custom?.value = 5
73 | #expect(schema.custom?.value == 5)
74 |
75 | schema.custom = nil
76 | #expect(schema.custom == nil)
77 | }
78 |
79 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
80 | @Test
81 | func customDefaultedValueEncoding() {
82 | let schema = TestSchema()
83 |
84 | #expect(schema.customDefaulted == NonCodableNonRawRepresentable(value: 1))
85 |
86 | schema.customDefaulted = NonCodableNonRawRepresentable(value: 42)
87 | #expect(schema.customDefaulted.value == 42)
88 |
89 | schema.customDefaulted = NonCodableNonRawRepresentable(value: 5)
90 | #expect(schema.customDefaulted.value == 5)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/PrefsCodable Strategies/Base64StringDataPrefsCodingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Base64StringDataPrefsCodingTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct Base64StringDataPrefsCodingTests {
13 | struct MyType: Equatable, Codable {
14 | var id: Int
15 | var name: String
16 | }
17 |
18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
19 | @PrefsSchema final class TestSchema: @unchecked Sendable {
20 | @Storage var storage = .dictionary
21 | @StorageMode var storageMode = .storageOnly // important for unit tests in this file!
22 |
23 | // MARK: - Static Constructors
24 |
25 | @Pref(coding: .base64DataString()) var data: Data?
26 |
27 | // MARK: - Chaining Constructor
28 |
29 | @Pref(coding: MyType.jsonDataPrefsCoding.base64DataString()) var myTypeChained: MyType?
30 | }
31 |
32 | // MARK: - Static Constructors
33 |
34 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
35 | @Test
36 | func data() async throws {
37 | let schema = TestSchema()
38 |
39 | let testData = Data([0x01, 0x02, 0x03])
40 |
41 | schema.data = testData
42 | let getString: String = try #require(schema.storage.storageValue(forKey: "data"))
43 | #expect(getString == "AQID")
44 | #expect(schema.data == testData)
45 | }
46 |
47 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
48 | @Test
49 | func myTypeChained() async throws {
50 | let schema = TestSchema()
51 |
52 | let testType = MyType(id: 123, name: "foo")
53 |
54 | schema.myTypeChained = testType
55 | let getString: String = try #require(schema.storage.storageValue(forKey: "myTypeChained"))
56 | // just check for non-empty content, we won't check actual content since it's not fully deterministic
57 | #expect(getString.count > 10)
58 | #expect(schema.myTypeChained == testType)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/PrefsCodable Strategies/CompressedDataPrefsCodingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompressedDataPrefsCodingTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct CompressedDataPrefsCodingTests {
13 | struct MyType: Equatable, Codable {
14 | var id: Int
15 | var name: String
16 | }
17 |
18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
19 | @PrefsSchema final class TestSchema: @unchecked Sendable {
20 | @Storage var storage = .dictionary
21 | @StorageMode var storageMode = .storageOnly // important for unit tests in this file!
22 |
23 | // MARK: - Static Constructors
24 |
25 | @Pref(coding: .compressedData(algorithm: .zlib)) var data: Data?
26 |
27 | // MARK: - Chaining Constructor
28 |
29 | @Pref(
30 | coding: MyType
31 | .jsonDataPrefsCoding
32 | .compressedData(algorithm: .zlib)
33 | ) var myTypeChained: MyType?
34 | }
35 |
36 | // MARK: - Static Constructors
37 |
38 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
39 | @Test
40 | func data() throws {
41 | let schema = TestSchema()
42 |
43 | let testData = Data([0x01, 0x02, 0x03])
44 |
45 | schema.data = testData
46 | #expect(schema.storage.storageValue(forKey: "data") as Data? == Data([0x63, 0x64, 0x62, 0x06, 0x00]))
47 | #expect(schema.data == testData)
48 | }
49 |
50 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
51 | @Test
52 | func myTypeChained() throws {
53 | let schema = TestSchema()
54 |
55 | let testType = MyType(id: 123, name: "foo")
56 |
57 | schema.myTypeChained = testType
58 | let getData: Data = try #require(schema.storage.storageValue(forKey: "myTypeChained"))
59 | // just check for non-empty content, we won't check actual content since it's not fully deterministic
60 | #expect(getData.count > 10)
61 | #expect(schema.myTypeChained == testType)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/PrefsCodable Strategies/ISO8601StringDatePrefsCodingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ISO8601StringDatePrefsCodingTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct ISO8601StringDatePrefsCodingTests {
13 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
14 | @PrefsSchema final class TestSchema: @unchecked Sendable {
15 | @Storage var storage = .dictionary
16 | @StorageMode var storageMode = .storageOnly // important for unit tests in this file!
17 |
18 | // MARK: - Static Constructors
19 |
20 | @Pref(coding: .iso8601DateString) var date: Date?
21 | }
22 |
23 | // MARK: - Static Constructors
24 |
25 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
26 | @Test
27 | func dateISO8601String_SetValue() async throws {
28 | let schema = TestSchema()
29 |
30 | let testDate = Date(timeIntervalSinceReferenceDate: 757_493_476)
31 |
32 | schema.date = testDate
33 | #expect(schema.storage.storageValue(forKey: "date") as String? == "2025-01-02T06:51:16Z")
34 | #expect(schema.date == testDate)
35 | }
36 |
37 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
38 | @Test
39 | func dateISO8601String_SetStorageValue() async throws {
40 | let schema = TestSchema()
41 |
42 | let testDate = Date(timeIntervalSinceReferenceDate: 757_493_476)
43 |
44 | #expect(schema.date == nil)
45 |
46 | schema.storage.setStorageValue(forKey: "date", to: "2025-01-02T06:51:16Z")
47 | #expect(schema.date == testDate)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/PrefsCodable Strategies/URLStringPrefsCodingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLStringPrefsCodingTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct URLStringPrefsCodingTests {
13 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
14 | @PrefsSchema final class TestSchema: @unchecked Sendable {
15 | @Storage var storage = .dictionary
16 | @StorageMode var storageMode = .cachedReadStorageWrite
17 |
18 | // MARK: - Static Constructors
19 |
20 | @Pref(coding: .urlString) var url: URL?
21 | }
22 |
23 | // MARK: - Static Constructors
24 |
25 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
26 | @Test
27 | func urlString() async throws {
28 | let schema = TestSchema()
29 |
30 | let testURL = try #require(URL(string: "https://www.example.com"))
31 |
32 | schema.url = testURL
33 | #expect(schema.storage.storageValue(forKey: "url") as String? == "https://www.example.com")
34 | #expect(schema.url == testURL)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/PrefsSchemaPrefsStorageTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsSchemaPrefsStorageTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitCore
9 | import Testing
10 |
11 | @Suite
12 | struct PrefsSchemaStorageMacroTests {
13 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
14 | @PrefsSchema final class TestSchemaA: @unchecked Sendable {
15 | @Storage var storage = DictionaryPrefsStorage()
16 | @StorageMode var storageMode = .cachedReadStorageWrite
17 |
18 | @Pref var foo: Int?
19 | }
20 |
21 | /// Note: public access level
22 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
23 | @PrefsSchema public final class TestSchemaB: @unchecked Sendable {
24 | @Storage public var storage = .dictionary
25 | @StorageMode public var storageMode = .cachedReadStorageWrite
26 |
27 | @Pref public var foo: Int?
28 | }
29 |
30 | /// No logic testing. Just ensure compiler is happy.
31 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
32 | @Test
33 | func instantiate() {
34 | _ = TestSchemaA()
35 | _ = TestSchemaB()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/RawRepresentableArrayTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentableArrayTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitCore
9 | import Testing
10 |
11 | @Suite(.serialized)
12 | struct RawRepresentableArrayTests {
13 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))"
14 |
15 | static var suite: UserDefaults {
16 | UserDefaults(suiteName: domain)!
17 | }
18 |
19 | enum StringEnum: String, RawRepresentable {
20 | case one
21 | case two
22 | }
23 |
24 | enum IntEnum: Int, RawRepresentable {
25 | case one = 1
26 | case two = 2
27 | }
28 |
29 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
30 | @PrefsSchema final class TestSchema: @unchecked Sendable {
31 | @Storage var storage: AnyPrefsStorage
32 | @StorageMode var storageMode: PrefsStorageMode
33 |
34 | init(storage: PrefsStorage, storageMode: PrefsStorageMode) {
35 | self.storage = AnyPrefsStorage(storage)
36 | self.storageMode = storageMode
37 | }
38 |
39 | enum Key: String {
40 | case arrayA
41 | case arrayADefaulted
42 | case arrayB
43 | case arrayBDefaulted
44 | }
45 |
46 | @Pref(
47 | key: Key.arrayA.rawValue,
48 | coding: [StringEnum].rawRepresentableArrayPrefsCoding
49 | ) var arrayA: [StringEnum]?
50 |
51 | @Pref(
52 | key: Key.arrayADefaulted.rawValue,
53 | coding: [StringEnum].rawRepresentableArrayPrefsCoding
54 | ) var arrayADefaulted: [StringEnum] = [.one, .two]
55 |
56 | @Pref(
57 | key: Key.arrayB.rawValue,
58 | coding: [IntEnum].rawRepresentableArrayPrefsCoding
59 | ) var arrayB: [IntEnum]?
60 |
61 | @Pref(
62 | key: Key.arrayBDefaulted.rawValue,
63 | coding: [IntEnum].rawRepresentableArrayPrefsCoding
64 | ) var arrayBDefaulted: [IntEnum] = [.one, .two]
65 | }
66 |
67 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
68 | static var schemas: [TestSchema] {
69 | [
70 | TestSchema(storage: .dictionary, storageMode: .cachedReadStorageWrite),
71 | TestSchema(storage: .dictionary, storageMode: .storageOnly),
72 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .cachedReadStorageWrite),
73 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .storageOnly)
74 | ]
75 | }
76 |
77 | // MARK: - Init
78 |
79 | init() async throws {
80 | UserDefaults.standard.removePersistentDomain(forName: Self.domain)
81 | }
82 |
83 | // MARK: - Tests
84 |
85 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
86 | @Test(arguments: schemas)
87 | func stringRepresentable(schema: TestSchema) throws {
88 | schema.arrayA = [.two, .two, .one]
89 | #expect(schema.arrayA == [.two, .two, .one])
90 |
91 | #expect(schema.arrayADefaulted == [.one, .two])
92 |
93 | schema.arrayADefaulted = [.two, .two, .one]
94 | #expect(schema.arrayADefaulted == [.two, .two, .one])
95 |
96 | // check underlying storage is as expected
97 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayA.rawValue) as? [String])
98 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayADefaulted.rawValue) as? [String])
99 | }
100 |
101 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
102 | @Test(arguments: schemas)
103 | func intRepresentable(schema: TestSchema) throws {
104 | schema.arrayB = [.two, .two, .one]
105 | #expect(schema.arrayB == [.two, .two, .one])
106 |
107 | #expect(schema.arrayBDefaulted == [.one, .two])
108 |
109 | schema.arrayBDefaulted = [.two, .two, .one]
110 | #expect(schema.arrayBDefaulted == [.two, .two, .one])
111 |
112 | // check underlying storage is as expected
113 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayB.rawValue) as? [Int])
114 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayBDefaulted.rawValue) as? [Int])
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/PrefsKitCoreTests/RawRepresentableDictionaryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawRepresentableDictionaryTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitCore
9 | import Testing
10 |
11 | @Suite(.serialized)
12 | struct RawRepresentableDictionaryTests {
13 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))"
14 |
15 | static var suite: UserDefaults {
16 | UserDefaults(suiteName: domain)!
17 | }
18 |
19 | enum StringEnum: String, RawRepresentable {
20 | case one
21 | case two
22 | }
23 |
24 | enum IntEnum: Int, RawRepresentable {
25 | case one = 1
26 | case two = 2
27 | }
28 |
29 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
30 | @PrefsSchema final class TestSchema: @unchecked Sendable {
31 | @Storage var storage: AnyPrefsStorage
32 | @StorageMode var storageMode: PrefsStorageMode
33 |
34 | init(storage: PrefsStorage, storageMode: PrefsStorageMode) {
35 | self.storage = AnyPrefsStorage(storage)
36 | self.storageMode = storageMode
37 | }
38 |
39 | enum Key: String {
40 | case dictA
41 | case dictADefaulted
42 | case dictB
43 | case dictBDefaulted
44 | }
45 |
46 | @Pref(
47 | key: Key.dictA.rawValue,
48 | coding: [String: StringEnum].rawRepresentableDictionaryPrefsCoding
49 | ) var dictA: [String: StringEnum]?
50 |
51 | @Pref(
52 | key: Key.dictADefaulted.rawValue,
53 | coding: [String: StringEnum].rawRepresentableDictionaryPrefsCoding
54 | ) var dictADefaulted: [String: StringEnum] = ["a": .one, "b": .two]
55 |
56 | @Pref(
57 | key: Key.dictB.rawValue,
58 | coding: [String: IntEnum].rawRepresentableDictionaryPrefsCoding
59 | ) var dictB: [String: IntEnum]?
60 |
61 | @Pref(
62 | key: Key.dictBDefaulted.rawValue,
63 | coding: [String: IntEnum].rawRepresentableDictionaryPrefsCoding
64 | ) var dictBDefaulted: [String: IntEnum] = ["a": .one, "b": .two]
65 | }
66 |
67 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
68 | static var schemas: [TestSchema] {
69 | [
70 | TestSchema(storage: .dictionary, storageMode: .cachedReadStorageWrite),
71 | TestSchema(storage: .dictionary, storageMode: .storageOnly),
72 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .cachedReadStorageWrite),
73 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .storageOnly)
74 | ]
75 | }
76 |
77 | // MARK: - Init
78 |
79 | init() async throws {
80 | UserDefaults.standard.removePersistentDomain(forName: Self.domain)
81 | }
82 |
83 | // MARK: - Tests
84 |
85 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
86 | @Test(arguments: schemas)
87 | func stringRepresentable(schema: TestSchema) throws {
88 | schema.dictA = ["a": .two, "b": .two, "c": .one]
89 | #expect(schema.dictA == ["a": .two, "b": .two, "c": .one])
90 |
91 | #expect(schema.dictADefaulted == ["a": .one, "b": .two])
92 |
93 | schema.dictADefaulted = ["a": .two, "b": .two, "c": .one]
94 | #expect(schema.dictADefaulted == ["a": .two, "b": .two, "c": .one])
95 |
96 | // check underlying storage is as expected
97 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictA.rawValue) as? [String: String])
98 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictADefaulted.rawValue) as? [String: String])
99 | }
100 |
101 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
102 | @Test(arguments: schemas)
103 | func intRepresentable(schema: TestSchema) throws {
104 | schema.dictB = ["a": .two, "b": .two, "c": .one]
105 | #expect(schema.dictB == ["a": .two, "b": .two, "c": .one])
106 |
107 | #expect(schema.dictBDefaulted == ["a": .one, "b": .two])
108 |
109 | schema.dictBDefaulted = ["a": .two, "b": .two, "c": .one]
110 | #expect(schema.dictBDefaulted == ["a": .two, "b": .two, "c": .one])
111 |
112 | // check underlying storage is as expected
113 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictB.rawValue) as? [String: Int])
114 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictBDefaulted.rawValue) as? [String: Int])
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/PrefsKitTypesTests/PrefsStorage Traits/PrefsStorageInitializableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageInitializableTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitTypes
9 | import Testing
10 |
11 | /// Test prefs storage inits for PList and JSON.
12 | @Suite(.serialized)
13 | struct PrefsStorageInitializableTests {
14 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))"
15 |
16 | typealias StorageBackend = PrefsStorage & PrefsStorageInitializable
17 | static var storageBackends: [any StorageBackend.Type] {
18 | [
19 | DictionaryPrefsStorage.self
20 | // (`UserDefaultsPrefsStorage` does not conform to the protocol)
21 | ]
22 | }
23 |
24 | typealias Key1 = TestContent.Basic.Root.Key1
25 | typealias Key2 = TestContent.Basic.Root.Key2
26 | typealias Key3 = TestContent.Basic.Root.Key3
27 | typealias Key4 = TestContent.Basic.Root.Key4
28 | typealias Key5 = TestContent.Basic.Root.Key5
29 | typealias Key6 = TestContent.Basic.Root.Key6
30 | typealias Key7 = TestContent.Basic.Root.Key7
31 | typealias Key8 = TestContent.Basic.Root.Key8
32 | typealias Key9 = TestContent.Basic.Root.Key9
33 | typealias Key10 = TestContent.Basic.Root.Key10
34 | typealias Key11 = TestContent.Basic.Root.Key11
35 |
36 | // MARK: - JSON Tests
37 |
38 | @Test(arguments: Self.storageBackends)
39 | func initJSONData(storageType: any PrefsStorageInitializable.Type) async throws {
40 | let data = try #require(TestContent.Basic.jsonString.data(using: .utf8))
41 | let storage = try storageType.init(
42 | from: data,
43 | format: .json(strategy: TestContent.Basic.JSONPrefsStorageImportStrategy())
44 | )
45 |
46 | try await TestContent.Basic.checkContent(in: storage)
47 | }
48 |
49 | @Test(arguments: Self.storageBackends)
50 | func initJSONString(storageType: any PrefsStorageInitializable.Type) async throws {
51 | let storage = try storageType.init(
52 | from: TestContent.Basic.jsonString,
53 | format: .json(strategy: TestContent.Basic.JSONPrefsStorageImportStrategy())
54 | )
55 |
56 | try await TestContent.Basic.checkContent(in: storage)
57 | }
58 |
59 | // MARK: - PList Tests
60 |
61 | @Test(arguments: Self.storageBackends)
62 | func initPListData(storageType: any PrefsStorageInitializable.Type) async throws {
63 | let data = try #require(TestContent.Basic.plistString.data(using: .utf8))
64 | let storage = try storageType.init(from: data, format: .plist())
65 |
66 | try await TestContent.Basic.checkContent(in: storage)
67 | }
68 |
69 | @Test(arguments: Self.storageBackends)
70 | func initPListString(storageType: any PrefsStorageInitializable.Type) async throws {
71 | let storage = try storageType.init(from: TestContent.Basic.plistString, format: .plist())
72 |
73 | try await TestContent.Basic.checkContent(in: storage)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Tests/PrefsKitTypesTests/PrefsStorage/PrefsStorage+Static Tests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorage+Static Tests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import PrefsKitTypes
9 | import Testing
10 |
11 | @Suite struct PrefsStorageStaticTests {
12 | /// No logic testing, just ensure compiler is happy with our syntax sugar.
13 | @Test
14 | func varSyntax() {
15 | let _: PrefsStorage = AnyPrefsStorage(.dictionary)
16 | let _: PrefsStorage = AnyPrefsStorage(.dictionary(root: [:]))
17 | let _: PrefsStorage = AnyPrefsStorage(.userDefaults)
18 | let _: PrefsStorage = AnyPrefsStorage(.userDefaults(suite: .standard))
19 |
20 | let _: PrefsStorage = DictionaryPrefsStorage()
21 | let _: PrefsStorage = UserDefaultsPrefsStorage()
22 |
23 | let _: PrefsStorage = .dictionary
24 | let _: PrefsStorage = .dictionary(root: [:])
25 | let _: PrefsStorage = .userDefaults
26 | let _: PrefsStorage = .userDefaults(suite: .standard)
27 | }
28 |
29 | /// No logic testing, just ensure compiler is happy with our syntax sugar.
30 | @Test
31 | func anySyntax() {
32 | func foo(_ storage: any PrefsStorage) { }
33 |
34 | foo(AnyPrefsStorage(.dictionary))
35 | foo(AnyPrefsStorage(.dictionary(root: [:])))
36 | foo(AnyPrefsStorage(.userDefaults))
37 | foo(AnyPrefsStorage(.userDefaults(suite: .standard)))
38 |
39 | foo(DictionaryPrefsStorage())
40 | foo(UserDefaultsPrefsStorage())
41 |
42 | foo(.dictionary)
43 | foo(.dictionary(root: [:]))
44 | foo(.userDefaults)
45 | foo(.userDefaults(suite: .standard))
46 | }
47 |
48 | /// No logic testing, just ensure compiler is happy with our syntax sugar.
49 | @Test
50 | func someSyntax() {
51 | func foo(_ storage: some PrefsStorage) { }
52 |
53 | foo(AnyPrefsStorage(.dictionary))
54 | foo(AnyPrefsStorage(.dictionary(root: [:])))
55 | foo(AnyPrefsStorage(.userDefaults))
56 | foo(AnyPrefsStorage(.userDefaults(suite: .standard)))
57 |
58 | foo(DictionaryPrefsStorage())
59 | foo(UserDefaultsPrefsStorage())
60 |
61 | // doesn't work. huh?
62 | foo(.dictionary)
63 | foo(.dictionary(root: [:]))
64 | foo(.userDefaults)
65 | foo(.userDefaults(suite: .standard))
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/PrefsKitTypesTests/PrefsStorage/PrefsStorageArrayTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsStorageArrayTests.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | @testable import PrefsKitTypes
9 | import Testing
10 |
11 | @Suite(.serialized)
12 | struct PrefsStorageArrayTests {
13 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))"
14 |
15 | static var storageBackends: [AnyPrefsStorage] {
16 | [
17 | AnyPrefsStorage(.dictionary),
18 | AnyPrefsStorage(.userDefaults(suite: UserDefaults(suiteName: domain)!))
19 | ]
20 | }
21 |
22 | // MARK: - Init
23 |
24 | init() async throws {
25 | UserDefaults.standard.removePersistentDomain(forName: Self.domain)
26 | }
27 |
28 | // MARK: - Atomic
29 |
30 | @Test(arguments: Self.storageBackends)
31 | func intArray(storage: AnyPrefsStorage) async throws {
32 | storage.setStorageValue(forKey: "foo", to: [1, 2, 3])
33 |
34 | let key = AnyAtomicPrefsKey<[Int]>(key: "foo")
35 |
36 | let value: [Int] = try #require(storage.value(forKey: key))
37 |
38 | #expect(value == [1, 2, 3])
39 | }
40 |
41 | @Test(arguments: Self.storageBackends)
42 | func stringArray(storage: AnyPrefsStorage) async throws {
43 | storage.setStorageValue(forKey: "foo", to: ["a", "b", "c"])
44 |
45 | let key = AnyAtomicPrefsKey<[String]>(key: "foo")
46 |
47 | let value = try #require(storage.value(forKey: key))
48 |
49 | #expect(value == ["a", "b", "c"])
50 | }
51 |
52 | @Test(arguments: Self.storageBackends)
53 | func boolArray(storage: AnyPrefsStorage) async throws {
54 | storage.setStorageValue(forKey: "foo", to: [true, false, true])
55 |
56 | let key = AnyAtomicPrefsKey<[Bool]>(key: "foo")
57 |
58 | let value = try #require(storage.value(forKey: key))
59 |
60 | #expect(value == [true, false, true])
61 | }
62 |
63 | @Test(arguments: Self.storageBackends)
64 | func doubleArray(storage: AnyPrefsStorage) async throws {
65 | storage.setStorageValue(forKey: "foo", to: [1.5, 2.5, 3.5] as [Double])
66 |
67 | let key = AnyAtomicPrefsKey<[Double]>(key: "foo")
68 |
69 | let value = try #require(storage.value(forKey: key))
70 |
71 | #expect(value == [1.5, 2.5, 3.5])
72 | }
73 |
74 | @Test(arguments: Self.storageBackends)
75 | func floatArray(storage: AnyPrefsStorage) async throws {
76 | storage.setStorageValue(forKey: "foo", to: [1.5, 2.5, 3.5] as [Float])
77 |
78 | let key = AnyAtomicPrefsKey<[Float]>(key: "foo")
79 |
80 | let value = try #require(storage.value(forKey: key))
81 |
82 | #expect(value == [1.5, 2.5, 3.5])
83 | }
84 |
85 | @Test(arguments: Self.storageBackends)
86 | func dataArray(storage: AnyPrefsStorage) async throws {
87 | storage.setStorageValue(forKey: "foo", to: [Data([0x01]), Data([0x02])])
88 |
89 | let key = AnyAtomicPrefsKey<[Data]>(key: "foo")
90 |
91 | let value = try #require(storage.value(forKey: key))
92 |
93 | #expect(value == [Data([0x01]), Data([0x02])])
94 | }
95 |
96 | @Test(arguments: Self.storageBackends)
97 | func anyPrefsArrayArray(storage: AnyPrefsStorage) async throws {
98 | let array: [Any] = [1, 2, "string", true]
99 | storage.setUnsafeStorageValue(forKey: "foo", to: array)
100 |
101 | let value: [Any] = try #require(storage.unsafeStorageValue(forKey: "foo") as? [Any])
102 | try #require(value.count == 4)
103 |
104 | #expect(value[0] as? Int == 1)
105 | #expect(value[1] as? Int == 2)
106 | #expect(value[2] as? String == "string")
107 | #expect(value[3] as? Bool == true)
108 | }
109 |
110 | // MARK: - Nested
111 |
112 | @Test(arguments: Self.storageBackends)
113 | func nestedStringArray(storage: AnyPrefsStorage) async throws {
114 | storage.setStorageValue(forKey: "foo", to: [["a", "b"], ["c"]])
115 |
116 | let key = AnyAtomicPrefsKey<[[String]]>(key: "foo")
117 |
118 | let value = try #require(storage.value(forKey: key))
119 |
120 | #expect(value == [["a", "b"], ["c"]])
121 | }
122 |
123 | @Test(arguments: Self.storageBackends)
124 | func nestedAnyArray(storage: AnyPrefsStorage) async throws {
125 | let array: [Any] = [["a", 2], [true]]
126 | storage.setUnsafeStorageValue(forKey: "foo", to: array)
127 |
128 | let value: [Any] = try #require(storage.unsafeStorageValue(forKey: "foo") as? [Any])
129 | try #require(value.count == 2)
130 |
131 | let subArray1 = try #require(value[0] as? [Any])
132 | try #require(subArray1.count == 2)
133 | #expect(subArray1[0] as? String == "a")
134 | #expect(subArray1[1] as? Int == 2)
135 |
136 | let subArray2 = try #require(value[1] as? [Any])
137 | try #require(subArray2.count == 1)
138 | #expect(subArray2[0] as? Bool == true)
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Tests/PrefsKitTypesTests/TestContent/TestContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestContent.swift
3 | // PrefsKit • https://github.com/orchetect/PrefsKit
4 | // © 2025 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | enum TestContent { }
8 |
--------------------------------------------------------------------------------